Vue3.0源码学习——更新流程分析

507 阅读3分钟

「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」。

前言

前文 Vue3.0源码学习——初始化流程分析(3.patch过程) 学习了首次挂载的过程中,在 patch 函数中会一路调用 mountComponent挂载组件 =》 setupRenderEffect 副作用安装函数,这个函数的执行过程中会建立更新机制,当前组件响应式数据发生变化将会重新执行更新函数,内部又会递归调用 patch

调用栈

为了方便查看更新的过程,首先新建一个能触发Vue更新机制的页面

  <div id="app">
    <h1>vue3 更新流程</h1>
    <p>{{ counter }}</p>
  </div>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          counter: 1
        }
      },
      mounted() {
        setInterval(() => {
          this.counter++
        }, 1000)
      }
    })

    app.mount('#app')
  </script>
  • 这个页面中有一个响应式的数据 counter,在页面加载完成后,每隔1秒会将 counter +1

  • 在浏览期中打开页面,然后找到 patch 函数定义的位置 /packages/runtime-core/src/renderer.ts,在 switch 处打上断点,这里是判断当前要比较新的 vonde 的类型,立马就进入了断点,证明 counter 的值已经改变了并触发了 patch(此时还未渲染到页面上)

image.png

  • 此时查看调用栈就是在更新流程中调用的函数

图片.png

从下往上看

  • anonymous 自定义的 setInterval 方法
  • set 响应式拦截
  • set3 调用拦截函数
  • trigger 触发组件更新函数
  • triggerEffects 调用触发函数 => queueJob => queueFlush => 这里其实是一个异步更新机制,将需要更新的地方放入队列然后在异步队列中一次性更新 => flushJobs

图片.png

  • run => componentUpdateFn 这里就是调用了 setupRenderEffect 副作用安装函数中的组件更新函数
  • ptach

单步调试

  • 在测试页面中打断点,然后根据调用栈找到对应调用函数的地方

图片.png

  • 单步进入响应式拦截操作 PublicInstanceProxyHandlers 函数,位置 /packages/runtime-core/src/componentPublicInstance.ts,触发了第一次 set 拦截(proxy对象),并会重新对 counter 赋值

图片.png

  • 进入到 set 拦截函数,位置 /packages/reactivity/src/baseHandlers.ts 触发 trigger 组件更新函数,由于是更新 counter 因此走else if分支

图片.png

  • 进入 trigger,位置/packages/reactivity/src/effect.ts,触发 triggerEffects

图片.png

  • 进入 triggerEffects,遍历依赖收集,依次触发依赖的 scheduler

图片.png

  • 进入 scheduler ,就到了 setupRenderEffect 中 ,这个 scheduler 就是将当前组件的更新函数放入队列中,这里是一个异步队列 queueJob,将在将来某个时候执行,异步函数最终要执行 componentUpdateFn 组件更新函数,这里的 effect 是异步的方式调用 componentUpdateFn 函数产生的副作用

图片.png

  • componentUpdateFn 一开始已经挂载过,isMounted === true,因此走else 分支,也就是更新流程

图片.png

图片.png

  • 更新流程中拿到新旧vnode,nextTreeprevTree然后放入 patch 进行比较更新并更新视图 图片.png

  • 再单步执行完成 patch ,这时候就可以看到视图发生了变化

image.png

  • 到这里就跑完了更新流程

源码赏析

  • 副作用安装函数 packages\runtime-core\src\renderer.ts
const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    ...
  ) => {
    // 组件更新函数
    // 对patch产生了调用,在调用之前会会获取渲染函数的结果,也就是当前组件的vnode
    // 在首次执行渲染函数时,其实已经建立了依赖关系
    const componentUpdateFn = () => {
      if (!instance.isMounted) { // 首次挂载
        ...
      } else {
        // 更新组件
        ...
        // 获取最新的vnode
        const nextTree = renderComponentRoot(instance)
        ...
        // 获取缓存的oldVnode
        const prevTree = instance.subTree
        instance.subTree = nextTree
        
        ... 
        // 执行diff patch
        patch(
          prevTree,
          nextTree,
          ...
        )
        ...
      }
    }

    // create reactive effect for rendering
    // 为组件的渲染创建一个响应式的副作用函数
    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn, // 执行函数
      () => queueJob(instance.update), // scheduler定时器任务
      instance.scope // track it in component's effect scope
    ))
    
    // 前面被queueJob的是effect.run
    const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
    update.id = instance.uid

    ...

    update()
  }
  • 异步更新队列 packages\runtime-core\src\scheduler.ts
export function queueJob(job: SchedulerJob) {
  ...
  if (
    (!queue.length ||
      !queue.includes(
        job,
        isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
      )) &&
    job !== currentPreFlushParentJob
  ) {
    if (job.id == null) {
      queue.push(job) // 将更新函数直接放入队列
    } else {
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
    queueFlush() // 启动批量任务执行
  }
}

... 

function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs) // 异步执行队列中的更新函数
  }
}

流程图总结

建立更新机制

图片.png

更新过程

Vue3更新流程(2).png