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;
}