Vue 的 template 是如何编译成真正的 HTML 并作到双向绑定等等特殊功能的呢?以往这个问题对我来讲一直是个黑洞。最近看了 Vue 的源码,对模板编译的整个过程的脉络有了更为清晰的了解。javascript
在这张图中,咱们能够看到 Vue 的模板编译是在 $mount 的过程当中进行的,在 $mount 的时候执行了 compile 这个方法来将 template 里的内容转换成真正的 HTML 代码。complie 以后执行的事情也蛮重要的,这个咱们留到最后再说。complie 最终生成 render 函数,等待调用。这个方法分为三步:vue
在了解 parse 的过程以前,咱们须要了解 AST,AST 的全称是 Abstract Syntax Tree,也就是所谓抽象语法树,用来表示代码的数据结构。在 Vue 中我把它理解为嵌套的、携带标签名、属性和父子关系的 JS 对象,以树来表现 DOM 结构。
下面是 Vue 里的 AST 的定义:java
咱们能够看到 AST 有三种类型,而且经过 children 这个字段层层嵌套造成了树状的结构。而每个 AST 节点存放的就是咱们的 HTML 元素、插值表达式或文本内容。AST 正是 parse 函数生成和返回的。
parse 函数里定义了许多的正则表达式,经过对标签名开头、标签名结尾、属性字段、文本内容等等的递归匹配。把字符串类型的 template 转化成了树状结构的 AST。node
// parse 里定义的一些正则 export const onRE = /^@|^v-on:/ //匹配 v-on export const dirRE = /^v-|^@|^:/ //匹配 v-on 和 v-bind export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/ //匹配 v-for 属性 export const forIteratorRE = /\((\{[^}]*\}|[^,]*),([^,]*)(?:,([^,]*))?\)/ //匹配 v-for 的多种形式
咱们能够把这个过程理解为一个截取的过程,它把 template 字符串里的元素、属性和文本一个个地截取出来,其中的细节十分琐碎,涉及到各类不一样状况(好比不一样类型的 v-for,各类 vue 指令、空白节点以及父子关系等等),咱们再也不赘述。正则表达式
假设咱们有一个元素<div id="test">texttext</div>
,在 parse 完以后会变成以下的结构并返回:数据结构
ele1 = { type: 1, tag: "div", attrsList: [{name: "id", value: "test"}], attrsMap: {id: "test"}, parent: undefined, children: [{ type: 3, text: 'texttext' } ], plain: true, attrs: [{name: "id", value: "'test'"}] }
在第二步中,会对 parse 生成的 AST 进行静态内容的优化。静态内容指的是和数据没有关系,不须要每次都刷新的内容。标记静态节点的做用是为了在后面作 Vnode 的 diff 时起做用,用来确认一个节点是否应该作 patch 仍是直接跳过。optimize 的过程分为两步:函数
关于这一段咱们能够直接看源码:优化
function markStatic (node: ASTNode) { // 标记 static 属性 node.static = isStatic(node) if (node.type === 1) { // 注意这个判断逻辑 if ( !isPlatformReservedTag(node.tag) && node.tag !== 'slot' && node.attrsMap['inline-template'] == null ) { return } for (let i = 0, l = node.children.length; i < l; i++) { const child = node.children[i] markStatic(child) if (!child.static) { node.static = false } } } }
上面的代码中有几个须要注意的地方:this
isStatic 函数顾名思义是判断该节点是否 static 的函数,符合以下内容的节点就会被认为是 static 的节点:spa
1. 若是是表达式AST节点,直接返回 false 2. 若是是文本AST节点,直接返回 true 3. 若是元素是元素节点,阶段有 v-pre 指令 || 1. 没有任何指令、数据绑定、事件绑定等 && 2. 没有 v-if 和 v-for && 3. 不是 slot 和 component && 4. 是 HTML 保留标签 && 5. 不是 template 标签的直接子元素而且没有包含在 for 循环中 则返回 true
若是知足上面的全部条件,那么这个节点的 static 就会被置为 false 而且不递归子元素,当不知足上面某一个条件时,递归子元素判断子元素是否 static,只有全部元素都是 static 的时候,该元素才是 static。
这部分理解起来很简单,只有当一个节点是 static 而且其不能只拥有一个静态文本节点时才能被称为 static root。由于做者认为这种状况去作优化,其消耗会超过得到的收益。
if (node.static && node.children.length && !( node.children.length === 1 && node.children[0].type === 3 )) { node.staticRoot = true return } else { node.staticRoot = false }
生成 render 的 generate 函数的输入也是 AST,它递归了 AST 树,为不一样的 AST 节点建立了不一样的内部调用方法,等待后面的调用。生成 render 函数的过程以下:
几种内部方法 _c:对应的是 createElement 方法,顾名思义,它的含义是建立一个元素(Vnode) _v:建立一个文本结点。 _s:把一个值转换为字符串。(eg: {{data}}) _m:渲染静态内容
假设咱们有这么一段 template
<template> <div id="test"> {{val}} <img src="http://xx.jpg"> </div> </template>
最终会被转换成这样子的函数字符串
{render: "with(this){return _c('div',{attrs:{"id":"test"}},[[_v(_s(val))]),_v(" "),_m(0)])}"}
整个 Vue 渲染过程,前面咱们说了 complie 的过程,在作完 parse、optimize 和 generate 以后,咱们获得了一个 render 函数字符串。那么接下来 Vue 作的事情就是 new watcher,这个时候会对绑定的数据执行监听,render 函数就是数据监听的回调所调用的,其结果即是从新生成 vnode。当这个 render 函数字符串在第一次 mount、或者绑定的数据更新的时候,都会被调用,生成 Vnode。若是是数据的更新,那么 Vnode 会与数据改变以前的 Vnode 作 diff,对内容作改动以后,就会更新到咱们真正的 DOM 上啦~