需要储备的前置知识: Event Loop、宏任务、微任务。
Event Loop
同步和异步
众所周知,js的一大特点就是js是单线程,可以理解为,同一个时间只能做一件事。因为js的主要用途是与用户交互啊,操作DOM啊等等,所以它只能是单线程,那么为什么呢?试想一下,如果js不是单线程而是多线程,那么如果一个线程在某个DOM节点上添加内容,而另一个线程则要删除这个节点,这时浏览器应该以哪个线程为准呢?所以js设计出来就是单线程,这也让我们操作更为简单。
但是单线程就意味着,所有的任务都需要排队,只有等前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就只能一直等待。我们知道计算是使用CPU,输入输出是IO,当我们使用ajax请求数据的时候,我们等待结果出来再往下执行的话,在这个等待时间中CPU其实是空闲着的,所以设计者意识到这段时间主线程完全可以不管IO,挂起处于等待中的任务,先运行排在后面的任务。等到IO返回了结果,再回过头把挂起的任务继续执行下去。于是,所有任务可以分成两种,一种是同步任务,另一种是异步任务。同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。而异步任务不进入主线程,而是被放入到任务队列(task queue)中。
任务队列
任务队列是一个事件的队列,也可以理解成消息的队列,是一个先进先出的数据结构,排在前面的事件,会优先被主线程读取。
异步执行的运行机制是:
- 所有同步任务都在主线程上执行,形成一个执行栈。
- 除主线程之外,还存在一个任务队列,异步任务不进入主线程,而是被放入到其中,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
- 一旦执行栈中的所有同步任务执行完毕(也就是说主线程空了),系统就会读取任务队列,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上一步
主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(也称为事件循环)。
宏任务(task)
宏任务代表一个个离散的、独立工作单元。浏览器完成一个宏任务,在下一个宏任务执行开始前,会对页面进行重新渲染。主要包括创建主文档对象、解析HTML、执行主线JS代码以及各种事件如页面加载、输入、网络事件和定时器等。
宏任务按顺序执行,且浏览器在每个宏任务之间渲染页面,也就是说,浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 (task->渲染->task->...)。
微任务(Microtasks)
微任务:微任务是更小的任务,是在当前宏任务执行结束后立即执行的任务。如果存在微任务,浏览器会在清空微任务之后再重新渲染。常见的微任务有 promise、process.nextTick、MutationObserver等。
所有微任务也按顺序执行,且在以下场景会立即执行所有微任务:
- 每个回调之后且js执行栈中为空。
- 每个宏任务结束后。
setTimeout
我们知道setTimeout是定时器,指定某些代码、函数在多少时间之后执行,接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。但是如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。
setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在任务队列的尾部添加一个事件,因此要等到同步任务和任务队列现有的事件都处理完,才会得到执行。
vue的具体实现
- 异步:只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
- 批量:如果同一个watcher被多次触发,只会被推入到队列中一次。去重对于避免不必要的计算和DOM 操作是非常重要的。然后,在下一个的事件循环tick中,Vue 刷新队列执行实际工作。
- 异步策略:Vue 在内部对异步队列尝试使用原生的 Promise.then 、 MutationObserver 和setImmediate ,如果执行环境都不支持,则会采用 setTimeout(fn, 0) 代替。
那么我们来看看源码中具体是怎样做的吧。
在响应式中,我们知道修改数据,执行setter,里面核心就是调用dep的notify方法。
dep
在dep的notify方法中,我们看到,就是拿取所有的watcher,遍历依次执行每个watcher的update方法。
watcher
在watcher的update方法中,我来看到,就是几个判断,判断lazy、sync属性,这两个我们一般都不会设置,所以核心就是调用queueWatcher -> watcher入队操作。
scheduler
首先先获取watcher的id,然后判断是否已经存在于队列中,这样就可以避免重复添加,如果没有并且没有处于刷新,那么watcher就入队。
然后判断是否处于等待状态,如果没有,那么就异步地执行任务队列。
flushSchedulerQueue就是确切执行方法(拿取watcher,调用run方法),nextTick就是使用异步的方式去刷新。那么我们就跟踪去看看nextTick。
next-tick
首先先拿取传入进来的执行函数(也就是flushSchedulerQueue),进行容错封装,在文件上面定义了一个空数组callbacks,将封装的函数放入到这个数组中。然后判断如果没有处于挂起状态,就执行timerFunc函数。
在这个方法上面,定义了timerFunc函数。
所以我们看到,首选异步解决方案是Promise,并且是以微任务的方式执行回调函数。如果不支持promise,那么再判断是否支持MutationObserver,如果这两个都不支持,那么再判断是否支持setImmediate,如果都不支持,那么最后再使用setTimeout(flushCallbacks, 0)。
我们再看看flushCallbacks是什么。
其实就是封装的执行函数,也就是flushSchedulerQueue。