本节须要筹备知识点:Event Loop、Promise

对于Event Loop介绍参考阮一峰老师的文章:

  1. http://www.ruanyifeng.com/blo...
  2. https://www.ruanyifeng.com/bl...

对于Promise:https://developer.mozilla.org...

上一节学习了Vue通过Object.defineProperty拦挡数据变动的响应式原理,数据变动后会触发notify办法来告诉变更,这一节沿着图谱持续往下啃,收到告诉后Vue会开启一个异步更新队列

两个问题:

  1. Vue开启一个异步更新队列,为什么不是同步而是异步?
  2. 不晓得你有没有发现在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/bl...

因而,既要保障所有的依赖都精确更新,又要保障不能频繁渲染成为了首要问题,当初咱们批改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原理剖析

下面的代码中尽管咱们开启了一个队列,并且胜利将工作推入队列中进行执行,但实质上还是同步推入和执行的,咱们要让它变成异步队列

于是就到了Promise发挥作用的时候了,对于宏工作和微工作的介绍请参考:
https://zhuanlan.zhihu.com/p/...

咱们创立nextTick函数,nextTick接管一个回调函数,返回一个状态为fulfilled的Promise,并将回调函数传给then办法

// 创立Promiselet 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...

三、联合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是这么做的,又到了小学语文之——提炼中心思想的时候了

  1. 监听到数据变动后调用dep.notify()进行告诉,将工作放入队列,且雷同的工作只增加一次
  2. 调用Promise.resolve().then(flushCallbacks),将执行工作的函数推入微工作队列,期待所有的同步工作实现后将进行执行
  3. 所有同步执行实现,执行flushCallbacks函数进行渲染

比照一下咱们本人写的代码,你学会了吗?

以上演示代码已上传github:

https://github.com/Mr-Jemp/Vu...

前面要学习的内容在这里:

Vue—对于响应式(三、Diff Patch原理剖析)

Vue—对于响应式(四、深刻学习Vue响应式源码)

本文由博客一文多发平台 OpenWrite 公布!