在《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.lengthlet nextBlock: Block | undefinedlet prevMovedBlock: Block | undefinedwhile (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 = 1i = 2oldIndex = 0nextBlock = nullprevMovedBlock = null

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

  2. 循环第二轮

    childCtx.key = 2i = 1oldIndex = 1

    更新作用域

  3. 循环第三轮

    childCtx.key = 3i = 0oldIndex = 2nextBlock = 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 = 2i = 3oldIndex = 2nextBlock = nullprevMovedBlock = null

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

  2. 循环第二轮

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

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

  3. 循环第三轮

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

    因为blocks[oldIndex+1] === nextBlock,因而不必挪动元素

  4. 循环第四轮

    childCtx.key = 1i = 0oldIndex = 0

    因为i === oldIndex,因而不必挪动元素

后续

和DOM节点增删相干的操作咱们曾经理解得差不多了,前面咱们一起浏览对于事件绑定、属性和v-modal等指令的源码吧!