Vue源码解析:v-for指令如何实现列表的重新渲染?

2026-05-22 23:432阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计2728个文字,预计阅读时间需要11分钟。

在《Petite-vue源码剖析-v-if和v-for的工作原理》一文中,我们了解到+v-for在静态视图中的工作原理。接下来,我们将深入探讨在更新渲染时+v-for是如何运作的。

逐行解析:typescript// 文件路径: .\src\directives\for.ts

+v-for在更新渲染时的运作原理如下:

1. 检测依赖:当DOM元素或其子元素发生变化时,Vue会检测到这些变化并触发更新渲染。

2.遍历数据:v-for指令会遍历数据源中的每个元素。

3.创建元素:对于每个数据元素,Vue会创建对应的DOM元素。

4.绑定事件:如果v-for指令中包含v-on指令,Vue会绑定相应的事件处理器。

5.更新DOM:Vue会更新DOM,将新创建的元素插入到正确的位置。

具体实现步骤:

1. 获取数据源:从v-for指令中获取数据源。

2.创建元素:遍历数据源,为每个数据元素创建对应的DOM元素。

3.绑定事件:如果存在v-on指令,则绑定相应的事件处理器。

4.插入元素:将创建的元素插入到DOM中正确的位置。

5.更新DOM:完成所有元素的创建和插入后,Vue会更新DOM,确保视图与数据保持同步。

总结:

+v-for在更新渲染时,通过遍历数据源、创建元素、绑定事件和更新DOM等步骤,实现动态渲染列表或表格等功能。

在《petite-vue源码剖析-v-if和v-for的工作原理》我们了解到v-for在静态视图中的工作原理,而这里我们将深入了解在更新渲染时v-for是如何运作的。

逐行解析

// 文件 ./src/directives/for.ts /* [\s\S]*表示识别空格字符和非空格字符若干个,默认为贪婪模式,即 `(item, index) in value` 就会匹配整个字符串。 * 修改为[\s\S]*?则为懒惰模式,即`(item, index) in value`只会匹配`(item, index)` */ const forAliasRE = /([\s\S]*?)\s+(?:in)\s+([\s\S]*?)/ // 用于移除`(item, index)`中的`(`和`)` const stripParentRE= /^\(|\)$/g // 用于匹配`item, index`中的`, index`,那么就可以抽取出value和index来独立处理 const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ type KeyToIndexMap = Map<any, number> // 为便于理解,我们假设只接受`v-for="val in values"`的形式,并且所有入参都是有效的,对入参有效性、解构等代码进行了删减 export const _for = (el: Element, exp: string, ctx: Context) => { // 通过正则表达式抽取表达式字符串中`in`两侧的子表达式字符串 const inMatch = exp.match(forAliasRE) // 保存下一轮遍历解析的模板节点 const nextNode = el.nextSibling // 插入锚点,并将带`v-for`的元素从DOM树移除 const parent = el.parentElement! const anchor = new Text('') parent.insertBefore(anchor, el) parent.removeChild(el) const sourceExp = inMatch[2].trim() // 获取`(item, index) in value`中`value` let valueExp = inMatch[1].trim().replace(stripParentRE, '').trim() // 获取`(item, index) in value`中`item, index` let indexExp: string | undefined let keyAttr = 'key' let keyExp = el.getAttribute(keyAttr) || el.getAttribute(keyAttr = ':key') || el.getAttribute(keyAttr = 'v-bind:key') if (keyExp) { el.removeAttribute(keyExp) // 将表达式序列化,如`value`序列化为`"value"`,这样就不会参与后面的表达式运算 if (keyAttr === 'key') keyExp = JSON.stringify(keyExp) } let match if (match = valueExp.match(forIteratorRE)) { valueExp = valueExp.replace(forIteratorRE, '').trim() // 获取`item, index`中的item indexExp = match[1].trim() // 获取`item, index`中的index } let mounted = false // false表示首次渲染,true表示重新渲染 let blocks: Block[] let childCtxs: Context[] let keyToIndexMap: KeyToIndexMap // 用于记录key和索引的关系,当发生重新渲染时则复用元素 const createChildContexts = (source: unknown): [Context[], KeyToIndexMap] => { const map: KeyToIndexMap = new Map() const ctxs: Context[] = [] if (isArray(source)) { for (let i = 0; i < source.length; i++) { ctxs.push(createChildContext(map, source[i], i)) } } return [ctxs, map] } // 以集合元素为基础创建独立的作用域 const createChildContext = ( map: KeyToIndexMap, value: any, // the item of collection index: number // the index of item of collection ): Context => { const data: any = {} data[valueExp] = value indexExp && (data[indexExp] = index) // 为每个子元素创建独立的作用域 const childCtx = createScopedContext(ctx, data) // key表达式在对应子元素的作用域下运算 const key = keyExp ? evaluate(childCtx.scope, keyExp) : index map.set(key, index) childCtx.key = key return childCtx } // 为每个子元素创建块对象 const mountBlock = (ctx: Conext, ref: Node) => { const block = new Block(el, ctx) block.key = ctx.key block.insert(parent, ref) return block } ctx.effect(() => { const source = evaluate(ctx.scope, sourceExp) // 运算出`(item, index) in items`中items的真实值 const prevKeyToIndexMap = keyToIndexMap // 生成新的作用域,并计算`key`,`:key`或`v-bind:key` ;[childCtxs, keyToIndexMap] = createChildContexts(source) if (!mounted) { // 为每个子元素创建块对象,解析子元素的子孙元素后插入DOM树 blocks = childCtxs.map(s => mountBlock(s, anchor)) mounted = true } else { // 更新渲染逻辑!! // 根据key移除更新后不存在的元素 for (let i = 0; i < blocks.length; i++) { if (!keyToIndexMap.has(blocks[i].key)) { blocks[i].remove() } } const nextBlocks: Block[] = [] let i = childCtxs.length let nextBlock: Block | undefined let prevMovedBlock: Block | undefined while (i--) { const childCtx = childCtxs[i] const oldIndex = prevKeyToIndexMap.get(childCtx.key) let block if (oldIndex == null) { // 旧视图中没有该元素,因此创建一个新的块对象 block = mountBlock(childCtx, newBlock ? newBlock.el : anchor) } else { // 旧视图中有该元素,元素复用 block = blocks[oldIndex] // 更新作用域,由于元素下的`:value`,`{{value}}`等都会跟踪scope对应属性的变化,因此这里只需要更新作用域上的属性,即可触发子元素的更新渲染 Object.assign(block.ctx.scope, childCtx.scope) if (oldIndex != i) { // 元素在新旧视图中的位置不同,需要移动 if ( blocks[oldIndex + 1] !== nextBlock || prevMoveBlock === nextBlock ) { prevMovedBlock = block // anchor作为同级子元素的末尾 block.insert(parent, nextBlock ? nextBlock.el : anchor) } } } nextBlocks.unshift(nextBlock = block) } blocks = nextBlocks } }) return nextNode } 难点突破

