关于vue.js:petitevue源码剖析vfor重新渲染工作原理

41次阅读

共计 5303 个字符,预计需要花费 14 分钟才能阅读完成。

在《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,因而不必挪动元素

后续

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

正文完
 0