petite-vue源码剖析-v-if和v-for的工作原理

深入v-if的工作原理

人肉单步调试:

  1. 调用createApp根据入参生成全局作用域rootScope,创建根上下文rootCtx
  2. 调用mount
    构建根块对象rootBlock,并将其作为模板执行解析处理;
  3. 解析时识别到v-scope属性,以全局作用域rootScope为基础运算得到局部作用域scope,并以根上下文rootCtx为蓝本一同构建新的上下文ctx,用于子节点的解析和渲染;
  4. 获取$template属性值并生成HTML元素;
  5. 深度优先遍历解析子节点(调用walkChildren);
  6. 解析 OFFLINE

解析 OFFLINE

书接上一回,我们继续人肉单步调试:

  1. 识别元素带上v-if属性,调用_if原指令对元素及兄弟元素进行解析;
  2. 将附带v-if和跟紧其后的附带v-else-ifv-else的元素转化为逻辑分支记录;
  3. 循环遍历分支,并为逻辑运算结果为true的分支创建块对象并销毁原有分支的块对象(首次渲染没有原分支的块对象),并提交渲染任务到异步队列。
// 文件 ./src/walk.ts

// 为便于理解,我对代码进行了精简
export const walk = (node: Node, ctx: Context): ChildNode | null | void {
  const type = node.nodeType
  if (type == 1) {
    // node为Element类型
    const el = node as Element

    let exp: string | null

    if ((exp = checkAttr(el, 'v-if'))) {
      return _if(el, exp, ctx) // 返回最近一个没有`v-else-if`或`v-else`的兄弟节点
    }
  }
}
// 文件 ./src/directives/if.ts

interface Branch {
  exp?: string | null // 该分支逻辑运算表达式
  el: Element // 该分支对应的模板元素,每次渲染时会以该元素为模板通过cloneNode复制一个实例插入到DOM树中
}

export const _if = (el: Element, exp: string, ctx: Context) => {
  const parent = el.parentElement!
  /* 锚点元素,由于v-if、v-else-if和v-else标识的元素可能在某个状态下都不位于DOM树上,
   * 因此通过锚点元素标记插入点的位置信息,当状态发生变化时则可以将目标元素插入正确的位置。
   */
  const anchor = new Comment('v-if')
  parent.insertBefore(anchor, el)

  // 逻辑分支,并将v-if标识的元素作为第一个分支
  const branches: Branch[] = [
    {
      exp, 
      el
    }
  ]

  /* 定位v-else-if和v-else元素,并推入逻辑分支中
   * 这里没有控制v-else-if和v-else的出现顺序,因此我们可以写成
   * 
   * 但效果为变成,最后的分支永远没有机会匹配。
   */
  let elseEl: Element | null
  let elseExp: string | null
  while ((elseEl = el.nextElementSibling)) {
    elseExp = null
    if (
      checkAttr(elseEl, 'v-else') === '' ||
      (elseExp = checkAttr(elseEl, 'v-else-if'))
    ) {
      // 从在线模板移除分支节点
      parent.removeChild(elseEl)
      branches.push({ exp: elseExp, el: elseEl })
    }
    else {
      break
    }
  }

  // 保存最近一个不带`v-else`和`v-else-if`节点作为下一轮遍历解析的模板节点
  const nextNode = el.nextSibling
  // 从在线模板移除带`v-if`节点
  parent.removeChild(el)

  let block: Block | undefined // 当前逻辑运算结构为true的分支对应块对象
  let activeBranchIndex: number = -1 // 当前逻辑运算结构为true的分支索引

  // 若状态发生变化导致逻辑运算结构为true的分支索引发生变化,则需要销毁原有分支对应块对象(包含中止旗下的副作用函数监控状态变化,执行指令的清理函数和递归触发子块对象的清理操作)
  const removeActiveBlock = () => {
    if (block) {
      // 重新插入锚点元素来定位插入点
      parent.insertBefore(anchor, block.el)
      block.remove()
      // 解除对已销毁的块对象的引用,让GC回收对应的JavaScript对象和detached元素
      block = undefined
    }
  }

  // 向异步任务对立压入渲染任务,在本轮Event Loop的Micro Queue执行阶段会执行一次
  ctx.effect(() => {
    for (let i = 0; i < branches.length; i++) {
      const { exp, el } = branches[i]
      if (!exp || evaluate(ctx.scope, exp)) {
        if (i !== activeBranchIndex) {
          removeActiveBlock()
          block = new Block(el, ctx)
          block.insert(parent, anchor)
          parent.removeChild(anchor)
          activeBranchIndex = i
        }
        return
      }
    }

    activeBranchIndex = -1
    removeActiveBlock()
  })

  return nextNode
}

下面我们看看子块对象的构造函数和insertremove方法

// 文件 ./src/block.ts

export class Block {
  constuctor(template: Element, parentCtx: Context, isRoot = false) {
    if (isRoot) {
      // ...
    }
    else {
      // 以v-if、v-else-if和v-else分支的元素作为模板创建元素实例
      this.template = template.cloneNode(true) as Element
    }

    if (isRoot) {
      // ...
    }
    else {
      this.parentCtx = parentCtx
      parentCtx.blocks.push(this)
      this.ctx = createContext(parentCtx)
    }
  }
  // 由于当前示例没有用到