最开始查看 nextTick 这个方法的时候, 眼瞎看成了 nextClick... 我还在疑问难道是下一次 click 之后处理事件...
然后用这个方法的时候, 就只知道是用在 DOM 更新之后调用回调方法.
这时就产生了一堆疑问:
1)DOM 更新后? 难道修改数据之后, DOM 没有及时更新, 还有延迟? 但是页面上看到的就是实时更新呀, 难道还有什么猫腻?
2)它是怎么监听到 DOM 被更新了
3)它和异步的 setTimeout,setInterval 有没有关系?
深入了解后才发现里面有大学问...
在理解 nextTick 之前, 先来一段代码
- setTimeout(function(){
- console.log(11)
- },300)
这段代码很简单, 一般人都会说, 300ms 之后控制台打印出 11.
但是, 一定是精确的 300ms 之后马上打印出 11 吗. 答案是不一定. 为什么? 这就涉及到下面的知识点
1. JS 为什么是单线程
深究原因我不是很清楚, 但是我是这样理解的: 假如 JS 是多线程, 意思是如果我对同一个 DOM 进行操作, 那么都会同时处理. 那这时一个线程我对一个按钮修改颜色为 red, 同时另外一个线程对这个按钮修改颜色为 blue. 那浏览器到底是执行哪一个呢, 这样就矛盾了. 所以这就能很好理解为什么要设计成单线程了.
2. Event loop
既然是单线程, 那么事件任务就一定会在主线程上排队执行. 同一时间就只能按队列执行一个方法. 要是某个方法要花费很长时间, 那后面的方法就只能等待了, 这是极其不能忍受的. 所以 JS 设计者把任务分成了同步任务和异步任务. 同步任务即主线程 (执行栈) 上运行的任务, 而异步任务则是挂载到一个任务队列里面. 等待主线程的所有任务执行完成后(栈空), 通知任务队列可以把可执行的任务放到主线程里面执行. 异步任务放到主线程中执行完后, 栈又空了, 又通知任务队列把异步任务放到主线程中执行. 这个过程一直持续, 直到异步任务执行完成, 这个持续重复的过程就叫 Event loop. 而一次循环就是一次 tick.
注意:
1) 这里异步任务例如 setTimeout 这种, 实际上是先由浏览器其它模块 (应该是 IO 设备) 处理之后, 它的回调函数才再加入到任务队列里面. 注意是回调函数.
2) onclick,onmouseover 等都属于异步任务. 回调都会挂载到任务队列.
3. microtast(微任务)和 macrotask(宏任务)
任务队列里面异步任务也分 macrotast(标准说法是 task)和 microtast(标准说法中它是不属于 task 的).
典型的 microtast 包含: Promises(浏览器原生 Promise),MutationObserver,Object.observe(已废弃), 以及 Node.JS 中的 process.nextTick,UI rendering(UI 渲染)
典型的 macrotast 包含: script 整体代码(这个很重要),setTimeout(最短 4ms) , setInterval(最短 10ms),MessageChannel, 以及只有 IE 支持的 setImmediate.
执行优先级上, 先执行宏任务 macrotask, 再执行微任务 mincrotask
process.nextTick> Promise.then > MutationObserver > setImmediate > setTimeout.
注意:
1) 对于 microtast 和 macrotask, 这两个在一次 event loop 中, microtask 在这一次循环中是一直取一直取, 直到清空 microtask 队列, 而 macrotask 则是一次循环取一次.
2) 相当于事件循环的过程是: 主线程 (栈空)---> 取一个 macrotask 执行 ---->查看有没有 microtask, 如果有就执行该任务直到清空 microtask 队列, 然后执行下一个 macrotask 任务 --->又取 macrotask 执行 --->清空 microtask 里面的任务 . 重复第二和第三的步骤直到 macrotask 任务队列也执行完毕
3) 如果执行事件循环的过程中又加入了异步任务, 如果是 macrotask, 则放到 macrotask 末尾, 等待下一轮循环再执行. 如果是 macrotask, 则放到本次 event loop 中的 microtask 任务末尾继续执行. 直到 microtask 队列清空.
4) 为什么宏任务先执行, 反而处理时间还比微任务慢呢? 因为 script 整体也是 macrotask, 就先把 script 里面的代码放到主线程执行, 如果再遇到 macrotask, 就把它放到 macrotask 任务队列末尾, 由于一次 event loop 只能取一个 macrotask, 所以遇到的宏任务就需要等待其它轮次的事件循环了; 如果遇到 microtask, 则放到本次循环的 microtask 队列中去. 这样就能明白为什么 microtask 会比 macrotask 先处理了.
到这里, 上面那个 300ms 的定时器为什么不一定是精确的 300ms 之后打印就能理解了:
因为 300ms 的 setTimeout 并不是说 300ms 之后立马执行, 而是 300ms 之后被放入任务列表里面. 等待事件循环, 等待它执行的时候才能执行代码. 如果异步任务列表里面只有它这个 macrotask 任务, 那么就是精确的 300ms. 但是如果 还有 microtast 等其它的任务, 就不止 300ms 了.
所以, 下面的代码也能很好理解了
- for(var i = 0; i <3; i++) {
- console.log("for:"+i);
- var time=setTimeout(function() {
- console.log("setTime:"+i);
- }, 300);
- console.log(time)
- }
这个运行的结果是:
1) 当执行 for 循环的时候, 定义了 3 个定时器, 由于 setTimeout 是异步任务, 所以这三个定时器, 每个都会在 300ms 之后加入任务队列.
2) 此时执行代码, 输出 for:xx, 并打印对应定时器的标识.
3) 300ms 之后, 每个 setTimeout 的回调函数加入到任务队列, 这时候 for 循环早就执行完毕了.
4) 执行完循环之后, 此时相当于主线程栈空了, 通知任务队列, 把异步任务放到主线程执行, 这时候就开始执行 setTimeout 的回调函数. 由于这时 setTimeout 匿名回调函数保持对外部变量 i 的引用, 而此时的 i 由于主线程执行完之后变成了 3, 所以最终再打印出 3 个 setTime:3.
再来分析一下下面的代码:
- console.log(1);
- setTimeout(function(){
- console.log(2)
- },0);
- new Promise(function(resolve){
- console.log(3)
- for( var i=100 ; i>0 ; i-- ){
- i==1 && resolve()
- }
- console.log(4)
- }).then(function(){
- console.log(5)
- }).then(function(){
- console.log(6)
- });
- console.log(7);
1) 由于 script 也属于 macrotask, 所以整个 script 里面的内容都放到了主线程 (任务栈) 中, 按顺序执行代码. 然后遇到 console.log(1), 直接打印 1.
2) 遇到 setTimeout, 表示在 0 秒后才加入任务队列, 根据第 3 大点的 第 3 点注意事项, 这个 setTimeout 会被放到下一个事件循环的 macrotask 里面, 这次不会执行.
3) 执行遇到 new Promise,new Promise 在实例化的过程中所执行的代码都是同步进行的, 只有回调 .then()才是 microtask. 所以先直接打印 3, 执行完循环, 然后再打印 4. 然后遇到第一个 .then(), 属于 microtask, 加入到本次循环的 microtask 队列里面. 接着向下执行又遇到一个 .then() , 又加入到本次循环的 microtask 队列里面. 然后继续向下执行.
4) 遇到 console.log(7), 直接打印 7. 直到此时, 一个事件循环的 macrotask 执行完成, 然后去查看此次循环是否还有 microtask, 发现还有刚才的 .then() , 立即放到主线程执行, 打印出 5. 然后发现还有第二个 .then(), 立即放到主线程执行, 打印出 6 . 此时 microtask 任务列表清空完了. 到此第一次循环完成.
5) 第二次事件循环, 从 macrotask 任务列表里面找到了第一次放进的 setTimeout, 放到主线程执行, 打印出 2.
6) 所以最终的结果就是 1 3 4 7 5 6 2
上面说了这么多, 就是为了下面做铺垫
vue 的 nextTick 使用方法:
接收两个参数:
第一个是回调函数, 即 DOM 更新之后需要做的操作.
第二个是回调函数中, this 指针的指向.
vue.nextTick(cb,obj)
vm.$nextTick(cb). 注意实例中使用 nextTick 的时候, cb 回调函数的 this 指向已经绑定为当前实例了.
这里附上 vue 2.6 版本 nextTick 源码的链接 nextTick,2.5 版本与 2.6 有些不一样.
- export function nextTick (cb?: Function, ctx?: Object) {
- let _resolve
- callbacks.push(() => { // 第一步
- if (cb) {
- try {
- cb.call(ctx)
- } catch (e) {
- handleError(e, ctx, 'nextTick')
- }
- } else if (_resolve) {
- _resolve(ctx)
- }
- })
- if (!pending) { // 第二步
- pending = true
- timerFunc()
- }
- // $flow-disable-line
- if (!cb && typeof Promise !== 'undefined') { // 第三步
- return new Promise(resolve => {
- _resolve = resolve
- })
- }
- }
每次调用 Vue.nextTick(cb) :
1)cb 函数经处理压入 callbacks 数组, 并且指定了 cb 的 this 指向.
2)pending 表示是否正在执行回调即是否已经有异步任务在主线程执行, 由于 pending 这个标识最初为 false, 所以把它设置为 true, 然后调用 timerFunc(). 这个是用来触发异步回调函数的.
3)如果没有传入回调函数, 并且支持 promise 的时候, 则返回一个 promise 的调用
4)timerFunc()最初就看 Promise(延迟调用) ,MutationObserver(监听变化),setImmediate ,setTimeout 这四个中谁的兼容当前浏览器, 谁就优先用来做异步 API 来处理回调函数.
对于为什么是下一个 tick, 我有问题:
1)在下次 DOM 更新循环结束之后执行延迟回调. 在修改数据之后立即使用这个方法, 获取更新后的 DOM. 这是官方对于 nextTick 的说法.
2)在设置了 vm.xxx='xxx'的时候, 如果立即去 DOM 的内容, 获取到的并不是最新的值, 说明 DOM 的更新一定是异步的, 因为同步的话就能获取到修改后的内容了. 但是 nextTick 的回调函数, 在调用后要么属于 microtask, 要么就是 macrotask,
3)如果是 macrotask 则好理解一点, 因为执行代码遇到这个 macrotask 则会被添加到 macrotask 的末尾, 等待 event loop 取到它的时候才执行, 而执行一次 macrotask 之后, 如果 microtask 列表为空了, 就会执行 UI rendering, 页面就渲染成最新的内容. 这时候是能获取到更新后的内容的.
4)那如果是 microtask, 就是在当前 event loop 中需要执行完毕, 是属于当前的 tick, 而这个时候是怎么获取到 DOM 更新的内容的???
对于上面的这个问题, 好像要涉及到 watcher 中的 update 和 queueWatcher . 暂时就先放到一边. 反正作用是搞懂了, 原理还差一点.
如果有明白这个问题的, 麻烦给我讲解一下. 先谢谢了.
来源: https://www.cnblogs.com/zjjDaily/p/10478634.html