从源码上看v-for
参考文章:1.5.Vue模板编译原理-AST生成Render字符串
在前面一篇文章《Vue相关指令(一)》中讲到代码生成器,当时没有详细讲源码,现在讲讲。其实代码生成器的入口就是generate函数:
export function generate (
//处理好的抽象语法树
ast: ASTElement | void,
//一些选项,data啥的
options: CompilerOptions
): CodegenResult {
// 初始化一些options
const state = new CodegenState(options)
// 重点在这里!
// 有没有生成抽象语法树ast?有就传入ast和options生成代码:没有就直接生成一个空div
const code = ast ? genElement(ast, state) : '_c("div")'
return {
// 生成代码之后最外层包一个 with(this) 之后返回
render: `with(this){return ${code}}`,
// staticRenderFns这个数组中的函数与VDOM中的diff算法优化相关
// 那些被标记为staticRoot的虚拟节点 会单独生成静态节点渲染函数。
staticRenderFns: state.staticRenderFns
}
}
这个diff算法啥的后面再说,没个几千字说不完啦。这里我们看看重点,这个入口函数的重点就是得到code这一块代码。所以我们看一看genElement这个函数:
export function genElement (el: ASTElement, state: CodegenState): string {
//对一些标签属性,如v-once,v-if等等的处理
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
//先判断有没有v-for,有的话就处理v-for
//forProcessed表示for属性处理完了
} else if (el.for && !el.forProcessed) {
//执行genFor
return genFor(el, state)
//再判断有没有v-if,有的话就处理
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
//这个else后面讲
} else {...}
看看上面的代码,我们能得出一个结论:"哇哦!VUE真的没有骗我们!for的优先级真的比if要高!",其他也看不出啥了。
好,重点又转移到genFor上了,接下来再来看看genFor,这里有些key警告什么的被我删掉了,如果好奇的也可以点击这里直接看源码:src/compiler/codegen/index.js
//渲染v-for指令
function genFor (
el,
state,
altGen,
altHelper
) {
//获取for的值
var exp = el.for;
//获取别名item
var alias = el.alias;
//key
var iterator1 = el.iterator1 ? ("," + (el.iterator1)) : '';
//index
var iterator2 = el.iterator2 ? ("," + (el.iterator2)) : '';
//...一些关于key的警告
// 表示for属性处理完了 避免递归 因为后面还要继续调用genElement生成子节点的代码
el.forProcessed = true;
//拼凑_l函数 目前并没有传递altHelper altGen这两个东西 所以我们直接忽略他
//(其实我也不知道这是啥,没办法跟你们讲..如果你们知道的话可以在评论告诉我呀)
return (altHelper || '_l') + "((" + exp + ")," +
"function(" + alias + iterator1 + iterator2 + "){" +
//递归他的子节点,生成子节点的代码(p也有子节点,是文本节点)
"return " + ((altGen || genElement)(el, state)) +
'})'
}
"_l"其实就是用来生成列表VNode的,它只是一个缩写,对应的函数其实是renderList,后面会讲一下renderList的代码。现在的话,我们先来看看上面那一串return回去的丑丑的字符串。我们可以举个例子来看看里面都是什么好东西。
先拿个例子,来源:Vue.js 源码分析(十八) 指令篇 v-for 指令详解
<div id="app">
<p v-for="(item,key,index) in infos">{{index}}:{{key}}:{{item}}</p>
</div>
<script>
var app = new Vue({
data(){return {infos:{name:'gege',age:12}}},
el:'#app'
})
</script>
目前只看里面的<p>标签,如果我们的代码是这样的格式:
<p v-for="(item,key,index) in infos">{{index}}:{{key}}:{{item}}</p>
那么对于<p>标签会生成这样子的字符串:
_l( (infos), function( item, key, index){
//_c函数能够生成一个VNode节点返回
return _c(
//节点类型是p节点
'p',
//节点内容是文本节点,_v创建一个文本节点
[ _v (
//文本内容是:index的字符串形式 + ":" + key的字符串形式 + ":" + item的字符串形式
_s(index) + ":" + _s(key) + ":" + _s(item)
) ]
}),0 //这个零是用来区分节点的,不用管它
好啦,知道这个字符串是啥之后,v-for的代码就差不多讲完了,其实还挺少的。(实际上我写这篇文章的过程真的很漫长很漫长,回头一看,居然只写了这么一点...)
最后看点简单的,我们来看看"_l"是怎么渲染的,也就是renderList函数的内容。这一部分很长但是挺简单的,源代码在src/core/instance/render-helpers/render-list.js,坚持看完哦:
//渲染v-for指令
function renderList (
//我们在data中定义的渲染数据
val,
//渲染函数
render
) {
var ret, i, l, keys, key;
//如果val是个数组或字符串
if (Array.isArray(val) || typeof val === 'string') {
//将ret定义成val一样大小的数组
ret = new Array(val.length);
//遍历val数组|字符串
for (i = 0, l = val.length; i < l; i++) {
//依次调用render函数,参数1为值,参数2为索引,返回VNode,并把结果VNode保存到ret里面。
ret[i] = render(val[i], i);
}
//如果是一个数字
} else if (typeof val === 'number') {
ret = new Array(val);
for (i = 0; i < val; i++) {
//遍历 参数1值为1,2,3...val,参数2是索引。
ret[i] = render(i + 1, i);
}
//如果是一个对象
} else if (isObject(val)) {
//得到对象的所有键值
keys = Object.keys(val);
ret = new Array(keys.length);
for (i = 0, l = keys .length; i < l; i++) {
key = keys[i];
//参数1值为key对应的值,参数2是对象的key,参数3是index。
ret[i] = render(val[key], key, i);
}
}
//如果ret存在(成功调用了)
if (isDef(ret)) {
//则给该数组添加一个_isVList标记,值为true
(ret)._isVList = true;
}
//最后返回ret
return ret
每个节点的内容都交给render,然后render帮我们生成虚拟节点了之后,放到ret这个收集节点的数组里面。最后这个数组会作为参数,交给父节点的"_c"函数去处理。
从源码来看render函数字符串的生成
那现在我们再回到最开始的代码中,从<div>开始捋一遍整个代码生成render函数字符串的过程。(晕,好不容易看完怎么又讲回去了...)这里为了大家方便看,直接粘贴过来了:
对于这一段代码:
<div id="app">
<p v-for="(item,key,index) in infos">{{index}}:{{key}}:{{item}}</p>
</div>
<script>
var app = new Vue({
data: {
infos:{
name:'gege',
age:12
}
},
el:'#app'
})
</script>
我们执行generate函数:
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
//之前我们看到这里了
const code = ast ? genElement(ast, state) : '_c("div")'
return {
// 生成代码之后最外层包一个 with(this) 之后返回
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
执行之后,去除那两个反单引号,最终render函数的内容其实是这样的:
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
//这下面就是我们上面分析的p标签对应的代码字符串
}, _l((infos), function (item, key, index) {
return _c('p', [_v(_s(index) + ":" + _s(key) + ":" + _s(item))])
}),0)
}
div啥的是怎么来的嘞?
不知道你记不记得前面的那个被我省略的else?genElement函数里面的那个?现在来看看它的真面目哈~这里依然删掉了一些代码,好奇的朋友可以去src/compiler/codegen/index.js上看,我们理清主要逻辑就好。
else {
// component or element
let code
if (el.component) {
// ...component组件处理生成函数
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
// attributes节点属性处理生成函数
data = genData(el, state)
}
// 处理子节点并调用genChildren()
const children = el.inlineTemplate ? null : genChildren(el, state, true)
// 主要看这一句 el.tag是标签名
code = `_c('${el.tag}'${
data ? `,${data}` : '' // 这里放了在标签中写上的属性
}${
children ? `,${children}` : '' // 这里放子节点
})`
}
// 静态class style 处理
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
我们在el上挂载的那个div对应的render代码就是从这里出来滴,有没有很眼熟!看看我们最终生成的代码,对应关系就明确了:
with(this) {
//生成div节点 el.tag对应了div
return _c('div', {
//这里是我们在div标签中写上的属性
attrs: {
"id": "app"
}
//这下面就是我们之前分析的p标签对应的代码字符串
}, _l((infos), function (item, key, index) {
return _c('p', [_v(_s(index) + ":" + _s(key) + ":" + _s(item))])
}),0)
}
那我们再来看看genChildren函数吧~
export function genChildren (
el: ASTElement,
//一些初始化之后的选项
state: CodegenState,
checkSkip?: boolean,
altGenElement?: Function,
altGenNode?: Function
): string | void {
//得到所有的子节点
const children = el.children
//如果存在子节点
if (children.length) {
const el: any = children[0]
//如果子节点的长度等于1,而且存在v-for,而且不是模板或插槽
if (children.length === 1 &&
el.for &&
el.tag !== 'template' &&
el.tag !== 'slot'
) {
//normalizationType表示子节点规范的类型
const normalizationType = checkSkip
//看看是不是组件或某些节点类型,这里我们子节点是p类型,所以会返回0
? state.maybeComponent(el) ? `,1` : `,0`
: ``
//执行genElement,得到子节点的render函数字符串
return `${(altGenElement || genElement)(el, state)}${normalizationType}`
}
//其他情况就执行这里的代码
const normalizationType = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0
const gen = altGenNode || genNode
//把子节点以数组的形式返回出去
return `[${children.map(c => gen(c, state)).join(',')}]${
normalizationType ? `,${normalizationType}` : ''
}`
}
}
所以说,实际上是在生成父节点div的过程中,处理子节点的时候,调用了genChildren,然后才开始处理p标签的。具体流程如下:
-
对于根节点
<div>生成的抽象语法树,调用generate,开始生成render函数的代码字符串。 -
从
generate函数进入到genElement函数,根据不同的指令来执行不同的代码生成函数。 -
执行
genElement的时候,进入其中的else块,由于她不是组件,所以又进入else块。在这个时候处理了我们写在<div>中的特性,然后开始执行genChildren处理<div>的子节点<p>。 -
在执行
genChildren的时候,又调用了genElement函数,然后在genElement进入判断。由于<p>身上有v-for指令,进入genFor函数。 -
在
genFor函数中生成<p>对应的render代码字符串。 -
由于
<p>中存在文本节点,作为<p>的子节点,所以又进行一次genElement生成文本节点代码,这里就不啰嗦了,直接给代码:[_v(_s(index)+":"+_s(key)+":"+_s(item))] -
<p>的子节点递归完成,拼接<p>的render函数字符串。_l( (infos), function( item, key, index){ return _c( 'p', [ _v ( _s(index) + ":" + _s(key) + ":" + _s(item) ) ] }),0 -
<div>的子节点处理结束,返回到父节点<div>的genElement上来,根据之前处理的属性,子节点等,形成下面的字符串。_c('div', { attrs: { "id": "app" } }, _l((infos), function (item, key, index) { return _c('p', [_v(_s(index) + ":" + _s(key) + ":" + _s(item))]) }),0) -
最后,返回到
generate函数中,用with(this){return ${code}}包括起来,形成最终的render代码。with(this) { return _c('div', { attrs: { "id": "app" } //这下面就是我们上面分析的p标签对应的代码字符串 }, _l((infos), function (item, key, index) { return _c('p', [_v(_s(index) + ":" + _s(key) + ":" + _s(item))]) }),0) }