Vue Compile原理分析
Vue中Compile是一个非常复杂的内容,Compile的主要作用是解析模板,生成渲染模板的render, 而render的作用主要是为了生成VNode, Compile主要分为3大块:parse 接受template原始模板,按着模板的节点和数据生成对应的astoptimize 遍历ast的每一个节点,标记静态节点,这样就知道哪部分不会变化,于是在页面需要更新时,减少去对比这部分DOM,提升性
Vue中Compile是一个非常复杂的内容,Compile的主要作用是解析模板,生成渲染模板的render, 而render的作用主要是为了生成VNode, Compile主要分为3大块:
- parse 接受template原始模板,按着模板的节点和数据生成对应的ast
- optimize 遍历ast的每一个节点,标记静态节点,这样就知道哪部分不会变化,于是在页面需要更新时,减少去对比这部分DOM,提升性能
- generate 把前两步生成完善的ast,组成render字符串,然后将render字符串通过
new Function
的方式转换成渲染函数
1. Compile从新建实例到Compile结束的主要流程
Vue编译template, 生成render发生在$mount
这个函数中
Vue.prototype.$mount = function(el) {
var options = this.$options;
if (!options.render) {
// 获取template模板
var tpl = options.template;
if (tpl) {
tpl = document.querySelector(tpl).innerHTML;
}
if (tpl) {
var ref = compileToFunctions(tpl, {}, this);
options.render = ref.render;
options.staticRenderFns = ref.staticRenderFns;
}
}
// 执行上面生成的render, 生成DOM,挂载DOM
return mount.call(this, el);
}
compileToFunctions
的生成流程
// compileToFunctions是通过createCompiler执行返回的
var ref$1 = createCompiler();
var { compileToFunctions } = ref$1;
createCompiler
的生成流程
// createCompiler 是通过 createCompilerCreator 生成的
// createCompilerCreator 会传入一个 baseCompile 的函数
var createCompiler = createCompilerCreator(
function baseCompile(template, options) {
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
optimize(ast, options);
}
var code = generate(ast, options);
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
);
function createCompilerCreator (baseCompile) {
return function () {
// 作用是合并选项,并且调用 baseCompile
function compile(template) {
var compiled = baseCompile(template);
return compiled;
}
return {
compile,
// compileToFunctions 用来生成 render 和 staticRenderFns
// compileToFunctions 其内核就是 baseCompile
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
function createCompileToFunctionFn (compile) {
// 作为缓存,防止每次都重新编译
// template 字符串 作为 key
// 值为 render 和 staticRenderFns
var cache = Object.create(null);
return function compileToFunctions(template, options, vm) {
var key = template;
// 有缓存的时候直接取出缓存中的结果即可
if (cache[key]) return cache[key];
// compile 是 createCompilerCreator 传入的 compile
var compiled = compile(template, options);
var res = {
// 将字符串render解析成函数
render: new Function(compiled.render),
staticRenderFns: compiled.staticRenderFns.map(function(code) {
return new Function(code, fnGenErrors);
})
};
cache[key] = res;
return cache[key];
}
}
2. parse主要流程
parse的主要作用就是将template字符串转化为ast,ast为抽象语法树,是一种以树形结果来表示模板语法结构,比如:
{
tag: 'div',
type: 1, // 1->节点; 2->表达式,比如{{isShow}}; 3->纯文本
children: [{
type: 3,
text: '11'
}]
}
parse
整个流程非常复杂,需要一步步深入
// pase接收template字符串
function parse(template) {
// 缓存模板中解析的每个节点的ast
// 是一个数组存放模板中按顺序从头到尾每个标签的AST
// 主要用来理清节点父子关系
var stack = [];
// 根节点,是ast
var root;
// 当前解析标签的父节点
// 这样才知道当前解析节点的父节点是谁,才把这个节点添加给响应节点的children
// 根节点没有父节点,所以是undefined
var currentParent;
// parseHTML 负责 template 中的标签匹配,再传入 start, end, chars 等方法
parseHTML(template, {
start, // 处理头标签
end, // 处理尾标签
chars // 处理文本
});
return root;
}
function parseHTML(html, options) {
while(html) {
// 寻找 < 的起始位置
var textEnd = html.indexOf('<'),
text,
rest,
next;
// 如果模板起始位置是标签开头 <
// 如果匹配到 <, 那这个有可能是头标签上的,也有可能是尾标签上的
if (textEnd === 0) {
// 如果是尾标签得到 <
// 比如html = '</div>', 匹配出 endTagMatch=['</div>', 'div']
var endTagMatch = html.match(endTag);
if (endTagMatch) {
// endTagMatch[0]='</a>'
html = html.substring(endTagMatch[0].length);
// 处理尾标签的方法
options.end();
continue;
}
// 如果是起始标签的 <
// parseStartTag作用是,匹配标签存在的属性,截断template
// 比如: html='<div></div>'
// parseStartTag处理之后, startTagMatch={tagName: 'div', attrs: []}
var startTagMatch = parseStartTag();
// 匹配到起始标签之后
if (startTagMatch) {
// 处理起始标签
options.start(...);
continue;
}
}
// 模板起始位置不是 <, 而是文字
if (textEnd >= 0) {
text = html.substring(0, textEnd);
html = html.substring(n);
}
// 处理文字
if (options.chars && text) {
options.chars(text);
}
}
}
处理头标签, 当parseHTML匹配到一个首标签,都会把该标签的信息传递给start
function start(tag, attrs, unary) {
// 创建AST节点
var element = createASTElement(tag, attrs, currentParent);
// 设置根节点,一个模板仅有一个根节点
if (!root) {
root = element;
}
// 处理父子关系
if (currentParent) {
currentParent.children.push(element);
element.parent = currentParent;
}
// 不是单标签(input, img), 都需要保存到stack
if (!unary) {
currentParent = element;
stack.push(element);
}
}
function createASTElement(tag, attrs, parent) {
// 创建一个AST结构,保存数据
/*
模板上的属性经过parseHTML解析成一个数组
[{ name: 'hoho', value: '333'},{ name: 'href', value: '444' }]
makeAttrMap可以将其转化成如下结构
{ hoho: '333', href: '444' }
*/
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
parent,
children: []
}
}
处理尾标签匹配到尾标签,比如</div>
的时候,就会调用传入的end
方法
function end() {
// 标签解析结束,移除该标签
/*
比如有如下一段html:
<div>
<section></section>
<p></p>
</div>
stack匹配到两个头标签之后 stack=['div', 'section']
然后匹配到</section>, 则移除stack中的section, 并且重设 currentParent
stack=['div']
currentParent='div'
再匹配到</p>, p的父节点就是div了,父子顺序就正确了
*/
stack.length -= 1;
currentParent = stack[stack.length - 1];
}
处理文本字符串,当parseHTML
去匹配<
的时候,发现template开头到<
还有一段距离,那么这段距离就是文本了,这段文本会给chars
方法处理
// chars的主要作用就是为父节点添加文本子节点
// 文本子节点有两种类型
// 1. 普通型,直接存为文本子节点
// 2. 表达式型,需要经过parseText处理
/*
比如表达式{{isShow}}会被解析成
{
expression: toString(isShow),
tokens: [{@binding: "isShow"}]
}
其主要是为了把表达式isShow拿到,方便从实例上获取
*/
function chars(text) {
// 必须存在根节点,不能用文字开头
if (!currentParent) return;
var children = currentParent.children;
// 通过parseText解析成字符串,判断是否含有双括号表达式,比如{{isShow}}
// 如果含有表达式,会存放多一点信息
var res = parseText(text);
if (res) {
children.push({
type: 2,
expression: res.expression,
tokens: res.tokens,
text,
});
}
// 普通字符串,直接存在字符串节点
else if (!children.length || children[children.length - 1].text !== ' ) {
children.push({
type: 3,
text,
});
}
}
下面来看一个完整的Parse流程
<div>11</div>
- 开始循环template,匹配到第一个头标签
div
, 传入start, 生成对应的AST,该div的ast变成根节点root,并设置其为当前父节点currentParent, 保存节点存储数组stack
stack = [{ tag: 'div', children: [] }]
第一轮处理结束,template截断到第一次匹配到的位置11</div>
- 开始第二次遍历,开始匹配到
<
, 发现<
不在开头,从开头位置到<
有一段普通字符串,调用chars,传入字符串,发现其没有双括号等表达式,直接给父节点添加简单子节点
currentParent.children.push({ type: 3, text: '11' });
此时
stack = [{ tag: 'div', children: [{ type: 3, text: '11' }] }]
第二轮结束,template截断到刚刚匹配完的字符串,此时template=</div>
- 开始第三轮遍历,继续寻找
<
, 发现就在开头,但是这是一个结束标签,标签名是div
。这个标签匹配完毕,也会从stack中移除,第3次遍历结束,template继续阶段,此时template为空,遍历结束。
{
tag: 'div',
type: 1,
children: [{ type: 3, text: '11' }]
}
3. 标签解析
上一小节讲到了是通过parseHTML
这个方法来对template
进行循环遍历的。就是不断的将模板字符串匹配然后截断,直到字符串为空,其中和阶段有关的一个重要函数就是advance
// 截断模板字符串,并保存当前的位置
function advance(n) {
index += n;
html = html.substring(n);
}
-
如果 < 在template开头
如果是尾标签的<
, 那么交给parseEndTag
处理
如果是头标签的>
, 那么交给parseStartTag
处理 -
如果 < 不在template开头,那么表明开头到 < 的这段位置是字符串,也需要用到
advance
去截断字符串。
template = '<div>111</div>';
parseHTML(template);
parseStartTag
作用是处理头标签
- 把头标签的所有信息及合起来,包括属性,标签名
- 匹配完成之后,同样调用advance去截断template
- 把标签信息返回
function parseStartTag() {
// html ='<div name=1>111</div>'
// start = ["<div", "div", index: 0]
var start = html.match(startTagOpen);
if (start) {
// 存储本次头标签的信息
var match = {
tagName: start[1],
attrs: [],
start: index
}
}
// start[0]是<div
// 阶段之后template="name=1 >111</div>"
advance(start[0].length);
var end, attr;
// 循环匹配属性内容,保存属性列表
// 直到template遍历到头标签的 >
while (
// 匹配不到头标签的 > , 开始匹配 属性内容
!(end = html.match(startTagClose))
// 开始匹配属性内容
// attr = ["name=1", "name", "="]
&& (attr = html.match(attribute))
) {
advance(attr[0].length);
match.attrs.push(attr);
}
// 匹配到起始标签的 > , 标签属性那些已经匹配完毕了
// 返回收集到的标签信息
if (end) {
advance(end[0].length);
// 如果是单标签,那么 unarySlash 的值是 / , 比如 <input />
match.unarySlash = end[1];
match.end = index;
return match;
}
}
经过以上的parseStartTag会处理返回如下内容
// <div name=1></div>
{
tagName: 'div',
// 头标签中的属性信息
attrs: [
[" name=1", "name", "=", undefined, undefined, "1"]
],
unarySlash: "", // 表示这个表示是否是单标签
start: 0, // 头标签的 < 在template中的位置
end: 12 // 头标签的 > 在template中的位置
}
通过parseStartTag返回的头信息,最后传给了handleStartTag
function handleStartTag(match) {
var tagName = match.tagName;
var unarySlash = match.unarySlash;
// 判断是不是单标签,input, img这些
var unary = isUnaryTag$$1(tagName) || !!unarySlash;
var l = match.attrs.length;
var attrs = new Array(l);
// 把属性数组转化成对象
for (let i = 0; i < l; i++) {
var args = match.attrs[i];
var value = args[3] || args[4] || args[5] || '';
attrs[i] = {
name: args[1],
value
}
}
// 不是单标签才存到stack
if (!unary) {
stack.push({
tag: tagName,
attrs
});
}
if (options.start) {
options.start(
tagName, attrs, unary,
match.start, match.end
);
}
}
当使用endTag这个正则成功匹配到尾标签时,会调用parseEndTag
function parseEndTag(tagName, start, end) {
var pos, lowerCasedTagName;
if (tagName) {
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].tagName === tagName) break;
}
}
else {
// 如果没有提供标签签名,那么关闭所有存在stack中的起始标签
pos = 0;
}
// 批量 stack pop 位置后的所有标签
if (pos >= 0) {
// 关闭 pos 位置之后所有的起始标签,避免有些标签没有尾标签
// 比如stack.length = 7, pos = 5, 那么就关闭最后两个
for (var i = stack.length - 1; i >= pos; i--) {
if (options.end) {
options.end(stack[i].tag, start, end);
}
}
// 匹配完闭合标签之后,就把匹配的标签头给移除了
stack.length = pos;
}
}
/*
简要流程如下
<div>
<header>
<span></span>
</header>
</div>
一开始会匹配到3个头标签 stack = [div, header, span]
然后开始匹配到span, 然后去stack末尾去找span
确定span在stack中的位置pos后,批量闭合stack的pos之后所有的标签
*/
4.属性解析
处理class分为两种,一种静态的class, 一种动态的class
function transformNode(el, options) {
var staticClass = getAndRemoveAttr(el, 'class');
if (staticClass) {
el.staticClass = JSON.stringify(staticClass);
}
// 处理动态class
var classBinding = getBindingAttr(el, 'class', false);
if (classBinding) {
el.classBinding = classBinding;
}
}
/*
<span class="a" :class="b"></span>
转化为下面的
{
classBinding: "b"
staticClass: ""a""
tag: "span"
type: 1
}
*/
处理style几乎和class一模一样
function transformNode$1(el, options) {
var staticStyle = getAndRemoveAttr(el, 'style');
if (staticStyle) {
// 比如绑定 style="height:0;width:0"
// parseStyleText 解析得到对象 { height:0,width:0 }
el.staticStyle = JSON.stringify(parseStyleText(staticStyle));
}
// :style="{height:a}" 解析得 {height:a}
var styleBinding = getBindingAttr(el, 'style', false);
if (styleBinding) {
el.styleBinding = styleBinding;
}
}
/*
<span style="width:0" :style="{height: a}"></span>
转化成下面的
{
staticStyle: "{"width":"0"}"
styleBinding: "{height:a}"
tag: "span"
type: 1
}
*/
解析v-for用的是processFor
function processFor(el) {
var exp = getAndRemoveAttr(el, 'v-for');
if (exp) {
// 比如指令是v-for="(item, index) in arr"
// res = {for: "arr", alias: "item", iterator1: "index"}
var res = parseFor(exp);
if (res) {
// 把res和el属性合并起来
extend(el, res);
}
}
}
/*
<div v-for="(item, index) in arr"></div>
可以转化为
{
alias: "item",
for: "arr",
iterator1: "index",
tag: "div",
type: 1,
}
*/
解析v-if用的是processIf
function processIf(el) {
var exp = getAndRemoveAttr(el, 'v-if');
if (exp) {
el.if = exp;
(el.ifConditions || el.ifConditions = []).push({
exp,
block: el
})
} else {
// 对于 v-else 和 v-else-if 没有做太多的处理
// 这二者会调用之后的 processIfConditions
if (getAndRemoveAttr(el, 'v-else') !== null) {
el.else = true;
}
var elseif = getAndRemoveAttr(el, 'v-else-if');
if (el.elseif) {
el.elseif = elseif;
}
}
}
function processIfConditions(el, parent) {
var prev = findPrevElement(parent.children);
if (prev && prev.if) {
(prev.ifConditions || prev.ifConditions = []).push({
exp: el.elseif,
block: el
});
}
}
/*
<div>
<p></p>
<div v-if="a"></div>
<strong v-else-if="b"></strong>
<span v-else></span>
</div>
解析之后生成如下
{
tag: "header",
type: 1,
children:[{
tag: "header",
type: 1,
if: "a",
ifCondition:[
{exp: "a", block: {header的ast 节点}}
{exp: "b", block: {strong的ast 节点}}
{exp: undefined, block: {span的ast节点}}
]
},{
tag: "p"
type: 1
}]
}
*/
slot的解析是通过processSlot
来解析的
function processSlot(el) {
if (el.tag === 'slot') {
el.slotName = el.attrsMap.name;
}
else {
var slotScope = getAndRemoveAttr(el, 'slot-scope');
el.slotScope = slotScope;
// slot的名字
var slotTarget = el.attrsMap.slot;
if (slotTarget) {
el.slotTarget = slotTarget === '""'
? '"default"'
: slotTarget
}
}
}
/*
<span>
<slot name="header" :a="num" :b="num"></slot>
</span>
以上模板解析成
{
{
tag: "span"
type: 1
children:[{
attrsMap: {name: " header", :a: "num", :b: "num"}
slotName: "" header""
tag: "slot"
type: 1
}]
}
父组件模板
<div>
<child >
<p slot="header" slot-scope="c"> {{ c }}</p>
</child>
</div>
解析成
{
children: [{
tag: "child",
type: 1,
children: [{
slotScope: "c",
slotTarget: ""header "",
tag: "p",
type: 1
}]
}],
tag: "div",
type: 1
}
*/
Vue自带属性v-
, :
, @
三种符号的属性名,会分开处理
先来看:
的情况,经过Vue的处理,:
开头的属性会被放入el.props
或者el.attrs
中。
当我们给指令添加了.prop
的时候
<!-- 这个属性会被存放到el.props中 -->
<div :name.props="myName"></div>
props是直接添加到DOM属性上的,attrs是直接显示在标签上的。添加props的时候,需要转化成驼峰法,因为DOM元素的props不支持-
连接的。
当匹配到@
或者v-on
时,属于事件添加,没有太多处理
<div @click="aaa" @keyup="bbb"></div>
<!--
{
events: {
click: { value: 'aaa' },
keyup: { value: 'bbb' }
}
}
-->
v-
开头的会全部保存到el.directives
中
<div v-a:key="bbb"></div>
<!--
{
directives: [{
arg: "key",
modifiers: undefined,
name: "a",
rawName: "v-a:key",
value: "bbb"
}]
}
-->
普通属性
直接存放进el.attrs
中
<div bbb="ccc"></div>
<!--
{
attrs: [{
name: 'bbb',
value: 'ccc'
}]
}
-->
总体源码如下
var onRE = /^@|^v-on:/;
var dirRE = /^v-|^@|^:/;
var bindRE = /^:|^v-bind:/;
var modifierRE = /\.[^.]+/g;
var argRE = /:(.*)$/;
function processAttrs(el) {
var list = el.attrsList;
var i, l, name, rawName, value, modifiers, isProp;
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name;
value = list[i].value;
// 判断属性是否带有 'v-' , '@' , ':'
if (dirRE.test(name)) {
// mark element as dynamic
el.hasBindings = true;
// 比如 v-bind.a.b.c = "xxzxxxx"
// 那么 modifiers = {a: true, b: true, c: true}
modifiers = parseModifiers(name);
// 抽取出纯名字
if (modifiers) {
// name = "v-bind.a.b.c = "xxzxxxx" "
// 那么 name= v-bind
name = name.replace(modifierRE, '');
}
// 收集动态属性,v-bind,可能是绑定的属性,可能是传入子组件的props
// bindRE = /^:|^v-bind:/
if (bindRE.test(name)) {
// 抽取出纯名字,比如 name= v-bind
// 替换之后,name = bind
name = name.replace(bindRE, '');
isProp = false;
if (modifiers) {
// 直接添加到 dom 的属性上
if (modifiers.prop) {
isProp = true;
// 变成驼峰命名
name = camelize(name);
if (name === 'innerHtml')
name = 'innerHTML';
}
// 子组件同步修改
if (modifiers.sync) {
addHandler(el,
// 得到驼峰命名
"update:" + camelize(name),
// 得到 "value= $event"
genAssignmentCode(value, "$event")
);
}
}
// el.props 的作用上面有说,这里有部分是 表单的必要属性都要保存在 el.props 中
if (
isProp ||
// platformMustUseProp 判断这个属性是不是要放在 el.props 中
// 比如表单元素 input 等,属性是value selected ,checked 等
// 比如 tag=input,name=value,那么value 属性要房子啊 el.props 中
(!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name))
) {
(el.props || (el.props = [])).push({
name,
value
});
}
// 其他属性放在 el.attrs 中
else {
(el.attrs || (el.attrs = [])).push({
name,
value
});
}
}
// 收集事件,v-on , onRE = /^@|^v-on:/
else if (onRE.test(name)) {
// 把 v-on 或者 @ 去掉,拿到真正的 指令名字
// 比如 name ="@click" , 替换后 name = "click"
name = name.replace(onRE, '');
addHandler(el, name, value, modifiers, false);
}
// 收集其他指令,比如 "v-once",
else {
// 把v- 去掉,拿到真正的 指令名字
name = name.replace(dirRE, '');
// name = "bind:key" , argMatch = [":a", "a"]
var argMatch = name.match(argRE);
var arg = argMatch && argMatch[1];
if (arg) {
// 比如 name = "bind:key" ,去掉 :key
// 然后 name = "bind"
name = name.slice(0, -(arg.length + 1));
}
(el.directives || (el.directives = [])).push({
name,
rawName,
value,
arg,
modifiers
});
}
} else {
(el.attrs || (el.attrs = [])).push({
name,
value
});
}
}
}
optimize
是Compile的三大步骤之一,是一个性能优化的手段
// ... parse
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
optimize(ast, options);
}
// ...generate
它能遍历AST子树,检测纯静态的子树,即永不需要更改的DOM,Vue内部进行Optimize的方法就是给节点加上static
属性。
function optimize(root, options) {
if (!root) return;
makeStatic$1(root);
makeStaticRoots(root);
}
先来看看Vue是如何判断static节点的
function isStatic(node) {
// 文字表达式
if (node.type === 2) return false;
// 纯文本
if (node.type === 3) return true;
return node.pre || ( // 如果添加了v-pre指令,表明节点不需要解析了
!node.hasBindings && // 不能存在指令,事件等
!node.if && // 不能存在v-if
!node.for && // 不能存在v-for
!['slot', 'component'].indexOf(node.tag) > -1 && // 节点名称不能是slot和component
isPlatformReserverdTag(node.tag) && // 需要时正常的HTML标签
!isDirectChildOfTemplateFor(node) && // 父辈节点不能是template或者带有v-for
Object.keys(node).every(isStaticKey) // 该节点所有的属性都需要是静态节点的静态属性
);
}
上面提到的makeStatic$1
方法,主要用来标记节点是否是静态节点
// 标记节点是否是静态节点
function markStatic$1(node) {
node.static = isStatic(node);
if (node.type !== 1) return
// 不要将组件插槽内容设置为静态。
// 这就避免了
// 1、组件无法更改插槽节点
// 2、静态插槽内容无法热加载
if (
// 正常 thml 标签 才往下处理,组件之类的就不可以
!isPlatformReservedTag(node.tag) &&
// 标签名是 slot 才往下处理
node.tag !== 'slot' &&
// 有 inline-tempalte 才往下处理
node.attrsMap['inline-template'] == null
) {
return
}
// 遍历所有孩子,如果孩子 不是静态节点,那么父亲也不是静态节点
var l = node.children.length
for (var i = 0;i < l; i++) {
var child = node.children[i];
// 递归设置子节点,子节点再调用子节点
markStatic$1(child);
if (!child.static) {
node.static = false;
}
}
if (node.ifConditions) {
var c = node.ifConditions.length
for (var j = 1; j < c; j++) {
// block 是 节点的 ast
var block = node.ifConditions[j].block;
markStatic$1(block);
if (!block.static) {
node.static = false;
}
}
}
}
第二步就是标记静态根节点
// 标记根节点是否是静态节点
function markStaticRoots(node) {
if (node.type === 1) return
// 要使一个节点符合静态根的条件,它应该有这样的子节点
// 不仅仅是静态文本。否则,吊装费用将会增加
// 好处大于坏处,最好总是保持新鲜。
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) {
var l = node.children.length
for (var i = 0; i < l; i++) {
markStaticRoots(
node.children[i]
);
}
}
}
markStatic$1 这个函数只是为 markStaticRoots 服务的,是为了先把每个节点都处理之后,更加方便快捷静态根节点。
被判断为静态根节点的条件
- 该节点所有的子孙节点都是静态节点
- 必须存在子节点
- 子节点不能是纯文本节点。Vue不会将这种节点标记为静态节点,如果将这种节点也标记为静态节点,会起到负优化的作用,下面讨论为什么给纯文本节点标记为静态节点,是一种负优化
首先标记为静态节点需要维护静态模板存储对象,这个信息存储在_staticTrees
中。随着静态根节点的增加,这个存储对象会越来越大,那么占用的内存也会越来越多,势必要增加一些不必要的存储
其实这个问题涉及到 render 和 静态 render 的合作,
<div>
<span>
<strong>我是静态文本</strong>
</span>
<span v-if="testStaticRender"></span>
</div>
生成的render函数是这样的
with(this) {
return _c('div', [
// 这个函数就是去获取静态模板的,这样会产生很多额外的调用
_m(0),
(testStaticRender ? _c('span') : _e())
])
}
genarate
generate的作用是根据生成的AST节点,拼接成字符串,而这个字符串可以被转化为函数,函数执行后,就会生成VNode
// options用来传入一些判断函数或者指令
function generate(ast, options) {
// CodegenState 给实例初始化编译状态
var state = new CodegenState(options);
// genElement 将AST转化为字符串
var code = ast ? genElement(ast, state) : '_c("div")';
return {
render: "with(this){ return " + code + "}",
staticRenderFns: state.staticRenderFns
}
}
function CodegenState(options) {
this.options = options;
// class$1 用于处理AST中的class
// style$1 用于处理AST中的style
this.dataGenFns = [ class$1.genData, style$1.genData];
this.directives = { on , bind, cloak, model,text ,html]
// 用来存放静态根节点的render函数
this.staticRenderFns = [];
}
genElement是AST拼接成字符串的重点函数,主要是处理各种节点,并且拼接起来
// 这个里面主要是一个个的处理函数
function genElement(el, state) {
if (
el.staticRoot && !el.staticProcessed
) {
// 拼接静态节点
return genStatic(el, state)
}
else if (
el.for && !el.forProcessed
) {
return genFor(el, state)
}
else if (
el.if && !el.ifProcessed
) {
return genIf(el, state)
}
else if (el.tag === 'slot') {
return genSlot(el, state)
}
else {
var code;
// 处理 is 绑定的组件
if (el.component) {
code = genComponent(el.component, el, state);
}
// 上面所有的解析完之后,会走到这一步
else {
// 当 el 不存在属性的时候,el.plain = true
var data = el.plain ? undefined : genData$2(el, state);
// 处理完父节点,遍历处理所有子节点
var children = genChildren(el, state);
code = `_c(
'${el.tag}'
${data ? ("," + data) : ''}
${children ? ("," + children) : ''}
)`
}
return code
}
}
拼接静态节点
function genStatic(el, state) {
el.staticProcessed = true;
state.staticRenderFns.push(
"with(this){ return " + genElement(el, state) + "}"
);
return `_m(${
state.staticRenderFns.length - 1
})`;
}
拼接v-if节点
// el.ifCondition 是用来存放条件数组的
function genIf(el, state) {
el.isProcessed = true;
return genIfConditions(
el.ifConditions.slice(),
state
);
}
/*
<div>
<p v-if="isShow"></p>
<span v-else-if="isShow == 2"></span>
<section v-else></section>
</div>
会编译成如下
{
tag:"div",
children:[{
tag:"p",
ifCondition:[{
exp: "isShow",
block: {..p 的 ast 节点}
},{
exp: "isShow==2",
block: {..span 的 ast 节点}
},{
exp: undefined,
block: {..section 的 ast 节点}
}]
}]
}
*/
7. 事件拼接
function genData$2(el, state) {
var data = '{';
// 组件自定义事件,比如`<div @click="a"></div>`
if (el.events) {
data += genHandlers(el.events, false) + ',';
}
// 原生DOM事件,比如 `@click.native`
if (el.nativeEvents) {
data += genHandlers(el.nativeEvents, true) + ',';
}
data = data.replace(/,$/, '') + '}';
return data;
}
从上面的函数可以知道,不管是组件自定义事件还是原生DOM事件,都是调用的genHandlers
function genHandlers(events, isNative) {
var res = isNative ? 'nativeOn:{' : 'on:{';
var handler = events[name];
for (var name in events) {
res += ` ${name}:${genHandler(name, handler)}, `
}
return res.slice(0, -1) + '}';
}
修饰符内部配置
var modifierCode = {
stop: '$event.stopPropagation();',
prevent: '$event.preventDefault();',
ctrl: genGuard("!$event.ctrlKey"),
shift: genGuard("!$event.shiftKey"),
alt: genGuard("!$event.altKey"),
meta: genGuard("!$event.metaKey"),
self: genGuard("$event.target !== $event.currentTarget"),
left: genGuard("'button' in $event && $event.button !== 0"),
middle: genGuard("'button' in $event && $event.button !== 1"),
right: genGuard("'button' in $event && $event.button !== 2")
};
var genGuard = function(condition) {
return ` if ( ${ condition } ) return null `
};
/*
比如添加了stop修饰符的,会这么拼接
"function($event ){ " +
"$event.stopPropagation();" +
" return "+ aaa +"($event);" +
"}"
*/
键盘修饰符
// keys是一个数组,保存的是添加的修饰符,可以是数字,可以是键名
function genKeyFilter(keys) {
var key = keys.map(genFilterCode).join('&&');
return `if( !('button' in $event) && ${ key } )
return null `;
}
function genFilterCode(key) {
var keyVal = parseInt(key);
// 如果key是数字,那直接返回字符串
if (keyVal) {
return "$event.keyCode!==" + keyVal
}
// 如果key是键名,比如`enter`
// 这个键名可能不在keyCodes, keyNames中,可以支持自定义
var keyCode = keyCodes[key]; // 获取键值,keyName="Enter"
var keyName = keyNames[key]; // 获取键名,keyCode=13
// $event.keyCode 是按下的键的值
// $event.key 是按下键的名
// 比如我们按下字母`V`, 那此时的keyCode是86, key是'v'
return `
_k(
$event.keyCode ,
${ key } , ${ keyCode },
${ $event.key } , ${ keyName }
)
`
}
// 返回的这个_k本体其实就是`checkKeyCodes`函数
function checkKeyCodes(
eventKeyCode, key, keyCode,
eventKeyName, keyName
) {
// 比如 key 传入的是自定义名字 aaaa
// keyCode 从Vue 定义的 keyNames 获取 aaaa 的实际数字
// keyName 从 Vue 定义的 keyCode 获取 aaaa 的别名
// 并且以用户定义的为准,可以覆盖Vue 内部定义的
var mappedKeyCode = config.keyCodes[key] || keyCode;
// 该键只在 Vue 内部定义的 keyCode 中
if (keyName && eventKeyName && !config.keyCodes[key]) {
return isKeyNotMatch(keyName, eventKeyName)
}
// 该键只在 用户自定义配置的 keyCode 中
else if (mappedKeyCode) {
return isKeyNotMatch(mappedKeyCode, eventKeyCode)
}
// 原始键名
else if (eventKeyName) {
return hyphenate(eventKeyName) !== key
}
}
核心的genHandler方法
function genHandler(name, handler) {
// 没有绑定回调,返回一个空函数
if (!handler) {
return 'function(){}';
}
// 如果绑定的是数组,则逐个递归一遍
if (Array.isArray(handler)) {
return "[" + handler.map(handler => {
return genHandler(name, handler);
}).join(",") + "]";
}
// 开始解析单个回调
var isMethodPath = simplePathRE.test(handler.value);
var isFunctionExpression = fnExpRE.test(handler.value);
// 没有modifier
if (!handler.modifiers) {
if (isMethodPath || isFunctionExpression) {
return handler.value;
}
// 内连语句,需要包裹一层
return "function($event){" + handler.value + ";}";
}
else {
var code = "";
var genModifierCode = ""; // 保存内部修饰符
for (var key in handler.modifier) {
if (modifierCode[key]) {
}
// 精确修饰符
else if (key === 'exact') {
}
// 普通按键
else {
keys.push(key);
}
}
}
// 开始拼接事件回调
// 拼接Vue定义外的按键修饰符
if (keys.length) {
code += genKeyFilter(keys);
}
// 把prevent和stop这样的修饰符在按键过滤之后执行
if (genModifierCode) {
code += genModifierCode;
}
// 事件主体回调
var handlerCode = isMethodPath ?
// 执行你绑定的函数
"return " + handler.value + "($event)" :
(
isFunctionExpression
? "return " + handler.value + "$event"
: handler.value
);
return `function($event){
${code + handlerCode}
}`
}
拼接事件回调的3个重点
- 拼接按键的修饰符
- 拼接内置修饰符
- 拼接事件回调
更多推荐
所有评论(0)