Vue源码阅读(22):v-for 指令的源码解析

 我的开源库:

开始正文前,大家可以先复习一下 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-小山"。

 渲染的页面如下所示:

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值