Skip to content

手写 mini-vue3

x

Intro

vue3 源码结构

  • runtime
  • compiler
  • reactivity

x

runtime 与 reactive

  • 如果熟悉渲染函数,可以直接用 runtime 和 reactive 写应用;

template 与渲染函数

  • template ,即将模板编译成 渲染函数

Reactive

effect & reactive

  • reactive与effect是怎样建立联系的呢?
  • 依赖收集 track : 保存副作用与它的依赖关系;
  • 触发更新 trigger:当依赖变更时,找到并执行依赖于它的副作用。
  • 执行顺序

    • 1、执行副作用函数
    • 2、执行过程中发现依赖
    • 3、 在 get 中,对响应式对象的依赖收集
    • 4、 在set中,响应式对象发生变化时触发更新

targetMap

  • 用于 存储副作用,并建立副作用和依赖的对应关系。
  • 一个副作用可能依赖多个响应式对象,一个响应式对象可能有多个属性,一个属性又可能被多个副作用依赖。
  • 因此,数据结构设计:
// 一个 new WeakMap
{
  // key 是 reactiveObject, value 是 Map
  [target]: {
      // key 是 reactiveObject 的 key, value 是 Set
      [key]: [] 
  }
}
  • WeakMap 里,当 reactiveObject 不再使用,GC 可以自动回收。

特殊问题处理

  • 1) reactive(reactive(obj))

    • isReactive(target) 判断是否被代理过:若是,则返回 target。
  • 2) a = reactive(obj); b = reactive(obj);

    • proxyMap 缓存 obj 的代理
    • 同一对象,不重复代理。
  • 3) hasChanged

  • 4) 深层对象代理

    • 延迟(懒)递归:get 触发时,才开始往下递归。
    • return isObject(result) ? reactive(result) : result
  • 5) 数组

    • if (oldLength !== target.length)
    • 手动 trigger(target, 'length');
  • 6) 嵌套 effect

    • 问题
      effect(() => {
         effect(() => {
              console.log('observed.b is', observed.b);
          });
          console.log('observed.a is', observed.a);
     });
    
    // observed.a 更新,无法触发effect
    
    • 解决: effectStack 存放当前执行的副作用

ref

  • class RefImpl {}
  • toReactive

computed

  • class ComputedImpl {}
  • this._dirty: 记录依赖是否被更新
  • lazy: 懒执行,第一次 effectFn() 不立即执行
  • scheduler: 如果存在,scheduler() 优先执行;否则执行 effectFn()。

Runtime

VNode

  • 种类

    • Element: 普通元素,document.createElement
    • Text: 文本结点,document.createTextNode
    • Fragment: 不会被真实渲染的结点,children 会挂在其父节点上
    • Component: 组件
  • ShapeFlags

    • 一组标记,用于快速辨识 VNode 类型和 children 类型。
    const enum ShapeFlags {
      ELEMENT = 1, // 普通元素
      FUNCTIONAL_COMPONENT = 1 << 1, // 函数组件
      STATEFUL_COMPONENT = 1 << 2, // 状态组件
      TEXT_CHILDREN = 1 << 3, // 文本子节点
      ARRAY_CHILDREN = 1 << 4, // 数组子节点
      SLOTS_CHILDREN = 1 << 5, // 插槽子节点
      TELEPORT = 1 << 6, // 传送组件
      SUSPENSE = 1 << 7, // 悬念组件
    }
    
  • h 函数(渲染函数)

    • 模板更接近编译器
    • 参数:标签名、prop、包含其子节点的数组

mount

  • render
  • mount(vnode, container)

patch

  • 流程图

    • 在 render 中,判断 n2 是否存在:若存在,则需要进行 patch;不存在的话,则原先的 n1 需要卸载。
    • 在 patch 中,n1 和 n2 类型相同,可以直接进行 process;不相同,则意味着节点整个发生变化,需要先将 n1 卸载,再挂载 n2。
    • 在 processXXX 中,若 n1 存在,则可以复用 n1 的节点,只需要更新内容,而不需要重新创建新的节点;而如果不存在,需要重新挂载。

    x

  • O(n) 的启发式算法

      1. 两个不同类型的元素会产生出不同的树;
      1. 开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定;
  • patchChildren

    • 3 * 3 种情况: Text, Array, null
    • patchArrayChildren: c1、c2 都是数组
  • VNode.anchor

    • anchor 是 Fragment 专有属性。
    • 子元素根据anchor来确定在dom中的位置。
    • Fragment 首尾各 upsert 一个空 TextNode 作为 anchor。
  • Patch流程细节

x

Component

  • prop & attr
  • normalizeVNode
  • 主动更新

    • 场景:内部状态改变,导致组件重新渲染,称为主动更新。
    • mountComponent -> instance.update -> effect(render)
  • 被动更新

    • 场景:父组件传入的 props 值改变,导致子组件重新渲染,称为子组件的被动更新。
    • 调用 updateComponent

Scheduler

延迟 render

  • 针对 Component 的render 方法
  • 同步函数对所有依赖计算完成之后,再执行 render。
  • 复用 computed 方法的懒计算机制(即 scheduler 属性)
  • queue 保存去重之后的 job,然后 Promise.resolve().then(flushJobs)

nextTick

  • 始终等待当前任务队列 flush 之后,放入浏览器的微任务队列,等待下一次 Event-Loop 执行。

Compiler

编译步骤

  • 模版代码 --parse--> AST --transform--> AST + codegenNode --codegen--> 渲染函数代码
  • parse: 原始的模版字符串,通过 parse 转换成 原始 AST 抽象语法树。
  • transform: AST 经过 transform 生成 codegenNode。

    • codegenNode 是 AST 转化为渲染函数的中间代码,解析原始 AST 语义而来。
    • transform 的作用在于,处理像 v-if 这类带有特殊语义的语法节点。
  • codegen: 遍历 codegenNode,递归生成渲染函数代码。

parse 的实现

  • 什么是 AST?

    • 对于模版字符串 <div id="foo" v-if="ok">Hello {{name}}</div>
    • 依次有:元素节点、属性节点、指令节点、文本节点、插值节点
  • AST 节点类型

const NodeTypes = {
  ROOT: 'ROOT',
  ELEMENT: 'ELEMENT',
  TEXT: 'TEXT',
  SIMPLE_EXPRESSION: 'SIMPLE_EXPRESSION',
  INTERPOLATION: 'INTERPOLATION',
  ATTRIBUTE: 'ATTRIBUTE',
  DIRECTIVE: 'DIRECTIVE',
};
  • Whitespace 优化

codegen

  • tranverseNode/tranversechildren
  • createTextVNode/createInterpolationVNode/createElementVNode

Directives

  • 类型

    • 属性指令:v-on、v-bind、v-html
    • 结构指令:v-if、v-for;会改变 DOM 的结构。
    • v-model
  • v-for

    • 依赖 renderList() 方法
    • 模版语法
    <div v-for="(item, index) initems ">{{item + index)}}</div>
    
    • 渲染函数
    h(
      Fragment,
      null,
      renderList(items, (item, index) => h(tag, null, item + index))
    )
    
  • v-if

    • 模版语法
    <div v-if="ok"></div>
    
    • 渲染函数
    ok ? h('div') : h(Text, null, '')
    
  • v-model

    • 本质上一个语法糖
    • 模版语法
    <input v-mode="name" />
    <!--本质上是-->
    <input :value="name" @input="name = $event.target.value" />
    

组件的引入

  • 组件引入方法:在 createApp() 里声明 components
createApp({
  compoents: { Foo }
});
  • runtime 里配合 resolveComponent 使用

源码

mini-vue2