从原理上理解 vue3 的队列模型

简介: 从原理上理解 vue3 的队列模型

普通的队列模型

1686381660437.png


普通的队列模型,现实生活中随处可见,饭堂的排队,先来的先打饭。

它有以下特点:

  1. 是一个有先后顺序的列表
  2. 可以出队和入队
  3. 先进先出

vue 的队列模型


三个队列


vue 有 3 个队列,分别为组件数据更新前组件数据更新组件数据更新后队列。

3 个队列,都围绕着组件数据更新前中后执行的。

1686381642368.png

什么是组件数据更新?


<template>
  <p>{{name}}</p>
</template>
<script lang="ts">
import { ref, defineComponent } from 'vue'
export default defineComponent({
  setup(){
    const name = ref("vue setup")
    return {
      name
    }
  }
})
</script>

上面代码中,组件数据更新,就是指组件的 name 的值更新,DOM 的值也被更新的过程(虽然 DOM 值更新但 UI 仍未更新,因为 js 线程仍在运行代码)

两种不同的队列模型


vue 的 3 个队列中,有 2 种不同的队列模型

  1. 组件数据更新前(后面称 Pre 队列)、组件数据更新后队列 (后面称 Post 队列)—— 先进先出,无优先级,不可插队,允许递归
  2. 组件数据更新队列 —— 优先级高的 Job 先执行,允许插队,允许递归

这里先介绍一下组件数据更新队列


组件数据更新队列


1686381613547.png

如图所示,每个 Job 有一个属性 id,id 小的先执行,且中途可以插队。

那为什么要这么设计呢?我们拆分成几个问题一一回答:

Job 的 id 是怎么取值的?允许插队,id 小的 Job 先执行,有什么意义?

Job 的 id 是 vue 组件内部实例的 uid 属性。是一个不会重复的计数器。第一个组件 uid 是 0,第二个组件 uid 是 1,如此类推。

id 小的 Job 先执行,这保证了,父组件永远比子组件先更新(因为先创建父组件,再创建子组件,子组件可能依赖父组件的数据)

仅仅当父组件数据更新完毕,才能更新子组件


试想,如果子组件数据更新先执行,在这之后,如果父组件更新了数据(子组件依赖该数据),那么子组件还需要再执行一次数据更新。

因此,id 小的先执行,即保证了数据的正确性,又提升了数据更新的性能

这也是自上而下的**单向数据流**所决定的组件数据更新顺序。

什么是递归?什么时候允许递归?

如上图所示:当一个 Job 在执行时,将它自身再加入队列,这种情况称为递归。

默认情况下,Job 是不能递归的


允许 Job 递归的情况:组件更新的 Job、watch 的 callback(这也是个 Job)

什么情况下,组件更新会发生递归?

下面是一个例子:

定义全局属性:


const app = createApp(App)
app.config.globalProperties.$loading = {
  isLoading: ref(false),
}

父组件:


<template>
  <div>
    <div>{{ $loading.isLoading.value }}</div>
    <button @click='add'>{{ count }}</button>
    <Children :count='count' />
  </div>
</template>
<script setup lang='ts'>
// 省略 import
const count = ref(0)
function add() {
  count.value = count.value + 1
}
</script>

子组件:


<template>
  <div>
    <p>{{ count }}</p>
  </div>
</template>
<script lang='ts'>
import { ref, defineComponent, onUpdated } from 'vue'
export default defineComponent({
  props: {
    count: {
      type: Number,
      required: true
    }
  },
  watch: {
    count() {
      this.$loading.isLoading.value = !this.$loading.isLoading.value
    }
  }
})
</script>

有父子两个组件,点击 button 后,其数据流如下:

  1. 父组件 count 自增
  2. 子组件属性被修改,触发 watch,修改 loading
  3. loading 被修改,父组件更新 DOM

我们这里会发现,这个例子并不满足单向数据流:父组件正在进行数据更新时,子组件修改了全局属性,导致父组件需要进行更新。

因此父组件需要再次进入数据更新队列,再次执行更新,才能保证数据正确。这种情况就是递归。

如果过多的打破单向数据流,会导致多次递归执行更新,可能会导致性能下降


Pre/Post 队列


