最开始查看nextTick这个方法的时候,眼瞎当作了nextClick。。。我还在疑问难道是下一次click以后处理事件。。。vue
而后用这个方法的时候,就只知道是用在DOM更新以后调用回调方法。node
这时就产生了一堆疑问:git
1)DOM更新后?难道修改数据以后,DOM没有及时更新,还有延迟?可是页面上看到的就是实时更新呀,难道还有什么猫腻?github
2)它是怎么监听到DOM被更新了数组
3)它和异步的setTimeout、setInterval有没有关系?promise
深刻了解后才发现里面有大学问。。。浏览器
在理解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(已废弃)、以及nodejs中的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 。暂时就先放到一边。反正做用是搞懂了,原理还差一点。
若是有明白这个问题的,麻烦给我讲解一下。先谢谢了。