blog-md
blog-md copied to clipboard
san.nextTick与事件循环
san.nextTick与事件循环
在san框架中,发生数据变更时会更新视图,这个更新过程是异步执行的。
在读源码的过程中发现这个异步调用的san.nextTick
方法使用了html5的API:MutationObserver
,引发了对事件循环执行顺序的好奇。。
研究了一下,现在就来聊聊san.nextTick
与事件循环。
事件循环中的microtask & macrotask
Tasks, microtasks, queues and schedules这篇chrome开发者的文章详细描述了event loop
中的几种不同类型并提供了在线例子,强烈推荐详细阅读。
以下仅对上文做下简单介绍以及总结。
任务类型
简而言之,一个浏览器js主环境只可以有一个event loop,但是可以拥有多个任务队列。
任务队列包含两种类型,microtask和macrotask。
1. microtask: process.nextTick(nodejs) > promise.then/MutationObserver
2. macrotask: script、setTimeout、setInterval、setImmediate、I/O、UI rendering
执行顺序
所有的异步任务都会按类型推入不同的异步队列中,同步代码执行后开始执行异步任务。顺序如下:
- 首先取出macrotask中的第一个任务,
js stack
开始执行 -
js stack
中执行代码,同步代码直接执行,遇到异步任务放入相应的队列 -
js stack
执行完毕,开始microtask任务 - microtask任务队列执行完毕(1000个以内,多余的保留在microtask等待),如果macrotask中第一个任务已经执行完毕,就开始下一个任务,否则继续将此task中的代码加入
js stack
高能部分
需要注意的是,鼠标键盘行为在事件循环中都是一个macrotask,如果事件有向上冒泡行为,那么每一次冒泡触发的回调都会在js stack
中执行并清空调用栈,此时就会开始microtask任务队列。
如果是通过inner.click()
方式进行调用,那么这一段script代码会被加入到事件循环的第一个macrotask,本次点击触发多次冒泡的所有回调都会直接进入js stack
执行,如果js stack
没有清空,是不会执行microtask的。
这部分有点绕,对着上面链接文章里的在线例子多看几遍吧。
process.nextTick
nodejs中的process.nextTick
永远早于其他microtask,因为:
“process.nextTick 永远大于 promise.then,原因其实很简单。。。在Node中,_tickCallback在每一次执行完TaskQueue中的一个任务后被调用,而这个_tickCallback中实质上干了两件事:
- nextTickQueue中所有任务执行掉(长度最大1e4,Node版本v6.9.1)
- 第一步执行完后执行_runMicrotasks函数,执行microtask中的部分(promise.then注册的回调)所以很明显process.nextTick > promise.then”
MutationObserver
MutationObserver给开发者们提供了一种能在某个范围内的DOM树发生变化时作出适当反应的能力.该API设计用来替换掉在DOM3事件规范中引入的Mutation事件.
详细见 https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
简单来说,MutationObserver是HTML5的新的API,当dom节点发生变化时触发回调。
需要注意的是,当一个microtask中已经存在一个MutationObserver任务时,会进入MutationObserver的pending状态,此后其他MutationObserver触发也不会再把回调塞到microtask中了。
为了解决这个问题,我们通过san.nextTick
源码来看下是如何处理的。
san.nextTick源码解析
/**
* @file 在下一个时间周期运行任务
* @author errorrik([email protected])
*/
var bind = require('./bind');
var each = require('./each');
/**
* 下一个周期要执行的任务列表
*
* @inner
* @type {Array}
*/
// 当MutationObserver检测到dom节点变动时(这个节点变动是nextTick手动触发的)
// 并不会把回调塞到microtask中,而是把触发的回调塞到nextTasks中
// 真正进入microtask队列的回调函数是nextHandler => 执行nextTasks里的所有函数
var nextTasks = [];
/**
* 执行下一个周期任务的函数
*
* @inner
* @type {Function}
*/
var nextHandler;
/**
* 在下一个时间周期运行任务
*
* @inner
* @param {Function} fn 要运行的任务函数
* @param {Object=} thisArg this指向对象
*/
function nextTick(fn, thisArg) {
// 如果传了第二个参数就绑定this
if (thisArg) {
fn = bind(fn, thisArg);
}
// 每次的异步任务fn塞到数组里
nextTasks.push(fn);
// 初始化了没
if (nextHandler) {
return;
}
nextHandler = function () {
// copy一份
var tasks = nextTasks.slice(0);
// 然后把tasks清空,handler销毁
nextTasks = [];
nextHandler = null;
// 开始执行所有的tasks
each(tasks, function (task) {
task();
});
};
// 浏览器支持MutationObserver不
if (typeof MutationObserver === 'function') {
var num = 1;
var observer = new MutationObserver(nextHandler);
var text = document.createTextNode(num);
observer.observe(text, {
characterData: true
});
// 手动触发文本节点的改变
text.data = ++num;
}
// 高版本IE下回退
else if (typeof setImmediate === 'function') {
setImmediate(nextHandler);
}
// 都不支持,setTimeout吧
else {
setTimeout(nextHandler, 0);
}
}
exports = module.exports = nextTick;
从源码中我们可以看到,san会根据浏览器支持的特性判断,现代浏览器就用MutationObserver,不支持会回退到setImmediate和setTimeout。
前面提到过,microtask中还有Promise.then
方法,无论是MutationObserver还是Promise,谁先执行谁先进microtask队列。其实Vue的nextTick方法就把Promise放在了第一位。
ps,这里的Promise必须是浏览器原生支持的,polyfill之类的就不资磁了。
san.nextTick在框架中的调用
/**
* 组件内部监听数据变化的函数
*
* @private
* @param {Object} change 数据变化信息
*/
Component.prototype._dataChanger = function (change) {
var len = this.dataChanges.length;
if (!len) {
// 当所有的数据改变行为结束后
// 实例的更新视图函数通过san.nextTick推入异步队列
nextTick(this.updateView, this);
}
while (len--) {
switch (changeExprCompare(change.expr, this.dataChanges[len].expr)) {
case 1:
case 2:
if (change.type === DataChangeType.SET) {
this.dataChanges.splice(len, 1);
}
}
}
this.dataChanges.push(change);
};
上述代码可以看到,所有的change都会push到实例的dataChanges属性中,updateView会在microtask队列完成,在一轮事件循环中,不管一个组件更新了多少次data数据,都只会触发一次dom更新,最大化的优化了性能。
这里要注意的是,如果是开发者手动更新dom节点,会立即触发repaint/reflow,以保证dom属性的实时性。
关于浏览器的渲染机制后续再写。
总结
对事件循环和san.nextTick
已经做了简单的介绍,microtask和macrotask的执行顺序以及san对于异步更新视图的方式又有了新的认识。
如果开发者在生命周期内要做异步任务处理时,不妨考虑使用san.nextTick
。
课后题
const App = san.defineComponent ({
template:`
<div>{{name}}</div>
`,
attached() {
this.data.set('name','zhangzhe');
setTimeout(() => console.log('setTimeout'));
Promise.resolve().then(() => console.log('promise'));
san.nextTick(() => console.log('nextTick'));
}
})
new App().attach(document.body);
猜猜log顺序是怎样的?
友情提示,通常课上讲的是1+1得2的时候,课后题都不会太简单的。。