简单聊一聊Vue3组件更新过程

目录
  • 前言
  • 副作用渲染函数更新组件的过程
  • 核心逻辑:patch流程
    • 1.处理组件
    • 2.处理普通元素
  • 总结

前言

组件渲染的过程,本质上就是把各种把各种类型的 vnode 渲染成真实 DOM。我们也知道了组件是由模板、组件描述对象和数据构成的,数据的变化会影响组件的变化。组件的渲染过程中创建了一个带副作用的渲染函数,当数据变化的时候就会执行这个渲染函数来触发组件的更新。本文我们就具体分析一下组件的更新过程。

副作用渲染函数更新组件的过程

我们先来回顾一下带副作用渲染函数 setupRenderEffect 的实现,但是这次我们要重点关注更新组件部分的逻辑:

const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) = >{
  instance.update = effect(function componentEffect() {
    if (!instance.isMounted) {} else {
      let {
        next,
        vnode
      } = instance
      if (next) {
        updateComponentPreRender(instance, next, optimized)
      } else {
        next = vnode
      }
      const nextTree = renderComponentRoot(instance) const prevTree = instance.subTree
      instance.subTree = nextTree
      patch(prevTree, nextTree, hostParentNode(prevTree.el), getNextHostNode(prevTree), instance, parentSuspense, isSVG)
      next.el = nextTree.el
      }
  },
prodEffectOptions)
}

可以看到,更新组件主要做三件事情:更新组件 vnode 节点、渲染新的子树 vnode、根据新旧子树vnode 执行 patch 逻辑。

首先是更新组件 vnode 节点,这里会有一个条件判断,判断组件实例中是否有新的组件 vnode(用next 表示),有则更新组件 vnode,没有 next 指向之前的组件 vnode。为什么需要判断,这其实涉及一个组件更新策略的逻辑,我们稍后会讲。

接着是渲染新的子树 vnode,因为数据发生了变化,模板又和数据相关,所以渲染生成的子树 vnode也会发生相应的变化。

最后就是核心的 patch 逻辑,用来找出新旧子树 vnode 的不同,并找到一种合适的方式更新 DOM,接下来我们就来分析这个过程。

核心逻辑:patch流程

我们先来看 patch 流程的实现代码:

const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) = >{
  if (n1 && !isSameVNodeType(n1, n2)) {
    anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, true) n1 = null
  }
  const {
    type,
    shapeFlag
  } = n2
  switch (type) {
  case Text:
    break
  case Comment:
    break
  case Static:
    break
  case Fragment:
    break
  default:
    if (shapeFlag & 1) {
      processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
    } else if (shapeFlag & 6) {
      processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
    } else if (shapeFlag & 64) {} else if (shapeFlag & 128) {}
  }
}
function isSameVNodeType(n1, n2) {
  return n1.type === n2.type && n1.key === n2.key
}

在这个过程中,首先判断新旧节点是否是相同的 vnode 类型,如果不同,比如一个 div 更新成一个ul,那么最简单的操作就是删除旧的 div 节点,再去挂载新的 ul 节点。

如果是相同的 vnode 类型,就需要走 diff 更新流程了,接着会根据不同的 vnode 类型执行不同的处理逻辑,这里我们仍然只分析普通元素类型和组件类型的处理过程。

1.处理组件

如何处理组件的呢?举个例子,我们在父组件 App 中里引入了 Hello 组件:

<template>
  <div>
    <p>This is an app.</p>
    <hello :msg="msg"></hello>
    <button @click="toggle">Toggle msg</button>
  </div>
</template>
<script>
export default {
  data () {
    return { msg: 'Vue' }
  },
  methods: {
    toggle () {
      this.msg = this.msg === 'Vue' ? 'World' : 'Vue'
    }
  }
}
</script>

Hello 组件中是 <div>包裹着一个 <p> 标签, 如下所示:

<template>
  <div>
    <p>Hello, {{ msg }}</p>
  </div>
