上篇文章 模版编译之生成AST 中将模版转为了 AST
,这篇文章会将 AST
转为最终的 render
函数。
静态节点
模版编译之生成AST 中我们转换的例子是 "<div><span>3<5吗</span><span>?</span></div>"
,可以看到所有的节点不涉及变量和任何 vue
的指令,因此当 虚拟 dom 重新 patch 的时候是不需要进行 diff
的,所以我们可以在生成 render
函数的时候将这些静态节点单独标记出来。
生成 render
函数前,我们对所有的 AST
进行遍历标记,options
提供一些平台相关的变量,isReservedTag
函数来判断是否是合法的标签,比如 div
、span
等,虚拟dom之组件 有写过。
const options = {
isReservedTag,
};
optimize(ast, options);
export function optimize(root, options) {
if (!root) return;
isPlatformReservedTag = options.isReservedTag;
// first pass: mark all non-static nodes.
markStatic(root);
// second pass: mark static roots.
markStaticRoots(root);
}
optimize
函数分为两步,第一步就是找出所有的静态节点 markStatic
,第二步是在第一步的基础上找到静态根节点 markStaticRoots
。
看一下 markStatic
的实现:
function markStatic(node) {
node.static = isStatic(node);
if (node.type === 1) {
if (!isPlatformReservedTag(node.tag)) {
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;
}
}
}
}
首先调用了 isStatic
函数标记当前 node
。
function isStatic(node) {
if (node.type === 2) {
// expression
return false;
}
if (node.type === 3) {
// text
return true;
}
return isPlatformReservedTag(node.tag);
}
如果是一个正常的节点默认就会标记为 isPlatformReservedTag(node.tag)
返回的 true
。
接下来会调用 for
循环判断子节点的情况,如果子节点存在非 static
的节点,当前节点会修正为 false
。
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i];
markStatic(child);
if (!child.static) {
node.static = false;
}
}
接下来是第二步,标记静态根节点 markStaticRoots
。
export function optimize(root, options) {
if (!root) return;
isPlatformReservedTag = options.isReservedTag;
// first pass: mark all non-static nodes.
markStatic(root);
// second pass: mark static roots.
markStaticRoots(root);
}
function markStaticRoots(node) {
if (node.type === 1) {
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
if (
node.static &&
node.children.length &&
!(node.children.length === 1 && node.children[0].type === 3)
) {
node.staticRoot = true;
return;
} else {
node.staticRoot = false;
}
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i]);
}
}
}
}
因为处理静态节点也是有代价的,渲染的时候需要维护一个静态节点树。如果某个 dom
节点仅包含一个文本节点,此时进行 diff
其实代价很低,这种情况我们就将 staticRoot
置为 false
,不把它看成静态节点。
通过调用 optimize
函数,我们就可以将下边 Ast
:
标记出静态根节点:
后边生成 render
函数时候我们只会用到 staticRoot
,static
就用不到了。
render 代码生成
const ast = parse(template);
const options = {
isReservedTag,
};
optimize(ast, options);
const code = generate(ast);
optimize
标记完静态根节点后就调用 generate
函数来生成 render
函数。
export class CodegenState {
staticRenderFns;
constructor() {
this.staticRenderFns = [];
}
}
export function generate(ast) {
const state = new CodegenState();
const code = genElement(ast, state);
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns,
};
}
定义了一个 CodegenState
,类中保存一些数据,在生成 code
的过程中进行传递。
接下来看一下 genElement
的实现:
export function genElement(el, state) {
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state);
} else {
// component or element
let code;
let data; // 先不考虑
const children = genChildren(el, state);
code = `_c('${el.tag}'${
data ? `,${data}` : "" // data
}${
children ? `,${children}` : "" // children
})`;
return code;
}
}
首先判断是否是静态根节点并且是否在生成静态根节点的过程中,满足情况的话调用 genStatic
函数。
function genStatic(el, state) {
el.staticProcessed = true;
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`);
return `_m(${state.staticRenderFns.length - 1})`;
}
我们将生成的静态节点的 code
push
到 staticRenderFns
中,最终通过 _m
函数进行包裹,_m
函数后边会讲。
此时会走回 genElement
函数中,因为已经将 staticProcessed
标记为了 true
,因此就会进入 else
分支中。
export function genElement(el, state) {
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state);
} else {
// component or element
let code;
let data; // 先不考虑
const children = genChildren(el, state);
code = `_c('${el.tag}'${
data ? `,${data}` : "" // data
}${
children ? `,${children}` : "" // children
})`;
return code;
}
}
调用 genChilden
生成子节点的 code
,最后将 tag
、data
、childern
传给 _c
函数,_c
函数后边讲。
来看一下 genChildren
。
export function genChildren(el, state) {
const children = el.children;
if (children.length) {
const gen = genNode;
return `[${children.map((c) => gen(c, state)).join(",")}]`;
}
}
function genNode(node, state) {
if (node.type === 1) {
return genElement(node, state);
} else {
return genText(node);
}
}
export function genText(text) {
// JSON.stringify 是为了给 text 加双引号,作为参数传给 _v
return `_v(${JSON.stringify(text.text)})`;
}
for
循环调用 genNode
,如果是 type === 1
继续调用 genElement
,否则的话调用 genText
生成 text
节点,这里的 _v
函数也后边讲。
通过 generate
函数:
export function generate(ast) {
const state = new CodegenState();
const code = genElement(ast, state);
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns,
};
}
对于 "<div><span>3<5吗</span><span>?</span></div>"
模版 generate
返回的对象如下:
_m(0)
代表取 staticRenderFns
的第一个值,"with(this){return _c('div',[_c('span',[_v(\"3<5吗\")]),_c('span',[_v(\"?\")])])}"
其实就是 render
函数的字符串形式了。
调用 generate
函数后,我们只需要通过 Function
函数生成最终的 render
函数即可。
const code = generate(ast);
const render = new Function(code.render);
当然因为 render
函数中我们还使用了 _m、_c、_v
函数,下边看一下这些函数的实现。
_c _v _m
_c
_c
其实就是生成一个正常的 vnode
,和我们之前在 render
中接收到的 createElement
其实是同一个函数。
new Vue({
el: "#root",
data() {
return {
text: "world",
title: "hello",
};
},
components: { Hello },
methods: {
click() {
this.title = "hello2";
// this.text = "hello2";
},
},
render(createElement) {
const test = createElement(
"div",
{
on: {
// click: this.click,
},
},
[
createElement("Hello", { props: { title: this.title } }),
this.text,
]
);
return test;
},
});
他们调用的都是 createElement
函数。
_v
这些字母函数都定义在 src/core/instance/render-helpers/index.js
中:
并且挂在了 Vue
的原型对象上,这样在 render
函数中就可以访问到了。
我们来先看一下 _v
做了什么。
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
很简单,生成了一个 text
的 VNode
。
_m
_m
对应 renderStatic
, 接收一个下标参数,也就是 staticRenderFns
对应的位置。
export function renderStatic (
index
){
const cached = this._staticTrees || (this._staticTrees = [])
let tree = cached[index]
// if has already-rendered static tree and not inside v-for,
// we can reuse the same tree.
if (tree) {
return tree
}
// otherwise, render a fresh tree.
tree = cached[index] = this.$options.staticRenderFns[index].call(
this._renderProxy,
null,
this // for render fns generated for functional component templates
)
markStatic(tree, `__static__${index}`, false)
return tree
}
加了一个 cashed
,如果缓存没有命中,就调用相应的 staticRenderFns
函数来生成 VNode
,当然 staticRenderFns
也会提前调用 new Function
将字符串实例化为函数。
上边的 markStatic(tree, __static__${index}, false)
函数是将 VNode
加上 isStatic
标记,这样以后在 diff
的过程中可以直接跳过。
function markStaticNode (node, key, isOnce) {
node.isStatic = true
node.key = key
node.isOnce = isOnce
}
总
今天是模版编译的最后一步了,第一步是 模版编译之分词,第二步是 模版编译之生成AST,今天是最后一步,遍历 AST
包装一些字母函数 _c
、_m
等生成 render
函数的字符串,最后通过 new Function
来生成 render
函数。
因为目前为止我们的模版还没有涉及到变量以及一些 v-
指令,所以上边的模版还属于静态模版,引入了 staticRenderFns
来生成。未来几篇文章会介绍包含变量的文本、常用的 v-if
、v-for
指令等,一步步完善我们的模版编译。
文章对应源码详见 vue.windliang.wang/。