页面是如何更新的

前言

这是vue3系列源码的第七章,使用的vue3版本是3.2.45

推荐

createApp都发生了什么

mount都发生了什么

页面到底是从什么时候开始渲染的

setup中的内容到底是什么时候执行的

ref reactive是怎么实现的

响应式到底是怎么实现的

背景

在上一篇文章中,我们看了一下响应式的基本原理,详细的了解了get 时是如何触发的依赖收集,当数据发生更改的时候,set 是如何根据收集到的effect 来触发对应的更新流程的。在set 的触发流程里面,我们一直追踪,最终追踪到了componentUpdateFn这个函数。我们在渲染的时候就执行过这个函数,那么现在又到了这个函数,很明显,这个函数往下,就是页面的更新流程了。

前置

我们的页面还是和上一篇文章一样。

js 复制代码
<template>
  <div>{{ aa }}</div>
  <div>{{ bb.name }}</div>
  <div @click="change">点击</div>
</template>
<script setup>
import { ref, reactive } from 'vue'

const change = () => {
  aa.value = '小识'
  bb.name = '谭记'
}

const aa = ref('小石')
const bb = reactive({ name: '潭记' })

</script>

这里我们接上一篇的流程,直接到aa.value = '小识'触发的set 流程中的componentUpdateFn函数中。

componentUpdateFn

js 复制代码
   const componentUpdateFn = () => {
      if (!instance.isMounted) {
        ...
      } else {
        // updateComponent
        // This is triggered by mutation of component's own state (next: null)
        // OR parent calling processComponent (next: VNode)
        let { next, bu, u, parent, vnode } = instance
        let originNext = next
        let vnodeHook: VNodeHook | null | undefined
        if (next) {
          next.el = vnode.el
          updateComponentPreRender(instance, next, optimized)
        } else {
          next = vnode
        }
        toggleRecurse(instance, true)

        const nextTree = renderComponentRoot(instance)
       
        const prevTree = instance.subTree
        instance.subTree = nextTree
        patch(
          prevTree,
          nextTree,
          // parent may have changed if it's in a teleport
          hostParentNode(prevTree.el!)!,
          // anchor may have changed if it's in a fragment
          getNextHostNode(prevTree),
          instance,
          parentSuspense,
          isSVG
        )
        next.el = nextTree.el
        if (originNext === null) {
          // self-triggered update. In case of HOC, update parent component
          // vnode el. HOC is indicated by parent instance's subTree pointing
          // to child component's vnode
          updateHOCHostEl(instance, nextTree.el)
        }
       }
    }

这里我们走更新的这一部分流程。

这一部分的核心流程是:

  • const nextTree = renderComponentRoot(instance)

renderComponentRoot

这个函数其实都挺熟的了,我们在页面到底是从什么时候开始渲染的ref reactive是怎么实现的两篇文章里面,都提到过这个函数。

他最主要的作用就是执行了render 函数,得到了组件的vnode

js 复制代码
 result = normalizeVNode(
        render!.call(
          proxyToUse,
          proxyToUse!,
          renderCache,
          props,
          setupState,
          data,
          ctx
        )
      )

这里我们先看一下这里涉及到的参数,其中setupState

我们发现,此时变量aa 的值已经发生了改变,这没问题,但是此时bb 的值竟然也更新了。我们明明才执行到aa.value = '小识',为什么bb的值也已经发生了改变?我们先放在这里,我们接着往下看。

那么既然执行了render 函数,那么必然的,会触发get 。 这个时候我们再到trackRefValue函数中看看,看看和初次渲染时候有什么不一样。

trackRefValue

js 复制代码
function trackRefValue(ref) {
    if (shouldTrack && activeEffect) {
        ref = toRaw(ref);
        if ((process.env.NODE_ENV !== 'production')) {
            trackEffects(ref.dep || (ref.dep = createDep()), {
                target: ref,
                type: "get" /* TrackOpTypes.GET */,
                key: 'value'
            });
        }
        else {
            trackEffects(ref.dep || (ref.dep = createDep()));
        }
    }
}

我们看一下参数ref