</template>
<script>
export default {
  props: { msg: String }
}
</script>

点击 App 组件中的按钮执行 toggle 函数,就会修改 data 中的 msg,并且会触发 App 组件的重新渲染。

结合前面对渲染函数的流程分析,这里 App 组件的根节点是 div 标签,重新渲染的子树 vnode 节点是一个普通元素的 vnode,应该先走 processElement 逻辑。组件的更新最终还是要转换成内部真实DOM 的更新,而实际上普通元素的处理流程才是真正做 DOM 的更新,由于稍后我们会详细分析普通元素的处理流程,所以我们先跳过这里,继续往下看。

和渲染过程类似,更新过程也是一个树的深度优先遍历过程,更新完当前节点后,就会遍历更新它的子节点,因此在遍历的过程中会遇到 hello 这个组件 vnode 节点,就会执行到 processComponent 处理逻辑中,我们再来看一下它的实现,我们重点关注一下组件更新的相关逻辑:

const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) = >{
  if (n1 == null) {} else {
    updateComponent(n1, n2, parentComponent, optimized)
  }
}
const updateComponent = (n1, n2, parentComponent, optimized) = >{
  const instance = (n2.component = n1.component) if (shouldUpdateComponent(n1, n2, parentComponent, optimized)) {
    instance.next = n2 invalidateJob(instance.update) instance.update()
  } else {
    n2.component = n1.component n2.el = n1.el
  }
}

可以看到,processComponent 主要通过执行 updateComponent 函数来更新子组件,updateComponent 函数在更新子组件的时候,会先执行 shouldUpdateComponent 函数,根据新旧子组件 vnode 来判断是否需要更新子组件。这里你只需要知道,在 shouldUpdateComponent 函数的内部,主要是通过检测和对比组件 vnode 中的 props、chidren、dirs、transiton 等属性,来决定子组件是否需要更新。

这是很好理解的,因为在一个组件的子组件是否需要更新,我们主要依据子组件 vnode 是否存在一些会影响组件更新的属性变化进行判断,如果存在就会更新子组件。

虽然 Vue.js 的更新粒度是组件级别的,组件的数据变化只会影响当前组件的更新,但是在组件更新的过程中,也会对子组件做一定的检查,判断子组件是否也要更新,并通过某种机制避免子组件重复更新。

我们接着看 updateComponent 函数,如果 shouldUpdateComponent 返回 true ,那么在它的最后,先执行 invalidateJob(instance.update)避免子组件由于自身数据变化导致的重复更新,然后又执行了子组件的副作用渲染函数 instance.update 来主动触发子组件的更新。

再回到副作用渲染函数中,有了前面的讲解,我们再看组件更新的这部分代码,就能很好地理解它的逻辑了:

let {
  next,
  vnode
} = instance
if (next) {
  updateComponentPreRender(instance, next, optimized)
} else {
  next = vnode
}
const updateComponentPreRender = (instance, nextVNode, optimized) = >{
  nextVNode.component = instance
  const prevProps = instance.vnode.props
  instance.vnode = nextVNode
  instance.next = null
  updateProps(instance, nextVNode.props, prevProps, optimized) 		  	    updateSlots(instance, nextVNode.children)
}

结合上面的代码,我们在更新组件的 DOM 前,需要先更新组件 vnode 节点信息,包括更改组件实例的 vnode 指针、更新 props 和更新插槽等一系列操作,因为组件在稍后执行 renderComponentRoot时会重新渲染新的子树 vnode ,它依赖了更新后的组件 vnode 中的 props 和 slots 等数据。

所以我们现在知道了一个组件重新渲染可能会有两种场景,一种是组件本身的数据变化,这种情况下next 是 null;另一种是父组件在更新的过程中,遇到子组件节点,先判断子组件是否需要更新,如果需要则主动执行子组件的重新渲染方法,这种情况下 next 就是新的子组件 vnode。

