Vue 3 v-for key 原理核心笔记

一、问题:为什么不能用 index 作 key

1.1 问题代码示例

xml 复制代码
<!-- ❌ 错误用法:用 index 作 key -->
<div v-for="(item, index) in list" :key="index">
  <input v-model="item.name" />
  {{ item.name }}
</div>

1.2 具体会发生什么

perl 复制代码
// 初始数据
list = [
  { id: 1, name: '张三' },  // index: 0
  { id: 2, name: '李四' },  // index: 1
  { id: 3, name: '王五' }   // index: 2
]
​
// 删除第一个元素后
list = [
  { id: 2, name: '李四' },  // 现在 index: 0
  { id: 3, name: '王五' }   // 现在 index: 1
]
​
// 用 index 作 key 的问题:
// 旧vnode[0] (key=0, 对应"张三") vs 新vnode[0] (key=0, 对应"李四")
// Vue 认为 key=0 还是同一个节点,只是内容变了
// 于是复用 <div> 和 <input> 的 DOM
// 结果:input 的显示值变成了"李四",但 DOM 的 value 状态还保留着"张三"

二、编译阶段:模板如何变成代码

2.1 模板编译结果

javascript 复制代码
<!-- 你的模板代码 -->
<div v-for="(item, index) in list" :key="item.id">
  {{ item.name }}
</div>
// ↓ 编译后生成的 render 函数 ↓
​
function render(_ctx, _cache) {
  return (_openBlock(true),  // 开启一个 Fragment block
  _createElementBlock(_Fragment, null, 
    // 🔍 关键:调用 _renderList 遍历 list
    _renderList(_ctx.list, (item, index) => {
      return (_openBlock(),
      _createElementBlock("div", {
        key: item.id,           // ← key 在这里传递给 createElementBlock
      }, [
        _toDisplayString(item.name)  // 动态文本
      ], 1 /* TEXT 动态标志 */))
    }), 
    128 /* KEYED_FRAGMENT */  // ← 这是关键!因为有 :key
  ))
}

2.2 编译器的判断逻辑

javascript 复制代码
// 编译器伪代码逻辑
function compileVFor(element) {
  const hasKeyBinding = element.hasBinding('key');  // 检查是否有 :key
  
  if (hasKeyBinding) {
    // 有 :key → 设置 KEYED_FRAGMENT 标志 (128)
    fragmentFlag = 128;  // PatchFlags.KEYED_FRAGMENT
  } else {
    // 没有 :key → 设置 UNKEYED_FRAGMENT 标志 (256)
    fragmentFlag = 256;  // PatchFlags.UNKEYED_FRAGMENT
  }
  
  // 生成的代码中会传递这个 fragmentFlag
  return `createElementBlock(Fragment, null, renderList(list, renderItem), ${fragmentFlag})`;
}

三、运行阶段:renderList 的工作原理

3.1 renderList 源码核心

javascript 复制代码
// renderList 函数简化版(只看数组的情况)
function renderList(source, renderItem, cache, index) {
  let ret = [];
  
  // 遍历数组,为每个元素调用 renderItem
  for (let i = 0; i < source.length; i++) {
    // 🔍 关键:每次调用 renderItem 都传入当前元素和索引
    ret[i] = renderItem(
      source[i],  // 当前元素
      i,          // 当前索引
      undefined,  // 保留参数
      cache? cache[i] : undefined  // 缓存(如果有)
    );
  }
  
  return ret;  // 返回 vnode 数组
}

3.2 renderItem 是什么?

ini 复制代码
// 你的 v-for 模板编译成的 renderItem
const renderItem = (item, index) => {
  // 这个函数是编译器根据你的模板生成的
  return _createElementBlock("div", {
    key: item.id,  // 你的 :key="item.id"
  }, _toDisplayString(item.name));
};

3.3 第一次渲染和更新时的区别

ini 复制代码
// 第一次渲染
const list = [{id: 1, name: '张三'}, {id: 2, name: '李四'}];
const vnodes1 = renderList(list, renderItem);
// vnodes1 = [
//   { type: 'div', key: 1, children: '张三', el: null },
//   { type: 'div', key: 2, children: '李四', el: null }
// ]
​
// Vue 创建真实 DOM
// div1 -> <div>张三</div>
// div2 -> <div>李四</div>
// 并设置 vnode.el = 对应的 DOM 元素
​
// ------------------------------
// 用户删除第一个元素后
list.splice(0, 1);  // 现在 list = [{id: 2, name: '李四'}]
​
// 响应式触发重新渲染
// 重新执行 renderList
const vnodes2 = renderList(list, renderItem);
// vnodes2 = [
//   { type: 'div', key: 2, children: '李四', el: null }
// ]
​
// 🔍 注意:vnodes2[0] 是全新的 vnode 对象!
// 它的 key 是 2,对应 id=2 的数据

四、vnode 的生成与 key 的存储

4.1 createBaseVNode 如何提取 key

