写文章不容易,点个赞呗兄弟
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue 版本 【2.5.17】
如果你觉得排版难看,请点击 下面链接 或者 拉到 下面 关注公众号 也可以吧
【Vue 原理】Compile – 源码版 之 generate 节点拼接
终于走到了 Vue 渲染三巨头的最后一步了,那就是 generate,反正文章已经写完了,干脆早点发完,反正这部分内容大家也不会马上看哈哈
或者先看白话版好了
Compile – 白话版
然后,generate 的作用就是,解析 parse 生成的 ast 节点,拼接成字符串,而这个字符串,是可以被转化成函数执行的。函数执行后,会生成对应的 Vnode
Vnode 就是 Vue 页面的基础,我们就可以根据他完成 DOM 的创建和更新了
比如这样
ast
{
tag:"div",
children:[],
attrsList:[{name:111}]
}
拼接成函数
"_c('div', { attrs:{ name:111} }, [])"
转成函数
new Function(传入上面的字符串)
生成 Vnode
{
tag: "div",
data:{attrs: {name: "111"}
},
children: undefined
}
本文概览
本文主要讲的是如果去把 生成好的 ast 拼接成 函数字符串(跟上面那个转换一样),而 ast 分为很多种,而每一种的拼接方式都不一样,我们会针对每一种方式来详细列出来
下面将会讲这么多种类型节点的拼接
静态节点,v-for 节点,v-if 节点,slot 节点,组件节点,子节点 等的拼接,内容较多却不复杂,甚至有点有趣
那我们就来看看 generate 本身的函数源码先
比较简短
function generate(ast, options) {var state = new CodegenState(options);
var code = ast ? genElement(ast, state) : '_c("div")';
return {render: "with(this){return" + code + "}",
// 专门用于存放静态根节点的
staticRenderFns: state.staticRenderFns
}
}
对上面出现的几个可能有点迷惑的东西解释一下
参数 options
options 是传入的一些判断函数或者指令函数,如下,不一一列举
{
expectHTML: true,
modules: modules$1,
directives: directives$1
....
};
函数 CodegenState
给该实例初始化编译的状态,下面会有源码
函数 genElement
把 ast 转成字符串的 罪魁祸首
generate 返回值
你也看到了
1 返回 genElement 拼接后的字符串 code
这就是作为 render 的主要形态,包了一层 with
render 会有一块内容专门说,with 就不多说了哈,就是为了为 render 绑定实例为上下文
2 返回 静态根节点 的 静态 render
这是一个 数组,因为一个模板里面可能存在多个静态根节点,那么就要把这些静态根节点都转换成 render 字符串保存起来,就是保存在数组中
上面是静态根节点?简单就是说,第一静态,第二某一部分静态节点的最大祖宗,如下图
两个 span 就是 静态根节点,他们都是他们那个静态部分的最大祖宗,而 div 下 有 v-if 的子节点,所以 div 不是静态根节点
然后下面这个静态模板,解析得到 render 放到 staticRenderFns 是这样的
<div name="a">
<span>111</span>
</div>
staticRenderFns=[
`
with(this){
return _c('div',
{attrs:{"name":"a"}},[111])]
)
}
`
]
而 staticRenderFns 也会在 render 模块下详细记录
CodegenState
初始化实例的编译状态
function CodegenState(options) {
this.options = options;
this.dataGenFns = [klass$1.genData, style$1.genData];
this.directives = {on , bind, cloak, model,text ,html]
this.staticRenderFns = [];};
因为这个函数是给实例初始化一些属性的,看到很明显就是给实例添加上了很多属性,this.xxxx 什么的,那么我们就对 CodegenState 这个函数中添加的属性解释一下。
属性 dataGenFns
这个数组,存放的是两个函数
style$1.genData 处理 ast 中的 style,包括动态静态的 style
klass$1.genData 处理 ast 中的 class,包括动态静态的 class
比如
<div class="a" :class="aa"
style="height:0" :style="{width:0}">
</div>
解析成 ast
{
tag: "div",
type: 1,
staticStyle: "{"height":"0"}",
styleBinding: "{width:0}",
staticClass: ""a"",
classBinding: "name"
}
解析成字符串
`_c('div',{
staticClass:"a",
class:name,
staticStyle:{"height":"0"},
style:{width:0}
})
`
staticClass:"a",
class:name,
staticStyle:{"height":"0"},
style:{width:0}
})
`
dataGenFns 会在后面拼接节点数据的时候调用到
属性 directives
这也是个数组,存放的是 Vue 自有指令的独属处理函数
包括以下几个指令的处理函数
v-on,绑定事件
v-bind,绑定属性
v-cloak,编译前隐藏 DOM
v-model,双向绑定
v-text,插入文本
v-html,插入 html
当你在模板中使用到以上的指令的时候,Vue 会调用相应的函数先进行处理
属性 staticRenderFns
一个数组,用来存放静态根节点的 render 函数,上面有提到过一点
每个实例都独有这个属性,如果没有静态根节点就为空
比如下面这个模板,有两个静态根节点
然后在实例 的 staticRenderFns 中就存放两个 静态 render
那么我们现在就来看,generate 的重点函数,genElement
genElement
这是 ast 拼接成 字符串 的重点函数,主要是处理各种节点,并且拼接起来
静态节点,v-for 节点,v-if 节点,slot 节点,组件节点,子节点 等,有一些省略了
可以简单看看下面的源码
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
}
}
重点是其中的各种处理函数,通过各种条件来选择函数进行处理,并且会有一个 xxxProcessed 属性,作用是证明是否已经进行过 xxx 方面的处理了,比如 forProcessed = true,证明已经拼接过他的 v-for 了
在相应的函数中,会被这个属性设置为 true,然后递归的时候,就不会再调用相应的函数
以上的各种函数中会调用 genElement,以便递归处理其他节点
genElement 按顺序处理自身各种类型的节点后,开始 genData$2 拼接节点的数据,比如 attr,prop 那些,然后再使用 genChildren 处理 子节点
拼接节点数据会在独立一篇文章记录,内容很多
下面我们来一个个看其中涉及的节点处理函数
拼接静态节点
function genStatic(el, state) {
el.staticProcessed = true;
state.staticRenderFns.push("with(this){return" + genElement(el, state) + "}"
);
return `
_m(${state.staticRenderFns.length - 1})
`
}
太简单了,给一个模板看一下就可以了
处理完,存储静态 render,并返回字符串 “_m(0)” , 很简短吼
意思就是获取 staticRenderFns 中的第一个值
其中的值,也是调用 genElement 得到的静态 render
拼接 vIf 节点
专门用于处理带有 v-if 的节点
function genIf(el, state) {
el.ifProcessed = true; // 避免递归
return genIfConditions(el.ifConditions.slice(),
state
)
}
看到 parse 文章的,想必应该知道 el.ifCondition 是什么了吧
简单说一下吧,el.ifCondition 是用来存放条件节点的数组
什么叫条件节点啊?
比如 你有这样的模板
像 上面的 p,span,section 三个节点都是条件节点,不会直接存放到父节点的 children 中
因为并不是马上显示的
然后他们解析得到的 ast,都会被存放到 p 节点的 ifCondition 中
像这样
{
tag:"div",
children:[{
tag:"p",
ifCondition:[{
exp: "isShow",
block: {..p 的 ast 节点}
},{
exp: "isShow==2",
block: {..span 的 ast 节点}
},{
exp: undefined,
block: {..section 的 ast 节点}
}]
}]
}
el.ifCondition 就是把 这个数组复制一遍(我又学到了,之前我并不知道可以这么去复制数组)
然后传给 genIfCondition,看看源码
function genIfConditions(conditions, state,) {
// 当 没有条件的时候,就返回 _e,意思是空节点
if (!conditions.length) {return '_e()'
}
// 遍历一遍之后,就把条件剔除
var condition = conditions.shift();
if (condition.exp) {
return (
condition.exp + "?" +
genElement(condition.block,state) + ":" +
genIfConditions(conditions, state)
)
} else {return genElement(condition.block,state)
}
}
这个函数的作用呢,是这样的
1、按顺序处理 ifCondition 中的每一个节点,并且会移出数组
2、并且每一个节点使用 三元表达式 去拼接
3、递归调用 genIfConditions 去处理剩下的 ifCondition
按下面的模板来说明一下流程
第一轮
ifCondition = [p,span,section]
获取 ifCondition 第一个节点,也就是 p,并移出 ifCondition 数组
此时 ifCondition = [span,section]
p 节点有表达式 isShow,需要三元表达式拼接,变成
"isShow ? _c('p') : genIfConditions(剩下的 ifCondition)"
第二轮
genIfConditions 同样获取第一个节点,span
此时 ifCondition = [section]
span 有表达式 isShow==2,需要拼接三元表达式,变成
" isShow ?
_c('p') :
(isShow==2 ? _c( 'span') : genIfConditions(剩下的 ifCondition) )"
第三轮
genIfConditions 同样获取第一个节点,section
此时 ifCondition = []
section 没有表达式,直接处理节点,拼接成
" isShow ?
_c('p') :
(isShow==2 ? _c( 'span') : _c('section') )"
然后就处理完啦,上面的字符串,就是 genIf 处理后拼接上的字符串
接下来看是怎么拼接带有 v -for 的指令的
拼接 v -for 节点
function genFor(el, state) {
var exp = el.for;
var alias = el.alias;
var iterator1 = el.iterator1 ? ("," + el.iterator1) : '';
var iterator2 = el.iterator2 ? ("," + el.iterator2) : '';
el.forProcessed = true; // avoid recursion
return ('_l (' + exp + ",function(" + alias + iterator1 + iterator2 + "){" +
"return" + genElement(el, state) +
'})'
)
}
大家应该都可以看得懂的吧,给个例子
`_c('div', _l( arr ,function(item,index){return _c('span') } )`
就这样,v-for 就解析成了一个 _l 函数,这个函数会遍历 arr,遍历一遍,就生成一个节点
下面在看看是如何处理子节点的
拼接子节点
function genChildren(el, state) {
var children = el.children;
if (!children.length) return
return` [$ {children.map(function(c) {if (node.type === 1) {return genElement(node, state)
}
return`_v($ {
text.type === 2
? text.expression
: ("'"+ text.text +"'")
})`
}).join(','))
}]`
}
同样的,这个函数也是很简单的吼
就是遍历所有子节点,逐个处理子节点,然后得到一个新的数组
1、当子节点 type ==1 时,说明是标签,那么就要 genElement 处理一遍
2、否则,就是文本节点
如果 type =2,那么是表达式文本,否则,就是普通文本
普通文本,需要左右两边加引号。表达式是个变量,需要在实例上获取,所以不用加双引号
举个例子
解析成字符串
`_c('div',[ _c('span') ,_c('section') ,_c('a') ])`
拼接插槽
function genSlot(el, state) {
var slotName = el.slotName || '"default"';
var children = genChildren(el, state);
var res = `
_t(${slotName} ,
${children ? ("," + children) : ''}
)
`
var attrs =
el.attrs && "{" +
el.attrs.map(function(a) {return camelize(a.name) + ":" + a.value;
}).join(',') +
"}";
if (attrs && !children) {res += ",null";}
// _t 的参数顺序是 slotName,children,attrs,bind
if (attrs) {res += "," + attrs;}
return res + ')'
}
genSlot 主要是处理子节点 和 绑定在 slot 上的 attrs
属性 attrs 会逐个拼接成 xx:xx 的形式,合成一个新的数组,然后通过 逗号隔开成字符串
原 attrs = [{ name:"a-a" ,value:"aaa"},
{name:"b-b" ,value:"bbb"}
]
转换后,name 会变成驼峰
attrs = 'aA:"aaa", bB:"bbb"'
看下例子,一个 slot,绑定属性 a 作为 scope,并且有 span 作为默认内容
解析成这样
_c('div',[_t("default", [_c('span')] ,{a:aa} )] )
然后剩最后一个了,解析组件的节点
拼接组件
function genComponent(componentName, el, state) {var children = genChildren(el, state, true);
return `_c(${componentName},
${genData$2(el, state)}
${children ? ("," + children) : ''}
)`
}
其实,解析组件,就是把他先当做普通标签进行处理,在这里并没有做什么特殊的操作
但是这个方法是用来处理【带有 is 属性】的节点
否则 就不会存在 el.component 这个属性,就不会调用 genComponent
拼接成下面这样,而其中的 is 属性的拼接在 下篇文章 genData$2 中会有说明
`_c('div',[_c("test",{tag:"a"})])`
那如果直接写组件名作为标签,是怎么处理?
也没有做什么特殊处理,具体看 genElement 最后那段
同样当做普通标签先解析
看个例子
解析成这样的字符串
`_c('div',[ _c('test', [_c('span')] )] )`
走个流程
看了上面这么多的处理函数,各种函数处理后得到的字符串是相加的关系
然后现在用一个小例子来实现以下拼接步骤
1、先解析最外层 div,得到字符串
`_c('div' `
genChildren 开始解析子节点
2、处理 strong,这是一个静态根节点,genStatic 处理得到字符串
`_c('div' , [ _m(0) `
3、处理 p 节点,genIf 处理拼接字符串
`_c('div' , [ _m(0) , isShow? _c(p) :_e() `
4、处理 span 节点,genFor 拼接字符串
`_c( 'div' , [_m(0) ,
isShow? _c(p) :_e() ,
_l(arr,function(item,index){return _c('span')})
`
5、处理 test 组件节点,genComponent 拼接
`_c( 'div' , [_m(0) ,
isShow? _c(p) :_e() ,
_l(arr,function(item,index){return _c('span')}),
_c('test')
`
6、genChildren 处理完所有子节点拼接上末尾的括号得到
`_c( 'div' , [_m(0) ,
isShow? _c(p) :_e() ,
_l(arr,function(item,index){return _c('span')}),
_c('test')
])
`
然后整个 genElement 流程就处理完了
上面得到的 字符串,只要转换成函数,就可以执行了,于是也就可以得到我们的 Vnode
感悟
有时你会想,看这个东西有什么用啊,其实你只做正常项目的话,你的确大可不必去看这部分的内容,但是如果你真的想胸有成竹,百分百掌握 Vue,你就必须看,因为你可以做更多东西
比如之前接了个外包,要根据别人打包好的文件,去还原别人的源码!
难度之大之复杂,你也想得出来,不过幸好我看过源码,打包后的文件,模板全是 render 函数,所以我可以手动还原出来原始模板!
虽然我也可以写一个 反编译模板函数,但是工作量太大,没得想法了。还原的难度就在于 render 变成模板了,因为其他的什么 method 等是原封不动的哈哈,可是直接照抄
最后
鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,有重谢