实际上,Pre/Post 队列,比普通队列复杂一点

1686381556123.png

该类型的队列,执行内容有哪些?

举几个例子:

  1. watch 函数有一个参数 flush,默认为 pre,会将 watch 的 callback 加入到 Pre 队列; flush 设置为 post,则会加入到 POST 队列
  2. mounted 声明周期,是在 Post 队列执行的,因为要等组件更新、 DOM 挂载上去再执行

为什么会有执行和等待两个队列?

因为执行队列时,会将 Job 加入队列,可能会加入多次,在等待队列转成执行队列过程中,可以最后统一执行一次去重

什么时候会递归?


<template>
  <div @click='add'>
    {{list}}
  </div>
</template>
<script setup>
import { reactive,watch } from 'vue'
const list = reactive([])
watch(list, ()=>{
  if(list.length < 10){
    list.push(1)
  }
})
function add(){
  list.push(1)
}
</script>

watch callback 默认在 Pre 队列执行;watch callback 里面又改变了自身,使 callback 又加入 Pre 的等待队列。

当然,一般是不会这么写直接导致递归的,递归往往是间接导致的。如:watch 修改了一个 ref,ref 触发依赖,又导致了 watch 监听的值改变,导致递归。


总结


使用队列模型,能更好的描述、更好的进行解耦, vue 的生命周期、组件更新、单向数据流等设计。

组件更新队列,使用了一个带有 Job id 的可插队队列。

  1. 延迟执行,能够对重复 Job 进行去重,提升性能
  2. 保证了,组件一定是先执行父组件,在执行子组件的数据更新,该顺序是单向数据流决定的
  3. 递归情况下,能够正确处理组件更新顺序,以及保证了数据的正确性

使用 Pre/Post 队列,:

  1. 延迟执行,能够对重复 Job 进行去重,提升性能
  2. 解析组件时,声明周期并没有立即执行,需要在特定时间执行(如组件 mounted 后)

下一篇文章,将会对 vue 队列的源码进行解析。

目录
相关文章
|
4天前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的《数据库系统原理》课程平台附带文章和源代码设计说明文档ppt
基于ssm+vue.js+uniapp小程序的《数据库系统原理》课程平台附带文章和源代码设计说明文档ppt
10 1
|
5天前
|
缓存 JavaScript 前端开发
Vue 3的响应式系统
【5月更文挑战第31天】Vue 3的响应式系统
9 1
|
5天前
|
JavaScript 前端开发 API
vue2 /vue3【nextTick】的使用方法及实现原理,一文全搞懂!
vue2 /vue3【nextTick】的使用方法及实现原理,一文全搞懂!
|
5天前
|
JavaScript 开发者
[vue2/vue3] -- 深入剖析v-model的原理、父子组件双向绑定的多种写法
[vue2/vue3] -- 深入剖析v-model的原理、父子组件双向绑定的多种写法
[vue2/vue3] -- 深入剖析v-model的原理、父子组件双向绑定的多种写法
|
5天前
|
JavaScript API
vue3父子组件相互调用方法详解
vue3父子组件相互调用方法详解
|
12天前
|
JavaScript API
Vue3 基础语法
该内容介绍了Vue项目的创建和Vue3的语法、响应式API、生命周期、组件通信及跨组件通信方法。包括使用`npm init vue@latest`创建项目,`npm install`初始化,Vue3的`setup`语法,`reactive`、`ref`、`computed`和`watch`的用法,生命周期图解,以及父子组件间的数据传递。此外,还提到了Vue3中使用`provide`和`inject`进行跨层数据传递,以及通过Pinia库进行状态管理。
39 0
Vue3 基础语法
|
15天前
|
JavaScript 定位技术 API
在 vue3 中使用高德地图
在 vue3 中使用高德地图
23 0
|
16天前
vue3 键盘事件 回车发送消息,ctrl+回车 内容换行
const textarea = textInput.value.textarea; //获取输入框元素
29 3
|
18天前
|
JavaScript 前端开发 CDN
vue3速览
vue3速览
29 0
|
18天前
|
设计模式 JavaScript 前端开发
Vue3报错Property “xxx“ was accessed during render but is not defined on instance
Vue3报错Property “xxx“ was accessed during render but is not defined on instance