• petitevue源码剖析vif和vfor的工作原理


    深入v-if的工作原理

    <div v-scope="App"></div>
    
    <script type="module">
      import { createApp } from 'https://unpkg.com/petite-vue?module'
    
      createApp({
        App: {
          $template: `
          <span v-if="status === 'offline'"> OFFLINE </span>
          <span v-else-if="status === 'UNKOWN'"> UNKOWN </span>
          <span v-else> ONLINE </span>
          `,
        }
        status: 'online'
      }).mount('[v-scope]')
    </script>
    

    人肉单步调试:

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

    解析<span v-if="status === 'offline'"> OFFLINE </span>

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

    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的出现顺序,因此我们可以写成
       * <span v-if="status=0"></span><span v-else></span><span v-else-if="status === 1"></span>
       * 但效果为变成<span v-if="status=0"></span><span v-else></span>,最后的分支永远没有机会匹配。
       */
      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)
        }
      }
      // 由于当前示例没有用到<template>元素,因此我对代码进行了删减
      insert(parent: Element, anchor: Node | null = null) {
        parent.insertBefore(this.template, anchor)
      }
    
      // 由于当前示例没有用到<template>元素,因此我对代码进行了删减
      remove() {
        if (this.parentCtx) {
          // TODO: function `remove` is located at @vue/shared
          remove(this.parentCtx.blocks, this)
        }
        // 移除当前块对象的根节点,其子孙节点都一并被移除
        this.template.parentNode!.removeChild(this.template) 
        this.teardown()
      }
    
      teardown() {
        // 先递归调用子块对象的清理方法
        this.ctx.blocks.forEach(child => {
          child.teardown()
        })
        // 包含中止副作用函数监控状态变化
        this.ctx.effects.forEach(stop)
        // 执行指令的清理函数
        this.ctx.cleanups.forEach(fn => fn())
      }
    }
    

    深入v-for的工作原理

    <div v-scope="App"></div>
    
    <script type="module">
      import { createApp } from 'https://unpkg.com/petite-vue?module'
    
      createApp({
        App: {
          $template: `
          <select>
            <option v-for="val of values" v-key="val">
              I'm the one of options
            </option>
          </select>
          `,
        }
        values: [1,2,3]
      }).mount('[v-scope]')
    </script>
    

    人肉单步调试:

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

    解析<option v-for="val in values" v-key="val">I'm the one of options</option>

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

    1. 识别元素带上v-for属性,调用_for原指令对该元素解析;
    2. 通过正则表达式提取v-for中集合和集合元素的表达式字符串,和key的表达式字符串;
    3. 基于每个集合元素创建独立作用域,并创建独立的块对象渲染元素。
    // 文件 ./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-for'))) {
          return _for(el, exp, ctx) // 返回最近一个没有`v-else-if`或`v-else`的兄弟节点
        }
      }
    }
    
    // 文件 ./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
        }
        // 由于我们示例只研究静态视图,因此重新渲染的代码,我们后面再深入了解吧
      })
    
      return nextNode
    }
    

    总结

    我们看到在v-ifv-for的解析过程中都会生成块对象,而且是v-if的每个分支都对应一个块对象,而v-for则是每个子元素都对应一个块对象。其实块对象不单单是管控DOM操作的单元,而且它是用于表示树结构不稳定的部分。如节点的增加和删除,将导致树结构的不稳定,把这些不稳定的部分打包成独立的块对象,并封装各自构建和删除时执行资源回收等操作,这样不仅提高代码的可读性也提高程序的运行效率。

    v-if的首次渲染和重新渲染采用同一套逻辑,但v-for在重新渲染时会采用key复用元素从而提高效率,可以重新渲染时的算法会复制不少。下一篇我们将深入了解v-for在重新渲染时的工作原理,敬请期待:)
    尊重原创,转载请注明来自:https://www.cnblogs.com/fsjohnhuang/p/15975744.html 肥仔John

  • 相关阅读:
    AudioToolbox学习(转)
    ios5键盘问题
    何时使用self.
    ios判断邮箱,手机号码,车牌号是否合法(正则表达)
    将图片重新绘制
    在UITableViewCell中获取所在的行数以及去除选中状态
    根据UIView获取其UIViewController
    在iOS中将string转成UTF8编码
    SQL提高性能
    oracle创建口令文件
  • 原文地址:https://www.cnblogs.com/fsjohnhuang/p/15975744.html
Copyright © 2020-2023  润新知