Vue—关于响应式(二、异步更新队列原理分析)
2022/10/26 4:24:56
本文主要是介绍Vue—关于响应式(二、异步更新队列原理分析),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
本节需要准备知识点:Event Loop、Promise
关于Event Loop介绍参考阮一峰老师的文章:
- http://www.ruanyifeng.com/blog/2013/10/event_loop.html
- https://www.ruanyifeng.com/blog/2014/10/event-loop.html
关于Promise:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise
[上一节]学习了Vue通过Object.defineProperty拦截数据变化的响应式原理,数据变化后会触发notify方法来通知变更,这一节沿着图谱继续往下啃,收到通知后Vue会开启一个异步更新队列
两个问题:
-
Vue开启一个异步更新队列,为什么不是同步而是异步?
-
不知道你有没有发现在Vue中修改data中的数据时,无论修改几次,最终模板只渲染一次,这是怎么做到的?
一、异步更新队列
先来看一段代码演示
把上一节的代码拿过来:
let x; let y; let f = (n) => n * 100; let active; let onXChange = function(cb) { active = cb; active(); }; class Dep { deps = new Set(); // 收集依赖 depend() { if (active) { this.deps.add(active); } } // 通知依赖更新 notify() { this.deps.forEach((dep) => dep()); } } let ref = (initValue) => { let value = initValue; let dep = new Dep(); return Object.defineProperty({}, "value", { get() { dep.depend(); return value; }, set(newValue) { value = newValue; dep.notify(); }, }); }; x = ref(1); onXChange(() => { y = f(x.value); console.log('onXChange', y); }); x.value = 2; x.value = 3;
假设我们现在不止依赖x,还有y、z,分别将x、y、z输出到页面上。我们现在依赖了x、y、z三个变量,那我们应该把这个onXChange函数名改成watch,就是它可以监听变化的意思,不单单只是监听一个x变化。
let x; let y; let z; x = ref(1); y = ref(2); z = ref(3); // 考虑到我们会依赖很多变量,因此将onXChange改成watch比较符合语义 watch(() => { document.write(` <p> x: ${f(x.value)}; y: ${f(y.value)}; z: ${f(z.value)} </p> `) });
可以看到这三个值都被打印在页面上
现在我们对x、y、z的value进行修改
x.value = 2; y.value = 3; z.value = 4;
查看页面,结果没有问题,每个数据的变化都被监听到并且进行了响应
既然结果是对的,那我们的问题是什么?
这个问题是:每次数据变化都进行了响应,每次都渲染了模板,如果数据变化了一百次、一千次呢?难道要重复渲染一百遍、一千遍吗?
我们都知道频繁操作dom会影响网页性能,涉及重排和重绘的知识感兴趣请阅读阮一峰老师的文章:
https://www.ruanyifeng.com/blog/2015/09/web-page-performance-in-depth.html
因此,既要保证所有的依赖都准确更新,又要保证不能频繁渲染成为了首要问题,现在我们修改x.value、y.value、z.value都是同步通知依赖进行更新的,有没有一种机制可以等到我修改这些值之后再执行更新任务呢?
这个答案是——异步。
异步任务会等到同步任务清空后执行,借助这个特点和我们前面的分析,我们需要:
- 创建一个队列用来存储任务
- 创建一个将任务推入队列的方法
- 创建一个用来执行队列中任务的方法
- Promise(用来创建微任务)
按照步骤我们创建如下代码:
// 创建任务队列 let queue = []; // 创建添加任务的方法 let queueJob = (job) => { // 过滤已经添加的任务 if (!queue.includes(job)) { queue.push(job); // 添加任务 flushJobs(); // 执行任务,请注意这里现在是伪代码 } }; // 创建执行任务的方法 let flushJobs = () => { let job; // 依次取出队列的任务赋值给job并执行,直到清空队列 while ((job = queue.shift()) !== undefined) { job(); } }; // 创建Promise,待定
接着我们需要修改一下notify的代码,监听到数据变化后不立即调用依赖进行更新,而是将依赖添加到队列中
notify() { this.deps.forEach(dep => queueJob(dep)); }
回到页面,我们发现页面上还是重复渲染了三次模板
那我们写的这段代码有什么用呢?异步又体现在哪里呢?接着往下看
二、 nextTick原理分析
上面的代码中虽然我们开启了一个队列,并且成功将任务推入队列中进行执行,但本质上还是同步推入和执行的,我们要让它变成异步队列
我们创建nextTick函数,nextTick接收一个回调函数,返回一个状态为fulfilled的Promise,并将回调函数传给then方法
// 创建Promise let nextTick = (cb) => Promise.resolve().then(cb);
然后只需要在添加任务时调用nextTick,将执行任务的flushJobs函数传给nextTick即可
let queueJob = (job) => { // 过滤已经添加的任务 if (!queue.includes(job)) { queue.push(job); // 添加任务 nextTick(flushJobs); // 推入微任务 } };
回到页面
虽然修改了x、y、z三个变量的value,最后页面上只渲染了一次。
再来总结一下这段代码的执行过程:
- 当修改x.value时会触发dep.notify()通知依赖更新,然后我们会开启一个队列将任务(这个任务就是active保存的回调函数)发给queueJob函数,queueJob函数判断当前任务有没有添加过,没有,添加当前任务并执行nextTick(Promise),由于Promise调用then方法时会将then中的回调函数推入微任务队列,所以flushJobs函数并不会立即执行,而是等到所有的同步任务都执行完成后再执行,也就是说要等到y、z修改value之后(如果后面还有别的同步代码则要继续等待),直到Event Loop下一个tick时才会执行flushJobs函数。(这三次通知触发的都是同一个active,所以queueJob只会往队列中添加一次任务)
- 因此,无论后面y、z的值进行多少次变更,当前这个更新任务只执行一次,这样就达到了优化的目的。
这也正是Vue采用的解决方案——异步更新队列,官方文档描述的很清楚
文档地址:https://cn.vuejs.org/v2/guide/reactivity.html#异步更新队列
三、结合Vue源码来看nextTick
在Vue中我们可以通过两种方式来调用nextTick:
- Vue.nextTick()
- this.$nextTick()
(至于什么时候使用nextTick,你不偷懒看了官方文档的话都能找到答案哈哈)
以下源码节选自vue2.6.11版本,这两个API分别在initGlobalAPI函数和renderMixin函数中挂载,它们都引用了nextTick函数
nextTick源码如下:
在内部它访问了外部的callbacks,这个callbacks就是前面提到的队列,nextTick一调用就给队列push一个回调函数,然后判断pending(pending的作用就是控制同一时间内只执行一次timerFunc),调用timerFunc(),最后返回了一个Promise(使用过nextTick的应该都知道吧)。
我们来看一下callbacks、pending、timerFunc是如何定义的
可以看到timerFunc函数只是调用了p.then方法并将flushCallbacks函数推入了微任务队列,而p是一个fulfilled状态的Promise,与我们自己的nextTick功能一致。
这个flushCallbacks函数又干了什么呢?
flushCallbacks中重新将pending置为初始值,复制callbacks队列中的任务后将队列清空,然后依次执行复制的任务,与我们自己的flushJobs函数功能一致。
看完上面的源码,可以总结出Vue是这么做的,又到了小学语文之——提炼中心思想的时候了
- 监听到数据变化后调用dep.notify()进行通知,将任务放入队列,且相同的任务只添加一次
- 调用Promise.resolve().then(flushCallbacks),将执行任务的函数推入微任务队列,等待所有的同步任务完成后将进行执行
- 所有同步执行完成,执行flushCallbacks函数进行渲染
对比一下我们自己写的代码,你学会了吗?
这篇关于Vue—关于响应式(二、异步更新队列原理分析)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-12-28Vue入门教程:从零开始搭建第一个Vue项目
- 2024-12-28Vue CLI入门指南:快速搭建Vue项目
- 2024-12-28Vue3基础知识入门教程
- 2024-12-28Vue3公共组件开发与使用入门教程
- 2024-12-28Vue CLI学习:新手入门教程
- 2024-12-28Vue CLI学习:轻松入门与实践指南
- 2024-12-28Vue3公共组件学习入门指南
- 2024-12-28Vue3公共组件学习:从入门到上手实战
- 2024-12-28Vue3学习:从入门到初级实战教程
- 2024-12-28Vue3学习:新手入门与初级教程