写文章不容易,点个赞呗兄弟
专一 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工做原理,源码版助于了解内部详情,让咱们一块儿学习吧
研究基于 Vue版本 【2.5.17】
若是你以为排版难看,请点击 下面连接 或者 拉到 下面关注公众号也能够吧html
【Vue原理】Compile - 源码版 之 generate 节点拼接 node
终于走到了 Vue 渲染三巨头的最后一步了,那就是 generate,反正文章已经写完了,干脆早点发完,反正这部份内容你们也不会立刻看哈哈express
或者先看白话版好了
Compile - 白话版 数组
而后,generate的做用就是,解析 parse 生成的 ast 节点,拼接成字符串,而这个字符串,是能够被转化成函数执行的。函数执行后,会生成对应的 Vnode函数
Vnode 就是Vue 页面的基础,咱们就能够根据他完成DOM 的建立和更新了学习
好比这样this
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 分为不少种,而每一种的拼接方式都不同,咱们会针对每一种方式来详细列出来spa
下面将会讲这么多种类型节点的拼接3d
静态节点,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 是传入的一些判断函数或者指令函数,以下,不一一列举
{ expectHTML: true, modules: modules$1, directives: directives$1 .... };
给该实例初始化编译的状态,下面会有源码
把 ast 转成字符串的 罪魁祸首
你也看到了
这就是做为 render 的主要形态,包了一层 with
render 会有一块内容专门说,with 就很少说了哈,就是为了为 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 模块下详细记录
初始化实例的编译状态
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 这个函数中添加的属性解释一下。
这个数组,存放的是两个函数
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 会在后面拼接节点数据的时候调用到
这也是个数组,存放的是 Vue 自有指令的独属处理函数
包括如下几个指令的处理函数
v-on,绑定事件
v-bind,绑定属性
v-cloak,编译前隐藏DOM
v-model,双向绑定
v-text,插入文本
v-html,插入html
当你在模板中使用到以上的指令的时候,Vue 会调用相应的函数先进行处理
一个数组,用来存放静态根节点的render 函数,上面有提到过一点
每一个实例都独有这个属性,若是没有静态根节点就为空
好比下面这个模板,有两个静态根节点
而后在实例 的 staticRenderFns 中就存放两个 静态 render
那么咱们如今就来看,generate 的重点函数,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
专门用于处理带有 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) } }
这个函数的做用呢,是这样的
一、按顺序处理 ifCondition 中的每个节点,而且会移出数组
二、而且每个节点使用 三元表达式 去拼接
三、递归调用 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 的指令的
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(',')) }]` }
一样的,这个函数也是很简单的吼
就是遍历全部子节点,逐个处理子节点,而后获得一个新的数组
一、当子节点 type ==1 时,说明是标签,那么就要 genElement 处理一遍
二、不然,就是文本节点
若是 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')] )] )`
看了上面这么多的处理函数,各类函数处理后获得的字符串是相加的关系
而后如今用一个小例子来实现如下拼接步骤
一、先解析最外层 div,获得字符串
`_c( 'div' `
genChildren 开始解析子节点
二、处理 strong,这是一个静态根节点,genStatic 处理获得字符串
`_c( 'div' , [ _m(0) `
三、处理 p 节点,genIf 处理拼接字符串
`_c( 'div' , [ _m(0) , isShow? _c(p) :_e() `
四、处理 span 节点, genFor 拼接字符串
`_c( 'div' , [ _m(0) , isShow? _c(p) :_e() , _l(arr,function(item,index){return _c('span')}) `
五、处理 test 组件节点,genComponent 拼接
`_c( 'div' , [ _m(0) , isShow? _c(p) :_e() , _l(arr,function(item,index){return _c('span')}), _c('test') `
六、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 等是原封不动的哈哈,但是直接照抄
鉴于本人能力有限,不免会有疏漏错误的地方,请你们多多包涵,若是有任何描述不当的地方,欢迎后台联系本人,有重谢