从底层看 v-for 如何处理对象遍历

173 阅读2分钟

结论

Vue 中对于遍历一个对象,如果该对象有实现自己的Symbol.iterator方法的话,会调用该方法来进行遍历得到结果。否则的话会按照 Object.keys 的顺序来进行遍历。

由于使用 Object.keys 遍历出来的结果是无序的,如果想保证对象的遍历顺序,则需要自己实现 Symbol.iterator 方法来控制每次调用 next 方法返回的都是什么值

拓展

1)v-for 中处理对象的循环是怎么样的?

我们在这里还是先用一个案例来解决这个问题:

const compiler = require('vue-template-compiler')
​
const res = compiler.compile(`<div class='root-container'>
  <div v-for="(item, index) in list" :key="index">
    渲染元素{{index}}
  </div>
</div>`)

执行完上述代码后,我们会得到一个结果对象,我们只需要关注里面的 render 方法即可。

下面就是 render 函数的结果:

with (this) {
  return _c('div', {
    staticClass: "root-container"
  }, _l((list), function (item, index) {
    return _c('div', { key: index }, [_v("\n    渲染元素" + _s(index) + "\n  ")])
  }), 0)
}

相关方法解析:

_c: 创建 VNode 节点
_v: 创建文本节点
_l:处理列表数据
_s:Object.prototype.toString 方法

我们只需要关注,_l 这个方法的内部实现即可。由于内部还有其他类型的处理,这里我们只讨论对象遍历,所以这里就只贴对对象的处理的逻辑。

// 这里的 val 就是传进去的遍历 list
if (isObject(val)) {
  // 如果当前支持 Symbol,并且 val 拥有 Symbol.iterator 方法
  if (hasSymbol && val[Symbol.iterator]) {
    ret = []
    const iterator: Iterator<any> = val[Symbol.iterator]()
    let result = iterator.next()
    // 循环执行 val 迭代器的 next 方法
    // 直到完毕
    while (!result.done) {
      ret.push(render(result.value, ret.length))
      result = iterator.next()
    }
  } else { // val 没有 Symbol.iterator 方法
    // 这里直接用 Object.keys 获取对象的 key 值去进行遍历
    keys = Object.keys(val)
    ret = new Array(keys.length)
    for (i = 0, l = keys.length; i < l; i++) {
      key = keys[i]
      ret[i] = render(val[key], key, i)
    }
  }
}
return ret

那到这里的话,其实已经证明了:如果该对象有实现自己的Symbol.iterator方法的话,会调用该方法来进行遍历得到结果。否则的话会按照 Object.keys 的顺序来进行遍历。

2)实现自己控制对象遍历顺序

首先来看正常的对象遍历顺序:

<template>
  <div>
    <div v-for="(item, index) in renderedObject" :key="index">
      {{ item }}
    </div>
  </div>
</template>
<script setup>
import { ref } from 'vue'
 
const renderedObject = ref({
  name: 'zs',
  age: '30',
  height: 190
})
</script>

执行完是这样:

image-20240812230711568.png

也就是现在的遍历顺序是:name、age、height

然后我们现在为该对象实现一个Symbol.iterator方法来让对象按自定义顺序渲染

<template>
  <div>
    <div v-for="(item, index) in renderedObject" :key="index">
      {{ item }}
    </div>
  </div>
</template>
<script setup>
import { ref } from 'vue'
const originalObject = {
  name: 'zs',
  age: '30',
  height: 190
}
const renderedObject = ref({
  [Symbol.iterator]() {
    // 闭包索引,记录调用的累计次数
    let nextIndex = 0
    // 控制渲染的对象 key 映射
    let keyMap = {
      0: 'height',
      1: 'age',
      2: 'name',
    }
    let len = Object.keys(originalObject).length
    return {
      next() {
        // 索引小于长度之前一直去取对象中的属性
        if (nextIndex < len) {
          const key = keyMap[nextIndex++]
          const res = originalObject[key]
          return {
            done: false,
            // 返回对应结果
            value: res
          }
        }
        return {
          done: true,
          value: undefined
        }
      }
    }
  }
})
</script>

我们再来看看输出结果:

image-20240812231107252.png

现在的遍历顺序是:height、age、name

至此,我们就实现了对象自定义渲染顺序,如果有帮助到你,请帮忙点个👍🏻。