php 复制代码
// 创建 vnode 的核心函数(简化版)
function createBaseVNode(type, props = null, children = null) {
  const vnode = {
    __v_isVNode: true,
    type,        // 如 'div'
    props,       // 所有属性,包括 key
    key: props && normalizeKey(props),  // 🔍 关键:从 props 中提取 key
    children,    // 子节点
    el: null,    // 指向真实 DOM,初始为 null
    // ... 其他属性
  };
  
  return vnode;
}
​
// 提取 key 的函数
function normalizeKey(props) {
  return props.key;  // 就是这么简单!
}
​
// 实际调用
const vnode = createBaseVNode('div', { key: 123, class: 'item' }, '张三');
// vnode.key = 123
// vnode.props = { key: 123, class: 'item' }

五、响应式系统触发更新(简化版)

php 复制代码
// 1. 数据变化触发更新
const list = ref([{id: 1, name: '张三'}, {id: 2, name: '李四'}]);
​
// 2. 删除第一个元素
list.value.splice(0, 1);
​
// 3. 响应式系统检测到变化,重新执行组件的渲染函数
// 4. 重新执行 render 函数
// 5. renderList 重新执行,生成新的 vnode 数组
// 6. 调用 patch 进行新旧 vnode 对比

六、Diff 算法的核心区别

6.1 两种算法的选择

当新旧 vnode 数组需要对比时,Vue 会根据编译阶段设置的 patchFlag来选择不同的算法:

scss 复制代码
// patchChildren 函数中的选择逻辑
const patchChildren = (n1, n2, container) => {
  const c1 = n1.children;  // 旧子节点数组
  const c2 = n2.children;  // 新子节点数组
  
  if (n2.patchFlag & 128) {  // KEYED_FRAGMENT
    // 有 key,使用 patchKeyedChildren
    patchKeyedChildren(c1, c2, container, ...);
  } else if (n2.patchFlag & 256) {  // UNKEYED_FRAGMENT
    // 无 key,使用 patchUnkeyedChildren
    patchUnkeyedChildren(c1, c2, container, ...);
  }
};

6.2 patchKeyedChildren(有 key 的算法核心)

ini 复制代码
// 简化版核心代码
function patchKeyedChildren(c1, c2, container) {
  // 1. 建立 key 到新索引的映射
  const keyToNewIndexMap = new Map();
  for (let i = 0; i < c2.length; i++) {
    const child = c2[i];
    if (child.key != null) {
      keyToNewIndexMap.set(child.key, i);
    }
  }
  
  // 2. 遍历旧节点,通过 key 查找匹配
  for (let i = 0; i < c1.length; i++) {
    const prevChild = c1[i];
    
    let newIndex;
    if (prevChild.key != null) {
      // 🔍 关键:通过 key 精准查找
      newIndex = keyToNewIndexMap.get(prevChild.key);
    }
    
    if (newIndex !== undefined) {
      // 找到了,更新节点
      patch(prevChild, c2[newIndex], container);
    } else {
      // 没找到,卸载节点
      unmount(prevChild);
    }
  }
}

6.3 patchUnkeyedChildren(无 key 的算法核心)

scss 复制代码
// 简化版核心代码
function patchUnkeyedChildren(c1, c2, container) {
  const commonLength = Math.min(c1.length, c2.length);
  
  // 🔍 关键:直接按索引比较
  for (let i = 0; i < commonLength; i++) {
    // 认为相同索引位置的就是同一个节点
    patch(c1[i], c2[i], container);
  }
  
  // 处理多出来的节点
  if (c1.length > c2.length) {
    // 卸载多余的旧节点
    for (let i = commonLength; i < c1.length; i++) {
      unmount(c1[i]);
    }
  } else if (c2.length > c1.length) {
    // 挂载新节点
    for (let i = commonLength; i < c2.length; i++) {
      patch(null, c2[i], container);
    }
  }
}
相关推荐
黄金决明子17 小时前
浏览器Window底层操作全解
前端·javascript
ydyd2026042117 小时前
设备OEE怎么提升?数据采集+分析优化的完整方案
java·服务器·前端
(╹◡╹)17 小时前
11.RK3588本地大模型内存评估优化
java·linux·前端
电气研究所18 小时前
Java + Redis 实现工业设备告警去抖,避免变频器故障重复推送
java·前端
AI视觉网奇18 小时前
cannot import name ‘model_urls‘ from ‘torchvision.models.resnet‘
linux·前端·javascript
Vuji18 小时前
深入 pi-agent 内核:完整追踪一次 CLI 交互的完整链路
前端·agent
zfoo-framework18 小时前
安装pi agent 支持duojie和deepseek
java·服务器·前端
l1m0_18 小时前
告别反复修改Prompt:AI生成React应用并落地开发的实战复盘
前端·react.js·ui·ai·设计
xexpertS19 小时前
前端工程转型实践:从 Ember 迁移到 React,提升构建速度与研发效能
前端·react.js·前端框架