页面是如何更新的

前言

这是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时候的注意事项。

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

相关推荐
Huazzi.14 分钟前
免费好用的静态网页托管平台全面对比介绍
前端·网络·github·web
吃土少女古拉拉21 分钟前
前端和后端
前端·学习笔记
寒雒1 小时前
【Python】实战:实现GUI登录界面
开发语言·前端·python
独上归州1 小时前
Vue与React的Suspense组件对比
前端·vue.js·react.js·suspense
Komorebi⁼2 小时前
Vue核心特性解析(内含实践项目:设置购物车)
前端·javascript·vue.js·html·html5
明月清风徐徐2 小时前
Vue实训---0-完成Vue开发环境的搭建
前端·javascript·vue.js
SameX2 小时前
HarmonyOS Next 企业数据备份与恢复策略
前端·harmonyos
SameX2 小时前
HarmonyOS Next 企业数据传输安全策略
前端·harmonyos
daopuyun2 小时前
LoadRunner小贴士|开发Web-HTTP/HTML协议HTML5相关视频应用测试脚本的方法
前端·http·html
李先静2 小时前
AWTK-WEB 快速入门(1) - C 语言应用程序
c语言·开发语言·前端