Vue3源码分析-完整update流程和diif算法

Posted 吐司FE

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue3源码分析-完整update流程和diif算法相关的知识,希望对你有一定的参考价值。

前言

在上一篇文章vue的首次渲染过程中提到在组件的首次渲染阶段会有一个副作用渲染函数setupRenderEffect,在这个函数内会使用响应式API Effect创建副作用函数componentEffect,这里只需要简单的理解为,当组件内的数据改动时这个由Effect包裹的componentEffect就会重新调用,在这个函数内部会判断当前组件是处于首次渲染还是更新,当组件内数据发生变化时会进入到update的分支,本文要看的diff流程也就是从这里开始。

PS: 当前的Vue版本是3.0.5。本文会忽略TELEPORT、SUSPENSE等特殊vnode类型,对于一些细微的优化也会忽略(比如patch函数的optimized参数)。

回顾setupRenderEffect

在组件的数据发生改变时会自动触发副作用渲染函数setupRenderEffect

// runtime-core/src/renderer.ts
import { effect } from '@vue/reactivity'

const setupRenderEffect = (instance, initialVNode, container, ...) => {
  // 在当前的组件实例上添加 update 方法,通过响应式方法 effect 创建
  instance.update = effect(function componentEffect({
    if (!instance.isMounted) {

      //...
      
    } else {
      // 非首次渲染,进入 update 阶段
      let { next, vnode } = instance
      // 缓存一份更新后的 vnode
      let originNext = next
      if (next) {
        // 这里需要把更新后的组件 vnode 赋值 el,因为下面第一次 渲染新的组件 vnode 时并没有设置 el
        next.el = vnode.el
        updateComponentPreRender(instance, next)
      } else {
        // 如果没有 next 直接指向当前 vnode
        next = vnode
      }
      // 渲染新的组件 vnode,因为数据发生了变化
      const nextTree = renderComponentRoot(instance)
      // 缓存旧的组件 vnode
      const preTree = instance.subTree
      // 更新实例上的 subTree 为新的组件 vnode
      instance.subTree = nextTree
      
      patch(prevTree, nextTree, ..., instance)
      // 更新 DOM 元素
      next.el = nextTree.el
    }
  }, prodEffectOptions)

这里主要看update的部分(即else代码块)。首先会先从组件实例instance里取出next(在处理组件一节会详细说明next存在与不存在的情况)。

组件内的数据发生了改变,所以要生成新的组件模板的vnode节点,在渲染阶段命名为subTree,然后还要保存一份旧的subTree,这样有了新旧subTree后就可以用patch函数更新DOM

patch函数

在进入具体的diff流程之前,我们不妨先想一下,当数据发生改变时,会有哪些变化类型?

实际上按照类型可以分为更新普通DOM元素和更新vue组件这两种情况。下面先关注一下patch函数的逻辑。

// runtime-core/src/renderer.ts
const patch = (n1, n2, container, ...) => {
  // n1 是旧节点,n2 是新节点
  // 如果 n1、n2 新旧节点的类型不同,直接销毁旧节点
  if(n1 && !isSameVNodeType(n1, n2)) {
    unmount(n1, parentComponent, parentSuspense, true)
    n1 = null
  }
  
  const { type, ref, shapeFlag } = n2
  
  switch(type) {
    // 处理文本节点
    case Text:
      processText(n1, n2, container, anchor)
      break
    // 处理注释
    case Comment:
      processCommentNode(n1, n2, container, anchor)
      break
    
    // ...
    
    default
      // 处理 DOM 元素
      if(shapeFlag & ShapeFlags.ELEMENT) {
        processElement(n1, n2, container, ...)
      // 处理 vue 组件
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        processComponent(n1, n2, container, ...)
      }
      
      // ...
  }
  
}

最开始先判断新旧节点的类型是否一样,如果不一样,可以设想某个节点从div标签变成span标签,最合理的方式是直接舍弃旧节点,重新挂载新的节点。

再往下会根据当前节点类型type进行特定的处理。比如文本节点执行processText的特定处理、注释节点执行processCommentNode的特定处理。这样的前置处理实际上是一个优化,在编译阶段,vue会将模版语法编译成渲染函数,这个时候会把第一个参数节点类型type填上,如果这个参数命中了这样的特殊节点,会直接执行相应的process方法。

default块儿里才是分析的重点,即处理普通DOM元素和vue组件。

处理DOM元素

先举一个栗子

以上是关于Vue3源码分析-完整update流程和diif算法的主要内容,如果未能解决你的问题,请参考以下文章

vue3源码分析——实现组件的挂载流程

vue3源码分析——实现组件的挂载流程

vue3源码分析——实现组件通信provide,inject

vue3源码分析——实现组件通信provide,inject

vue3源码分析——实现组件通信provide,inject

vue3源码分析——实现slots