响应式到底是怎么实现的文章里面我们看到了_rawValue_value的值的更新,所以这里的值是已经更新后的值。

这里的dep 字段也不再是空的,存的是activeEffect对象, 正是通过这个对象,我们的set流程才最终触发了页面更新的过程。

然后到了trackEffects函数,这里和初次渲染不同,少了核心的依赖收集的过程。

js 复制代码
function trackEffects(dep, debuggerEventExtraInfo) {
    let shouldTrack = false;
    if (effectTrackDepth <= maxMarkerBits) {
        if (!newTracked(dep)) {
            dep.n |= trackOpBit; // set newly tracked
            shouldTrack = !wasTracked(dep);
        }
    }
    else {
        // Full cleanup mode.
        shouldTrack = !dep.has(activeEffect);
    }
    if (shouldTrack) {
        dep.add(activeEffect);
        activeEffect.deps.push(dep);
        if ((process.env.NODE_ENV !== 'production') && activeEffect.onTrack) {
            activeEffect.onTrack(Object.assign({ effect: activeEffect }, debuggerEventExtraInfo));
        }
    }
}

这里的shouldTrack 最终得到的是false,没有今天依赖收集的部分。

那么get部分就结束了。

render 函数执行完了之后,就要更新<div>{{ aa }}</div>这一部分的vnode 了,render 执行的结果作为参数传入createBaseVNode函数中。

接下来会执行剩下代码的render函数,这里的值已经发生变化了。

js 复制代码
<div>{{ bb.name }}</div>
<div @click="change">点击</div>

patch

在通过renderComponentRoot(instance) 得到了更新内容的vnode之后,接下来就是patch环节了。

js 复制代码
const patch: PatchFn = (
   n1,
   n2,
   container,
   anchor = null,
   parentComponent = null,
   parentSuspense = null,
   isSVG = false,
   slotScopeIds = null,
   optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
 ) =>  {
 const { type, ref, shapeFlag } = n2
   switch (type) {
      case Fragment:
       processFragment(
         n1,
         n2,
         container,
         anchor,
         parentComponent,
         parentSuspense,
         isSVG,
         slotScopeIds,
         optimized
       )
       break
   }
   }

这里看一下参数:

  • n1,数据更改之前App.vue中代码段的vnode
  • n2,数据更新之后App.vue中代码段的vnode
  • container, #appDom对象
  • parentComponent,App.vue的组件实例

这里因为是代码段,所以最终执行了processFragment

processFragment

js 复制代码
 const processFragment = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
    const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!

    let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2

    if (n1 == null) {
    ...
    } else {
      if (
        patchFlag > 0 &&
        patchFlag & PatchFlags.STABLE_FRAGMENT &&
        dynamicChildren &&
        // #2715 the previous fragment could've been a BAILed one as a result
        // of renderSlot() with no valid children
        n1.dynamicChildren
      ) {
        // a stable fragment (template root or <template v-for>) doesn't need to
        // patch children order, but it may contain dynamicChildren.
        patchBlockChildren(
          n1.dynamicChildren,
          dynamicChildren,
          container,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds
        )
    }
  }

patchBlockChildren