你可能还会有疑问,这个子组件对应的新的组件 vnode 是什么时候创建的呢?答案很简单,它是在父组件重新渲染的过程中,通过 renderComponentRoot 渲染子树 vnode 的时候生成,因为子树 vnode是个树形结构,通过遍历它的子节点就可以访问到其对应的组件 vnode。再拿我们前面举的例子说,当App 组件重新渲染的时候,在执行 renderComponentRoot 生成子树 vnode 的过程中,也生成了hello 组件对应的新的组件 vnode。

所以 processComponent 处理组件 vnode,本质上就是去判断子组件是否需要更新,如果需要则递归执行子组件的副作用渲染函数来更新,否则仅仅更新一些 vnode 的属性,并让子组件实例保留对组件vnode 的引用,用于子组件自身数据变化引起组件重新渲染的时候,在渲染函数内部可以拿到新的组件vnode。

前面也说过,组件是抽象的,组件的更新最终还是会落到对普通 DOM 元素的更新。所以接下来我们详细分析一下组件更新中对普通元素的处理流程。

2.处理普通元素

我们再来看如何处理普通元素,我把之前的示例稍加修改,将其中的 Hello 组件删掉,如下所示:

<template>
  <div>
    <p>This is {{msg}}</p>
    <button @click="toggle">Toggle msg</button>
  </div>
</template>
<script>
export default {
  data () {
    return { msg: 'Vue' }
  },
  methods: {
    toggle () {
      this.msg === 'Vue' ? 'World' : 'Vue'
    }
  }
}

</script>

当我们点击 App 组件中的按钮会执行 toggle 函数,然后修改 data 中的 msg,这就触发了 App 组件的重新渲染。

App 组件的根节点是 div 标签,重新渲染的子树 vnode 节点是一个普通元素的 vnode,所以应该先走processElement 逻辑,我们来看这个函数的实现:

const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) = >{
  isSVG = isSVG || n2.type === 'svg'
  if (n1 == null) {} else {
    patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
  }
}
const patchElement = (n1, n2, parentComponent, parentSuspense, isSVG, optimized) = >{
  const el = (n2.el = n1.el) const oldProps = (n1 && n1.props) || EMPTY_OBJ 	 	const newProps = n2.props || EMPTY_OBJ
  patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG) const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
  patchChildren(n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG)
}

可以看到,更新元素的过程主要做两件事情:更新 props 和更新子节点。其实这是很好理解的,因为一个 DOM 节点元素就是由它自身的一些属性和子节点构成的。

首先是更新 props,这里的 patchProps 函数就是在更新 DOM 节点的 class、style、event 以及其它的一些 DOM 属性。

其次是更新子节点,我们来看一下这里的 patchChildren 函数的实现:

