我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
开始正文前,大家可以先复习一下 v-for 指令的用法,对应的官方文档点击这里。
今天和大家讲讲 v-for 指令的底层实现原理,它的底层实现其实很简单,主要看 v-for 指令对应的代码字符串是如何生成对应 vnode 的,我们以一个简单的例子进行解析。
new Vue({
el: '#app',
data() {
return {
names: ['小明', '小花', '小山']
}
},
methods: {},
template: `
<div id="app">
<h1 v-for="(item1, index1) in names">{{index1 + 1}}-{{item1}}</h1>
</div>
`
})
1,模板字符串 >>> 抽象语法树
生成的抽象语法树如上所示,h1 AST 节点中与 v-for 指令有关的属性有 alias、for、forProcessed、iterator1,这些属性将用于 v-for 指令对应代码字符串的生成。
2,抽象语法树 >>> 代码字符串
抽象语法树生成的代码字符串如下所示:
with(this){
return _c(
'div',
{attrs:{"id":"app"}},
_l(
(names),
function(item1,index1){
return _c('h1',[_v(_s(index1 + 1)+"-"+_s(item1))])
}
)
)
}
其中与 v-for 指令有关的代码字符串是:
_l(
(names),
function(item1,index1){
return _c('h1',[_v(_s(index1 + 1)+"-"+_s(item1))])
}
)
这里需要注意的是:
- v-for 指令生成的代码字符串是 _l 函数的调用,该函数有两个参数,第一个参数是被循环遍历的数据,第二个参数是一个函数。
- 第二个函数参数的作用是能够生成数据循环项所对应节点的 vnode。
接下来详细看看 _l 函数的源码。
3,_l 函数源码的解析
_l 是 renderList 函数的别名,所以我们需要看 renderList 函数的源码。
/* @flow */
import { toNumber, toString, looseEqual, looseIndexOf } from 'shared/util'
import { createTextVNode, createEmptyVNode } from 'core/vdom/vnode'
import { renderList } from './render-list'
import { renderSlot } from './render-slot'
import { resolveFilter } from './resolve-filter'
import { checkKeyCodes } from './check-keycodes'
import { bindObjectProps } from './bind-object-props'
import { renderStatic, markOnce } from './render-static'
import { bindObjectListeners } from './bind-object-listeners'
import { resolveScopedSlots } from './resolve-slots'
export function installRenderHelpers (target: any) {
......
target._l = renderList
......
}
renderList 函数定义在 src/core/instance/render-helpers/render-list.js,对应的代码如下所示,我在源码中写了大量的注释,看注释即可理解。
/* @flow */
import { isObject, isDef } from 'core/util/index'
/**
* Runtime helper for rendering v-for lists.
*/
export function renderList (
val: any,
render: (
val: any,
keyOrIndex: string | number,
index?: number
) => VNode
): ?Array<VNode> {
let ret: ?Array<VNode>, i, l, keys, key
if (Array.isArray(val) || typeof val === 'string') {
// 处理 val 是数组和字符串的情况,因为数组和字符串都能通过下标(data[index])进行访问,所以将他们放在一起进行处理
//
// ret 是最终返回的数据,该数据是一个数组,因为 renderList 是生成 v-for 指令绑定元素的 vnode,
// 所以最后的返回值一定是一个数组,并且数组的内容是 VNode。
ret = new Array(val.length)
// 开始遍历数组或者字符串
for (i = 0, l = val.length; i < l; i++) {
// render 函数的作用是:能够生成数据循环项所对应节点的 vnode。
// 在这里调用 render 生成循环项对应节点的 vnode,并将生成的 vnode 赋值给 ret[i]
ret[i] = render(val[i], i)
}
} else if (typeof val === 'number') {
// 处理 val 是数字的情况,在 vue 中,遍历的数据还可以是数字类型的
// 如果是数字类型的话,item 是:1、2、3 ... val
// index 是:0、1、2、... val - 1
ret = new Array(val)
// 使用 for 遍历 0 -> val - 1
for (i = 0; i < val; i++) {
// 和上面一样,执行 render 函数生成循环项对应节点的 vnode,然后将 vnode 赋值给 ret[i]
ret[i] = render(i + 1, i)
}
} else if (isObject(val)) {
// 处理 val 是对象的情况,当 val 是对象类型时,v-for 可以写成 (value, name, index) in object
// value 是对象键值对中的值;
// name 是对象键值对中的键;
// index 是当前处理键值对在所有键值对中的排序,从 0 开始;
//
// 获取 val 对象的键字符串数组
keys = Object.keys(val)
// 最终返回的数组,数组的长度是 val 对象中键值对的个数
ret = new Array(keys.length)
// 开始循环遍历键值的数组
for (i = 0, l = keys.length; i < l; i++) {
// 当前正在处理的键
key = keys[i]
// val[key] 当前正在处理键的值
// 执行 render 函数生成循环项对应节点的 vnode,并将 vnode 赋值给 ret[i]
ret[i] = render(val[key], key, i)
}
}
if (isDef(ret)) {
(ret: any)._isVList = true
}
// 返回生成好的 vnode 数组
return ret
}
4,最终生成的 VNode
最终生成的 vnode 如下所示,可以发现 div vnode 的子节点数组内有 3 个 h1 vnode 节点,这 3 个 h1 vnode 节点的子元素都是文本节点,文本分别是 "1-小明"、"2-小花"、"3-小山"。
渲染的页面如下所示: