Skip to content

Vue.js 设计与实现

书籍链接

Vue.js 设计与实现

前言

  • Vue.js 3.0 在模块的拆分和设计上做得非常合理。模块之间的耦合度非常低,很多模块可以独立安装使用,而不需要依赖完整的 Vue.js 运行时,例如 @vue/reactivity 模块。

  • Vue.js 3.0 在设计内建组件和模块时也花费了很多精力,配合构建工具以及 Tree-Shaking 机制,实现了内建能力的按需引入,从而实现了用户 bundle 的体积最小化。

  • Vue.js 3.0 的扩展能力非常强,我们可以编写自定义的渲染器,甚至可以编写编译器插件来自定义模板语法。同时,Vue.js 3.0 在用户体验上也下足了功夫。

  • 另外,Vue.js 3.0 中很多功能的设计需要谨遵规范。例如,想要使用 Proxy 实现完善的响应系统,就必须从 ECMAScript 规范入手,而 Vue.js 的模板解析器则遵从 WHATWG 的相关规范。所以,在理解 Vue.js 3.0 核心设计思想的同时,我们还能够间接掌握阅读和理解规范,并据此编写代码的方法。

本书内容并非“源码解读”,而是建立在笔者对 Vue.js 框架设计的理解之上,以由简入繁的方式介绍如何实现 Vue.js 中的各个功能模块。

本书将尽可能地从规范出发,实现功能完善且严谨的 Vue.js 功能模块。例如,通过阅读 ECMAScript 规范,基于 Proxy 实现一个完善的响应系统;通过阅读 WHATWG 规范,实现一个类 HTML 语法的模板解析器,并在此基础上实现一个支持插件架构的模板编译器。

除此之外,本书还会讨论以下内容:

● 框架设计的核心要素以及框架设计过程中要做出的权衡;

● 三种常见的虚拟 DOM(Virtual DOM)的 Diff 算法;

● 组件化的实现与 Vue.js 内建组件的原理;

● 服务端渲染、客户端渲染、同构渲染之间的差异,以及同构渲染的原理。

本书结构

第一篇(框架设计概览)

  • 第 1 章主要讨论了命令式和声明式这两种范式的差异,以及二者对框架设计的影响,还讨论了虚拟 DOM 的性能状况,最后介绍了运行时和编译时的相关知识,并介绍了 Vue.js 3.0 是一个运行时 + 编译时的框架。
  • 第 2 章主要从用户的开发体验、控制框架代码的体积、Tree-Shaking 的工作机制、框架产物、特性开关、错误处理、TypeScript 支持等几个方面出发,讨论了框架设计者在设计框架时应该考虑的内容。
  • 第 3 章从全局视角介绍 Vue.js 3.0 的设计思路,以及各个模块之间是如何协作的。

第二篇(响应系统)

  • 第 4 章从宏观视角讲述了 Vue.js 3.0 中响应系统的实现机制。从副作用函数开始,逐步实现一个完善的响应系统,还讲述了计算属性和 watch 的实现原理,同时讨论了在实现响应系统的过程中所遇到的问题,以及相应的解决方案。
  • 第 5 章从 ECMAScript 规范入手,从最基本的 Proxy、Reflect 以及 JavaScript 对象的工作原理开始,逐步讨论了使用 Proxy 代理 JavaScript 对象的方式。
  • 第 6 章主要讨论了 ref 的概念,并基于 ref 实现原始值的响应式方案,还讨论了如何使用 ref 解决响应丢失问题。

第三篇(渲染器)

  • 第 7 章主要讨论了渲染器与响应系统的关系,讲述了两者如何配合工作完成页面更新,还讨论了渲染器中的一些基本名词和概念,以及自定义渲染器的实现与应用。
  • 第 8 章主要讨论了渲染器挂载与更新的实现原理,其中包括子节点的处理、属性的处理和事件的处理。当挂载或更新组件类型的虚拟节点时,还要考虑组件生命周期函数的处理等。
  • 第 9 章主要讨论了“简单 Diff 算法”的工作原理。
  • 第 10 章主要讨论了“双端 Diff 算法”的工作原理。
  • 第 11 章主要讨论了“快速 Diff 算法”的工作原理。

第四篇(组件化)

  • 第 12 章主要讨论了组件的实现原理,介绍了组件自身状态的初始化,以及由自身状态变化引起的组件自更新,还介绍了组件的外部状态(props)、由外部状态变化引起的被动更新,以及组件事件和插槽的实现原理。
  • 第 13 章主要介绍了异步组件和函数式组件的工作机制和实现原理。对于异步组件,我们还讨论了超时与错误处理、延迟展示 Loading 组件、加载重试等内容。
  • 第 14 章主要介绍了 Vue.js 内建的三个组件的实现原理,即 KeepAlive、Teleport 和 Transition 组件。

第五篇(编译器)

  • 第 15 章首先讨论了 Vue.js 模板编译器的工作流程,接着讨论了 parser 的实现原理与状态机,以及 AST 的转换与插件化架构,最后讨论了生成渲染函数代码的具体实现。
  • 第 16 章主要讨论了如何实现一个符合 WHATWG 组织的 HTML 解析规范的解析器,内容涵盖解析器的文本模式、文本模式对解析器的影响,以及如何使用递归下降算法构造模板 AST。在解析文本内容时,我们还讨论了如何根据规范解码字符引用。
  • 第 17 章主要讨论了 Vue.js 3.0 中模板编译优化的相关内容。具体包括:Block 树的更新机制、动态节点的收集、静态提升、预字符串化、缓存内联事件处理函数、v-once 等优化机制。

第六篇(服务端渲染)

  • 第 18 章主要讨论了 Vue.js 同构渲染的原理。首先探讨了 CSR、SSR 以及同构渲染等方案各自的优缺点,然后探讨了 Vue.js 进行服务端渲染和客户端激活的原理,最后总结了编写同构代码时的注意事项。

书中代码实现部分

响应系统的作用与实现

javascript
function traverse(value, seen = new Set()) {
  // 如果要读取地数据是原始值,或者已被读取过,那么什么都不做
  if (typeof value !== 'object' || value === null || seen.has(value)) return

  // 将数据添加到 seen 中,代表遍历地读取过了,避免循环引用引起的死循环
  seen.add(value)

  // 暂时不考虑数组等其他结构
  // 假设 value就是一个对象,使用 for...in 读取对象的每一个值,并递归地调用 traverse 进行处理
  for (const k in value) {
    traverse(value[k], seen)
  }

  return value
}

// watch
function watch(source, cb, options = {}) {
  // 定义 getter
  let getter

  // 如果 source 是函数,说明用户传递的是 getter,所以直接把 source 赋值给 getter
  if (typeof source === 'function') {
    getter = source
  } else {
    // 否则调用 traverse 递归地读取
    getter = () => traverse(source)
  }

  // 定义旧值和新值
  let oldValue, newValue

  // cleanup 用来存储用户注册的过期回调
  let cleanup
  // 定义 onInvalidate 函数
  function onInvalidate(fn) {
    // 将过期回调存储到 cleanup 中
    cleanup = fn
  }

  // 提取 scheduler 调度函数作为一个独立的 job 函数
  const job = () => {
    // 在 sheduler 中重新执行副作用函数,得到的是新值
    newValue = effectFn()

    // 在调用回调函数 cb 之前,先调用过期回调
    if (cleanup) {
      cleanup()
    }

    // 当数据变化时,调用回调函数 cb, 将旧值和新值作为参数,将 onInvalidate 作为回调函数的第三个参数,以便用户使用
    cb(newValue, oldValue, onInvalidate)

    // 更新旧值,不然下一次慧得到错误的旧值
    oldValue = newValue
  }

  // 使用 effect 注册副作用函数时,开启 lazy 选项,并把返回值存储到 effectFn 中以便后续手动调用
  const effectFn = effect(() => getter(), {
    lazy: true,
    // 使用 job 函数作为调度器函数
    scheduler: () => {
      // 当 flush 的值为 'post' 时,代表调度函数需要将副作用函数放到一个微任务队列中,并等待 DOM 更新结束后再执行
      if (options.flush === 'post') {
        const p = Promise.resolve()
        p.then(job)
      } else {
        job()
      }
    },
  })

  if (options.immediate) {
    // 当 immediate 为 true 时立即执行 job, 从而触发回调执行
    job()
  } else {
    // 手动调用副作用函数,拿到的值就是旧值
    oldValue = effectFn()
  }
}

// 计算属性
function computed(getter) {
  // value 用来缓存上一次计算的值
  let value

  // dirty 标志,用来标识是否需要重新计算值,为 true 则意味着“脏”,需要计算
  let dirty = true

  // 把 getter 作为副作用函数,创建一个 lazy 的 effect
  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      dirty = true
      // 当计算属性依赖的响应式数据发生变化时,手动调用trigger函数触发响应
      trigger(obj, 'value')
    },
  })

  const obj = {
    // 当读取 value 时才执行 effectFn
    get value() {
      // 只有“脏”时才计算值,并将得到的值缓存到 value 中
      if (dirty) {
        value = effectFn()
        // 将 dirty 设置为false, 下一次访问直接使用缓存到 value 中的值
        dirty = false
      }

      // 当读取 value 时,手动调用 track 函数进行追踪
      track(obj, 'value')
      return value
    },
  }

  return obj
}

// 定义一个任务队列
const jobQueue = new Set()

// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()

// 一个标志代表是否正在刷新队列
let isFlushing = false
function flushJob() {
  // 如果队列正在刷新,则什么都不做
  if (isFlushing) return

  // 设置为 true, 代表正在刷新
  isFlushing = true
  // 在微任务队列中刷新 jobQueue 队列
  p.then(() => {
    jobQueue.forEach((job) => job())
  }).finally(() => {
    isFlushing = false
  })
}

// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
  // 如果没有 activeEffect,直接 return
  if (!activeEffect) return

  // 根据target 从“桶”中取得depsMap,它也是一个 Map 类型:key --> effects
  let depsMap = bucket.get(target)

  // 如果不存在depsMap,新建一个 Map 并与 target 建立关联
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }

  // 再根据 key 从 depsMap 中取得 deps, 它是一个 Set 类型,里面存储着所有与当前 key 关联的副作用函数:effects
  let deps = depsMap.get(key)

  // 如果不存在deps, 新建一个 Set 并与 key 关联
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }

  // 把当前激活的副作用函数添加到依赖集合 deps 中
  deps.add(activeEffect)

  // deps 就是一个与当前副作用函数存在联系的依赖集合
  // 将其添加到 activeEffect.deps 数组中
  activeEffect.deps.push(deps) // 新增
}

// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
  // 根据 target 从桶里取得 depsMap,它是 key -> effects
  const depsMap = bucket.get(target)

  if (!depsMap) return

  // 根据key取得所有的副作用函数 effects
  const effects = depsMap.get(key)

  const effectsToRun = new Set()
  effects &&
    effects.forEach((effectFn) => {
      if (effectFn !== activeEffect) {
        effectsToRun.add(effectFn)
      }
    })

  effectsToRun.forEach((effectFn) => {
    // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      // 否则直接执行副作用函数
      effectFn()
    }
  })
}

// 用一个全局变量存储当前激活的副作用函数
let activeEffect

// effect 栈
const effectStack = []

/**
 * cleanup 函数接收副作用函数作为参数,遍历副作用函数的 effectFn.deps 数组,该数组的每一项都是一个依赖集合,
 * 然后将该副作用函数从依赖集合中移除,
 * 最后重置 effectFn.deps 数组。
 *
 */
function cleanup(effectFn) {
  // 遍历effectFn.deps 数组
  for (let i = 0; i < effectFn.deps.length; i++) {
    // deps 是依赖集合
    const deps = effectFn.deps[i]
    // 将 effectFn 从依赖集合中移除
    deps.delete(effectFn)
  }

  // 最后需要重置 effectFn.deps 数组
  effectFn.deps.length = 0
}

// effect函数用于注册副作用函数
function effect(fn, options = {}) {
  const effectFn = () => {
    // 调用 cleanup 函数完成清除工作
    cleanup(effectFn) // 新增

    // 当 effectFn 执行时,将其设置为当前激活的副作用函数
    activeEffect = effectFn

    // 在调用副作用函数之前将当前副作用函数压入栈中
    effectStack.push(effectFn)

    // 将 fn 的执行结果存储到 res 中
    const res = fn()

    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把activeEffect还原为之前的值
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]

    // 返回结果
    return res
  }

  // 将 options 挂载到 effectFn 上
  effectFn.options = options // 新增

  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = []

  // 只有非 lazy 的时候,才执行
  if (!options.lazy) {
    effectFn()
  }

  // 将副作用函数作为返回值返回
  return effectFn
}

// 存储副作用函数的"桶"
const bucket = new WeakMap()

// 原始数据
const data = {
  foo: 1,
  bar: 2,
}

// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal

    // 把副作用函数从桶中取出并执行
    trigger(target, key)
  },
})

非原始值的响应式方案

javascript
// 唯一key标识
const ITERATE_KEY = Symbol()
const MAP_KEY_ITERATE_KEY = Symbol()

// 存储副作用函数的"桶"
const bucket = new WeakMap()

function traverse(value, seen = new Set()) {
  // 如果要读取地数据是原始值,或者已被读取过,那么什么都不做
  if (typeof value !== 'object' || value === null || seen.has(value)) return

  // 将数据添加到 seen 中,代表遍历地读取过了,避免循环引用引起的死循环
  seen.add(value)

  // 暂时不考虑数组等其他结构
  // 假设 value就是一个对象,使用 for...in 读取对象的每一个值,并递归地调用 traverse 进行处理
  for (const k in value) {
    traverse(value[k], seen)
  }

  return value
}

// 定义一个任务队列
const jobQueue = new Set()

// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()

// 一个标志代表是否正在刷新队列
let isFlushing = false
function flushJob() {
  // 如果队列正在刷新,则什么都不做
  if (isFlushing) return

  // 设置为 true, 代表正在刷新
  isFlushing = true
  // 在微任务队列中刷新 jobQueue 队列
  p.then(() => {
    jobQueue.forEach((job) => job())
  }).finally(() => {
    isFlushing = false
  })
}

// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
  // 如果没有 activeEffect 或者 禁止追踪 直接 return
  if (!activeEffect || !shouldTrack) return

  // 根据target 从“桶”中取得depsMap,它也是一个 Map 类型:key --> effects
  let depsMap = bucket.get(target)

  // 如果不存在depsMap,新建一个 Map 并与 target 建立关联
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }

  // 再根据 key 从 depsMap 中取得 deps, 它是一个 Set 类型,里面存储着所有与当前 key 关联的副作用函数:effects
  let deps = depsMap.get(key)

  // 如果不存在deps, 新建一个 Set 并与 key 关联
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }

  // 把当前激活的副作用函数添加到依赖集合 deps 中
  deps.add(activeEffect)

  // deps 就是一个与当前副作用函数存在联系的依赖集合
  // 将其添加到 activeEffect.deps 数组中
  activeEffect.deps.push(deps) // 新增
}

const TriggerType = {
  SET: 'SET',
  ADD: 'ADD',
  DELETE: 'DELETE',
}

// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key, type, newVal) {
  // 根据 target 从桶里取得 depsMap,它是 key -> effects
  const depsMap = bucket.get(target)

  if (!depsMap) return

  // 根据key取得所有的副作用函数 effects
  const effects = depsMap.get(key)

  const effectsToRun = new Set()

  effects &&
    effects.forEach((effectFn) => {
      if (effectFn !== activeEffect) {
        effectsToRun.add(effectFn)
      }
    })

  // 操作类型为 ADD 或 DELETE 时,并且为 Map 类型数据
  if (
    (type === TriggerType.ADD || type === TriggerType.DELETE) &&
    Object.prototype.toString.call(target) === '[object Map]'
  ) {
    const iterateEffects = depsMap.get(MAP_KEY_ITERATE_KEY)
    iterateEffects &&
      iterateEffects.forEach((effectFn) => {
        if (effectFn !== activeEffect) {
          effectsToRun.add(effectFn)
        }
      })
  }

  // 只有当操作类型为 ADD 或 DELETE 时,才触发与 ITERATE_KEY 相关联的副作用函数重新执行
  // 如果操作类型为SET, 并且目标对象为 Map 类型的数据。也应该触发那些与 ITERATE_KEY相关联的副作用函数重新执行
  if (
    type === TriggerType.ADD ||
    type === TriggerType.DELETE ||
    (type === TriggerType.SET &&
      Object.prototype.toString.call(target) === '[object Map]')
  ) {
    // 取得与 TERATE_KEY 相关联的副作用函数
    const iterateEffects = depsMap.get(ITERATE_KEY)
    iterateEffects &&
      iterateEffects.forEach((effectFn) => {
        if (effectFn !== activeEffect) {
          effectsToRun.add(effectFn)
        }
      })
  }

  // 当操作类型为 ADD 并且目标对象是数组时,应该取出并执行那些与 length 属性
  // 相关联的副作用函数
  if (type === TriggerType.ADD && Array.isArray(target)) {
    // 取出与 length 相关联的副作用函数
    const lengthEffects = depsMap.get('length')
    // 将这些副作用函数添加到 effectsToRun 中,待执行
    lengthEffects &&
      lengthEffects.forEach((effectFn) => {
        if (effectFn !== activeEffect) {
          effectsToRun.add(effectFn)
        }
      })
  }

  // 如果操作目标是数组,并且修改了数组的 length 属性
  if (Array.isArray(target) && key === 'length') {
    // 对于索引大于或等于新的 length 值的元素,
    // 需要把所有相关联的副作用函数取出并添加到 effectsToRun 中待执行
    depsMap.forEach((effects, key) => {
      // 字符串不是一个有效的数字,会将其转换为特殊的 NaN, NaN与任何数值进行比较都会返回false。
      if (key >= newVal) {
        effects.forEach((effectFn) => {
          if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn)
          }
        })
      }
    })
  }

  effectsToRun.forEach((effectFn) => {
    // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      // 否则直接执行副作用函数
      effectFn()
    }
  })
}

// 用一个全局变量存储当前激活的副作用函数
let activeEffect

// effect 栈
const effectStack = []

/**
 * cleanup 函数接收副作用函数作为参数,遍历副作用函数的 effectFn.deps 数组,该数组的每一项都是一个依赖集合,
 * 然后将该副作用函数从依赖集合中移除,
 * 最后重置 effectFn.deps 数组。
 *
 */

function cleanup(effectFn) {
  // 遍历effectFn.deps 数组
  for (let i = 0; i < effectFn.deps.length; i++) {
    // deps 是依赖集合
    const deps = effectFn.deps[i]
    // 将 effectFn 从依赖集合中移除
    deps.delete(effectFn)
  }

  // 最后需要重置 effectFn.deps 数组
  effectFn.deps.length = 0
}

// effect函数用于注册副作用函数
function effect(fn, options = {}) {
  const effectFn = () => {
    // 调用 cleanup 函数完成清除工作
    cleanup(effectFn) // 新增

    // 当 effectFn 执行时,将其设置为当前激活的副作用函数
    activeEffect = effectFn

    // 在调用副作用函数之前将当前副作用函数压入栈中
    effectStack.push(effectFn)

    // 将 fn 的执行结果存储到 res 中
    const res = fn()

    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把activeEffect还原为之前的值
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]

    // 返回结果
    return res
  }

  // 将 options 挂载到 effectFn 上
  effectFn.options = options // 新增

  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = []

  // 只有非 lazy 的时候,才执行
  if (!options.lazy) {
    effectFn()
  }

  // 将副作用函数作为返回值返回
  return effectFn
}

function readonly(obj) {
  return createReactive(obj, false, true)
}

function shallowReadonly(obj) {
  return createReactive(obj, true, true)
}

// 定义一个 Map 实例,存储原始对象到代理对象的映射
const reactiveMap = new Map()

function reactive(obj) {
  // 优先通过原始对象 obj 寻找之前创建的代理对象,如果找到了,直接返回已有的代理对象

  const existionProxy = reactiveMap.get(obj)
  if (existionProxy) return existionProxy

  // 否则,创建新的代理对象
  const proxy = createReactive(obj)
  // 存储到 Map 中,从而避免重复创建
  reactiveMap.set(obj, proxy)

  return proxy
}

function shallowReactive(obj) {
  return createReactive(obj, true)
}

const arrayInstrumentations = {}
;['includes', 'indexOf', 'lastIndexOf'].forEach((method) => {
  const originMethod = Array.prototype[method]
  arrayInstrumentations[method] = function (...args) {
    // this 是代理对象,先在代理对象中查找,将结果存储到 res 中
    let res = originMethod.apply(this, args)

    if (res === false || res == -1) {
      // res 为 false 说明没找到,通过 this.raw 拿到原始数组,再去其中查找并更新 res 值
      res = originMethod.apply(this.raw, args)
    }

    // 返回最终结果
    return res
  }
})

// 一个标记变量,代表是否进行追踪,默认值为 true, 即允许追踪
let shouldTrack = true
// 重写数组的 push 方法
;['push', 'pop', 'shift', 'unshift', 'splice'].forEach((method) => {
  // 取得原始 push 方法
  const originMethod = Array.prototype[method]

  // 重写
  arrayInstrumentations[method] = function (...args) {
    // 在调用原始方法前,禁止追踪
    shouldTrack = false
    // push 方法的默认行为
    let res = originMethod.apply(this, args)
    // 在调用原始方法之后,恢复原来的行为,即允许追踪
    shouldTrack = true
    return res
  }
})

// 抽离为独立函数,便于复用
function iterationMethod() {
  // 获取原始数据对象 target
  const target = this.raw
  // 获取原始迭代器方法
  const itr = target[Symbol.iterator]()

  const wrap = (val) =>
    typeof val === 'object' && val !== null ? reactive(val) : val

  // 调用 track 函数建立响应联系
  track(target, ITERATE_KEY)

  // 将其返回
  return {
    next() {
      // 调用原始迭代器的 next 方法获取 value 和 done
      const { value, done } = itr.next()
      return {
        // 如果 value 不是 undefined, 则对其进行包裹
        value: value ? [wrap(value[0]), wrap(value[1])] : value,
        done,
      }
    },
    // 实现可迭代协议
    [Symbol.iterator]() {
      return this
    },
  }
}

function valuesIterationMethod() {
  // 获取原始数据对象 target
  const target = this.raw
  // 通过 target.values 获取原始迭代器方法
  const itr = target.values()

  const wrap = (val) => (typeof val === 'object' ? reactive(val) : val)

  track(target, ITERATE_KEY)

  return {
    next() {
      const { value, done } = itr.next()
      return {
        value: wrap(value),
        done,
      }
    },
    [Symbol.iterator]() {
      return this
    },
  }
}

function keysIterationMethod() {
  // 获取原始数据对象 target
  const target = this.raw
  // 通过 target.values 获取原始迭代器方法
  const itr = target.keys()

  const wrap = (val) => (typeof val === 'object' ? reactive(val) : val)

  // 调用 track 函数追踪依赖,在副作用函数与 MAP_KEY_ITERATE_KEY 之间建立响应联系
  track(target, MAP_KEY_ITERATE_KEY)

  return {
    next() {
      const { value, done } = itr.next()
      return {
        value: wrap(value),
        done,
      }
    },
    [Symbol.iterator]() {
      return this
    },
  }
}

// 定义一个对象,将自定义的 add 方法定义到该对象下
const mutableInstrumentations = {
  add(key) {
    // this 仍指向代理对象,通过 raw 属性获取原始数据对象。
    const target = this.raw

    const hadKey = target.has(key)
    // 通过原始数据对象执行 add 方法添加具体的值。
    // 注意,这里不再需要.bind 了,因为是直接通过 target 调用并执行的
    const res = target.add(key)
    if (!hadKey) {
      // 调用 trigger 函数触发响应,并指定操作类型为 ADD
      trigger(target, key, TriggerType.ADD)
    }
    // 返回操作结果
    return res
  },
  delete(key) {
    const target = this.raw
    const hadKey = target.has(key)
    const res = target.delete(key)
    // 当要删除的元素确实存在时,才触发响应
    if (hadKey) {
      trigger(target, key, TriggerType.DELETE)
    }
    return res
  },
  get(key) {
    // 获取原始对象
    const target = this.raw
    // 判断读取的 key 是否存在
    const had = target.has(key)
    // 追踪依赖,建立响应联系
    track(target, key)
    // 如果存在,则返回结果。这里要注意的是,如果得到的结果 res 仍然是可代理的数据
    // 则要返回使用 reactive 包装后的响应式数据
    if (had) {
      const res = target.get(key)
      return typeof res === 'object' ? reactive(res) : res
    }
  },
  set(key, value) {
    const target = this.raw
    const had = target.has(key)
    // 获取旧值
    const oldValue = target.get(key)

    // 获取原始数据,由于 value 本身可能已经是原始数据,所以此时 value.raw 不存在,则直接使用 value
    const rawValue = value.raw || value
    // 设置新值
    target.set(key, rawValue)

    // 如果不存在,则说明是 ADD 类型的操作,意味着新增
    if (!had) {
      trigger(target, key, TriggerType.ADD)
    } else if (
      oldValue !== value ||
      (oldValue === oldValue && value === value)
    ) {
      // 如果不存在,且值改变,则是 SET 类型的操作,意味着修改
      trigger(target, key, TriggerType.SET)
    }
  },
  forEach(callback, thisArg) {
    const wrap = (val) => (typeof val === 'object' ? reactive(val) : val)
    const target = this.raw
    track(target, ITERATE_KEY)

    target.forEach((v, k) => {
      callback.call(thisArg, wrap(v), wrap(k), this)
    })
  },
  [Symbol.iterator]: iterationMethod,
  entries: iterationMethod,
  values: valuesIterationMethod,
  keys: keysIterationMethod,
}

// 封装 createReactive 函数,接收一个参数 isShallow,代表是否为浅响应,默认为false,即非浅响应
// 第三个参数 isReadonly,代表是否只读,默认为 false,即非只读
function createReactive(obj, isShallow = false, isReadonly = false) {
  return new Proxy(obj, {
    // 拦截读取操作
    get(target, key, receiver) {
      // 代理对象可以通过 raw 属性访问原始数据
      if (key === 'raw') {
        return target
      }

      if (key === 'size') {
        track(target, ITERATE_KEY)
        return Reflect.get(target, key, target)
      }

      // 返回定义在 mutableInstrumentations 对象下的方法
      return mutableInstrumentations[key]

      // 如果操作的目标对象是数组,并且 key 存在于 arrayInstrumentations
      // 那么返回定义在 arrayInstrumentations 上的值
      if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) {
        return Reflect.get(arrayInstrumentations, key, receiver)
      }

      // 非只读,且key的类型不为 symbol 的时候才需要建立响应联系
      if (!isReadonly && typeof key !== 'symbol') {
        // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
        track(target, key)
      }

      // 使用 Reflect.get 返回读取到的属性值
      const res = Reflect.get(target, key, receiver)

      if (isShallow) {
        return res
      }

      if (typeof res === 'object' && res !== null) {
        // 调用 reactive 将结果包装成响应式数据并返回
        return isReadonly ? readonly(res) : reactive(res)
      }

      // 正常返回 res
      return res
    },
    // 拦截设置操作
    set(target, key, newVal, receiver) {
      // 如果是只读的,则打印警告信息并返回
      if (isReadonly) {
        console.warn(`属性 ${key} 是只读的`)
        return true
      }

      // 先获取旧值
      const oldVal = target[key]

      // 如果属性不存在,则说明是在添加新属性,否则是设置已有属性
      // 如果代理目标是数组,则检测被设置的索引值是否小于数组长度
      // 如果是,则视作 SET 操作,否则是 ADD 操作
      const type = Array.isArray(target)
        ? Number(key) < target.length
          ? TriggerType.SET
          : TriggerType.ADD
        : Object.prototype.hasOwnProperty.call(target, key)
        ? TriggerType.SET
        : TriggerType.ADD

      // 设置属性值
      const res = Reflect.set(target, key, newVal, receiver)

      // target === receiver.raw 说明 receiver 就是 target 的代理对象
      if (target === receiver.raw) {
        // 比较新值和旧值,只有不全等,并且不都是 NaN 时才触发响应
        if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
          // 把副作用函数从桶中取出并执行,把 type 作为第三个参数传递给 trigger 函数
          trigger(target, key, type, newVal)
        }
      }

      return res
    },
    // 拦截删除操作
    deleteProperty(target, key) {
      // 如果是只读的,则打印警告信息并返回
      if (isReadonly) {
        console.warn(`属性 ${key} 是只读的`)
        return true
      }

      // 检查被操作的属性是否是对象自己的属性
      const hadKey = Object.prototype.hasOwnProperty.call(target, key)

      // 使用 Reflect.deleteProperty 完成属性的删除
      const res = Reflect.deleteProperty(target, key)

      if (res && hadKey) {
        // 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
        trigger(target, key, TriggerType.DELETE)
      }
    },
    // 拦截 in 操作
    has(target, key) {
      track(target, key)
      return Reflect.has(target, key)
    },
    // 拦截 for ... in 操作
    ownKeys(target) {
      // 将副作用函数与 ITERATE_KEY 关联, 如果操作目标 target 是数组,则使用 length 属性作为 key 并建立响应联系
      track(target, Array.isArray(target) ? 'length' : ITERATE_KEY)
      return Reflect.ownKeys(target)
    },
  })
}

原始值的响应式方案

javascript
function ref(val) {
  // 在 ref 函数内部创建包裹对象
  const wrapper = {
    value: val,
  }

  // 实现区分一个数据为ref:在 wrapper 对象上定义一个不可枚举的属性 __v_isRef,并且值为true
  Object.defineProperty(wrapper, '__v_isRef', {
    value: true,
  })

  // 将包裹对象变成响应式数据
  return reactive(wrapper)
}

function toRef(obj, key) {
  const wrapper = {
    get value() {
      return obj[key]
    },
    set value(val) {
      obj[key] = val
    },
  }

  // 定义 __v_isRef 属性
  Object.defineProperty(wrapper, '__v_isRef', {
    value: true,
  })

  return wrapper
}

function toRefs(obj) {
  const ret = {}
  // 使用 for...in 循环遍历对象
  for (const key in obj) {
    // 逐个调用 toRef 完成转换
    ret[key] = toRef(obj, key)
  }
  return ret
}

function proxyRefs(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver)
      // 自动脱 ref 实现,如果读取的值是 ref, 则返回它的 value 属性值
      return value.__v_isRef ? value.value : value
    },
    set(target, key, newValue, receiver) {
      // 通过 target 读取真实值
      const value = target[key]
      // 如果值是 Ref,则设置其对应的 value 属性值
      if (value.__v_isRef) {
        value.value = newValue
        return true
      }
      return Reflect.set(target, key, newValue, receiver)
    },
  })
}

挂载与更新

javascript
// 文本节点的 type 标识
const Text = Symbol()
// 注释节点的 type 标识
const Comment = Symbol()
// 片段节点的 type 标识
const Fragment = Symbol()

// 返回 false 说明不应该作为 DOM Properties 设置
function shouldSetAsProps(el, key, value) {
  // 特殊处理 form 为只读属性,只能通过setAttribute设置
  if (key === 'form' && el.tagName === 'INPUT') {
    return false
  }
  // 兜底
  return key in el
}

function createRenderer(options) {
  // 通过 options 得到操作DOM的API
  const { createElement, insert, setElementText, patchProps } = options

  function patchChildren(n1, n2, container) {
    // 判断新子节点的类型是否为文本节点
    if (typeof n2.children === 'string') {
      // 旧子节点的类型有三种可能性:无子节点, 文本子节点,数组子节点
      // 只有当旧子节点为数组子节点时,才需要逐个卸载,其他情况下什么都不做
      if (Array.isArray(n1.children)) {
        n1.children.forEach((c) => unmount(c))
      }

      // 最后将新的文本节点内容设置给容器元素
      setElementText(container, n2.children)
    } else if (Array.isArray(n2.children)) {
      // 说明新子节点是数组子节点
      // 判断旧子节点是否也是数组子节点
      if (Array.isArray(n1.children)) {
        // 核心diff算法
        // 简单做法:卸载旧的,挂载新的
        n1.forEach((c) => unmount(c))

        setElementText(container, n2.children)
      } else {
        // 旧子节点为文本子节点或不存在
        setElementText(container, '')
        n2.children.forEach((c) => patch(null, c, container))
      }
    } else {
      // 新子节点不存在
      if (Array.isArray(n1.children)) {
        n1.children.forEach((c) => unmount(c))
      } else if (typeof n1.children === 'string') {
        setElementText(container, '')
      }
      // 如果旧子节点也不存在,什么都不需要做
    }
  }

  function patchElement(n1, n2) {
    const el = (n2.el = n1.el)
    const oldProps = n1.props
    const newProps = n2.props

    // 1) 更新props
    for (const key in newProps) {
      if (newProps[key] !== oldProps[key]) {
        patchProps(el, key, oldProps[key], newProps[key])
      }
    }

    for (const key in oldProps) {
      if (!(key in newProps)) {
        patchProps(el, key, oldProps[key], null)
      }
    }

    // 2) 更新 children
    patchChildren(n1, n2, el)
  }

  function mountElement(vnode, container) {
    // 让 vnode.el 引用真实 DOM 元素
    const el = (vnode.el = createElement(vnode.type))

    // 如果 children 是字符串类型,直接设置为文本内容
    if (typeof vnode.children === 'string') {
      setElementText(el, vnode.children)
    } else if (Array.isArray(vnode.children)) {
      // 如果 children 为数组类型,分发新增子节点
      vnode.children.forEach((child) => {
        patch(null, child, el)
      })
    }

    // 如果 vnode.props 存在才处理它
    if (vnode.props) {
      for (const key in vnode.props) {
        // 调用 patchProps 函数即可
        patchProps(el, key, null, vnode.props[key])
      }
    }

    insert(el, container)
  }

  function patch(n1, n2, container) {
    // 如果 n1 存在,则对比 n1 和 n2 的类型
    if (n1 && n1.type !== n2.type) {
      // 如果新旧 vnode 的类型不同,则直接将旧 vnode 卸载
      unmount(n1)
      n1 = null
    }

    const { type } = n2
    if (typeof type === 'string') {
      // 普通标签元素
      if (!n1) {
        mountElement(n2, container)
      } else {
        patchElement(n1, n2)
      }
    } else if (typeof type === 'object') {
      // 如果 n2.type 的值的类型是对象,则它描述的是组件
    } else if (type === Text) {
      // 如果新 vnode 的类型是 Text,说明该 vnode 描述的是文本节点
      // 如果没有旧节点,则进行挂载
      if (!n1) {
        // 使用 createTextNode 创建文本节点
        const el = (n2.el = createText(n2.children))
        // 将文本节点插入到容器中
        insert(el, container)
      } else {
        // 如果旧 vnode 存在,只需要使用新文本节点的文本内容更新旧文本节点即可
        const el = (n2.el = n1.el)
        if (n2.children !== n1.children) {
          setText(el, n2.children)
        }
      }
    } else if (type === Fragment) {
      // 处理 Fragment 类型的 vnode
      if (!n1) {
        // 旧 vnode 不存在,则只需要将 Fragment 的 children 逐个挂载即可
        n2.children.forEach((c) => patch(null, c, container))
      } else {
        // 旧 vnode 存在,则只需要更新 Fragment 的 children 即可
        patchChildren(n1, n2, children)
      }
    }
  }

  function unmount(vnode) {
    // 在卸载时,如果卸载的 vnode 类型为 Fragment,则需要卸载其 children
    if (vnode.type === Fragment) {
      vnode.children.forEach((c) => unmount(c))
      return
    }
    // 获取 el 的父元素
    const parent = vnode.el.parentNode
    // 调用removeChild 移除元素
    if (parent) parent.removeChild(el)
  }

  function render(vnode, container) {
    if (vnode) {
      // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行打补丁
      patch(container._vnode, vnode, container)
    } else {
      if (container._vnode) {
        // 无新 vnode, 有旧 vnode, 卸载操作
        unmount(container._vnode)
      }
    }

    // 存储 vnode
    container._vnode = vnode
  }

  function hydrate(vnode, container) {}

  return {
    render,
    hydrate,
  }
}

const renderer = createRenderer({
  createElement(tag) {
    return document.createElement(tag)
  },
  setElementText(el, text) {
    el.textContent = text
  },
  // 用于在给定的 parent 下添加指定元素
  insert(el, parent, anchor = null) {
    parent.insertBefore(el, anchor)
  },
  // 将属性设置相关操作封装到 patchProps 函数中,并作为渲染器选项传递
  patchProps(el, key, prevValue, nextValue) {
    if (/^on/.test(key)) {
      // 定义 el._vei 为一个对象,存在事件名称到事件处理函数的映射
      let invokers = el._vei || (el._vei = {})

      let invoker = invokers[key]
      // 根据属性名称得到对应的事件名称,例如 onClick ---> click
      const name = key.slice(2).toLowerCase()
      if (nextValue) {
        if (!invoker) {
          // vei:vue event invoker
          // 将事件处理函数缓存到 el._vei[key] 下,避免覆盖
          invoker = el._vei[key] = (e) => {
            // e.timestamp 是事件发生的时间
            // 如果事件发生的时间早于事件处理函数绑定的时间,则不执行事件处理函数
            if (e.timeStamp < invoker.attached) return
            if (Array.isArray(invoker.value)) {
              invoker.value.forEach((fn) => fn(e))
            } else {
              // 否则直接作为函数调用
              invoker.value(e)
            }
          }

          // 当真正的事件处理函数执行时,会执行真正的事件处理函数
          invoker.value = nextValue
          // 添加invoker.attached 属性,存储事件处理函数被绑定的时间
          invoker.attached = performance.now()
          // 绑定 invoker 作为事件处理函数
          el.addEventListener(name, invoker)
        } else {
          // 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value 的值即可
          invoker.value = nextValue
        }
      } else if (invoker) {
        // 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
        el.removeEventListener(name, invoker)
      }
    } else if (key === 'class') {
      // 对 class 进行特殊处理
      el.className = nextValue || ''
    } else if (shouldSetAsProps(el, key, nextValue)) {
      // 获取该 DOM Properties 的类型
      const type = typeof el[key]
      // 如果是布尔类型,并且 value 是空字符串,则将其矫正为true
      if (type === 'boolean' && nextValue === '') {
        el[key] = true
      } else {
        el[key] = nextValue
      }
    } else {
      // 如果要设置的属性没有对应的 DOM Properties, 则使用 setAttribute函数设置属性
      el.setAttribute(key, nextValue)
    }
  },
  createText(text) {
    return document.createTextNode(text)
  },
  setText(el, text) {
    el.nodeValue = text
  },
})

简单 Diff 算法

javascript
// 文本节点的 type 标识
const Text = Symbol()
// 注释节点的 type 标识
const Comment = Symbol()
// 片段节点的 type 标识
const Fragment = Symbol()

// 返回 false 说明不应该作为 DOM Properties 设置
function shouldSetAsProps(el, key, value) {
  // 特殊处理 form 为只读属性,只能通过setAttribute设置
  if (key === 'form' && el.tagName === 'INPUT') {
    return false
  }
  // 兜底
  return key in el
}

// 通过配置创建具体平台的渲染器
function createRenderer(options) {
  // 通过 options 得到操作DOM的API
  const { createElement, insert, setElementText, patchProps } = options

  function patchChildren(n1, n2, container) {
    // 判断新子节点的类型是否为文本节点
    if (typeof n2.children === 'string') {
      // 旧子节点的类型有三种可能性:无子节点, 文本子节点,数组子节点
      // 只有当旧子节点为数组子节点时,才需要逐个卸载,其他情况下什么都不做
      if (Array.isArray(n1.children)) {
        n1.children.forEach((c) => unmount(c))
      }

      // 最后将新的文本节点内容设置给容器元素
      setElementText(container, n2.children)
    } else if (Array.isArray(n2.children)) {
      // 说明新子节点是数组子节点
      // 核心diff算法
      // 简单做法:卸载旧的,挂载新的
      const oldChildren = n1.children
      const newChildren = n2.children

      // 用来存储寻找过程中遇到的最大索引值
      let lastIndex = 0

      for (let i = 0; i < newChildren.length; i++) {
        const newVNode = newChildren[i]
        let j = 0

        // 在第一层循环中定义变量 find,代表是否在旧的一组子节点中找到可复用的节点,
        // 初始值为 false,代表没找到
        let find = false

        for (j; j < oldChildren.length; j++) {
          const oldVNode = oldChildren[j]
          if (newVNode.key === oldVNode.key) {
            // 一旦找到可复用的节点,则将变量 find 的值设为 true
            find = true
            patch(oldVNode, newVNode, container)
            if (j < lastIndex) {
              // 如果当前找到的节点在旧 children 中的索引小于最大索引值 lastIndex
              // 说明该节点对应的真实 DOM 需要移动
              // 先获取newVNode 的前一个 vnode, 即 prevVNode
              const prevVNode = newChildren[i - 1]
              // 如果 prevVNode 不存在,则说明当前 newVNode 是第一个节点,不需要移动它
              if (prevVNode) {
                // 由于我们要将 newVNode 对应的真实 DOM 移动到 prevVNode 所对应真实 DOM 后面,
                // 所以我们需要获取 prevVNode 所对应真实 DOM 的下一个兄弟节点,并将其作为锚点
                const anchor = prevVNode.el.nextSibling
                // 调用 insert 方法将 newVNode 对应的真实 DOM 插入到锚点元素前面,
                // 也就是 prevVNode 对应真实 DOM 的后面
                insert(newVNode.el, container, anchor)
              }
            } else {
              // 如果当前找到的节点在旧 children 中的索引不小于最大索引值
              // 则更新 lastIndex 的值
              lastIndex = j
            }
            break
          }
        }

        // 如果代码运行到这里,find 仍然为 false,
        // 说明当前 newVNode 没有在旧的一组子节点中找到可复用的节点
        // 也就是说,当前 newVNode 是新增节点,需要挂载
        if (!find) {
          // 为了将节点挂载到正确位置,我们需要先获取锚点元素
          // 首先获取当前 newVNode 的前一个 vnode 节点
          const prevVNode = newChildren[i - 1]
          let anchor = null
          if (prevVNode) {
            // 如果有前一个 vnode 节点,则使用它的下一个兄弟节点为锚点元素
            anchor = prevVNode.el.nextSibling
          } else {
            // 如果没有前一个 vnode 节点,说明即将挂载的新节点是第一个子节点
            // 这时我们使用容器元素的 firstChild 作为锚点
            anchor = container.firstChild
          }

          // 挂载 newVNode
          patch(null, newVNode, container, anchor)
        }
      }

      for (let i = 0; i < oldChildren.length; i++) {
        const oldVNode = oldChildren[i]
        const has = newChildren.find((vnode) => vnode.key === oldVNode.key)
        if (!has) {
          // 如果没有找到具有相同key值得节点,则说明需要删除该节点
          unmount(oldVNode)
        }
      }
    } else {
      // 新子节点不存在
      if (Array.isArray(n1.children)) {
        n1.children.forEach((c) => unmount(c))
      } else if (typeof n1.children === 'string') {
        setElementText(container, '')
      }
      // 如果旧子节点也不存在,什么都不需要做
    }
  }

  function patchElement(n1, n2) {
    const el = (n2.el = n1.el)
    const oldProps = n1.props
    const newProps = n2.props

    // 1) 更新props
    for (const key in newProps) {
      if (newProps[key] !== oldProps[key]) {
        patchProps(el, key, oldProps[key], newProps[key])
      }
    }

    for (const key in oldProps) {
      if (!(key in newProps)) {
        patchProps(el, key, oldProps[key], null)
      }
    }

    // 2) 更新 children
    patchChildren(n1, n2, el)
  }

  function mountElement(vnode, container, anchor) {
    // 让 vnode.el 引用真实 DOM 元素
    const el = (vnode.el = createElement(vnode.type))

    // 如果 children 是字符串类型,直接设置为文本内容
    if (typeof vnode.children === 'string') {
      setElementText(el, vnode.children)
    } else if (Array.isArray(vnode.children)) {
      // 如果 children 为数组类型,分发新增子节点
      vnode.children.forEach((child) => {
        patch(null, child, el)
      })
    }

    // 如果 vnode.props 存在才处理它
    if (vnode.props) {
      for (const key in vnode.props) {
        // 调用 patchProps 函数即可
        patchProps(el, key, null, vnode.props[key])
      }
    }

    insert(el, container, anchor)
  }

  function patch(n1, n2, container, anchor) {
    // 如果 n1 存在,则对比 n1 和 n2 的类型
    if (n1 && n1.type !== n2.type) {
      // 如果新旧 vnode 的类型不同,则直接将旧 vnode 卸载
      unmount(n1)
      n1 = null
    }

    const { type } = n2
    if (typeof type === 'string') {
      // 普通标签元素
      if (!n1) {
        // 挂载时将锚点元素作为第三个参数传递给 mountElement 函数
        mountElement(n2, container, anchor)
      } else {
        patchElement(n1, n2)
      }
    } else if (typeof type === 'object') {
      // 如果 n2.type 的值的类型是对象,则它描述的是组件
    } else if (type === Text) {
      // 如果新 vnode 的类型是 Text,说明该 vnode 描述的是文本节点
      // 如果没有旧节点,则进行挂载
      if (!n1) {
        // 使用 createTextNode 创建文本节点
        const el = (n2.el = createText(n2.children))
        // 将文本节点插入到容器中
        insert(el, container)
      } else {
        // 如果旧 vnode 存在,只需要使用新文本节点的文本内容更新旧文本节点即可
        const el = (n2.el = n1.el)
        if (n2.children !== n1.children) {
          setText(el, n2.children)
        }
      }
    } else if (type === Fragment) {
      // 处理 Fragment 类型的 vnode
      if (!n1) {
        // 旧 vnode 不存在,则只需要将 Fragment 的 children 逐个挂载即可
        n2.children.forEach((c) => patch(null, c, container))
      } else {
        // 旧 vnode 存在,则只需要更新 Fragment 的 children 即可
        patchChildren(n1, n2, children)
      }
    }
  }

  function unmount(vnode) {
    // 在卸载时,如果卸载的 vnode 类型为 Fragment,则需要卸载其 children
    if (vnode.type === Fragment) {
      vnode.children.forEach((c) => unmount(c))
      return
    }
    // 获取 el 的父元素
    const parent = vnode.el.parentNode
    // 调用removeChild 移除元素
    if (parent) parent.removeChild(el)
  }

  function render(vnode, container) {
    if (vnode) {
      // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行打补丁
      patch(container._vnode, vnode, container)
    } else {
      if (container._vnode) {
        // 无新 vnode, 有旧 vnode, 卸载操作
        unmount(container._vnode)
      }
    }

    // 存储 vnode
    container._vnode = vnode
  }

  function hydrate(vnode, container) {}

  return {
    render,
    hydrate,
  }
}

const renderer = createRenderer({
  // 创建标签节点
  createElement(tag) {
    return document.createElement(tag)
  },
  // 设置节点内容为文本
  setElementText(el, text) {
    el.textContent = text
  },
  // 用于在给定的 parent 下添加指定元素
  insert(el, parent, anchor = null) {
    parent.insertBefore(el, anchor)
  },
  // 将属性设置相关操作封装到 patchProps 函数中,并作为渲染器选项传递
  patchProps(el, key, prevValue, nextValue) {
    if (/^on/.test(key)) {
      // 定义 el._vei 为一个对象,存在事件名称到事件处理函数的映射
      let invokers = el._vei || (el._vei = {})

      let invoker = invokers[key]
      // 根据属性名称得到对应的事件名称,例如 onClick ---> click
      const name = key.slice(2).toLowerCase()
      if (nextValue) {
        if (!invoker) {
          // vei:vue event invoker
          // 将事件处理函数缓存到 el._vei[key] 下,避免覆盖
          invoker = el._vei[key] = (e) => {
            // e.timestamp 是事件发生的时间
            // 如果事件发生的时间早于事件处理函数绑定的时间,则不执行事件处理函数
            if (e.timeStamp < invoker.attached) return
            if (Array.isArray(invoker.value)) {
              invoker.value.forEach((fn) => fn(e))
            } else {
              // 否则直接作为函数调用
              invoker.value(e)
            }
          }

          // 当真正的事件处理函数执行时,会执行真正的事件处理函数
          invoker.value = nextValue
          // 添加invoker.attached 属性,存储事件处理函数被绑定的时间
          invoker.attached = performance.now()
          // 绑定 invoker 作为事件处理函数
          el.addEventListener(name, invoker)
        } else {
          // 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value 的值即可
          invoker.value = nextValue
        }
      } else if (invoker) {
        // 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
        el.removeEventListener(name, invoker)
      }
    } else if (key === 'class') {
      // 对 class 进行特殊处理
      el.className = nextValue || ''
    } else if (shouldSetAsProps(el, key, nextValue)) {
      // 获取该 DOM Properties 的类型
      const type = typeof el[key]
      // 如果是布尔类型,并且 value 是空字符串,则将其矫正为true
      if (type === 'boolean' && nextValue === '') {
        el[key] = true
      } else {
        el[key] = nextValue
      }
    } else {
      // 如果要设置的属性没有对应的 DOM Properties, 则使用 setAttribute函数设置属性
      el.setAttribute(key, nextValue)
    }
  },
  // 创建文本节点
  createText(text) {
    return document.createTextNode(text)
  },
  // 设置节点内容为文本
  setText(el, text) {
    el.nodeValue = text
  },
})

双端 Diff 算法

javascript
// 跟上边简单 Diff 算法 差别体现在以下两个函数
// 核心实现是 patchKeyedChildren函数

function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children
  const newChildren = n2.children

  let oldStartIdx = 0
  let oldEndIdx = oldChildren.length - 1
  let newStartIdx = 0
  let newEndIdx = newChildren.length - 1

  let oldStartVNode = oldChildren[oldStartIdx]
  let oldEndVNode = oldChildren[oldEndIdx]
  let newStartVNode = newChildren[newStartIdx]
  let newEndVNode = newChildren[newEndIdx]

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 增加两个判断分支,如果旧头/尾部节点为 undefined,则说明该节点已经被处理过了,需要跳过到下一个位置
    if (!oldStartVNode) {
      oldStartVNode = oldChildren[++oldStartIdx]
    } else if (!oldEndVNode) {
      oldEndVNode = oldChildren[--oldEndIdx]
    } else if (oldStartVNode.key === newStartVNode.key) {
      patch(oldStartVNode, newStartVNode, container)
      oldStartVNode = oldChildren[++oldStartIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else if (oldEndVNode.key === newEndVNode.key) {
      // 节点在新的顺序中仍然处于尾部,不需要移动,但仍需要打补丁
      patch(oldEndVNode, newEndVNode, container)
      oldEndVNode = oldChildren[--oldEndIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (oldStartVNode.key === newEndVNode.key) {
      patch(oldStartVNode, newEndVNode, container)
      // 将旧的一组子节点的头部节点对应的真实 DOM 节点 oldStartVNode.el 移动到
      // 旧的一组子节点的尾部节点对应的真实 DOM 节点后面
      insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)

      // 更新相关索引到下一个位置
      oldStartVNode = oldChildren[++oldStartIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (oldEndVNode.key === newStartVNode.key) {
      // 仍然需要调用 patch 函数进行打补丁
      patch(oldEndVNode, newStartVNode, container)
      // 移动 DOM 操作
      // oldEndVNode.el 移动到 oldStartVNode.el 前面
      insert(oldEndVNode.el, container, oldStartVNode.el)

      // 移动 DOM 完成后,更新索引值,并指向下一个位置
      oldEndVNode = oldChildren[--oldEndIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else {
      // 遍历旧 children, 试图寻找与 newStartVNode 拥有相同 key 值的元素
      const idxInOld = oldChildren.findIndex(
        (node) => node.key === newStartVNode.key
      )

      // idxInOld 大于 0,说明找到了可复用的节点,并且需要将其对应的真实 DOM 移动到头部
      if (idxInOld > 0) {
        // idxInOld 位置对应的 vnode 就是需要移动的节点
        const vnodeToMove = oldChildren[idxInOld]
        patch(vnodeToMove.el, newStartVNode, container)
        // 将 vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前,因此使用后者作为锚点
        insert(vnodeToMove.el, container, oldStartVNode.el)
        // 由于位置 idxInOld 处的节点所对应的真实 DOM 已经移动到别处,因此将其设置为 undefined
        oldChildren[idxInOld] = undefined
      } else {
        // 新增节点
        // 将 newStartVNode 作为新节点挂载到头部,使用当前头部节点 oldStartVNode.el 作为锚点
        patch(null, newStartVNode, container, oldStartVNode.el)
      }
      // 最后更新 newStartIdx 到下一个位置
      newStartVNode = newChildren[++newStartIdx]
    }
  }

  // 循环结束后检查索引值的情况
  if (oldEndIdx <= oldStartIdx && newStartIdx <= newEndIdx) {
    // 如果满足条件,则说明有新的节点遗留,需要挂载他们
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      patch(null, newChildren[i], container, oldStartVNode.el)
    }
  } else if (newEndIdx <= newStartIdx && oldStartIdx <= oldEndIdx) {
    // 移除操作
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
      unmount(oldChildren[i])
    }
  }
}

function patchChildren(n1, n2, container) {
  // 判断新子节点的类型是否为文本节点
  if (typeof n2.children === 'string') {
    // 旧子节点的类型有三种可能性:无子节点, 文本子节点,数组子节点
    // 只有当旧子节点为数组子节点时,才需要逐个卸载,其他情况下什么都不做
    if (Array.isArray(n1.children)) {
      n1.children.forEach((c) => unmount(c))
    }

    // 最后将新的文本节点内容设置给容器元素
    setElementText(container, n2.children)
  } else if (Array.isArray(n2.children)) {
    // 说明新子节点是数组子节点
    // 核心diff算法
    // 封装 patchKeyedChildren 函数处理两组子节点
    patchKeyedChildren(n1, n2, container)
  } else {
    // 新子节点不存在
    if (Array.isArray(n1.children)) {
      n1.children.forEach((c) => unmount(c))
    } else if (typeof n1.children === 'string') {
      setElementText(container, '')
    }
    // 如果旧子节点也不存在,什么都不需要做
  }
}

快速 Diff 算法

javascript
// 核心实现是 patchKeyedChildren函数
// 计算最长递增子序列函数 lis 自行实现
function patchKeyedChildren(n1, n2, container) {
  const newChildren = n2.children
  const oldChildren = n1.children

  // 更新相同的前置节点
  // 索引 j 指向新旧两组子节点的开头
  let j = 0
  let oldVNode = oldChildren[j]
  let newVNode = newChildren[j]

  // while 循环向后遍历,直到遇到拥有不同 key 值的节点为止
  while (oldVNode.key === newVNode.key) {
    // 调用 patch 函数进行更新
    patch(oldVNode, newVNode, container)
    // j自增
    j++
    oldVNode = oldChildren[j]
    newVNode = newChildren[j]
  }

  // 更新相同的后置节点
  let oldEnd = oldChildren.length - 1
  let newEnd = newChildren.length - 1
  oldVNode = oldChildren[oldEnd]
  newVNode = newChildren[newEnd]

  // while 循环从后向前遍历,直到遇到拥有不同 key 值得节点为止
  while (oldVNode.key === newVNode.key) {
    patch(oldVNode, newVNode, container)
    oldEnd--
    newEnd--
    oldVNode = oldChildren[oldEnd]
    newVNode = newChildren[newEnd]
  }

  // 预处理完毕后,处理新增节点与删除节点
  if (j > oldEnd && j <= newEnd) {
    // 如果满足下列条件则说明从 j --> newEnd 之间的节点应作为新节点插入
    // 锚点的索引
    const anchorIndex = newEnd + 1
    // 锚点元素
    const anchor =
      anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null
    // 采用 while 循环,调用 patch 函数逐个挂载新增节点
    while (j <= newEnd) {
      patch(null, newChildren[j++], container, anchor)
    }
  } else if (j > newEnd && j <= oldEnd) {
    // 删除多余的旧节点
    while (j <= oldEnd) {
      unmount(oldChildren[j++])
    }
  } else {
    // 处理非理性情况
    // 构造 source 数组
    // 新的一组子节点中剩余未处理节点的数量
    const count = newEnd - j + 1
    const source = new Array(count)
    source.fill(-1)

    // oldStart 和 newStart 分别为起始索引,即 j
    const oldStart = (newStart = j)
    let moved = false // 是否需要移动
    let pos = 0 // 代表遍历旧的一组子节点的过程中遇到的最大索引值 k

    // 1)两层for循环,遍历旧的一组子节点 O(n^2)
    // for (let i = oldStart; i <= oldEnd; i++) {
    //   const oldVNode = oldChildren[i]
    //   // 遍历新的一组子节点
    //   for (let k = newStart; k <= newEnd; k++) {
    //     const newVNode = newChildren[k]
    //     // 找到拥有相同 key 值的可复用节点
    //     if (oldVNode.key === newVNode.key) {
    //       patch(oldVNode, newVNode, container)
    //       // 由于数组 source 的索引是从 0 开始的,而未处理节点的索引未必从 0 开始,
    //       // 所以在填充数组时需要使用表达式 k - newStart 的值作为数组的索引值。
    //       // 外层循环的变量 i 就是当前节点在旧的一组子节点中的位置索引,因此直接将变量 i 的值赋给 source[k - newStart] 即可。
    //       source[k - newStart] = i
    //     }
    //   }
    // }

    // 2)构建索引表 O(n)
    const keyIndex = {}
    for (let i = newStart; i <= newEnd; i++) {
      keyIndex[newChildren[i].key] = i
    }

    let patched = 0 // 代表更新过的节点数量
    for (let i = oldStart; i <= oldEnd; i++) {
      oldVNode = oldChildren[i]

      if (patched <= count) {
        const k = keyIndex[oldVNode.key]
        if (typeof k !== 'undefined') {
          newVNode = newChildren[k]
          patch(oldVNode, newVNode, container)
          source[k - newStart] = i
          // 判断节点是否需要移动
          if (k < pos) {
            moved = true
          } else {
            pos = k
          }
        } else {
          unmount(oldVNode)
        }
      } else {
        // 如果更新过的节点数量大于需要更新的节点数量,卸载多余的节点
        unmount(oldVNode)
      }
    }

    if (moved) {
      // 计算最长递增子序列 返回的结果为最长递增子序列中的元素在 source 数组中的位置索引
      const seq = lis(source)

      // s 指向最长递增子序列的最后一个元素
      let s = seq.length - 1
      // i 指向新的一组子节点的最后一个元素
      let i = count - 1
      // for 循环使得 i 递减
      for (i; i >= 0; i--) {
        if (source[i] === -1) {
          // 说明索引为 i 的节点是全新的节点,应该将其挂载
          const pos = i + newStart // 在新 children 的真实位置索引
          const newVNode = newChildren[pos]
          const nextPos = pos + 1 // 该节点的下一个节点的位置索引
          // 锚点
          const anchor =
            nextPos < newChildren.length ? newChildren[nextPos].el : null
          // 挂载
          patch(null, newVNode, container, anchor)
        } else if (i !== seq[s]) {
          // 如果节点的索引 i 不等于 seq[s] 的值,说明该节点需要移动
          const pos = i + newStart
          const newVNode = newChildren[pos]
          const nextPos = pos + 1
          const anchor =
            nextPos < newChildren.length ? newChildren[nextPos].el : null
          insert(newVNode.el, container, anchor)
        } else {
          // 当 i === seq[s] 时,说明该位置的节点不需要移动
          s--
        }
      }
    }
  }
}

组件的实现原理

javascript
// resolveProps 函数用于解析组件 props 与 attrs 数据
// 实现中没有包含默认值、类型校验等内容的处理。
function resolveProps(options, propsData) {
  const props = {}
  const attrs = {}
  // 遍历为组件传递的 props 数据
  for (const key in propsData) {
    // 以字符串 on 开头的 props,无论是否显示地声明,都将其添加到 props 数据中,而不是添加到 attrs 中
    if (key in options || key.startsWith('on')) {
      // 如果为组件传递的 props 数据在组件自身的 props 选项中有定义,则将其视为合法的 props
      props[key] = propsData[key]
    } else {
      // 否则将其作为 attrs
      attrs[key] = propsData[key]
    }
  }

  // 最后返回 props 与 attrs 数据
  return [props, attrs]
}

// 全局变量,存储当前正在被初始化的组件实例
let currentInstance = null

// 该方法接收组件实例作为参数,并将该实例设置为 currentInstance
function setCurrentInstance(instance) {
  currentInstance = instance
}

function createRenderer(options) {
  // 其他代码

  // 第二种处理方式:composition类型 setup
  function mountComponent(vnode, container, anchor) {
    function onMounted(fn) {
      if (currentInstance) {
        currentInstance.mounted.push(fn)
      } else {
        console.error('onMounted 函数只能在 setup 中调用')
      }
    }

    // 定义 emit 函数,它接收两个参数
    // event:事件名称
    // payload:传递给事件处理函数的参数
    function emit(event, ...payload) {
      // 根据约定对事件名称进行处理,例如 change --> onChange
      const eventName = `on${event[0].toUppercase() + event.slice(1)}`
      // 根据处理后的事件名称去 props 中寻找对应的事件处理函数
      const handler = instance.props[eventName]
      if (handler) {
        // 调用事件处理函数并传递参数
        handler(...payload)
      } else {
        console.error('不存在')
      }
    }

    // 插槽
    // 直接使用编译好地 vnode.children 对象作为 slots 对象即可
    const slots = vnode.children || []

    const componentOptions = vnode.type
    let { render, data, setup, beforeCreate, propsOptions } = componentOptions
    beforeCreate && beforeCreate()
    const state = data ? reactive(data()) : null
    const [props, attrs] = resolveProps(propsOptions, vnode.props)
    const instance = {
      state,
      props: shallowReactive(props),
      isMounted: false,
      subTree: null,
      // 将插槽添加到组件实例上
      slots,
      // 用来存储通过 onMounted 函数注册的生命周期钩子函数
      mounted: [],
    }

    // setupContext
    const setupContext = { attrs, emit, slots }

    // 设置当前组件实例
    setCurrentInstance(instance)

    // 调用 setup 函数,将只读版本的 props 作为第一个参数传递,避免用户意外的修改 props 的值
    // 将 setupContext 作为第二个参数传递
    const setupResult = setup(shallowReadonly(instance.props), setupContext)

    // setup函数执行完毕后,重置当前组件实例
    setCurrentInstance(null)

    // setupState 用来存储由 setup 返回的数据
    let setupState = null
    // 如果 setup 函数的返回值是函数,则将其作为渲染函数
    if (typeof setupResult === 'function') {
      if (render) {
        console.error('setup 函数返回渲染函数,render选项将被忽略')
        render = setupResult
      } else {
        // 如果 setup 返回值不是函数,则作为数据状态赋值给 setupState
        setupState = setupResult
      }
    }

    vnode.component = instance

    const renderContext = new Proxy(instance, {
      get(target, key, receiver) {
        const { state, props, slots } = target
        if (k === '$slots') {
          return slots
        }

        if (state && k in state) {
          return state[k]
        } else if (k in props) {
          return props[k]
        } else if (setupState && k in setupState) {
          return setupState[k]
        } else {
          console.error('不存在')
        }
      },
      set(target, key, value, receiver) {
        const { state, props } = target
        if (state && k in state) {
          state[k] = value
        } else if (k in props) {
          console.warn(`Attempting to mutate prop "${k}". Props are readonly.`)
        } else if (setupState && k in setupState) {
          setupState[k] = value
        } else {
          console.error('不存在')
        }
      },
    })
    created && created.call(renderContext)
    // 将组件的 render 函数调用包装到 effect 内
    effect(
      () => {
        // 执行渲染函数,获取组件要渲染的内容,即 render 函数返回的虚拟 DOM
        // render的 this 指向响应式数据 state,同时将 state 作为 render 函数的第一个参数传递。
        const subTree = render.call(state, state)
        if (!instance.isMounted) {
          beforeMount && beforeMount.call(state)
          // 初次挂载,patch第一个参数为null
          patch(null, subTree, container, anchor)
          // 将组件实例的 isMounted 设置为true,这样当更新发生时就不会再次进行挂载操作,而是会执行更新
          instance.isMounted = true
          instance.mounted &&
            instance.mounted.forEach((hook) => hook.call(renderContext))
        } else {
          beforeUpdate && beforeUpdate.call(state)
          // 当 isMounted 为 true 时,说明组件已经被挂载,只需要完成自更新即可,
          // 所以在调用 patch 函数时,第一个参数为组件上一次渲染的子树,
          // 意思是,使用新的子树与上一次渲染的子树进行打补丁操作
          patch(instance.subTree, subTree, container, anchor)
          updated && updated.call(state)
        }
        // 更新组件实例的子树
        instance.subTree = subTree
      },
      {
        scheduler: queueJob,
      }
    )
  }

  // 第一张处理方式:option 类型
  function mountComponent(vnode, container, anchor) {
    // 通过 vnode 获取组件的选项对象,即 vnode.type
    const componentOptions = vnode.type
    // 获取组件的渲染函数 render
    const {
      render,
      data,
      beforeCreate,
      created,
      beforeMount,
      mounted,
      beforeUpdate,
      updated,
      // 从组件选项对象中取出 props 定义,即 propsOption
      props: propsOptions,
    } = componentOptions

    beforeCreate && beforeCreate()

    // 调用 data 函数获得原始数据,并调用 reactive 将其包装成响应式数据
    const state = reactive(data())

    // 调用 resolveProps 函数解析出最终的 props 数据与 attrs 数据
    const [props, attrs] = resolveProps(propsOptions, vnode.props)

    // 定义组件实例,包含组件有关的状态信息
    const instance = {
      state,
      // 将解析出的 props 数据包装为 shallowReactive 并定义到组件实例上
      props: shallowReactive(props),
      isMounted: false,
      subTree: null, // 组件渲染的内容,即子树
    }

    // 将组件实例设置到 vnode 上,用于后续更新
    vnode.component = instance

    // 创建渲染上下文对象,本质上是组件实例的代理
    // 实际上,除了组件自身的数据以及 props 数据之外,
    // 完整的组件还包含 methods、computed 等选项中定义的数据和方法,这些内容都应该在渲染上下文对象中处理。
    const renderContext = new Proxy(instance, {
      get(target, key, receiver) {
        // 取得组件自身状态与 props 数据
        const { state, props } = target
        if (state && key in state) {
          return state[k]
        } else if (k in props) {
          return props[k]
        } else {
          console.error('不存在')
        }
      },
      set(target, key, value, receiver) {
        const { state, key } = target
        if (state && key in state) {
          state[k] = value
        } else if (k in props) {
          console.warn(`Attempting to mutate prop "${k}". Props are readonly.`)
        } else {
          console.error('不存在')
        }
      },
    })

    created && created.call(renderContext)

    // 将组件的 render 函数调用包装到 effect 内
    effect(
      () => {
        // 执行渲染函数,获取组件要渲染的内容,即 render 函数返回的虚拟 DOM
        // render的 this 指向响应式数据 state,同时将 state 作为 render 函数的第一个参数传递。
        const subTree = render.call(state, state)
        if (!instance.isMounted) {
          beforeMount && beforeMount.call(state)
          // 初次挂载,patch第一个参数为null
          patch(null, subTree, container, anchor)
          // 将组件实例的 isMounted 设置为true,这样当更新发生时就不会再次进行挂载操作,而是会执行更新
          instance.isMounted = true
          mounted && mounted.call(state)
        } else {
          beforeUpdate && beforeUpdate.call(state)
          // 当 isMounted 为 true 时,说明组件已经被挂载,只需要完成自更新即可,
          // 所以在调用 patch 函数时,第一个参数为组件上一次渲染的子树,
          // 意思是,使用新的子树与上一次渲染的子树进行打补丁操作
          patch(instance.subTree, subTree, container, anchor)
          updated && updated.call(state)
        }
        // 更新组件实例的子树
        instance.subTree = subTree
      },
      {
        scheduler: queueJob,
      }
    )
  }

  // 其他代码
}

异步组件和函数式组件

javascript
// 异步组件
function defineAsyncComponent(options) {
  if (typeof options === 'function') {
    options = {
      loader: options,
    }
  }

  const { loader } = options

  // 一个变量,用来存储异步加载的组件
  let InnerComp = null

  // 记录重试次数
  let retries = 0
  // 封装 load 函数用来加载异步组件
  function load() {
    return loader().catch((err) => {
      if (options.onError) {
        return new Promise((resolve, reject) => {
          const retry = () => {
            resolve(load())
            retries++
          }
          const fail = () => reject(err)
          options.onError(retry, fail, retires)
        })
      } else {
        throw error
      }
    })
  }

  // 返回一个包装组件
  return {
    name: 'AsyncComponentWrapper',
    setup() {
      // 异步组件是否加载成功
      const loaded = ref(false)
      // 定义 error,当错误发生时,用来存储错误对象
      const error = shallowRef(null)
      // 一个标志,代表是否正在加载,默认为false
      const loading = ref(false)

      let loadingTimer = null

      // 存在 delay,当延迟到时后将 loading.value 设置为 true
      if (options.delay) {
        loadingTimer = setTimeout(() => {
          loading.value = true
        }, options.delay)
      } else {
        // 配置项不存在delay,则直接标记为加载中
        loading.value = true
      }

      // 调用 load 函数加载组件
      load()
        .then((c) => {
          InnerComp = c
          loaded.value = true
        })
        .catch((err) => (error.value = err))
        .finally(() => {
          loading.value = false
          // 加载完毕后,需要清除加载延时定时器
          clearTimeout(loadingTimer)
        })

      let timer = null
      if (options.timeout) {
        // 如果指定了超时时长,则开启一个定时器计时
        timer = setTimeout(() => {
          const err = new Error(
            `Async component timed out after ${options.timeout}ms.`
          )
          err.value = err
        }, options.timeout)
      }
      // 包装组件被卸载时清除定时器
      onUnmounted(() => clearTimeout(timer))

      // 占位内容
      const placeholder = { type: Text, children: '' }

      return () => {
        // 如果异步组件加载成功,则渲染该组件,否则渲染一个占位内容
        if (loaded.value) {
          return { type: InnerComp }
        } else if (error.value && options.errorComponent) {
          // 只有当错误存在且用户配置了 errorComponent 时才展示 Error组件
          // 同时将 error 作为 props 传递
          return {
            type: options.errorComponent,
            props: { error: error.value },
          }
        } else if (loading.value && options.loadingComponent) {
          // 如果异步组件正在加载,且用户指定了 Loading 组件,则渲染 Loading 组件
          return { type: options.loadingComponent }
        } else {
          return placeholder
        }
      }
    },
  }
}

// 重试机制模拟实现
// 模拟网络请求失败
function fetch() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('err')
    }, 1000)
  })
}

// 重试机制 onError(retry: fn, fail: fn)
function load(onError) {
  const p = fetch()

  return p.catch((err) => {
    // 当错误发生时,返回一个新的 Promise 实例,并调用 onError 回调,
    // 同时将 retry 函数作为 onError 回调的参数
    return new Promise((resolve, reject) => {
      // retry 函数,用来执行重试的函数,执行该函数会重新调用 load 函数并发送请求
      const retry = () => resolve(load(onError))
      const fail = () => reject(err)
      onError(retry, fail)
    })
  })
}

// load实用的例子
load((retry) => retry()).then((res) => console.log(res))

// 针对函数式组件的调整
function patch(n1, n2, container, anchor) {
  if (n1 && n1.type !== n2.type) {
    // 省略其他代码
  }

  const { type } = n2
  if (typeof type === 'string') {
    // 省略其他代码
  } else if (typeof type === 'object') {
    // 省略其他代码
  } else if (type === Text) {
    // 省略其他代码
  } else if (type === Fragment) {
    // 省略其他代码
  } else if (typeof type === 'object' || typeof type === 'function') {
    // 状态组件 以及 函数组件
    if (!n1) {
      mountComponent(n2, container, anchor)
    } else {
      patchComponent(n1, n2, anchor)
    }
  }
}

function mountComponent(vnode, container, anchor) {
  // 省略其他代码

  // 检查是否为函数式组件
  const isFunctional = typeof vnode.type === 'function'

  // 通过 vnode 获取组件的选项对象,即 vnode.type
  let componentOptions = vnode.type

  // 如果是函数式组件,则将 vnode.type 作为渲染函数,将 vnode.type.props 作为 props 选项定义即可
  if (isFunctional) {
    componentOptions = {
      render: vnode.type,
      props: vnode.type.props,
    }
  }

  // 省略其他代码
}

内建组件和模块

javascript
// KeepAlive组件
// 最基本的 KeepAlive 组件实现
const KeepAlive = {
  // KeepAlive 组件独有的属性,用作标识
  __isKeepAlive: true,
  props: {
    include: RegExp,
    exclude: RegExp,
  },
  setup(props, { emits }) {
    // 创建一个缓存对象
    // key:vnode.type
    // value:vnode
    const cache = new Map()
    // 当前 KeepAlive 组件的实例
    const instance = currentInstance
    // 对于 KeepAlive 组件来说,它的实例上存在特殊的 keepAliveCtx 对象,该对象由渲染器注入
    // 该对象会暴露渲染器的一些内部方法,其中 move 函数用来将一段 DOM 移动到另一个容器中
    const { move, createElement } = instance.keepAliveCtx
    // 创建隐藏容器
    const storageContainer = createElement('div')

    // KeepAlive 组件的实例上会被添加两个内部函数,分别是 _deActivate 和 _activate
    // 这两个函数会在渲染器中被调用
    instance._deActivate = (vnode) => {
      move(vnode, storageContainer) // 移动到隐藏容器
    }
    instance._activate = (vnode, container, anchor) => {
      move(vnode, container, anchor) // 移动到页面原先的容器中
    }
    return () => {
      // KeepAlive 的默认插槽就是要被 KeepAlive 的组件
      let rawVNode = slots.default()
      if (typeof rawVNode.type !== 'object') {
        // 如果不是组件,直接渲染即可,因为非组件的虚拟节点无法被 KeepAlive
        return rawVNode
      }

      // 获取“内部组件”的 name
      const name = rawVNode.type.name
      // 对 name 进行匹配
      if (
        name &&
        // 如果 name 无法被 include 匹配
        ((props.include && !props.include.test(name)) ||
          // 或者被 exclude
          (props.exclude && props.exclude.test(name)))
      ) {
        // 则直接渲染“内部组件”,不对其进行后续的缓存操作
        return rawVNode
      }

      // 在挂载时先获取缓存的组件 vnode
      const cachedVNode = cache.get(rawVNode.type)
      if (cachedVNode) {
        // 如果有缓存的内容,则说明不应该执行挂载,而应该执行激活
        // 继承组件实例
        rawVNode.component = cachedVNode.component
        rawVNode.keptAlive = true
      } else {
        // 如果没有缓存,则将其添加到缓存中,这样下次激活组件时就不会执行新的挂载动作了
        cache.set(rawVNode.type, rawVNode)
      }

      // 在组件 vnode 上添加 shouldKeepAlive 属性,并标记为 true,避免渲染器真的将组件卸载
      rawVNode.shouldKeepAlive = true
      // 将 KeepAlive 组件的实例也添加到 vnode 上,以便在渲染器中访问
      rawVNode.keepAliveInstance = instance
      // 渲染组件 vnode
      return rawVNode
    }
  },
}

function unmount(vnode) {
  if (vnode.type === 'Fragment') {
    vnode.children.forEach((c) => unmount(c))
    return
  } else if (typeof vnode.type === 'object') {
    // vnode.shouldKeepAlive 是一个布尔值,用来标识该组件是否应该被 KeepAlive
    if (vnode.shouldKeepAlive) {
      // 对于需要被 KeepAlive 的组件,我们不应该真的卸载它,而应调用该组件的父组件,
      // 即 KeepAlive 组件的 _deActivate 函数使其失活
      vnode.keepAliveInstance._deActivate(vnode)
    } else {
      unmount(vnode.component.subTree)
    }
    return
  }
  const parent = vnode.el.parentNode
  if (parent) {
    parent.removeChild(vnode.el)
  }
}

function patch(n1, n2, container, anchor) {
  if (n1 && n1.type !== n2.type) {
    unmount(n1)
    n1 = null
  }

  const { type } = n2

  if (typeof type === 'string') {
    // DOM节点,省略部分代码
  } else if (typeof type === Text) {
    // 文本节点,省略部分代码
  } else if (typeof type === Fragment) {
    // 片段节点,省略部分代码
  } else if (typeof type === 'object' || typeof type === 'function') {
    // 组件
    if (!n1) {
      // 如果该组件已经被 KeepAlive,则不会重新挂载它,而是会调用 _activate来激活它
      if (n2.keptAlive) {
        n2.keepAliveInstance._activate(n2, container, anchor)
      } else {
        mountComponent(n2, container, anchor)
      }
    } else {
      patchComponent(n1, n2, anchor)
    }
  }
}

function mountComponent(vnode, container, anchor) {
  // 省略部分代码

  const instance = {
    state,
    props: shallowReactive(props),
    isMounted: false,
    subTree: null,
    slots,
    mounted: [],
    // 只有 KeepAlive组件的实例下才会有 keepAliveCtx 属性
    keepAliveCtx: null,
  }

  // 检查当前要挂载的组件是否是 KeepAlive 组件
  const isKeepAlive = vnode.type.__isKeepAlive
  if (isKeepAlive) {
    // 在 KeepAlive 组件实例上添加 keepAliveCtx 对象
    instance.keepAliveCtx = {
      // move 函数用来移动一段 vnode
      move(vnode, container, anchor) {
        // 本质上是将组件渲染的内容移动到指定容器中,即隐藏容器中
        insert(vnode.component.subTree.el, container, anchor)
      },
      createElement,
    }
  }
  // 省略部分代码
}

// 一个基本的缓存实例实现
const _cache = new Map()
// TypeScript
const cache: KeepAliveCache = {
  get(key) {
    _cache.get(key)
  },
  set(key, value) {
    _cache.set(key, value)
  },
  delete(key) {
    _cache.delete(key)
  },
  forEach(fn) {
    _cache.forEach(fn)
  },
}
javascript
// Teleport组件
// Teleport组件定义
const Teleport = {
  __isTeleport: true,
  process(n1, n2, container, anchor, internals) {
    // 在这里处理渲染逻辑
    // 通过 internals 参数取得渲染器的内部方法
    const { patch, patchChildren, move } = internals
    // 如果旧 VNode n1 不存在,则是全新的挂载,否则执行更新
    if (!n1) {
      // 挂载
      // 获取容器,即挂载点
      const target =
        typeof n2.props.to === 'string'
          ? document.querySelector(n2.props.to)
          : n2.props.to
      // 将 n2.children 渲染到指定挂载点即可
      n2.children.forEach((c) => patch(null, c, target, anchor))
    } else {
      // 更新
      patchChildren(n1, n2, container)
      // 如果新旧 to 参数的值不同,则需要对内容进行移动
      if (n2.props.to !== n1.props.to) {
        // 获取新的容器
        const newTarget =
          typeof n2.props.to === 'string'
            ? document.querySelector(n2.props.to)
            : n2.props.to
        n2.children.forEach((c) => move(c, newTarget))
      }
    }
  },
}

// 对于 Teleport 组件来说,直接将其子节点编译成一个数组即可
function render() {
  return {
    type: Teleport,
    // 以普通 children 形式代表被 Teleport 的内容
    children: [
      // xxx
    ],
  }
}

function patch(n1, n2, container, anchor) {
  if (n1 && n1.type !== n2.type) {
    unmount(n1)
    n1 = null
  }

  const { type } = n2

  if (typeof type === 'string') {
    // DOM节点,省略部分代码
  } else if (typeof type === Text) {
    // 文本节点,省略部分代码
  } else if (typeof type === Fragment) {
    // 片段节点,省略部分代码
  } else if (typeof type === 'object' && type.__isTeleport) {
    // 组件选项中如果存在 __isTeleport 标识,则它是 Teleport 组件,
    // 调用 Teleport 组件选项中的 process 函数将控制权交接出去
    // 传递给 process 函数的第五个参数是渲染器的一些内部方法
    type.process(n1, n2, container, anchor, {
      patch,
      patchChildren,
      unmount,
      move(vnode, container, anchor) {
        insert(
          vnode.component ? vnode.component.subTree.el : vnode.el, // 移动一个组件或普通元素
          container,
          anchor
        )
      },
    })
  } else if (typeof type === 'object' || typeof type === 'function') {
    // 组件,省略部分代码
  }
}
javascript
// 原生DOM的过渡
const el = document.createElement('div')
el.classList.add('box')

// 创建DOM元素完成之后,到把 DOM 添加到 body 前,可以视作 beforeEnter 阶段
el.classList.add('enter-from')
el.classList.add('enter-active')

document.body.appendChild(el)

// 之所以需要调用两次requestAnimationFrame是由于浏览器Bug
// 使用 requestAnimationFrame 函数注册回调会在当前帧执行,除非其他代码已经调用了一次requestAnimationFrame 函数。
requestAnimationFrame(() => {
  requestAnimationFrame(() => {
    // DOM 添加到 body 之后,则可以视为 enter 阶段
    el.classList.remove('enter-from')
    el.classList.add('enter-to')

    // 进场动效结束,通过监听 transitionend 事件完成收尾工作
    el.addEventListener('transitionend', () => {
      el.classList.remove('enter-to')
      el.classList.remove('enter-active')
    })
  })
})

// 卸载元素
el.addEventListener('click', () => {
  // 将卸载动作封装到 performRemove 函数中
  const performRemove = () => el.parentNode.removeChild(el)

  // 设置初始状态:添加 leave-from 和 leave-active 类
  el.classList.add('leave-from')
  el.classList.add('leave-active')

  // 强制 reflow:使初始状态生效
  document.body.offsetHeight

  // 在下一帧切换状态
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      // 切换到结束状态
      el.classList.remove('leave-from')
      el.classList.add('leave-to')

      // 监听 transitionend 事件做收尾工作
      el.addEventListener('transitionend', () => {
        el.classList.remove('leave-to')
        el.classList.remove('leave-active')
        performRemove()
      })
    })
  })
})
javascript
// Transition组件
const Transition = {
  name: 'Transition',
  setup(props, { slots }) {
    return () => {
      // 通过默认插槽获取需要过渡的元素
      const innerVNode = slots.default()

      // 在过渡元素的 VNode 对象上添加 transition 相应的钩子函数
      innerVNode.transition = {
        beforeEnter(el) {
          // 设置初始状态
          el.classList.add('enter-from')
          el.classList.add('enter-active')
        },
        enter(el) {
          // 在下一帧切换到结束状态
          nextFrame(() => {
            // 移除 enter-from 类,添加 enter-to 类
            el.classList.remove('enter-from')
            el.classList.add('enter-to')
          })
          // 监听 transitionend 事件完成收尾工作
          el.addEventListener('transitionend', () => {
            el.classList.remove('enter-to')
            el.classList.remove('enter-active')
          })
        },
        leave(el, performRemove) {
          // 设置离场过渡的初始状态:添加 leave-from 和 leave-active 类
          el.classList.add('leave-from')
          el.classList.add('leave-active')

          // 强制reflow,使初始状态生效
          document.body.offsetHeight

          // 在下一帧修改状态
          nextFrame(() => {
            el.classList.remove('leave-from')
            el.classList.add('leave-to')

            // 监听 transitionend 事件完成收尾工作
            el.addEventListener('transitionend', () => {
              el.classList.remove('leave-to')
              el.classList.remove('leave-active')
              // 完成 DOM 元素的卸载
              performRemove()
            })
          })
        },
      }

      return innerVNode
    }
  },
}

function mountComponent(vnode, container, anchor) {
  const el = (vnode.el = createElement(vnode.type))

  if (typeof vnode.children === 'string') {
    setElementText(el, vnode.children)
  } else if (Array.sArray(vnode.children)) {
    vnode.children.forEach((child) => {
      patch(null, child, el)
    })
  }

  if (vnode.props) {
    for (const key in vnode.props) {
      patchProps(el, key, null, vnode.props[key])
    }
  }

  // 判断一个 vnode 是否需要过渡
  const needTransition = vnode.transition
  if (needTransition) {
    // 调用 transition.beforeEnter 钩子,并将 DOM 元素 作为参数传递
    vnode.transition.beforeEnter(el)
  }

  insert(el, container, anchor)

  if (needTransition) {
    // 调用 transition.enter 钩子,并将 DOM 元素 作为参数传递
    vnode.transition.enter(el)
  }
}

function unmount(vnode) {
  const needTransition = vnode.transition
  if (vnode.type === Fragment) {
    vnode.children.forEach((c) => unmount(c))
    return
  } else if (typeof vnode.type === 'object') {
    if (vnode.shouldKeepAlive) {
      vnode.keepAliveInstance._deActive(vnode)
    } else {
      unmount(vnode.component.subTree)
    }
    return
  }
  const parent = vnode.el.parentNode
  if (parent) {
    // 将卸载动作封装到 performRemove 函数中
    const performRemove = () => parent.removeChild(vnode.el)
  }
  if (needTransition) {
    // 如果需要过渡处理,则调用 transition.leave 钩子
    // 同时将 DOM 元素和 performRemove 函数作为参数传递
    vnode.transition.leave(vnode.el, performRemove)
  } else {
    // 如果不需要过渡处理,则直接执行卸载操作
    performRemove()
  }
}

编译器

javascript
// 编译器实现

// 定义状态机的状态
const State = {
  initial: 1,
  tagOpen: 2,
  tagName: 3,
  text: 4,
  tagEnd: 5,
  tagEndName: 6,
}

// 一个辅助函数,用于判断是否是字母
function isAlpha(char) {
  return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')
}

// 接收模板字符串作为参数,并将模板切割为 Token 返回
function tokenize(str) {
  // 状态机的当前状态:初始状态
  let currentState = State.initial
  // 用于缓存字符
  const chars = []
  // 生成的 Token 会存储到 tokens 数组中,并作为函数的返回值返回
  const tokens = []
  // 使用 while 循环开启自动机,只要模板字符串没有被消费尽,自动机就会一直运行
  while (str) {
    // 查看第一个字符
    const char = str[0]
    // switch 语句匹配当前状态
    switch (currentState) {
      // 状态机当前处于初始状态
      case State.initial:
        if (char === '<') {
          currentState = State.tagOpen
          str = str.slice(1)
        } else if (isAlpha(char)) {
          currentState = State.text
          chars.push(char)
          str = str.slice(1)
        }
        break
      // 状态机当前处于标签开始状态
      case State.tagOpen:
        if (isAlpha(char)) {
          currentState = State.tagName
          chars.push(char)
          str = str.slice(1)
        } else if (char === '/') {
          currentState = State.tagEnd
          str = str.slice(1)
        }
        break
      // 状态机当前处于标签名称状态
      case State.tagName:
        if (isAlpha(char)) {
          chars.push(char)
          str = str.slice(1)
        } else if (char === '>') {
          currentState = State.initial
          tokens.push({
            type: 'tag',
            name: chars.join(''),
          })
          chars.length = 0
          str = str.slice(1)
        }
        break
      // 状态机当前处于文本状态
      case State.text:
        if (isAlpha(char)) {
          chars.push(char)
          str = str.slice(1)
        } else if (char === '<') {
          currentState = State.tagOpen
          tokens.push({
            type: 'text',
            content: chars.join(''),
          })
          chars.length = 0
          str = str.slice(1)
        }
        break
      // 状态机当前处于标签结束状态
      case State.tagEnd:
        if (isAlpha(char)) {
          currentState = State.tagEndName
          chars.push(char)
          str = str.slice(1)
        }
        break
      // 状态机当前处于结束标签名称状态
      case State.tagEndName:
        if (isAlpha(char)) {
          chars.push(char)
          str = str.slice(1)
        } else if (char === '>') {
          currentState = State.initial
          tokens.push({
            type: 'tagEnd',
            name: chars.join(''),
          })
          chars.length = 0
          str = str.slice(1)
        }
        break
    }
  }
  return tokens
}

function parse(str) {
  const tokens = tokenize(str)
  const root = {
    type: 'Root',
    children: [],
  }
  // 创建 elementStack 栈,起初只有 Root 根节点
  const elementStack = [root]

  // 开启一个 while 循环扫描 tokens,直到所有 Token 都被扫描完毕为止
  while (tokens.length) {
    const parent = elementStack[elementStack.length - 1]

    // 当前扫描的 Token
    const t = tokens[0]
    switch (t.type) {
      case 'tag':
        const elementNode = {
          type: 'Element',
          tag: t.name,
          children: [],
        }
        // 将其添加到父级节点的 children 中
        parent.children.push(elementNode)
        // 将当前节点压入栈
        elementStack.push(elementNode)
        break
      case 'text':
        // 如果当前 Token 是文本,则创建 Text 类型的 AST 节点
        const textNode = {
          type: 'Text',
          content: t.content,
        }
        parent.children.push(textNode)
        break
      case 'tagEnd':
        // 遇到结束标签,将栈顶节点弹出
        elementStack.pop()
        break
    }
    // 消费已经扫描过的 token
    tokens.shift()
  }
  // 最后返回 AST
  return root
}

// 打印当前 AST 中节点的信息
function dump(node, indent = 0) {
  // 节点的类型
  const type = node.type
  // 节点的描述,如果是根节点,则没有描述
  // 如果是 Element 类型的节点,则使用 node.tag 作为节点的描述
  // 如果是 Text 类型的节点,则使用 node.content 作为节点的描述
  const desc =
    node.type === 'Root'
      ? ''
      : node.type === 'Element'
      ? node.tag
      : node.content

  // 打印节点的类型和描述信息
  console.log(`${'-'.repeat(indent)}${type}:${desc}`)

  // 递归地打印子节点
  if (node.children) {
    node.children.forEach((n) => dump(n, indent + 2))
  }
}

// 深度优先遍历实现对 AST 中的节点的访问
function traverseNode(ast, context) {
  // 当前节点,ast 本身就是 Root 节点
  context.currentNode = ast

  // 1. 增加退出阶段的回调函数数组
  const exitFns = []

  // context.nodeTransforms 是一个数组,其中每一个元素都是一个函数
  const transforms = context.nodeTransforms

  for (let i = 0; i < transforms.length; i++) {
    // 2. 转换函数可以返回另外一个函数,该函数即作为退出阶段的回调函数
    const onExit = transforms[i](context.currentNode, context)
    if (onExit) {
      exitFns.push(onExit)
    }

    // 由于任何转换函数都可能移除当前节点,因此每个转换函数执行完毕后,
    // 都应该检查当前节点是否已经被移除,如果被移除了,直接返回即可
    if (!context.currentNode) return
  }

  const children = context.currentNode.children
  if (children) {
    for (let i = 0; i < children.length; i++) {
      // 递归地调用 traverseNode 转换子节点之前,将当前节点设置为父节点
      context.parent = context.currentNode
      // 设置位置索引
      context.childIndex = i
      // 如果有子节点,则递归地调用 traverseNode 函数进行遍历
      traverseNode(children[i], context)
    }
  }

  // 在节点处理的最后阶段执行缓存到 exitFns 中的回调函数
  // 注意,这里我们要反序执行
  let i = exitFns.length
  while (i--) {
    exitFns[i]()
  }
}

// 封装 transform 函数,用来对 AST 进行转换
function transform(ast) {
  function transformElement(node, context) {
    // 进入节点
    // 返回一个会在退出节点时执行的回调函数
    return () => {
      // 将转换代码编写在退出阶段的回调函数中,
      // 这样可以保证该标签节点的子节点全部被处理完毕
      if (node.type !== 'Element') {
        // 如果被转换的节点不是元素节点,则什么都不做
        return
      }

      // 1. 创建 h 函数调用语句,
      // h 函数调用的第一个参数是标签名称,因此我们以 node.tag 来创建一个字符串字面量节点
      // 作为第一个参数
      const callExp = createCallExpression('h', [createStringLiteral(node.tag)])
      // 2. 处理 h 函数调用的参数
      node.children.length === 1
        ? // 如果当前标签节点只有一个子节点,则直接使用子节点的 jsNode 作为参数
          callExp.arguments.push(node.children[0].jsNode)
        : // 如果当前标签节点有多个子节点,则创建一个 ArrayExpression 节点作为参数
          callExp.arguments.push(
            // 数组的每个元素都是子节点的 jsNode
            createArrayExpression(node.children.map((c) => c.jsNode))
          )
      // 3. 将当前标签节点对应的 JavaScript AST 添加到 jsNode 属性下
      node.jsNode = callExp
    }
  }

  function transformText(node, context) {
    if (node.type !== 'Text') {
      return
    }
    // 文本节点对应的 JavaScript AST 节点其实就是一个字符串字面量,
    // 因此只需要使用 node.content 创建一个 StringLiteral 类型的节点即可
    // 最后将文本节点对应的 JavaScript AST 节点添加到 node.jsNode 属性下
    node.jsNode = createStringLiteral(node.content)
  }

  // 转换 Root 根节点
  function transformRoot(node) {
    return () => {
      if (node.type !== 'Root') {
        return
      }

      const vnodeJSAST = node.children[0].jsNode

      node.jsNode = {
        type: 'FunctionDecl',
        id: {
          type: 'Identifier',
          name: 'render',
        },
        params: [],
        body: [
          {
            type: 'ReturnStatement',
            return: vnodeJSAST,
          },
        ],
      }
    }
  }

  // 在 transform 函数内创建 context 对象
  const context = {
    // 增加 currentNode 用来存储当前正在转换的节点
    currentNode: null,
    // 增加 childIndex 用来存储当前节点在父节点的 children 中的位置索引
    childIndex: 0,
    // 增加 parent 用来存储当前转换节点的父节点
    parent: null,
    // 用于替换节点的函数,接收新节点作为参数
    replaceNode(node) {
      // 为了替换节点,我们需要修改 AST
      // 找到当前节点在父节点的 children 中的位置:context.childIndex
      // 然后使用新节点替换即可
      context.parent.children[context.childIndex] = node
      // 由于当前节点已经被新节点替换掉了,因此我们需要将 currentNode 更新为新节点
      context.currentNode = node
    },
    // 用于删除当前节点
    removeNode() {
      if (context.parent) {
        // 调用数组的 splice 方法,根据当前节点的索引删除当前节点
        context.parent.children.splice(context.childIndex, 1)
        // 将 context.currentNode 置空
        context.currentNode = null
      }
    },
    nodeTransforms: [transformElement, transformText, transformRoot],
  }
  traverseNode(ast, context)
  dump(ast)
}

// 创建 StringLiteral 节点
function createStringLiteral(value) {
  return {
    type: 'StringLiteral',
    value,
  }
}

// 创建 Identifier 节点
function createIdentifier(name) {
  return {
    type: 'Identifier',
    name,
  }
}

// 创建 ArrayExpression 节点
function createArrayExpression(elements) {
  return {
    type: 'ArrayExpression',
    elements,
  }
}

// 创建 CallExpression 节点
function createCallExpression(callee, arguments) {
  return {
    type: 'CallExpression',
    callee: createIdentifier(callee),
    arguments,
  }
}

function compile(template) {
  const ast = parse(template)
  transform(ast)
  const code = generate(ast.jsNode)
  return code
}

function generate(node) {
  const context = {
    code: '',
    push(code) {
      context.code += code
    },
    // 当前缩进的级别,初始值为 0,即没有缩进
    currentIndent: 0,
    // 该函数用来换行,即在代码字符串的后面追加 \n 字符,
    // 另外,换行时应该保留缩进,所以我们还要追加 currentIndent * 2 个空格字符
    newline() {
      context.code += `\n` + ` `.repeat(context.currentIndent)
    },
    // 用来缩进,即让 currentIndent 自增后,调用换行函数
    indent() {
      context.currentIndent++
      context.newline()
    },
    // 取消缩进,即让 currentIndent 自减后,调用换行函数
    deIndent() {
      context.currentIndent--
      context.newline()
    },
  }

  genNode(node, context)
  return context.code
}

function genNode(node, context) {
  switch (node.type) {
    case 'FunctionDecl':
      genFunctionDecl(node, context)
      break
    case 'ReturnStatement':
      genReturnStatement(node, context)
      break
    case 'CallExpression':
      genCallExpression(node, context)
      break
    case 'StringLiteral':
      genStringLiteral(node, context)
      break
    case 'ArrayExpression':
      genArrayExpression(node, context)
      break
  }
}

function genFunctionDecl(node, context) {
  // 从 context 对象中取出工具函数
  const { push, indent, deIndent } = context
  // node.id 是一个标识符,用来描述函数的名称,即 node.id.name
  push(`function ${node.id.name}`)
  push(`(`)
  // 调用 genNodeList 为函数的参数生成代码
  genNodeList(node.params, context)
  push(`)`)
  push(`{}`)
  // 缩进
  indent()
  // 为函数体生成代码,这里递归地调用了 genNode 函数
  node.body.forEach((n) => genNode(n, context))
  // 取消缩进
  deIndent()
  push(`}`)
}

function genNodeList(nodes, context) {
  const { push } = context
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i]
    genNode(node, context)
    if (i < nodes.length - 1) {
      push(', ')
    }
  }
}

function genArrayExpression(node, context) {
  const { push } = context
  push('[')
  genNodeList(node.elements, context)
  push(']')
}

function genReturnStatement(node, context) {
  const { push } = context
  push(`return `)
  genNode(node.return, context)
}

function genStringLiteral(node, context) {
  const { push } = context
  push(`'${node.value}'`)
}

function genCallExpression(node, context) {
  const { push } = context
  // 取得被调用函数名称和参数列表
  const { callee, arguments: args } = node
  // 生成函数调用代码
  push(`${callee.name}(`)
  // 调用 genNodeList 生成参数代码
  genNodeList(args, context)
  // 补全括号
  push(`)`)
}

compile(`<div><p>Vue</p><p>Template</p></div>`)

解析器

javascript
// 定义文本模式,作为一个状态表
const TextModes = {
  DATA: 'DATA',
  RCDATA: 'RCDATA',
  RAWTEXT: 'RAWTEXT',
  CDATA: 'CDATA',
}

// 解析器函数,接收模板作为参数
function parse(str) {
  // 定义上下文对象
  const context = {
    // source 是模板内容,用于在解析过程中进行消费
    source: str,
    // 解析器当前处于文本模式,初始模式为 DATA
    mode: TextModes.DATA,
    // advanceBy 函数用来消费指定数量的字符,它接收一个数字作为参数
    advanceBy(num) {
      // 根据给定字符数 num,截取位置 num 后的模板内容,并替换当前模板内容
      context.source = context.source.slice(num)
    },
    // 无论是开始标签还是结束标签,都可能存在无用的空白字符,例如 <div    >
    advanceSpaces() {
      // 匹配空白字符
      const match = /^[\t\r\n\f ]+/.exec(context.source)
      if (match) {
        // 调用 advanceBy 函数消费空白字符
        context.advanceBy(match[0].length)
      }
    },
  }
  // 调用 parseChildren 函数开始进行解析,它返回解析后得到的子节点
  // parseChildren 函数接收两个参数:
  // 第一个参数是上下文对象 context
  // 第二个参数是由父代节点构成的节点栈,初始时栈为空
  const nodes = parseChildren(context, [])
  // 解析器返回 Root 根节点
  return {
    type: 'Root',
    // 使用 nodes 作为根节点的 children
    children: nodes,
  }
}

function parseChildren(context, ancestors) {
  // 定义 nodes 数组存储子节点,它将作为最终的返回值
  let nodes = []
  // 从上下文对象中取得当前状态,包括模式 mode 和模板内容 source
  const { mode, source } = context
  // 开启 while 循环,只要满足条件就会一直对字符串进行解析
  // 关于 isEnd() 后文会详细讲解
  while (!isEnd(context, ancestors)) {
    let node
    // 只有 DATA 模式和 RCDATA 模式才支持插值节点的解析
    if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
      if (mode === TextModes.DATA && source[0] === '<') {
        if (source[1] === '!') {
          // 注释
          node = parseComment(context)
        } else if (source.startsWith('<!CDATA[')) {
          // CDATA
          node = parseCDATA(context, ancestors)
        } else if (source[1] === '/') {
          // 状态机遭遇了闭合标签,此时应该抛出错误,因为它缺少与之对应的开始标签
          console.error('无效的结束标签')
          continue
        } else if (/[a-z]/i.test(source[1])) {
          // 标签
          node = parseElement(context, ancestors)
        }
      } else if (source.startsWith(`{{`)) {
        // 解析插值
        node = parseInterpolation(context)
      }
    }
    // node 不存在,说明处于其他模式,即非 DATA 模式且非 RCDATA 模式
    // 这时一切内容都作为文本处理
    if (!node) {
      // 解析文本节点
      node = parseText(context)
    }
    // 将节点添加到 nodes 数组中
    nodes.push(node)
  }
  // 当 while 循环停止后,说明子节点解析完毕,返回子节点
  return nodes
}

function parseElement(context, ancestors) {
  // 调用 parseTag 函数解析开始标签
  const element = parseTag(context)
  if (element.isSelfClosing) return element
  // 切换到正确的文本模式
  if (element.tag === 'textarea' || element.tag === 'title') {
    // 如果由 parseTag 解析得到的标签是 <textarea> 或 <title>,则切换到 RCDATA 模式
    context.mode = TextModes.RCDATA
  } else if (/style|xmp|iframe|noembed|noframes|noscript/.test(element.tag)) {
    // 如果由 parseTag 解析得到的标签是:
    // <style>、<xmp>、<iframe>、<noembed>、<noframes>、<noscript>
    // 则切换到 RAWTEXT 模式
    context.mode = TextModes.RAWTEXT
  } else {
    // 否则切换到 DATA 模式
    context.mode = TextModes.DATA
  }

  ancestors.push(element)
  element.children = parseChildren(context, ancestors)
  ancestors.pop()

  if (context.source.startsWith(`</${element.tag}>`)) {
    // 再次调用 parseTag 函数解析结束标签,传递了第二个参数:'end'
    parseTag(context, 'end')
  } else {
    // 缺少闭合标签
    console.error(`${element.tag} 标签缺少闭合标签`)
  }

  return element
}

function isEnd(context, ancestors) {
  // 当模板内容解析完毕后,停止
  if (!context.source) return true
  // 与父级节点栈内所有节点做比较
  for (let i = ancestors.length - 1; i >= 0; --i) {
    // 只要栈中存在与当前结束标签同名的节点,就停止状态机
    if (context.source.startsWith(`</${ancestors[i].tag}>`)) {
      return true
    }
  }
}

function parseTag(context, type = 'start') {
  // 从上下文对象中拿到 advanceBy 函数
  const { advanceBy, advanceSpaces } = context
  // 处理开始标签和结束标签的正则表达式不同
  const match =
    type === 'start'
      ? // 匹配开始标签
        /^<([a-z][^\t\r\n\f />]*)/i.exec(context.source)
      : // 匹配结束标签
        /^<\/([a-z][^\t\r\n\f />]*)/i.exec(context.source)
  // 匹配成功后,正则表达式的第一个捕获组的值就是标签名称
  const tag = match[1]
  // 消费正则表达式匹配的全部内容,例如 '<div' 这段内容
  advanceBy(match[0].length)
  // 消费标签中无用的空白字符
  advanceSpaces()

  // 调用 parseAttributes 函数完成属性与指令的解析,并得到 props 数组,
  // props 数组是由指令节点与属性节点共同组成的数组
  const props = parseAttributes(context)

  // 在消费匹配的内容后,如果字符串以 '/>' 开头,则说明这是一个自闭合标签
  const isSelfClosing = context.source.startsWith('/>')
  // 如果是自闭合标签,则消费 '/>', 否则消费 '>'
  advanceBy(isSelfClosing ? 2 : 1)
  // 返回标签节点
  return {
    type: 'Element',
    // 标签名称
    tag,
    // 将 props 数组添加到标签节点上
    props,
    // 子节点留空
    children: [],
    // 是否自闭合
    isSelfClosing,
  }
}

function parseAttributes(context) {
  const { advanceBy, advanceSpaces } = context
  // 用来存储解析过程中产生的属性节点和指令节点
  const props = []

  // 开启 while 循环,不断地消费模板内容,直至遇到标签的“结束部分”为止
  while (!context.source.startsWith('>') && !context.source.startsWith('/>')) {
    // 解析属性或指令
    // 该正则用于匹配属性名称
    const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)
    // 得到属性名称
    const name = match[0]
    // 消费属性名称
    advanceBy(name.length)
    // 消费属性名称与等于号之间的空白字符
    advanceSpaces()
    // 消费等于号
    advanceBy(1)
    // 消费等于号与属性值之间的空白字符
    advanceSpaces()

    // 属性值
    let value = ''
    // 获取当前模板内容的第一个字符
    const quote = context.source[0]
    // 判断属性值是否被引号引用
    const isQuoted = quote === '"' || quote === "'"

    if (isQuoted) {
      // 属性值被引号引用,消费引号
      advanceBy(1)
      // 获取下一个引号的索引
      const endQuoteIndex = context.source.indexOf(quote)

      if (endQuoteIndex > -1) {
        // 获取下一个引号之前的内容作为属性值
        value = context.source.slice(0, endQuoteIndex)
        // 消费属性值
        advanceBy(value.length)
        // 消费引号
        advanceBy(1)
      } else {
        // 缺少引号错误
        console.error('缺少引号')
      }
    } else {
      // 代码运行到这里,说明属性值没有被引号引用
      // 下一个空白字符之前的内容全部作为属性值
      const match = /^[^\t\r\n\f >]+/.exec(context.source)
      // 获取属性值
      value = match[0]
      // 消费属性值
      advanceBy(value.length)
    }
    // 消费属性值后面的空白字符
    advanceSpaces()
    // 使用属性名称 + 属性值创建一个属性节点,添加到 props 数组中
    props.push({
      type: 'Attribute',
      name,
      value,
    })
  }

  // 将解析结果返回
  return props
}

