面试被大神问到 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()。
看到这里可以了解到基本实现原理,回调事件使用了新增微任务或最差新增红任务的方式,这个任务是在微任务队列中的最后一个任务
是和事件队列有关,需要确定的就是任务执行的时机,在最后一个微任务或者第一个宏任务的时候。
这样执行回调是最好的。
谢谢,不对的请指点。