上述代码最难理解就是通过key复用元素那一段了

const nextBlocks: Block[] = [] let i = childCtxs.length let nextBlock: Block | undefined let prevMovedBlock: Block | undefined while (i--) { const childCtx = childCtxs[i] const oldIndex = prevKeyToIndexMap.get(childCtx.key) let block if (oldIndex == null) { // 旧视图中没有该元素,因此创建一个新的块对象 block = mountBlock(childCtx, newBlock ? newBlock.el : anchor) } else { // 旧视图中有该元素,元素复用 block = blocks[oldIndex] // 更新作用域,由于元素下的`:value`,`{{value}}`等都会跟踪scope对应属性的变化,因此这里只需要更新作用域上的属性,即可触发子元素的更新渲染 Object.assign(block.ctx.scope, childCtx.scope) if (oldIndex != i) { // 元素在新旧视图中的位置不同,需要移动 if ( /* blocks[oldIndex + 1] !== nextBlock 用于对重复键减少没必要的移动(如旧视图为1224,新视图为1242) * prevMoveBlock === nextBlock 用于处理如旧视图为123,新视图为312时,blocks[oldIndex + 1] === nextBlock导致无法执行元素移动操作 */ blocks[oldIndex + 1] !== nextBlock || prevMoveBlock === nextBlock ) { prevMovedBlock = block // anchor作为同级子元素的末尾 block.insert(parent, nextBlock ? nextBlock.el : anchor) } } } nextBlocks.unshift(nextBlock = block) }

我们可以通过示例通过人肉单步调试理解

示例1

旧视图(已渲染): 1,2,3
新视图(待渲染): 3,2,1

  1. 循环第一轮

    childCtx.key = 1 i = 2 oldIndex = 0 nextBlock = null prevMovedBlock = null

    prevMoveBlock === nextBlock
    于是将旧视图的block移动到最后,视图(已渲染): 2,3,1

  2. 循环第二轮

    childCtx.key = 2 i = 1 oldIndex = 1

    更新作用域

  3. 循环第三轮

    childCtx.key = 3 i = 0 oldIndex = 2 nextBlock = block(.key=2) prevMovedBlock = block(.key=1)

    于是将旧视图的block移动到nextBlock前,视图(已渲染): 3,2,1

示例2 - 存在重复键

旧视图(已渲染): 1,2,2,4
新视图(待渲染): 1,2,4,2

此时prevKeyToIndexMap.get(2)返回2,而位于索引为1的2的信息被后者覆盖了。

  1. 循环第一轮

    childCtx.key = 2 i = 3 oldIndex = 2 nextBlock = null prevMovedBlock = null

    于是将旧视图的block移动到最后,视图(已渲染): 1,2,4,2

  2. 循环第二轮

    childCtx.key = 4 i = 2 oldIndex = 3 nextBlock = block(.key=2) prevMovedBlock = block(.key=2)

    于是将旧视图的block移动到nextBlock前,视图(已渲染): 1,2,4,2

  3. 循环第三轮

    childCtx.key = 2 i = 1 oldIndex = 2 nextBlock = block(.key=4) prevMovedBlock = block(.key=4)

    由于blocks[oldIndex+1] === nextBlock,因此不用移动元素

  4. 循环第四轮

childCtx.key = 1 i = 0 oldIndex = 0

由于i === oldIndex,因此不用移动元素

和React通过key复用元素的区别?

React通过key复用元素是采取如下算法

  1. 第一次遍历新旧元素(左到右)
    1. 若key不同即跳出遍历,进入第二轮遍历
      • 此时通过变量lastPlacedIndex记录最后一个key匹配的旧元素位置用于控制旧元素移动
    2. 若key相同但元素类型不同,则创建新元素替换掉旧元素
  2. 遍历剩下未遍历的旧元素 - 以旧元素.key为键,旧元素为值通过Map存储
  3. 第二次遍历剩下未遍历的新元素(左到右)
    1. 从Map查找是否存在的旧元素,若没有则创建新元素
    2. 若存在则按如下规则操作:
      • 若从Map查找的旧元素的位置大于lastPlacedIndex则将旧元素的位置赋值给lastPlacedIndex,若元素类型相同则复用旧元素,否则创建新元素替换掉旧元素
      • 若从Map查找的旧元素的位置小于lastPlacedIndex则表示旧元素向右移动,若元素类型相同则复用旧元素,否则创建新元素替换掉旧元素(lastPlacedIndex的值保持不变)
  4. 最后剩下未遍历的旧元素将被删除

第二次遍历时移动判断是,假定lastPlacedIndex左侧的旧元素已经和新元素匹配且已排序,若发现旧元素的位置小于lastPlacedIndex,则表示lastPlacedIndex左侧有异类必须向右挪动。

petite-vue的算法是

  1. 每次渲染时都会生成以元素.key为键,元素为值通过Map存储,并通过prevKeyToIndexMap保留指向上一次渲染的Map
  2. 遍历旧元素,通过当前Map筛选出当前渲染中将被移除的元素,并注意移除
  3. 遍历新元素(右到左)
    1. 若key相同则复用
    2. 若key不同则通过旧Map寻找旧元素,并插入最右最近一个已处理的元素前面

它们的差别

  1. petite-vue无法处理key相同但元素类型不同的情况(应该说不用处理比较适合),而React可以

    // petite-vue createApp({ App: { // 根本没有可能key相同而元素类型不同嘛 $template: ` <div v-for="item in items" :key="item.id"></div> ` } }) // React function App() { const items = [...] return ( items.map(item => { if (item.type === 'span') { return (<span key={item.id}></span>) } else { return (<div key={item.id}></div>) } }) ) }

  2. 由于petite-vue对重复key进行优化,而React会对重复key执行同样的判断和操作

  3. petite-vue是即时移动元素,而React是运算后再移动元素,并且对于旧视图为123,新视图为312而言,petite-vue将移动3次元素,而React仅移动2次元素

后续

和DOM节点增删相关的操作我们已经了解得差不多了,后面我们一起阅读关于事件绑定、属性和v-modal等指令的源码吧!
尊重原创,转载请注明来自:www.cnblogs.com/fsjohnhuang/p/15989941.html 肥仔John

欢迎添加我的公众号一起深入探讨技术手艺人的那些事!

如果您觉得本文的内容有趣就扫一下吧!捐赠互勉!
  

本文共计2728个文字,预计阅读时间需要11分钟。

在《Petite-vue源码剖析-v-if和v-for的工作原理》一文中,我们了解到+v-for在静态视图中的工作原理。接下来,我们将深入探讨在更新渲染时+v-for是如何运作的。

逐行解析:typescript// 文件路径: .\src\directives\for.ts

+v-for在更新渲染时的运作原理如下:

1. 检测依赖:当DOM元素或其子元素发生变化时,Vue会检测到这些变化并触发更新渲染。

2.遍历数据:v-for指令会遍历数据源中的每个元素。

3.创建元素:对于每个数据元素,Vue会创建对应的DOM元素。

4.绑定事件:如果v-for指令中包含v-on指令,Vue会绑定相应的事件处理器。

5.更新DOM:Vue会更新DOM,将新创建的元素插入到正确的位置。

具体实现步骤:

1. 获取数据源:从v-for指令中获取数据源。

2.创建元素:遍历数据源,为每个数据元素创建对应的DOM元素。

3.绑定事件:如果存在v-on指令,则绑定相应的事件处理器。

4.插入元素:将创建的元素插入到DOM中正确的位置。

5.更新DOM:完成所有元素的创建和插入后,Vue会更新DOM,确保视图与数据保持同步。

总结:

+v-for在更新渲染时,通过遍历数据源、创建元素、绑定事件和更新DOM等步骤,实现动态渲染列表或表格等功能。

在《petite-vue源码剖析-v-if和v-for的工作原理》我们了解到v-for在静态视图中的工作原理,而这里我们将深入了解在更新渲染时v-for是如何运作的。

逐行解析

// 文件 ./src/directives/for.ts /* [\s\S]*表示识别空格字符和非空格字符若干个,默认为贪婪模式,即 `(item, index) in value` 就会匹配整个字符串。 * 修改为[\s\S]*?则为懒惰模式,即`(item, index) in value`只会匹配`(item, index)` */ const forAliasRE = /([\s\S]*?)\s+(?:in)\s+([\s\S]*?)/ // 用于移除`(item, index)`中的`(`和`)` const stripParentRE= /^\(|\)$/g // 用于匹配`item, index`中的`, index`,那么就可以抽取出value和index来独立处理 const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ type KeyToIndexMap = Map<any, number> // 为便于理解,我们假设只接受`v-for="val in values"`的形式,并且所有入参都是有效的,对入参有效性、解构等代码进行了删减 export const _for = (el: Element, exp: string, ctx: Context) => { // 通过正则表达式抽取表达式字符串中`in`两侧的子表达式字符串 const inMatch = exp.match(forAliasRE) // 保存下一轮遍历解析的模板节点 const nextNode = el.nextSibling // 插入锚点,并将带`v-for`的元素从DOM树移除 const parent = el.parentElement! const anchor = new Text('') parent.insertBefore(anchor, el) parent.removeChild(el) const sourceExp = inMatch[2].trim() // 获取`(item, index) in value`中`value` let valueExp = inMatch[1].trim().replace(stripParentRE, '').trim() // 获取`(item, index) in value`中`item, index` let indexExp: string | undefined let keyAttr = 'key' let keyExp = el.getAttribute(keyAttr) || el.getAttribute(keyAttr = ':key') || el.getAttribute(keyAttr = 'v-bind:key') if (keyExp) { el.removeAttribute(keyExp) // 将表达式序列化,如`value`序列化为`"value"`,这样就不会参与后面的表达式运算 if (keyAttr === 'key') keyExp = JSON.stringify(keyExp) } let match if (match = valueExp.match(forIteratorRE)) { valueExp = valueExp.replace(forIteratorRE, '').trim() // 获取`item, index`中的item indexExp = match[1].trim() // 获取`item, index`中的index } let mounted = false // false表示首次渲染,true表示重新渲染 let blocks: Block[] let childCtxs: Context[] let keyToIndexMap: KeyToIndexMap // 用于记录key和索引的关系,当发生重新渲染时则复用元素 const createChildContexts = (source: unknown): [Context[], KeyToIndexMap] => { const map: KeyToIndexMap = new Map() const ctxs: Context[] = [] if (isArray(source)) { for (let i = 0; i < source.length; i++) { ctxs.push(createChildContext(map, source[i], i)) } } return [ctxs, map] } // 以集合元素为基础创建独立的作用域 const createChildContext = ( map: KeyToIndexMap, value: any, // the item of collection index: number // the index of item of collection ): Context => { const data: any = {} data[valueExp] = value indexExp && (data[indexExp] = index) // 为每个子元素创建独立的作用域 const childCtx = createScopedContext(ctx, data) // key表达式在对应子元素的作用域下运算 const key = keyExp ? evaluate(childCtx.scope, keyExp) : index map.set(key, index) childCtx.key = key return childCtx } // 为每个子元素创建块对象 const mountBlock = (ctx: Conext, ref: Node) => { const block = new Block(el, ctx) block.key = ctx.key block.insert(parent, ref) return block } ctx.effect(() => { const source = evaluate(ctx.scope, sourceExp) // 运算出`(item, index) in items`中items的真实值 const prevKeyToIndexMap = keyToIndexMap // 生成新的作用域,并计算`key`,`:key`或`v-bind:key` ;[childCtxs, keyToIndexMap] = createChildContexts(source) if (!mounted) { // 为每个子元素创建块对象,解析子元素的子孙元素后插入DOM树 blocks = childCtxs.map(s => mountBlock(s, anchor)) mounted = true } else { // 更新渲染逻辑!! // 根据key移除更新后不存在的元素 for (let i = 0; i < blocks.length; i++) { if (!keyToIndexMap.has(blocks[i].key)) { blocks[i].remove() } } const nextBlocks: Block[] = [] let i = childCtxs.length let nextBlock: Block | undefined let prevMovedBlock: Block | undefined while (i--) { const childCtx = childCtxs[i] const oldIndex = prevKeyToIndexMap.get(childCtx.key) let block if (oldIndex == null) { // 旧视图中没有该元素,因此创建一个新的块对象 block = mountBlock(childCtx, newBlock ? newBlock.el : anchor) } else { // 旧视图中有该元素,元素复用 block = blocks[oldIndex] // 更新作用域,由于元素下的`:value`,`{{value}}`等都会跟踪scope对应属性的变化,因此这里只需要更新作用域上的属性,即可触发子元素的更新渲染 Object.assign(block.ctx.scope, childCtx.scope) if (oldIndex != i) { // 元素在新旧视图中的位置不同,需要移动 if ( blocks[oldIndex + 1] !== nextBlock || prevMoveBlock === nextBlock ) { prevMovedBlock = block // anchor作为同级子元素的末尾 block.insert(parent, nextBlock ? nextBlock.el : anchor) } } } nextBlocks.unshift(nextBlock = block) } blocks = nextBlocks } }) return nextNode } 难点突破

上述代码最难理解就是通过key复用元素那一段了

const nextBlocks: Block[] = [] let i = childCtxs.length let nextBlock: Block | undefined let prevMovedBlock: Block | undefined while (i--) { const childCtx = childCtxs[i] const oldIndex = prevKeyToIndexMap.get(childCtx.key) let block if (oldIndex == null) { // 旧视图中没有该元素,因此创建一个新的块对象 block = mountBlock(childCtx, newBlock ? newBlock.el : anchor) } else { // 旧视图中有该元素,元素复用 block = blocks[oldIndex] // 更新作用域,由于元素下的`:value`,`{{value}}`等都会跟踪scope对应属性的变化,因此这里只需要更新作用域上的属性,即可触发子元素的更新渲染 Object.assign(block.ctx.scope, childCtx.scope) if (oldIndex != i) { // 元素在新旧视图中的位置不同,需要移动 if ( /* blocks[oldIndex + 1] !== nextBlock 用于对重复键减少没必要的移动(如旧视图为1224,新视图为1242) * prevMoveBlock === nextBlock 用于处理如旧视图为123,新视图为312时,blocks[oldIndex + 1] === nextBlock导致无法执行元素移动操作 */ blocks[oldIndex + 1] !== nextBlock || prevMoveBlock === nextBlock ) { prevMovedBlock = block // anchor作为同级子元素的末尾 block.insert(parent, nextBlock ? nextBlock.el : anchor) } } } nextBlocks.unshift(nextBlock = block) }

我们可以通过示例通过人肉单步调试理解

示例1

旧视图(已渲染): 1,2,3
新视图(待渲染): 3,2,1

  1. 循环第一轮

    childCtx.key = 1 i = 2 oldIndex = 0 nextBlock = null prevMovedBlock = null

    prevMoveBlock === nextBlock
    于是将旧视图的block移动到最后,视图(已渲染): 2,3,1

  2. 循环第二轮

    childCtx.key = 2 i = 1 oldIndex = 1

    更新作用域

  3. 循环第三轮

    childCtx.key = 3 i = 0 oldIndex = 2 nextBlock = block(.key=2) prevMovedBlock = block(.key=1)

    于是将旧视图的block移动到nextBlock前,视图(已渲染): 3,2,1

示例2 - 存在重复键

旧视图(已渲染): 1,2,2,4
新视图(待渲染): 1,2,4,2

此时prevKeyToIndexMap.get(2)返回2,而位于索引为1的2的信息被后者覆盖了。

  1. 循环第一轮

    childCtx.key = 2 i = 3 oldIndex = 2 nextBlock = null prevMovedBlock = null

    于是将旧视图的block移动到最后,视图(已渲染): 1,2,4,2

  2. 循环第二轮

    childCtx.key = 4 i = 2 oldIndex = 3 nextBlock = block(.key=2) prevMovedBlock = block(.key=2)

    于是将旧视图的block移动到nextBlock前,视图(已渲染): 1,2,4,2

  3. 循环第三轮

    childCtx.key = 2 i = 1 oldIndex = 2 nextBlock = block(.key=4) prevMovedBlock = block(.key=4)

    由于blocks[oldIndex+1] === nextBlock,因此不用移动元素

  4. 循环第四轮

childCtx.key = 1 i = 0 oldIndex = 0

由于i === oldIndex,因此不用移动元素

和React通过key复用元素的区别?

React通过key复用元素是采取如下算法

  1. 第一次遍历新旧元素(左到右)
    1. 若key不同即跳出遍历,进入第二轮遍历
      • 此时通过变量lastPlacedIndex记录最后一个key匹配的旧元素位置用于控制旧元素移动
    2. 若key相同但元素类型不同,则创建新元素替换掉旧元素
  2. 遍历剩下未遍历的旧元素 - 以旧元素.key为键,旧元素为值通过Map存储
  3. 第二次遍历剩下未遍历的新元素(左到右)
    1. 从Map查找是否存在的旧元素,若没有则创建新元素
    2. 若存在则按如下规则操作:
      • 若从Map查找的旧元素的位置大于lastPlacedIndex则将旧元素的位置赋值给lastPlacedIndex,若元素类型相同则复用旧元素,否则创建新元素替换掉旧元素
      • 若从Map查找的旧元素的位置小于lastPlacedIndex则表示旧元素向右移动,若元素类型相同则复用旧元素,否则创建新元素替换掉旧元素(lastPlacedIndex的值保持不变)
  4. 最后剩下未遍历的旧元素将被删除

第二次遍历时移动判断是,假定lastPlacedIndex左侧的旧元素已经和新元素匹配且已排序,若发现旧元素的位置小于lastPlacedIndex,则表示lastPlacedIndex左侧有异类必须向右挪动。

petite-vue的算法是

  1. 每次渲染时都会生成以元素.key为键,元素为值通过Map存储,并通过prevKeyToIndexMap保留指向上一次渲染的Map
  2. 遍历旧元素,通过当前Map筛选出当前渲染中将被移除的元素,并注意移除
  3. 遍历新元素(右到左)
    1. 若key相同则复用
    2. 若key不同则通过旧Map寻找旧元素,并插入最右最近一个已处理的元素前面

它们的差别

  1. petite-vue无法处理key相同但元素类型不同的情况(应该说不用处理比较适合),而React可以

    // petite-vue createApp({ App: { // 根本没有可能key相同而元素类型不同嘛 $template: ` <div v-for="item in items" :key="item.id"></div> ` } }) // React function App() { const items = [...] return ( items.map(item => { if (item.type === 'span') { return (<span key={item.id}></span>) } else { return (<div key={item.id}></div>) } }) ) }

  2. 由于petite-vue对重复key进行优化,而React会对重复key执行同样的判断和操作

  3. petite-vue是即时移动元素,而React是运算后再移动元素,并且对于旧视图为123,新视图为312而言,petite-vue将移动3次元素,而React仅移动2次元素

后续

和DOM节点增删相关的操作我们已经了解得差不多了,后面我们一起阅读关于事件绑定、属性和v-modal等指令的源码吧!
尊重原创,转载请注明来自:www.cnblogs.com/fsjohnhuang/p/15989941.html 肥仔John

欢迎添加我的公众号一起深入探讨技术手艺人的那些事!

如果您觉得本文的内容有趣就扫一下吧!捐赠互勉!