Vue2 Parser
Data Flow
- HTML Template:html string
- Parser:ast nodes & expressions
- Optimizer:markStatic
- Code Generator:with(this) { createElement(...) }
- Render:VNodes
- Node Patching:VNode Diff
- DOM:finnaly...
Parser
Paser 分为三类,HTML Parser、文本解析器(Text Parser)、过滤器解析器(Filter Parser)。
Vue 中的 HTML Parser 是在某个已有的 Parser 基础上改造而成的。原有 Parser 是一个不断解析传入 HTML 的有限状态机,不停地对标签开头、属性、标签结尾及文本等类型进行正则匹配,并处理匹配得到的内容。 其内部维护了一个堆栈,可以很好地处理节点间的层级关系。
Vue 通过原 HTML Parser 提供的接口,传入钩子函数,分别对标签、文本等做额外处理。比如说,HTML 中的文本要按照文本解析器,将模板的插值和纯文本解析出来,这样才可以将变量动态代入,生成字符串。最终, HTML 通过 Parser 解析,得到了 AST。
Code Example
Optimizer
优化器(Optimizer)在 HTML Parser 生成的 AST 的基础上,对静态节点进行标记,以提高重渲染以及 node patching 时的性能:node patching 时静态节点完全不变,所以可以跳过比较; 重渲染时意味着可以在新的渲染过程中服用第一次渲染时生成好的 DOM Node。
Generator
Generator 在这里指代码生成器。Generator 从 AST 节点的根开始遍历,把所以节点转化为类似 _c(tag,data,children) 的字符串。这些字符串最后经过运行,也就得到了货真价实的 VNodes。也就是说, _c 和实例的 _h 函数是一样的。它在 Vue 实例,也就是组件,的初始化时就被诸如。除了 _c,Generator 中还添加了许多类似的函数,用于创建 VNode,比如 _s,对应 createTextNode。 这些函数在 Vue 实例的 renderMixin 时,被挂载到实例原型上。
Node Patching
VNode
虚拟节点通过一些特定的选项来表示真实的 DOM 结构。由于只涉及 JS 计算,所以在需要批量操作的情况下,操作虚拟节点要比操作 DOM 开销要小。
主要有以下几类:
- EmptyVNode:空节点
- TextVNode:文本节点
- ElementVNode:元素节点
- ComponentVNode:组件节点
- CloneVNode:克隆节点
渲染函数通过 AST Node 生成了 VNode,这些 VNode 可能被复用,此时会拷贝出一个新节点用来渲染,这个节点就是克隆节点。
function cloneVNode(vnode) {
const clonedChild = (vnode.children || []).map((vnode) => cloneVNode(vnode));
const cloned = createElement(vnode.tag, vnode.data, clonedChild);
const copyProps = [
"text",
"isComment",
"isStatic",
"key",
"elm",
"context",
"ns",
"componentOptions",
];
copyProps.map((prop) => (cloned[prop] = vnode[prop]));
return cloned;
}
关于 VNode 的属性更多解释见 vnode.js
使用 VNode 作为节点状态和 DOM 之间的中间层,可以避免性能浪费。每次渲染时,VNode 可以和上一次的 VNode 进行比较,以便重新生成 DOM 或是只改变 DOM 的一部分。
CreateElement
VueJS createElement API 解析如下:
createElement(
'name', // HTML 元素名、组件名或者函数也行
{ // 一些可选参数
class: { /* loaded: isLoaded */ }, // :class
style: { /* background: isRed ? 'red' : 'white' */ }, // :style
props: { /* name */ }, // 组件 Props
attrs: { /* id, class */ }, // HTML attributes
domProps: { /* innerHTML、innerText */ }, // DOM props
on: { /* !~click: () => {} */ }, // 通过 Vue.$emit 触发的事件;感叹号和波浪号分别代表 capture 和 once
nativeOn: { /* click: () => {} */ }, // 原生 DOM 事件
scopedSlot: { /* default: props => h('div', {}, props.text) */ }, // 作用域插槽
key: '/* keyName */',
ref: '/* refName */',
},
[] // 子节点或者文本节点
)
Node Diff
对比节点可简述为:
- 两个节点是否相同?结束更新。
- 两个节点是否是静态节点?结束更新。
- 新节点有 text 属性?
- 两个节点都有子节点?如果子节点不同则更新子节点。
- 只有新节点有子节点?
- 只有旧节点有子节点?
- 旧节点有 text 属性?
对比子节点是用一个从两边至中间的循环,分别比较新前旧前、新后旧后、新后旧前、新前旧后。
de-indent
de-indent 是 Vue 解析 SFC 是引用的一个工具包。能将代码中额外的前置缩进去掉。它做了以下三件事情:
- 将源码拆分为行
- 遍历代码行,找到最小缩进(或空格)数量
- 每行都去除最小缩进(或空格)数量
简单实现
其实我在写博客时碰到过额外的前置缩进这种问题。当时,写了一个 highlight.js 的 Vue 组件封装,用来在 Markdown 的 HTML 代码中写代码高亮,如:
<p>
<Highlight lang='js'>
export default {
hello: 'world'
}
</Highlight>
</p>
看起来没啥问题,但是这段文本传到 Vue 组件内部,就会带上一些不必要的缩进,和前后两个多余的换行。
export default {
hello: "world",
};
对于缩进,简单处理如下。将源码拆分为行,找出最小的前置空格数量,记长度为 minSpace,然后每行去除这个等长的前置空格:
const splits = codes.split(/\n/);
const tabs = splits.map((x) => x.search(/[^\s]/));
const minSpace = Math.min(...tabs);
const reMinSpace = new RegExp(`\\s{${minSpace}}`);
codes = splits.map((x) => x.replace(reMinSpace, "")).join("\n");
源码解析
思路是一模一样的,但是 de-indent 这玩意儿性能高很多倍。
var splitRE = /\r?\n/g;
var emptyRE = /^\s*$/;
var needFixRE = /^(\r?\n)*[\t\s]/;
module.exports = function deindent(str) {
// 如果第一行前没有空格,直接退出
if (!needFixRE.test(str)) {
return str;
}
// 将代码拆分成行
var lines = str.split(splitRE);
/* 这里只循环了一次就找到了最小空格数量,并且做了优化 */
var min = Infinity;
// type 用来记录缩进是 tab 还是空格
var type, cur, c;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (!emptyRE.test(line)) {
if (!type) {
c = line.charAt(0);
if (c === " " || c === "\t") {
type = c;
cur = count(line, type);
if (cur < min) {
min = cur;
}
} else {
return str;
}
} else {
cur = count(line, type);
if (cur < min) {
min = cur;
}
}
}
}
// 用 String.slice 切除前置空格,性能最好
return lines
.map(function (line) {
return line.slice(min);
})
.join("\n");
};
function count(line, type) {
var i = 0;
while (line.charAt(i) === type) {
i++;
}
return i;
}