模板编译:模板 => render函数,这个过程分两步:先将模板解析成AST(Abstract Syntax Tree,抽象语法树),然后再使⽤AST⽣成渲染函数。
由于静态节点不需要总是重新渲染,所以在⽣成AST之后、⽣成渲染函数之前这个阶段,需要做⼀个操作,那就是遍历⼀遍AST,给所有静态节点做⼀个标记,这样在虚拟DOM中更新节点时,如果发现节点有这个标记,就不会重新渲染它。
所以,在⼤体逻辑上,模板编译分三部分内容:
- 将模板解析为AST(解析器)
- 遍历AST标记静态节点(优化器)
- 使⽤AST⽣成渲染函数(代码⽣成器)
-
解析器:在解析器内部,分成了很多⼩解析器,其中包括过滤器解析器、⽂本
解析器和HTML解析器。然后通过⼀条主线将这些解析器组装在⼀起,在使⽤模板时,我们可以在其中使⽤过滤器,⽽过滤器解析器的作⽤就是⽤来解析过滤器的。
⽂本解析器就是⽤来解析⽂本的。你可能会问,⽂本就是⼀段⽂字,有什么好解析的?其实⽂本解析器的主要作⽤是⽤来解析带变量的⽂本,什么是带变量的⽂本?下⾯这段代码中的name 就是变量,⽽这样的⽂本叫作带变量的⽂本:Hello {{ name }}
,不带变量的⽂本是⼀段纯⽂本,不需要使⽤⽂本解析器来解析。最后也是最重要的是HTML解析器,它是解析器中最核⼼的模块,它的作⽤就是解析模板,每当解析到HTML标签的开始位置、结束位置、⽂本或者注释时,都会触发钩⼦函数,然后将相关信息通过参数传递出来。主线上做的事就是监听HTML解析器。每当触发钩⼦函数时,就⽣成⼀个对应的AST节点。⽣成AST前,会根据类型使⽤不同的⽅式⽣成不同的AST。例如,如果是⽂本节点,就⽣成⽂本类型的AST。这个AST其实和vnode有点类似,都是使⽤JavaScript中的对象来表⽰节
点。当HTML解析器把所有模板都解析完毕后,AST也就⽣成好了。 -
优化器:优化器的⽬标是遍历AST,检测出所有静态⼦树(永远都不会发⽣变化的DOM节点)并给其打标记。例如:
<p>我是静态节点,我不需要发⽣变化</p>
,在上⾯的代码中,p 标签就是⼀个静态节点,它没有使⽤任何变量,所以⼀旦⾸次渲染完毕后,⽆论状态怎么变,这个节点都不需要重新渲染。当AST中的静态⼦树被打上标记后,每次重新渲染时,就不需要为打上标记的静态节点创建新的虚拟节点,⽽是直接克隆已存在的虚拟节点。在虚拟DOM的更新操作中,如果发现两个节点是同⼀个节点,正常情况下会对这两个节点进⾏更新,但是如果这两个节点是静态节点,则可以直接跳过更新节点的流程。总体来说,优化器的主要作⽤是避免⼀些⽆⽤功来提升性能。因为静态节点除了⾸次渲染,后续不需要任何重新渲染操作。标记优化 对静态语法做静态标记 markup(静态节点如div下有p标签内容不会变化) diff来做优化 静态节点跳过diff操作
Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用
等待后续节点更新,如果是静态的,不会在比较children了 -
代码⽣成器:是模板编译的最后⼀步,它的作⽤是将AST转换成渲染函数中的内容,这个内容可以称为“代码字符串”。例如,⼀个简单的模板:
<p title="Berwin" @click="c">1</p>
,⽣成后的代码字符串是:with(this){return _c('p',{attrs:{"title":"Berwin"},on:{"click":c}},[_v("1")])}
,这样⼀个代码字符串最终导出到外界使⽤时,会将代码字符串放到函数⾥,这个函数叫作渲染函数。当渲染函数被导出到外界后,模板编译的任务就完成了。那么,如何将代码字符串放到函数⾥?举个例⼦:const code = `with(this){return 'Hello Berwin'}` const hello = new Function(code) hello() // "Hello Berwin"
已知template
内容
<div id="app">
<h1 key1="key1">
h1-h1
<h3 key3="key3">{{a}}</h3>
</h1>
<h2 key2="key2">{{b}}</h2>
</div>
打印内容
var ast = parse(template.trim(), options); //console.log(ast)
var code = generate(ast, options);
ast
内容如下
{type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …}
attrs: [{name: "id", value: "app", dynamic: undefined, start: 5, end: 13}]
attrsList: [{name: "id", value: "app", start: 5, end: 13}]
attrsMap: {id: "app"}
children: (4) [{…}, {…}, {…}, {…}] 展开在下面
end: 132
parent: undefined
plain: false
rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}}
start: 0
static: false
staticRoot: false
tag: "div"
type: 1
上面的children
属性展开
0: {type: 1, tag: "h1", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …}
1: {type: 1, tag: "h3", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …}
2: {type: 3, text: " ", start: 89, end: 99, static: true}
3: {type: 1, tag: "h2", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …}
generate
函数
function generate (
ast,
options
) {
var state = new CodegenState(options);
var code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")';
return {
render: ("with(this){return " + code + "}"),
staticRenderFns: state.staticRenderFns
}
}
源代码 对照 code
代码
<div id="app">
<h1 key1="key1">
h1-h1
<h3 key3="key3">{{a}}</h3>
</h1>
<h2 key2="key2">{{b}}</h2>
</div>
_c('div',
{ attrs:{"id":"app"} },
[ _c('h1',{attrs:{"key1":"key1"}},[_v("\n h1-h1\n ")]),_c('h3',{attrs:{"key3":"key3"}},[_v(_s(a))]),_v(" "),
_c('h2',{attrs:{"key2":"key2"}},[_v(_s(b))])
]
)
渲染函数的作⽤是创建vnode
。渲染函数之所以可以⽣成vnode
,是因为代码字符串中会有很多函数调⽤(例如,上⾯⽣成的代码字符串中有两个函数调⽤ _c 和 _v
),这些函数是虚拟DOM提供的创建vnode
的⽅法。vnode
有很多种类型,不同的类型对应不同的创建⽅法,所以代码字符串中的 _c 和 _v
其实都是创建vnode
的⽅法,只是创建的vnode
的类型不同。例如,_c
可以创建元素类型的vnode
,⽽ _v
可以创建⽂本类型的vnode
。
解析器
解析器的作⽤:
解析器要实现的功能是将模板解析成AST。
例如:
< div >
<p>{{ name }}</p>
</div >
// 上⾯的代码是⼀个⽐较简单的模板,它转换成AST后的样⼦如下:
{
tag: "div"
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: undefined,
attrsList: [],
attrsMap: {},
children: [
{
tag: "p"
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: { tag: "div", ...},
attrsList: [],
attrsMap: {},
children: [{
type: 2,
text: "{{name}}",
static: false,
expression: "_s(name)"
}]
}
]
}
其实AST
并不是什么很神奇的东⻄,不要被它的名字吓倒。它只是⽤JavaScript
中的对象来描述⼀个节点,⼀个对象表⽰⼀个节点,对象中的属性⽤来保存节点所需的各种数据。⽐如parent
属性保存了⽗节点的描述对象,children
属性是⼀个数组,⾥⾯保存了⼀些⼦节点的描述对象。再⽐如,type
属性表⽰⼀个节点的类型等。当很多个独⽴的节点通过parent
属性和children
属性连在⼀起时,就变成了⼀个树,⽽这样⼀个⽤对象描述的节点树其实就是AST
。
解析器内部运⾏原理:
事实上,解析器内部也分了好⼏个⼦解析器,⽐如HTML
解析器、⽂本解析器以及过滤器解析器,其中最主要的是HTML
解析器。顾名思义,HTML
解析器的作⽤是解析HTML
,它在解析HTML
的过程中会不断触发各种钩⼦函数。这些钩⼦函数包括开始标签钩⼦函数、结束标签钩⼦函数、⽂本钩⼦函数以及注释钩⼦函数。
伪代码如下:
parseHTML(template, {
start(tag, attrs, unary) {
// 每当解析到标签的开始位置时,触发该函数
},
end() {
// 每当解析到标签的结束位置时,触发该函数
},
chars(text) {
// 每当解析到⽂本时,触发该函数
},
comment(text) {
// 每当解析到注释时,触发该函数
}
})
你可能不能很清晰地理解,下⾯我们举个简单的例⼦:
<div><p>我是Berwin</p></div>
当上⾯这个模板被HTML
解析器解析时,所触发的钩⼦函数依次是:start 、start 、chars 、end 和end 。
也就是说,解析器其实是从前向后解析的。解析到 <div>
时,会触发⼀个标签开始的钩⼦函数start
;然后解析到 <p>
时,⼜触发⼀次钩⼦函数start
;接着解析到我是Berwin
这⾏⽂本,此时触发了⽂本钩⼦函数chars
;然后解析到 </p>
,触发了标签结束的钩⼦函数end
;接着继续解析到 </div>
,此时⼜触发⼀次标签结束的钩⼦函数end
,解析结束。因此,我们可以在钩⼦函数中构建AST
节点。在start
钩⼦函数中构建元素类型的节点,在chars
钩⼦函数中构建⽂本类型的节点,在comment
钩⼦函数中构建注释类型的节点。
当HTML
解析器不再触发钩⼦函数时,就说明所有模板都解析完毕,所有类型的节点都在钩⼦函数中构建完成,即AST
构建完成。我们发现,钩⼦函数start
有三个参数,分别是tag 、attrs 和unary
,它们分别说明标签名、标签的属性以及是否是⾃闭合标签。⽽⽂本节点的钩⼦函数chars
和注释节点的钩⼦函数comment
都只有⼀个参数,只有text
。这是因为构建元素节点时需要知道标签名、属性和⾃闭合标识,⽽构建注释节点和⽂本节点时只需要知道⽂本即可。
什么是⾃闭合标签?举个简单的例⼦,input
标签就属于⾃闭合标签:<input type="text" />
,⽽div 标签就不属于⾃闭合标签:<div></div>
。
在start 钩⼦函数中,我们可以使⽤这三个参数来构建⼀个元素类型的AST节点,例如:
function createASTElement(tag, attrs, parent) {
return {
type: 1,
tag,
attrsList: attrs,
parent,
children: []
}
}
parseHTML(template, {
start(tag, attrs, unary) {
let element = createASTElement(tag, attrs, currentParent)
}
})
在上⾯的代码中,我们在钩⼦函数start 中构建了⼀个元素类型的AST节点。
如果是触发了⽂本的钩⼦函数,就使⽤参数中的⽂本构建⼀个⽂本类型的AST节点,例如:
parseHTML(template, {
chars(text) {
let element = { type: 3, text }
}
})
如果是注释,就构建⼀个注释类型的AST节点,例如:
parseHTML(template, {
comment(text) {
let element = { type: 3, text, isComment: true }
}
})
源码溯源
export function compileToFunctions(template) {
// 我们需要把html字符串变成render函数
// 1.把html代码转成ast语法树 ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法
// 很多库都运用到了ast 比如 webpack babel eslint等等
let ast = parse(template);
// 2.优化静态节点:对ast树进行标记,标记静态节点
if (options.optimize !== false) {
optimize(ast, options);
}
// 3.通过ast 重新生成代码
// 我们最后生成的代码需要和render函数一样
// 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
// _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本
let code = generate(ast);
// 使用with语法改变作用域为this 之后调用render函数可以使用call改变this 方便code里面的变量取值
let renderFn = new Function(`with(this){return ${code}}`);
return renderFn;
}
模板编译
前置知识
- 模板是
vue
开发中最常用的,即与使用相关联的原理 - 它不是
HTML
,有指令、插值、JS表达式,能实现循环、判断,因此模板一定转为JS
代码,即模板编译 - 面试不会直接问,但会通过
组件渲染和更新过程
考察
模板编译
vue template compiler
将模板编译为render
函数- 执行
render
函数,生成vnode
- 基于
vnode
在执行patch
和diff
- 使用
webpack vue-loader
插件,会在开发环境下编译模板
with语法
- 改变
{}
内自由变量的查找规则,当做obj
属性来查找 - 如果找不到匹配的
obj
属性,就会报错 with
要慎用,它打破了作用域规则,易读性变差
vue组件中使用render代替template
// 执行 node index.js
const compiler = require('vue-template-compiler')
// 插值
const template = `<p>{message}</p>`
with(this){return _c('p', [_v(_s(message))])}
// this就是vm的实例, message等变量会从vm上读取,触发getter
// _c => createElement 也就是h函数 => 返回vnode
// _v => createTextVNode
// _s => toString
// 也就是这样 with(this){return createElement('p',[createTextVNode(toString(message))])}
// h -> vnode
// createElement -> vnode
// 表达式
const template = `<p>{{flag ? message : 'no message found'}}</p>`
// with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}
// 属性和动态属性
const template = `
<div id="div1" class="container">
<img :src="imgUrl"/>
</div>
`
with(this){return _c('div',
{staticClass:"container",attrs:{"id":"div1"}},
[
_c('img',{attrs:{"src":imgUrl}})])}
// 条件
const template = `
<div>
<p v-if="flag === 'a'">A</p>
<p v-else>B</p>
</div>
`
with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}
// 循环
const template = `
<ul>
<li v-for="item in list" :key="item.id">{{item.title}}</li>
</ul>
`
with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}
// 事件
const template = `
<button @click="clickHandler">submit</button>
`
with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}
// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}
// render 函数
// 返回 vnode
// patch
// 编译
const res = compiler.compile(template)
console.log(res.render)
// ---------------分割线--------------
// 从 vue 源码中找到缩写函数的含义
function installRenderHelpers (target) {
target._o = markOnce;
target._n = toNumber;
target._s = toString;
target._l = renderList;
target._t = renderSlot;
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;
target._d = bindDynamicKeys;
target._p = prependModifier;
}