vue3的编译优化、异步更新策略、patch过程

682 阅读4分钟

1、vue3 编译过程

template ==> ast ===> 通过 render

模板 ==> 抽象语法树 ==> render函数

调用栈

目录位置:vue-next/packages/runtime-core/src/component.ts

mountComponent ---> setupComponent ---> finishComponentSetup

mountComponent 简化流程

目录:packages\runtime-core\src\renderer.ts

// 1.创建实例
const instance = createComponentInstance(...) // 创建组件实例
// ....
// 2组件安装 类似于 vue2_init  合并merge options,// 属性初始化 // 数据响应式 // 插槽处理
setupComponent(instance)
// ....
// 3、 安装渲染函数的副作用
setupRenderEffect()

在 finishComponent 方法内

    // ...
    if (compile && Component.template && !Component.render) {
      if (__DEV__) {
        startMeasure(instance, `compile`)
      }
      // 只有global版本带编译版本才会有这一步走 
      //(断点打到这里一步一步走下去看)
      Component.render = compile(Component.template, {  // 编译template生成render函数
        isCustomElement: instance.appContext.config.isCustomElement,
        delimiters: Component.delimiters
      })
      if (__DEV__) {
        endMeasure(instance, `compile`)
      }
    }
    // ....





关于 dev-compiler 命令 (查看vue3的编译优化)

npm run dev
npm run dev-compiler

打开localhost:5000

进入 packages/template-explorer 目录

可以看到 模板编译

1、静态节点提示

以下为 代码生成字符串

const _Vue = Vue
const { createVNode: _createVNode } = _Vue

// 这里就是静态提升  内存换时间
const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "1", -1 /* HOISTED */) 
const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "2", -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createVNode("p", null, "3", -1 /* HOISTED */)
const _hoisted_4 = /*#__PURE__*/_createVNode("p", null, "4", -1 /* HOISTED */)

return function render(_ctx, _cache) {
  with (_ctx) {
    const { toDisplayString: _toDisplayString, createVNode: _createVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue

    return (_openBlock(), _createBlock(_Fragment, null, [
      _createVNode("p", null, _toDisplayString(counter), 1 /* TEXT */),
      _hoisted_1,
      _hoisted_2,
      _hoisted_3,
      _hoisted_4
    ], 64 /* STABLE_FRAGMENT */))
  }
}

2、 补丁标记和动态属性记录

模板

<div :title='hello'>Hello World!</div>
<p>LALA~~</p>
import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "vue"

const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "LALA~~", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _createVNode("div", { title: _ctx.hello }, "Hello World!", 8 /* PROPS 这里就是patchFlag */, ["title"]),
    _hoisted_1
  ], 64 /* STABLE_FRAGMENT */))
}

patchFlag 为 8 表示只有属性需要更新 0b1000

patchFlag 为 1 表示只有动态文本 0b0001

patchFlag 为 9 表示有属性和动态文本需要 patch 更新比较 0b10011

3、关于事件 有 cacheHandlers 把事件缓存起来,避免重复创建

替换template为

<div :title='hello'>Hello World!</div>
<p @click="onclick">LALA~~</p>
import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _createVNode("div", { title: _ctx.hello }, "Hello World!", 8 /* PROPS */, ["title"]),
    _createVNode("p", {
      onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onclick(...args)))  // 事件会保存到_cache[1]
    }, "LALA~~")
  ], 64 /* STABLE_FRAGMENT */))
}

4、代码区块block 有动态节点会保存起来(用数组) 遍历的时候进行比较

创建区块

 _createBlock(_Fragment, null, [
    _createVNode("div", { title: _ctx.hello }, "Hello World!", 8 /* PROPS */, ["title"]),
    _createVNode("p", {
      onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onclick(...args)))  // 事件会保存到_cache[1]
    }, "LALA~~")
  ], 64 /* STABLE_FRAGMENT */))
}

// 在 虚拟节点vnode属性 dynamicChildren (数组)保存动态节点





vue3异步更新策略

起点位置:

组件的更新方法是个effect一个update函数

  const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // create reactive effect for rendering  更新函数
    instance.update = effect(function componentEffect() {
      //....
    }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
  }
const prodEffectOptions = {
  scheduler: queueJob,  //其实是在这里影响了
  // #1801, #2043 component render effects should allow recursive updates
  allowRecurse: true
}

1、关于effect方法

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }
  return effect
}

这里没找到options里 scheduler 的影响

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options // 这里放置了options
  return effect
}

关于trigger 触发更新

发现trigger用到了scheduler

  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    if (effect.options.scheduler) { // 发现在这里
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  effects.forEach(run)

2、关于 执行更新任务

目录位置:packages\runtime-core\src\scheduler.ts

export function queueJob(job: SchedulerJob) {
  // the dedupe search uses the startIndex argument of Array.includes()
  // by default the search index includes the current job that is being run
  // so it cannot recursively trigger itself again.
  // if the job is a watch() callback, the search will start with a +1 index to
  // allow it recursively trigger itself - it is the user's responsibility to
  // ensure it doesn't end up in an infinite loop.
  if (
    (!queue.length ||
      !queue.includes(
        job,
        isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
      )) &&
    job !== currentPreFlushParentJob
  ) {
    queue.push(job)  // 把effect的函数push到 queue
    queueFlush()
  }
}

function queueFlush() { // flushJobs 是 执行queue里存的effect函数
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}





vue3 patch 过程

1、首先vnode上的变化: children可以数组也可以是文本

2、dynamicChildren 、 dynamicProps 记录动态节点和属性比对,减少遍历操作

3、patchFlag 标注动态内容 目录位置:packages\shared\src\patchFlags.ts

4、shapeFlag 标记组件形态(是否是组件或teleport)

shapeFlag

export const enum ShapeFlags {
  ELEMENT = 1,
  FUNCTIONAL_COMPONENT = 1 << 1, // 2 函数组件
  STATEFUL_COMPONENT = 1 << 2, // 4  状态组件
  TEXT_CHILDREN = 1 << 3, // 8 文本孩子
  ARRAY_CHILDREN = 1 << 4, // 16 数组孩子
  SLOTS_CHILDREN = 1 << 5, // 32 插槽孩子
  TELEPORT = 1 << 6, // 传送
  SUSPENSE = 1 << 7, // 悬念
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 应该缓存的组件
  COMPONENT_KEPT_ALIVE = 1 << 9, // 被缓存的组件
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}

关于patchKeyChildren diff算法

packages\runtime-core\src\renderer.ts

1、掐头

2、掐尾

3、新的有多直接插入

4、旧的有多直接删

5、按旧的(方式)遍历一个一个找