js 复制代码
 const patchBlockChildren: PatchBlockChildrenFn = (
    oldChildren,
    newChildren,
    fallbackContainer,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds
  ) => {
    for (let i = 0; i < newChildren.length; i++) {
      const oldVNode = oldChildren[i]
      const newVNode = newChildren[i]
      // Determine the container (parent element) for the patch.
      const container =
        // oldVNode may be an errored async setup() component inside Suspense
        // which will not have a mounted element
        oldVNode.el &&
        // - In the case of a Fragment, we need to provide the actual parent
        // of the Fragment itself so it can move its children.
        (oldVNode.type === Fragment ||
          // - In the case of different nodes, there is going to be a replacement
          // which also requires the correct parent container
          !isSameVNodeType(oldVNode, newVNode) ||
          // - In the case of a component, it could contain anything.
          oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
          ? hostParentNode(oldVNode.el)!
          : // In other cases, the parent container is not actually used so we
            // just pass the block element here to avoid a DOM parentNode call.
            fallbackContainer
      patch(
        oldVNode,
        newVNode,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        true
      )
    }
  }

先看一下参数:

  • oldChildren, 小石潭记所在的两个div组成的数组,也就是旧的vnode
  • newChildren, 小识谭记所在的两个div组成的数组,也就是新的vnode

这里我们看到,页面上明明有三个div,为什么这里只有两个,点击的那个div不见了。

我们看一下传进来的参数:

js 复制代码
patchBlockChildren( n1.dynamicChildren, dynamicChildren, 
container, parentComponent, parentSuspense, isSVG, slotScopeIds )

这里传进来进行patch 的都是dynamicChildren,固定的元素不会被更新。

这个函数就是把新旧node依次拿出来进行对比。

这个过程和mount的过程很像,从fragment --> Element

然后在patchElement的时候,最终执行

js 复制代码
hostSetElementText(el, n2.children as string)

setElementText: (el, text) => {
    el.textContent = text
  }

对页面进行更新,此时,页面上的小石就会变成小识

接着又依次取出一个新老节点进行对比,这次更新了潭记谭记

异步更新

到了这里,我们其实也把页面的更新流程大概讲了一遍,那么还记得我们文中提到的问题,为什么我们明明走在aa.value = '小识'set 里面,但是我们走到更新流程的时候,发现bb.name的数据也更新了,难道bb.name = bb.name = '谭记'这一句也执行了吗。

那自然是肯定执行了的,那么到底是什么时候,趁我们不注意执行的。

在上一篇文章,响应式到底是怎么实现的中,我们在set的流程里面,最终走到了把更新任务加入任务队列这一步。

最终调用了这个函数。

js 复制代码
function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

这个函数是通过promise微任务来实现异步更新的。

那么我们问题的答案也就在这里。

我们这里面是打断点一步一步调试的,然后断点直接进入了这个then函数执行。

那么这里有一个特性,如果打断点进入then 函数,那么就会直接进入then里回调函数的执行,也就是说中间的任务会偷偷执行,断点进不去。

所以事情的真相 就是,我们在aa的数据更新了之后,我们断点进入了then 函数的回调,进入了微任务的执行,但是微任务的执行之前会先把所有的宏任务都执行完,所以在我们断点没有触及到的地方,bb.name的数据也更新了。然后我们到后面的流程去看的时候,就发现了上面的问题。

那么应该如何避免这个问题,很简单,只要我们跳过currentFlushPromise = resolvedPromise.then(flushJobs)这一句的执行。

你可以直接跳过queueFlush函数的执行或者在queueFlush函数里面执行到这一句之前直接跳出这个函数,这个时候,我们就能进入到bb.name = bb.name = '谭记'这一句代码的执行,然后进入对应的set过程。

总结

那么通过这一个简单的例子,我们总结一下vue3页面的更新流程:

  • 触发set,查找对应的更新函数
  • 将更新的函数加入任务队列,作为微任务进行异步更新
  • 继续触发其他的set过程,同上
  • 最后执行微任务,对相应的vnode进行逐级patch,直到更新页面

这里,我们还发现了断点调试中在面对promise时候的注意事项。

那么,以上就是页面更新流程的全部内容了。

相关推荐
持久的棒棒君10 分钟前
ElementUI 2.x 输入框回车后在调用接口进行远程搜索功能
前端·javascript·elementui
2401_8572979121 分钟前
秋招内推2025-招联金融
java·前端·算法·金融·求职招聘
一 乐26 分钟前
租拼车平台|小区租拼车管理|基于java的小区租拼车管理信息系统小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·微信·notepad++·拼车
undefined&&懒洋洋1 小时前
Web和UE5像素流送、通信教程
前端·ue5
winkee3 小时前
在 git commit 中使用 gpg key 进行签名
架构·前端框架·代码规范
大前端爱好者3 小时前
React 19 新特性详解
前端
小程xy3 小时前
react 知识点汇总(非常全面)
前端·javascript·react.js
随云6323 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
随云6323 小时前
WebGL编程指南之进入三维世界
前端·webgl
寻找09之夏4 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js