Vue2.x源码分析 - 模版编译以及挂载
  # # Vue2.x源码分析 - 模版编译以及挂载
由于2.x版本中引入虚拟DOM的缘故,Vue支持模板和手写render两种方式(也支持jsx,属于第三方插件帮你转换为render函数;当然模板方式最终也是转换为render函数,但属于官方内置编译器),官方推荐模板这种写法,降低使用门槛。
Vue是如何解析模板并挂载到真实的DOM上呢?
# # API层
先看下Vue API层是如何使用的:
    // 使用Vue.prototype.$mount(el)或者放在options.el里
    new Vue({template, data}).$mount(el)
    // or
    new Vue({ el, template, data})
    
 1
2
3
4
5
2
3
4
5
# # 源码层
先了解下源码入口区别:
- platforms/web/entry-runtime.js
- Vue运行时,简单说就是对外只导出Vue这个类,因为所有Vue的配置以及设置,都是挂载到Vue的原型上。
 
 - platforms/web/entry-compiler.js
- Vue内置编译器,主要是暴露一些函数出来。主要有compile函数,把template字符串转换为render函数。
 
 - platforms/entry-runtime-with-compiler.js
- 带编译器的运行时,最常使用的Vue依赖包。可通过new Vue({}).$mount(el)直接挂载DOM。
 
 
我们源码分析的是带编译器的运行时,以下看下笔者简化的源码流程,注意高亮的关键代码:
# # 1. 解析template模板,得到render函数并绑定到Vue选项上
const { render } = compileToFunctions(options.template, ...)- const { compileToFunctions } = 
createCompiler(baseOptions) 'src/platforms/web/compiler/index.js'const createCompiler = createCompilerCreator(
baseCompile(template, options)) 'src/compiler/index.js' 1.ast = parse(template.trim(), options)解析模板字符串生成 AST 'src/compiler/parser/index.js'* parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。 * type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本
 
- const { compileToFunctions } = 
 
              // AST数据结构
              // 最终生成的是一颗AST树结构
              declare type ASTElement = {
                  type: 1;
                  tag: string;
                  attrsList: Array<{ name: string; value: any }>;
                  attrsMap: { [key: string]: any };
                  parent: ASTElement | void;
                  children: Array<ASTNode>; // ASTNode = ASTElement | ASTText | ASTExpression;
              
                  processed?: true;
              
                  static?: boolean;
                  staticRoot?: boolean;
                  staticInFor?: boolean;
                  staticProcessed?: boolean;
                  hasBindings?: boolean;
              
                  text?: string;
                  attrs?: Array<{ name: string; value: any }>;
                  props?: Array<{ name: string; value: string }>;
                  plain?: boolean;
                  pre?: true;
                  ns?: string;
              
                  component?: string;
                  inlineTemplate?: true;
                  transitionMode?: string | null;
                  slotName?: ?string;
                  slotTarget?: ?string;
                  slotScope?: ?string;
                  scopedSlots?: { [name: string]: ASTElement };
              
                  ref?: string;
                  refInFor?: boolean;
              
                  if?: string;
                  ifProcessed?: boolean;
                  elseif?: string;
                  else?: true;
                  ifConditions?: ASTIfConditions;
              
                  for?: string;
                  forProcessed?: boolean;
                  key?: string;
                  alias?: string;
                  iterator1?: string;
                  iterator2?: string;
              
                  staticClass?: string;
                  classBinding?: string;
                  staticStyle?: string;
                  styleBinding?: string;
                  events?: ASTElementHandlers;
                  nativeEvents?: ASTElementHandlers;
              
                  transition?: string | true;
                  transitionOnAppear?: boolean;
              
                  model?: {
                      value: string;
                      callback: string;
                      expression: string;
                  };
              
                  directives?: Array<ASTDirective>;
              
                  forbidden?: true;
                  once?: true;
                  onceProcessed?: boolean;
                  wrapData?: (code: string) => string;
                  wrapListeners?: (code: string) => string;
              
                  // 2.4 ssr optimization
                  ssrOptimizability?: number;
              
                  // weex specific
                  appendAsTree?: boolean;
                  };
              
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
            2.  `optimize(ast, options)` 优化语法树 'src/compiler/optimizer.js'
                  * 很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,我们可以在 patch 的过程跳过对他们的比对。
                  * 只干两件事:1. 标记静态节点 2. 标记静态根(静态根一定是静态节点)
            3.  `code = generate(ast, options)` 生成代码\(code对象包含render函数\) 'src/compiler/codegen/index.js'
                  * 把优化后的 AST 树转换成可执行的代码。可执行代码最终获得VNode(虚拟DOM的基础)
              return (isShow) ?
                  _c('ul', {
                      staticClass: "list",
                      class: bindCls
                  },
                  _l((data), function(item, index) {
                      return _c('li', {
                      on: {
                          "click": function($event) {
                          clickItem(index)
                          }
                      }
                      },
                      [_v(_s(item) + ":" + _s(index))])
                  })
                  ) : _e()
              
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# # 2. 挂载渲染. mount.call(this, el)
 Vue.prototype.$mount'platforms/web/runtime/index.js'mountComponent'core/instance/lifecycle.js'new Watch(vm,
updateComponent) 1.vm._render()'src/core/instance/render.js'* 执行render函数,拿到VNode\(注意render 函数的参数h为vm.\$createElement\)
              const { render } = vm.$options
              return vnode = render.call(vm._renderProxy, vm.$createElement)
              
 1
2
3
2
3
            2.  `vm._update(VNode)` 'src/core/instance/lifecycle.js'
                  * 更新DOM, `vm.__patch__(preVNode, VNode)` 'src/platforms/web/runtime/patch.js'
                        * `createPatchFunction` --> `patch(oldVNode, vnode)` 'src/core/vdom/patch.js',详见[Virtual DOM实现](/blogs/vue/vue-code.3.vdom.html)
                              * diff等,真正的操作更新DOM
                              * 绑定class、style、event等
              const prevVnode = vm._vnode
              if (!prevVnode) {
                  // initial render
                  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
              } else {
                  // updates
                  vm.$el = vm.__patch__(prevVnode, vnode)
              }
              
 1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# # 完整流程
以下是vue挂载的整个流程,其实就是vnode生成真实dom的规则。其中还包括vue自定义组件的构建流程,详细说明可看Vue2.x源码分析 - 组件系统章节。

编辑  (opens new window)