VUE2 模板编译原理

VUE2 模板编译原理vue 模板的编译到渲染,结合源码的分析介绍从 template 到 AST,到 VNode(虚拟 DOM),再将 VNode 挂载渲染成真是的 DOM。本文从思路流程方面分析模板编译的整个过程,不着重一字一句的具体代码解读。这个过程的代码

大家好,欢迎来到IT知识分享网。

vue 模板的编译到渲染,结合源码的分析介绍从 template 到 AST,到 VNode(虚拟 DOM),再将 VNode 挂载渲染成真是的 DOM。本文从思路流程方面分析模板编译的整个过程,不着重一字一句的具体代码解读。这个过程的代码比较机械枯燥,可以参看文末的参考链接。


从 vue 模板到渲染成 dom,整个流程如图:

VUE2 模板编译原理

整体而言,Vue 的处理方式大致分为三步:

  • 将模板进行解析,得到一棵抽象语法树 AST(parse / optimize)
  • 根据抽象语法树 AST 得到虚拟 DOM 树(generate / render)
  • 将虚拟 DOM 渲染为真实的 DOM

注:抽象语法树(AST)是指对源码进行解析后形成的树状的语法结构。通俗地理解,有了 AST 以后,后续处理可以直接在树结构上进行,不用再处理源码中的各种书写格式、括号优先级等问题。

实现编译的核心源码入口:

// src/compiler/index.js export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { const ast = parse(template.trim(), options) if (options.optimize !== false) { optimize(ast, options) } const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } }) 

步骤一 将模板进行解析为抽象语法树 AST

parse() 方法是将源码转换成 AST 的方法,它的核心是调用 parseHTML() 方法解析。相关源码位于 src/compiler/parser/index.js。

parseHTML() 做的事就是从头扫描 HTML 字符串,按一定的规则判断当前字符是什么(标签、属性、文本、注释等等),并调用相应的回调方法,从而完成 HTML 字符串的解析,并返回 AST 树。

示例:

<!-- 模板 template --> <div class="counter"> <div>{{ count }}</div> <button @click="increment">+</button> </div> 

经过 parse / parseHTML 解析后生成的 AST 是这样的:

{ attrsList: [], attrsMap: { class: "counter" }, children: [{ attrsList: [], attrsMap: {}, children: [{ end: 37, expression: "_s(count)", start: 26, text: "{{ count }}", tokens: [{ @binding: "count" }] }], end: 43, parent: {...}, plain: true, rawAttrsMap: {}, start: 21, tag: "div", type: 1, },{ attrsList: [{ end: 69, name: "@click", start: 51, value: "increment", }], attrsMap:{ @click: "increment", }, children: [{ end: 71, start: 70, text: "+", type: 3 // text }], end: 80, events:{ click:{ dynamic: false, end: 69, start: 51, value: "increment" } }, hasBindings: true, parent: {...}, plain: false, rawAttrsMap:{ @click:{ end: 69, name: "@click", start: 51, value: "increment", } }, start: 43, tag: "button", type: 1 }], end: 86, parent: undefined, plain: false, rawAttrsMap:{ class:{ end: 20, name: "class", start: 5, value: "counter" } } start: 0, staticClass: "\"counter\"", tag: "div", type: 1 } 

这一步之后,进行 optimize() 处理。

optimize() 是对 AST 进行优化的过程,以提升后续渲染性能。这个方法位于 src/compiler/optimizer.js,作用是分析出纯静态的 DOM(不含表达式,可以直接渲染的 DOM),将它们放入常量中,在后续 patch 的过程中可以忽略它们。

optimize() 处理逻辑,当一个元素有表达式时肯定就不是静态的,当一个元素是文本节点时,肯定是静态的,如果子元素是非静态的,则父元素也是非静态的。

optimize() 处理后的 AST:

{ attrsList: [], attrsMap: { class: "counter" }, children: [{ attrsList: [], attrsMap: {}, children: [{ end: 37, expression: "_s(count)", start: 26, // 看这里 static: false, text: "{{ count }}", tokens: [{ @binding: "count" }] }], end: 43, parent: {...}, plain: true, rawAttrsMap: {}, // 看这里 static: false, staticRoot: false, start: 21, tag: "div", type: 1, },{ attrsList: [{ end: 69, name: "@click", start: 51, value: "increment", }], attrsMap:{ @click: "increment", }, children: [{ end: 71, start: 70, // 看这里 static: true, text: "+", type: 3 }], end: 80, events:{ click:{ dynamic: false, end: 69, start: 51, value: "increment" } }, hasBindings: true, parent: {...}, plain: false, rawAttrsMap:{ @click:{ end: 69, name: "@click", start: 51, value: "increment", } }, start: 43, // 看这里 static: false, staticRoot: false, tag: "button", type: 1 }], end: 86, parent: undefined, plain: false, rawAttrsMap:{ class:{ end: 20, name: "class", start: 5, value: "counter" } } start: 0, // 看这里 static: false, staticClass: "\"counter\"", staticRoot: false, tag: "div", type: 1 } 

步骤二 将 AST 树转化为 VNode (虚拟 DOM 树)

实现编译的核心源码入口:

// src/compiler/index.js export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { const ast = parse(template.trim(), options) if (options.optimize !== false) { optimize(ast, options) } const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } }) 

在 parse() 和 optimize() 运行完之后,执行 generate():

// src\compiler\codegen\index.js export function generate ( ast: ASTElement | void, options: CompilerOptions ): CodegenResult { const state = new CodegenState(options) const code = ast ? genElement(ast, state) : '_c("div")' return { render: `with(this){return ${code}}`, staticRenderFns: state.staticRenderFns } } export function genElement (el: ASTElement, state: CodegenState): string { //对一些标签属性的处理 if (el.staticRoot && !el.staticProcessed) { return genStatic(el, state) } else if (el.once && !el.onceProcessed) { return genOnce(el, state) // 处理v-once } else if (el.for && !el.forProcessed) { return genFor(el, state) // 处理v-for } else if (el.if && !el.ifProcessed) { return genIf(el, state) } else if (el.tag === 'template' && !el.slotTarget) { return genChildren(el, state) || 'void 0' } else if (el.tag === 'slot') { return genSlot(el, state) } else { // component or element let code //组件的处理 if (el.component) { code = genComponent(el.component, el, state) } else { //核心的body部分 //1、生成节点的数据对象data的字符串 const data = el.plain ? undefined : genData(el, state) //2、查找其子节点,生成子节点的字符串 const children = el.inlineTemplate ? null : genChildren(el, state, true) //3、将tag,data,children拼装成字符串 code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })` } // module transforms for (let i = 0; i < state.transforms.length; i++) { code = state.transforms[i](el, code) } return code } 

generate() 返回的 render 表达式结构像这样:

_c( // 1、标签 'div', //2、模板相关属性的数据对象 { ... }, //3、子节点,循环其模型 [ _c(...) ] 
<template> <div id="app"> <h1>Hello</h1> <span>{{message}}</span> </div> </template> // 上述dom 对应的虚拟 render 表达式 with(this){ return _c('div',{ attrs:{"id":"app"} }, [ _c('h1',[ _v("Hello") ]), _c('span',[ _v(_s(message)) ]) ]) } 

示例中出现较多的方法是_c(),即 vm._c(),这个方法本质是 createElement() 的封装(源码在src\core\instance\render.js)。除此之外,在 render() 方法中,还有可能出现_v()、_s()、_h()、_m() 等诸多辅助方法。

// src/core/instance/render-helpers/index.js export function installRenderHelpers (target: any) { target._o = markOnce // 处理 v-once target._n = toNumber // 处理修饰符.number <input v-model.number="age" type="number"> target._s = toString // ...... target._l = renderList target._t = renderSlot // 处理 slot target._q = looseEqual target._i = looseIndexOf target._m = renderStatic target._f = resolveFilter target._k = checkKeyCodes target._b = bindObjectProps target._v = createTextVNode target._e = createEmptyVNode target._u = resolveScopedSlots target._g = bindObjectListeners 

在后续组件进行挂载时,render 方法会被调用,这些辅助方法会将 render 转化为虚拟 DOM(VNode)。

虚拟 DOM(VNode)是什么样的?

可看一下 VNode 类:

export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching devtoolsMeta: ?Object; // used to store functional render context for devtools fnScopeId: ?string; // functional scope id support constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions, asyncFactory?: Function ) { this.tag = tag this.data = data this.children = children this.text = text this.elm = elm this.ns = undefined this.context = context this.fnContext = undefined this.fnOptions = undefined this.fnScopeId = undefined this.key = data && data.key this.componentOptions = componentOptions this.componentInstance = undefined this.parent = undefined this.raw = false this.isStatic = false this.isRootInsert = true this.isComment = false this.isCloned = false this.isOnce = false this.asyncFactory = asyncFactory this.asyncMeta = undefined this.isAsyncPlaceholder = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } } 

说白了,虚拟 DOM 就是一种使用 JS 数据结构模拟 DOM 元素及其关系的方法。

DOM 操作之所以慢,主要有两方面原因:

  • DOM 操作属于使用 JavaScript 调用浏览器提供的接口的过程,并不都是在 JS 引擎中直接完成,中间有不少的性能开销
  • DOM 本身非常庞大,属性和方法极多,构建、销毁、修改都有比较大的性能开销

使用 VNode 不再需要关注 DOM 元素所有的属性和方法,仅仅只需要关注元素类型、属性、子内容等即可,因此 VNode 可以解决上述两个导致 DOM 操作慢的问题。

除此之外,VNode 元素之间也会形成和 DOM 树类似的树状结构,开发者可以将 DOM 元素的对比、变更提前到 VNode 层面去完成,直到 VNode 完成变更以后,计算出发生变动的 VNode,最后再根据这些 VNode 去进行真实 DOM 元素的变更。这样就可以大大减少需要进行的 DOM 操作,从而提升性能。

虚拟 DOM(VNode) 因为是纯 JavaScript 数据结构,因此具有很好的跨平台性。

步骤三 挂载,将虚拟 DOM 渲染为真实的 DOM

Vue.prototype.

mount定义在platforms/web/runtime/index.js,mount 本质上是调用了 mountComponent()

// core/instance/lifecycle.js export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { // 省略一大段对render的判断 callHook(vm, 'beforeMount') let updateComponent updateComponent = () => { vm._update(vm._render(), hydrating) } // 定义Watcher new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true) // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm } 

vm._update() 的第一个参数 render 方法返回的虚拟 DOM,其负责将虚拟 DOM 渲染到真实的 DOM 中。

// core/instance/lifecycle.js // _update() 主要源码 const prevVnode = vm._vnode vm._vnode = vnode if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } 

这段代码首先判断了 vm._vnode 是否存在,如果不存在,则说明这个组件是初次渲染,否则说明之前渲染过,这一次渲染是需要进行更新。针对这两种情况,分别用不同的参数调用了 __patch__() 方法:

  • 如果是初次渲染,第一个参数是真实的 DOM 元素,后续使用 createElm() 根据虚拟 DOM 创建新 DOM。
  • 如果不是初次渲染,第一个参数是前一次渲染的虚拟 DOM,后续调用 patchVnode() 方法,进入虚拟 DOM 对比和更新的流程

最底层的处理逻辑,还是通过原始的 dom 操作方法 insertBefore、appendChild 等处理渲染的。

作者:越谷
链接:https://juejin.cn/post/
来源:稀土掘金

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/89743.html

(0)
上一篇 2024-10-18 16:45
下一篇 2024-10-18 19:45

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

关注微信