const patchChildren = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized = false) = >{
  const c1 = n1 && n1.children const prevShapeFlag = n1 ? n1.shapeFlag: 0 const c2 = n2.children const {
    shapeFlag
  } = n2
  if (shapeFlag & 8) {
    if (prevShapeFlag & 16) {
      unmountChildren(c1, parentComponent, parentSuspense)
    }
    if (c2 !== c1) {
      hostSetElementText(container, c2)
    }
  } else {
    if (prevShapeFlag & 16) {
      if (shapeFlag & 16) {
        patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      } else {
        unmountChildren(c1, parentComponent, parentSuspense, true)
      }
    } else {
      if (prevShapeFlag & 8) {
        hostSetElementText(container, '')
      }
      if (shapeFlag & 16) {
        mountChildren(c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      }
    }
  }
}

对于一个元素的子节点 vnode 可能会有三种情况:纯文本、vnode 数组和空。那么根据排列组合对于新旧子节点来说就有九种情况,我们可以通过三张图来表示。

首先来看一下旧子节点是纯文本的情况:

  • 如果新子节点也是纯文本,那么做简单地文本替换即可;
  • 如果新子节点是空,那么删除旧子节点即可;
  • 如果新子节点是 vnode 数组,那么先把旧子节点的文本清空,再去旧子节点的父容器下添加多个新子节点。

接下来看一下旧子节点是空的情况:

  • 如果新子节点是纯文本,那么在旧子节点的父容器下添加新文本节点即可;
  • 如果新子节点也是空,那么什么都不需要做;
  • 如果新子节点是 vnode 数组,那么直接去旧子节点的父容器下添加多个新子节点即可。

最后来看一下旧子节点是 vnode 数组的情况:

  • 如果新子节点是纯文本,那么先删除旧子节点,再去旧子节点的父容器下添加新文本节点;
  • 如果新子节点是空,那么删除旧子节点即可;
  • 如果新子节点也是 vnode 数组,那么就需要做完整的 diff 新旧子节点了,这是最复杂的情况,内部运用了核心 diff 算法。

总结

到此这篇关于Vue3组件更新过程的文章就介绍到这了,更多相关Vue3组件更新过程内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Vue组件更新数据v-model不生效的解决

    目录 组件更新数据v-model不生效 问题描述 原因分析 方法一 方法二 方法三 关于v-model失效的问题 解决办法 组件更新数据v-model不生效 问题描述 在使用Vue双向绑定(v-model)功能时,封装子组件通过Inject功能使用了父组件中的 model 中的属性进行双向绑定,此时在程序中去更新model的某个属性的值,发现子组件没有实时渲染. 原因分析 由于 JavaScript 的限制,Vue 不能检测数组和对象的变化.尽管如此我们还是有一些办法来回避这些限制并保证它们的响

  • 浅谈vue的props,data,computed变化对组件更新的影响

    本文介绍了vue的props,data,computed变化对组件更新的影响,分享给大家,废话不多说,直接上代码 /** this is Parent.vue */ <template> <div> <div>{{'parent data : ' + parentData}}</div> <div>{{'parent to children1 props : ' + parentToChildren1Props}}</div> <

  • 简单聊一聊Vue3组件更新过程

    目录 前言 副作用渲染函数更新组件的过程 核心逻辑:patch流程 1.处理组件 2.处理普通元素 总结 前言 组件渲染的过程,本质上就是把各种把各种类型的 vnode 渲染成真实 DOM.我们也知道了组件是由模板.组件描述对象和数据构成的,数据的变化会影响组件的变化.组件的渲染过程中创建了一个带副作用的渲染函数,当数据变化的时候就会执行这个渲染函数来触发组件的更新.本文我们就具体分析一下组件的更新过程. 副作用渲染函数更新组件的过程 我们先来回顾一下带副作用渲染函数 setupRenderEf

  • vue3简单封装input组件和统一表单数据详解

    目录 前言 准备工作 用原生 input 封装 Input 封装表单数据 使用表单数据 总结 前言 vue3 支持用 jsx 实现组件,摆脱了 vue 文件式的组件,不再需要额外的指令,写法非常接近 React,减少记忆负担. 本文简单的练习,用 vue3 组件封装 input 组件和统一表单数据. 准备工作 用vue create example创建项目,参数大概如下: 用原生 input 原生的 input,主要是 value 和 change,数据在 change 的时候需要同步. App

  • Vue3组件更新中的DOM diff算法示例详解

    目录 同步头部节点 同步尾部节点 添加新的节点 删除多余节点 处理未知子序列 移动子节点 建立索引图 更新和移除旧节点 移动和挂载新节点 最长递增子序列 总结 总结 在vue的组件更新过程中,新子节点数组相对于旧子节点数组的变化,无非是通过更新.删除.添加和移动节点来完成,而核心 diff 算法,就是在已知旧子节点的 DOM 结构.vnode 和新子节点的 vnode 情况下,以较低的成本完成子节点的更新为目的,求解生成新子节点 DOM 的系列操作. 举例来说,假说我们有一个如下的列表 <ul>

  • vue3组件中v-model的使用以及深入讲解

    目录 v-model input中使用双向绑定数据 组件中的v-model 其他写法 总结 v-model input中使用双向绑定数据 v-model在vue中我们经常用它与input输入框的输入值进行绑定,简单的实现原理大家也应该都知道 通过v-bind绑定value值 及结合@input输入事件动态改变绑定的value值来实现双向绑定,如下vue3实现代码: <template> <input type="text" :value="tryText&q

  •  用Vue Demi 构建同时兼容Vue2与Vue3组件库

    目录 前言: 一.Vue Demi 中的额外 API 1.isVue2 and isVue3 二.Vue Demi 入门 前言: Vue Demi 是一个很棒的包,具有很多潜力和实用性.我强烈建议在创建下一个 Vue 库时使用它. 根据创建者 Anthony Fu 的说法,Vue Demi 是一个开发实用程序,它允许用户为 Vue 2 和 Vue 3 编写通用的 Vue 库,而无需担心用户安装的版本. 以前,要创建支持两个目标版本的 Vue 库,我们会使用不同的分支来分离对每个版本的支持.对于现

  • Vue3组件挂载之创建组件实例详解

    目录 前情提要 mountComponent 创建组件实例 总结 前情提要 上文我们讲解了执行createApp(App).mount('#root')中的mount函数,我们分析了创建虚拟节点的几个方法,以及setRef的执行机制.本文我们继续讲解mountComponent,挂载组件的流程. 本文主要内容 createComponentInstance发生了什么? 如何标准化组件定义的props.emits? 为什么provide提供的值子组件都能访问到? 组件的v-model实现原理.组件

  • Vue3组件库框架搭建example环境的详细教程

    目录 1 搭建 example 开发环境 1.1 创建 example 项目 1.2 修改 package.json 1.3 修改 vite 配置文件 1.4 多环境支持 1.5 测试启动服务 2 测试 foo 组件 2.1 安装依赖 2.2 引入组件库 2.3 使用组件 2.4 运行查看效果 3 example 打包构建 前面用了大量篇幅介绍组件库的开发环境搭建,包括:创建组件.创建组件库入口.组件库样式架构.组件库公共包,做了一大堆工作,还不能预览示例组件 foo,本文便搭建 example

  • 京东 Vue3 组件库支持小程序开发的详细流程

    源码抢先看: https://github.com/jdf2e/nutui NutUI 3.0 官网:https://nutui.jd.com/3x/ 小程序多端适配 设计初衷 在跨端小程序的开发过程中,我们发现没有合适的组件库可以使用,尤其在做电商商城类场景的业务时,没有符合京东 App 规范的组件库为我们的小程序项目提供支持.为了填补这一空白,同时让 NutUI 组件库能够为更多的开发者带来便利,我们决定在 NutUI 3.0 中 增加小程序多端适配的能力. 如何适配 Taro 在小程序跨端

  • Vue3 组件的开发详情

    目录 一.前言 二.组件的开发 1.组件的构成 2.header部分组件的开发 3.footer组件的开发 4.修改App.vue 5.移除Helloword组件及相关代码 6.重启服务查看 三.最后 一.前言 果然长时间坐着或站着,会给腰带来很大负担,声明下我不是腰脱,就是个穿刺手术而已,身上有多处缝针没长好,所以会给肚子和腰带来一定的负担. 上一篇文章已经写了关于布局的开发,传送门<Vue3(三)网站首页布局开发 >,但是我们写代码,肯定是继承了优秀的代码风格,封装的特性,所以这里我们再对

  • vue3组件通信的方式总结及实例用法

    vue3组件通信方式为以下几种 props $emit $expose / ref $attrs v-model provide / inject Vuex mitt props <child :msg2="msg2" /> <script setup> const props = defineProps({ // 写法一 msg2:String // 写法二 msg2:{ type:String, default:'' } }) console.log(pro

随机推荐