面试被大神问到 nextTick 回调的时候,就是挂载结束的时候,可以获取到准确的dom节点
实现的原理是什么?今天做一个总结回复大神。。
网上找了这个问题,找到几篇相关的文章看了之后发现和源码的实现是不一致的,然后记录下读码过程,方便以后翻阅再补充。
先看vue源码的 nextTick 方法(部分单词借助了百度翻译)
// 声明公共数组,存储nextTick回调函数
var callbacks = [];
var pending = false; // 执行timerFunc函数时执行这个回调函数,处理在执行nextTick时新增的方法 function flushCallbacks () { pending = false; var copies = callbacks.slice(0); callbacks.length = 0; for (var i = 0; i < copies.length; i++) { copies[i](); } }
// 定义全局的timerFunc var timerFunc; // nextTick作为行为杠杆,和微任务队列,原生本地Promise事件,或者是 // MutationObserver事件 // MutationObserver 事件有兼容性问题,在IOS >= 9.3.3 ,这个行为会被完全阻止,所 // 以,如果客户端有Promise支持的时候,我们使用Promise更好 // 优先判断是否支持Promise if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve(); timerFunc = function () { p.then(flushCallbacks); // 此处大概的意思是:在webView中,事件Promise.then 不会完全中断,会陷入一种 // 奇怪的状态,我们需要让微任务队列继续执行,所以添加一个宏任务空计时器刷新 // 微任务队列,为啥这样可以刷新请百度搜索浏览器eventloop if (isIOS) { setTimeout(noop); } }; isUsingMicroTask = true; // 如果是IE判断是否支持MutationObserver } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // 原生Promise不被支持的时候,使用MutationObserver, // e.g. PhantomJS, iOS7, Android 4.4 // (#6466 MutationObserver is unreliable in IE11) var counter = 1; var observer = new MutationObserver(flushCallbacks); var textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); timerFunc = function () { counter = (counter + 1) % 2; textNode.data = String(counter); }; isUsingMicroTask = true; } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // 原生如果支持 setImmediate 还是先考虑,比setTimeout好些 timerFunc = function () { setImmediate(flushCallbacks); }; } else { // 最后实在都不支持,再用setTimeout。 timerFunc = function () { setTimeout(flushCallbacks, 0); }; } // 这里是 nextTick 函数具体实现,$nextTick就是它 function nextTick (cb, ctx) { var _resolve; callbacks.push(function () { if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); // 重点是这里判断,如果现在是在执行渲染结束的情况,渲染结束了,开始调用 // 上面被赋值好的 timerFunc ,执行这个函数会 // 触发执行 flushCallbacks 这个函数,他会遍历执行全部的callbacks // 为什么会有那么多的callback呢,因为nextTick每次被执行都会在callbacks中 // 推送一个事件,形成一个事件组就是 callbacks // 这里的pending 是一个全局的变量,默认值false,在flushCallBacks里会把 // pending = false;此处是一个锁保证nextTick仅有一次执行。 if (!pending) { pending = true; timerFunc(); } // 如果没有回调函数,vue会让nextTick返回一个promise对象返回结果 if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve) { _resolve = resolve; }) } }
看到这里,差不多明白是在选择一个可以是微任务或者宏任务的事件,找到这个事件等待nextTick触发。
还有一部分代码是在收集nextTick方法的回调函数
等待时机在有一部分代码执行这些函数。
还有一处不太明白,MutationObserver iOS5.1之前不支持,IE只有11支持,谷歌大部分支持,支持率还是很高的。
但是如何使用这个api呢?继续看
vue用到了MutationObserver,在使用中可以观察一部分你想观察的节点组,
还可以设置有哪些特性发生变化的时候回调函数会获取到。
// 找到一个要去观察的节点 var targetNode = document.getElementById('#app'); // 配置中添加需要观察的项 var config = { attributes: true, childList: true, subtree: true }; // 节点发生变化,回调函数会被执行 var callback = function(mutationsList) { for(var mutation of mutationsList) { if (mutation.type == 'childList') { console.log('节点被添加或是被移除惹。'); } else if (mutation.type == 'attributes') { console.log('这个 ' + mutation.attributeName + ' 被修改。'); } } }; // 创建一个观察dom变化的实例,关联到回调函数,callback是关键必填 var observer = new MutationObserver(callback); // 使用实例的方法把观察的区域和项目关联到一起 observer.observe(targetNode, config); // 调用这个方法可以解除观察, observer.disconnect();
这里基本上清楚了,vue里面是观察了一段代码 textnode + count 和 修改count,
如果可以执行nextTick的时候,它就去执行改变count随后触发 MutationObserve的回调函数 flushCallbacks()。
看到这里可以了解到基本实现原理,回调事件使用了新增微任务或最差新增红任务的方式,这个任务是在微任务队列中的最后一个任务
是和事件队列有关,需要确定的就是任务执行的时机,在最后一个微任务或者第一个宏任务的时候。
这样执行回调是最好的。
谢谢,不对的请指点。