function parseText(context) {
  // endIndex 为文本内容的结尾索引,默认将整个模板剩余内容都作为文本内容
  let endIndex = context.source.length
  // 寻找字符 < 的位置索引
  const ltIndex = context.source.indexOf('<')
  // 寻找定界符 {{ 的位置索引
  const delimiterIndex = context.source.indexOf('{{')
  // 取 ltIndex 和当前 endIndex 中较小的一个作为新的结尾索引
  if (ltIndex > -1 && ltIndex < endIndex) {
    endIndex = ltIndex
  }
  // 取 delimiterIndex 和当前 endIndex 中较小的一个作为新的结尾索引
  if (delimiterIndex > -1 && delimiterIndex < endIndex) {
    endIndex = delimiterIndex
  }
  // 此时 endIndex 是最终的文本内容的结尾索引,调用 slice 函数截取文本内容
  const content = context.source.slice(0, endIndex)
  // 消耗文本内容
  context.advanceBy(content.length)
  // 返回文本节点
  return {
    // 节点类型
    type: 'Text',
    // 文本内容
    content: decodeHtml(content),
  }
}

const namedCharacterReferences = {
  gt: '>',
  'gt;': '>',
  lt: '<',
  'lt;': '<',
  'ltcc;': '⪦',
}

const CCR_REPLACEMENTS = {
  0x80: 0x20ac,
  0x82: 0x201a,
  0x83: 0x0192,
  0x84: 0x201e,
  0x85: 0x2026,
  0x86: 0x2020,
  0x87: 0x2021,
  0x88: 0x02c6,
  0x89: 0x2030,
  0x8a: 0x0160,
  0x8b: 0x2039,
  0x8c: 0x0152,
  0x8e: 0x017d,
  0x91: 0x2018,
  0x92: 0x2019,
  0x93: 0x201c,
  0x94: 0x201d,
  0x95: 0x2022,
  0x96: 0x2013,
  0x97: 0x2014,
  0x98: 0x02dc,
  0x99: 0x2122,
  0x9a: 0x0161,
  0x9b: 0x203a,
  0x9c: 0x0153,
  0x9e: 0x017e,
  0x9f: 0x0178,
}

// 第一个参数为要被解码的文本内容
// 第二个参数是一个布尔值,代表文本内容是否作为属性值
function decodeHtml(rawText, asAttr = false) {
  let offset = 0
  const end = rawText.length
  // 经过解码后的文本将作为返回值被返回
  let decodedText = ''
  // 引用表中实体名称的最大长度
  let maxCRNameLength = 0
  // advance 函数用于消费指定长度的文本
  function advance(length) {
    offset += length
    rawText = rawText.slice(length)
  }
  // 消费字符串,直到处理完毕为止
  while (offset < end) {
    // 用于匹配字符引用的开始部分,如果匹配成功,那么 head[0] 的值将有三种可能:
    // 1. head[0] === '&',这说明该字符引用是命名字符引用
    // 2. head[0] === '&#',这说明该字符引用是用十进制表示的数字字符引用
    // 3. head[0] === '&#x',这说明该字符引用是用十六进制表示的数字字符引用
    const head = /&(?:#x?)?/i.exec(rawText)
    // 如果没有匹配,说明已经没有需要解码的内容了
    if (!head) {
      // 计算剩余内容的长度
      const remaining = end - offset
      // 将剩余内容加到 decodedText 上
      decodedText += rawText.slice(0, remaining)
      // 消费剩余内容
      advance(remaining)
      break
    }
    // head.index 为匹配的字符 & 在 rawText 中的位置索引
    // 截取字符 & 之前的内容加到 decodedText 上
    decodedText += rawText.slice(0, head.index)
    // 消费字符 & 之前的内容
    advance(head.index)

    // 如果满足条件,则说明是命名字符引用,否则为数字字符引用
    if (head[0] === '&') {
      let name = ''
      let value
      // 字符 & 的下一个字符必须是 ASCII 字母或数字,这样才是合法的命名字符引用
      if (/[0-9a-z]/i.test(rawText[1])) {
        if (!maxCRNameLength) {
          maxCRNameLength = Object.keys(namedCharacterReferences).reduce(
            (max, name) => Math.max(max, name.length),
            0
          )
        }

        // 从最大长度开始对文本进行截取,并试图去引用表中找到对应的项
        for (let length = maxCRNameLength; !value && length > 0; --length) {
          // 截取字符 & 到最大长度之间的字符作为实体名称
          name = rawText.substr(1, length)
          // 使用实体名称去索引表中查找对应项的值
          value = namedCharacterReferences[name]
        }

        // 如果找到了对应项的值,说明解码成功
        if (value) {
          // 检查实体名称的最后一个匹配字符是否是分号
          const semi = name.endsWith(';')
          // 如果解码的文本作为属性值,最后一个匹配的字符不是分号,
          // 并且最后一个匹配字符的下一个字符是等于号(=)、ASCII 字母或数字,
          // 由于历史原因,将字符 & 和实体名称 name 作为普通文本
          if (
            asAttr &&
            !semi &&
            /[=a-z0-9]/i.test(rawText[name.length + 1] || '')
          ) {
            decodedText += '&' + name
            advance(1 + name.length)
          } else {
            // 其他情况下,正常使用解码后的内容拼接到 decodedText 上
            decodedText += value
            advance(1 + name.length)
          }
        } else {
          // 如果没有找到对应的值,说明解码失败
          decodedText += '&' + name
          advance(1 + name.length)
        }
      } else {
        // 如果字符 & 的下一个字符不是 ASCII 字母或数字,则将字符 & 作为普通文本
        decodedText += '&'
        advance(1)
      }
    } else {
      // 判断是十进制表示还是十六进制表示
      const hex = head[0] === '&#x'
      // 根据不同进制表示法,选用不同的正则
      const pattern = hex ? /^&#x([0-9a-f]+);?/i : /^&#([0-9]+);?/
      // 最终,body[1] 的值就是 Unicode 码点
      const body = pattern.exec(rawText)

      if (body) {
        // 根据对应的进制,将码点字符串转换为数字
        const cp = Number.parseInt(body[1], hex ? 16 : 10)
        // 码点的合法性检查
        if (cp === 0) {
          // 如果码点值为 0x00,替换为 0xfffd
          cp = 0xfffd
        } else if (cp > 0x10ffff) {
          // 如果码点值超过 Unicode 的最大值,替换为 0xfffd
          cp = 0xfffd
        } else if (cp >= 0xd800 && cp <= 0xdfff) {
          // 如果码点值处于 surrogate pair 范围内,替换为 0xfffd
          cp = 0xfffd
        } else if ((cp >= 0xfdd0 && cp <= 0xfdef) || (cp & 0xfffe) === 0xfffe) {
          // 如果码点值处于 noncharacter 范围内,则什么都不做,交给平台处理
          // noop
        } else if (
          // 控制字符集的范围是:[0x01, 0x1f] 加上 [0x7f, 0x9f]
          // 去掉 ASICC 空白符:0x09(TAB)、0x0A(LF)、0x0C(FF)
          // 0x0D(CR) 虽然也是 ASICC 空白符,但需要包含
          (cp >= 0x01 && cp <= 0x08) ||
          cp === 0x0b ||
          (cp >= 0x0d && cp <= 0x1f) ||
          (cp >= 0x7f && cp <= 0x9f)
        ) {
          // 在 CCR_REPLACEMENTS 表中查找替换码点,如果找不到,则使用原码点
          cp = CCR_REPLACEMENTS[cp] || cp
        }
        // 解码后追加到 decodedText 上
        decodedText += String.fromCodePoint(cp)
        // 消费整个数字字符引用的内容
        advance(body[0].length)
      } else {
        // 如果没有匹配,则不进行解码操作,只是把 head[0] 追加到 decodedText 上并消费
        decodedText += head[0]
        advance(head[0].length)
      }
    }
  }

  return decodedText
}

function parseInterpolation(context) {
  // 消费开始定界符
  context.advanceBy('{{'.length)
  // 找到结束定界符的位置索引
  closeIndex = context.source.indexOf('}}')
  if (closeIndex < 0) {
    console.error('插值缺少结束定界符')
  }
  // 截取开始定界符与结束定界符之间的内容作为插值表达式
  const content = context.source.slice(0, closeIndex)
  // 消费表达式的内容
  context.advanceBy(content.length)
  // 消费结束定界符
  context.advanceBy('}}'.length)

  // 返回类型为 Interpolation 的节点,代表插值节点
  return {
    type: 'Interpolation',
    // 插值节点的 content 是一个类型为 Expression 的表达式节点
    content: {
      type: 'Expression',
      // 表达式节点的内容则是经过 HTML 解码后的插值表达式
      content: decodeHtml(content),
    },
  }
}

function parseComment(context) {
  context.advanceBy('<!--'.length)
  closeIndex = context.source.indexOf('-->')
  const content = context.source.slice(0, closeIndex)
  context.advanceBy(content.length)
  context.advanceBy('-->'.length)
  return {
    type: 'Comment',
    content,
  }
}

编译优化

javascript
const PatchFlags = {
  TEXT: 1, // 代表节点有动态的 textContent
  CLASS: 2, // 代表元素有动态的 class 绑定
  STYLE: 3, // 代表元素有动态的 style 绑定
  // 其他......
}

// 动态节点栈
const dynamicChildrenStack = []

// 当前动态节点集合
let currentDynamicChildren = null

// openBlock 用来创建一个新的动态节点集合,并将该集合压入栈中
function openBlock() {
  dynamicChildrenStack.push((currentDynamicChildren = []))
}
// closeBlock 用来将通过 openBlock 创建的动态节点集合从栈中弹出
function closeBlock() {
  currentDynamicChildren = dynamicChildrenStack.pop()
}

function createVNode(tag, props, children, flags) {
  const key = props && props.key
  props && delete props.key

  const vnode = {
    tag,
    props,
    key,
    patchFlags: flags,
  }

  if (typeof flags !== 'undefined' && currentDynamicChildren) {
    // 动态节点,将其添加到当前动态节点集合中
    currentDynamicChildren.push(vnode)
  }

  return vnode
}

render() {
  // 1. 使用 createBlock 代替 createVNode 来创建 block
   // 2. 每当调用 createBlock 之前,先调用 openBlock
  return (openBlock(), createBlock('div', null, [
    createVNode('p', { class: 'foo' }, null, 1),
    createVNode('p', { class: 'bar' }, null )
  ]))
}

function createBlock(tag, props, children) {
  // block 本质上也是一个 vnode
  const block = createVNode(tag, props, children)
   // 将当前动态节点集合作为 block.dynamicChildren
  block.dynamicChildren = currentDynamicChildren
  // 关闭 block
  closeBlock()
  // 返回
  return block
}

function patchElement(n1, n2) {
  const el = n2.el = n1.el
  const oldProps = n1.props
  const newProps = n2.props

  // 省略部分代码
  if (n2.patchFlags) {
    if (n2.patchFlags === 1) {
      // textContent
    } else if (n2.patchFlags === 2) {
      // class
    } else if (n2.patchFlags === 3) {
      // style
    } else {
      // 全量更新
      for (const key in newProps) {
        if (newProps[key] !== oldProps[key]) {
          patchProps(el, key, oldProps[key], newProps[key])
        }
      }

      for (const key in oldProps) {
        if(!(key in newProps)) {
          patchProps(el, key, oldProps[key], null)
        }
      }
    }

    patchChildren(n1, n2, el)
  }

  if (n2.dynamicChildren) {
    // 只会更新动态节点
    patchBlockChildren(n1, n2)
  } else {
    patchChildren(n1, n2, el)
  }
}

function patchBlockChildren(n1, n2) {
  for (let i = 0; i < n2.dynamicChildren.length; i++) {
    patchElement(n1.dynamicChildren[i], n2.dynamicChildren[i])
  }
}

// Block树
// v-if 在 Diff 过程中,渲染器能够根据 Block 的 key 值区分出更新前后的两个 Block 是不同的,并使用新的 Block 替换旧的Block。
const block = {
  tag: 'div',
    dynamicChildren: [
    /* Block(Section v-if) 或者 Block(Section v-else) */
    {
      tag: 'section',
      {
        key: 0 /* key 值会根据不同的 Block 而发生变化 */
      },
      dynamicChildren: [...] },
  ]
}

// v-for 让带有 v-for 指令的标签也作为 Block 角色即可。
// 前后v-for的节点数量或顺序不同时,
// 只能放弃根据dynamicChildren 数组中的动态节点进行靶向更新的思路,
// 并回退到传统虚拟DOM 的 Diff 手段,即直接使用 Fragment 的 children 而非 dynamicChildren 来进行 Diff 操作。
const block = {
  tag: 'div',
  dynamicChildren: [
    // 这是一个 Block,它有 dynamicChildren
    { tag: Fragment, dynamicChildren: [/* v-for 的节点 */] }
    { tag: 'i', children: ctx.foo, 1 /* TEXT */ },
    { tag: 'i', children: ctx.bar, 1 /* TEXT */ },
  ]
}

// 静态提升
// 把纯静态的节点提升到渲染函数之外后,在渲染函数内只会持有对静态节点的引用。
const hoist1 = createVNode('p', null, 'text')

function render() {
  return (openBlock(), createBlock('div', null, [
    hoist1, // 静态节点引用
    createVNode('p', null, ctx.title, 1 /* TEXT */)
  ]))
}

// 静态提升的 props 对象
const hoistProp = { foo: 'bar', a: 'b' }

function render(ctx) {
  return (openBlock(), createBlock('div', null, [
    createVNode('p', hoistProp, ctx.text)
  ]))
}

// 包含大量连续纯静态的标签节点:预字符串化
const hoistStatic = createStaticVNode('<p></p><p></p><p></p>...20 个...<p></p>')

render() {
return (openBlock(), createBlock('div', null, [
    hoistStatic
  ]))
}

// 对内联事件处理函数进行缓存
// Vue.js 3 不仅会缓存内联事件处理函数,配合 v-once 还可实现对虚拟 DOM 的缓存。
function render(ctx, cache) {
  return h(Comp, {
    // 将内联事件处理函数缓存到 cache 数组中
    onChange: cache[0] || (cache[0] = ($event) => (ctx.a + ctx.b))
  })
}

// v-once
render(ctx, cache) {
return (openBlock(), createBlock('div', null, [
  cache[1] || (
    setBlockTracking(-1), // 阻止这段 VNode 被 Block 收集
    cache[1] = h("div", null, ctx.foo, 1 /* TEXT */),
     setBlockTracking(1), // 恢复
    cache[1] // 整个表达式的值
  )
  ]))
}