在了解 san.js 的 nexttick 之前先来看一下 vue 的实现方式,因为它是有参考 vue 的 nexttick 的实现。关键代码会有注释
function noop() {}; // 空函数
const isIE = UA && /msie|trident/.test(UA); // 判断是否是 IE
const isIOS = UA && /iphone|ipad|ipod|ios/.test(UA); // 判断是否是 IOS
function isNative(Ctor) {
return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}
// 简单的报错处理
function handleError(e, ctx, info) {
console.error(e, ctx, info)
}
export let isUsingMicroTask = false; // 是否使用微任务标识
const callbacks = []; // 存回调函数的数组
let pending = false; // 是否已经向任务队列中添加一个任务标识。每当向任务队列中插入任务时,将 pending 设置为 ture
// 函数主要内容是,依次执行 callbacks 数组中的函数,并清空 callbacks。需要注意的是一轮事件循环中 flushCallbacks 函数只执行一次
function flushCallbacks() { // 1
pending = false;
const copies = callbacks.slice(0); // 这里使用 slice 函数当前事件循环的数组,得到一个副本数组。这样就实现了一轮事件循环执行完一次任务队列,并且防止了死循环
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) { // 判读当前环境是否支持 promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks); // 使用微任务执行
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true; // 表示为微任务
} else if (
!isIE &&
typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]')
) { // 判断当前环境是否支持 MutationObserver(排除了ie)
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter)); // 创建文本节点
observer.observe(textNode, {
characterData: true, // 当为 true 时,监听声明的 target 节点上所有字符的变化
})
timerFunc = () => {
counter = (counter + 1) % 2; // 文本节点的内容在 0/1之间切换。因为这里对 counter 取了模
textNode.data = String(counter); // 赋值到文本节点中,这样就可以监听得到。并且执行回调
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => { // 宏任务
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {// 宏任务
setTimeout(flushCallbacks, 0)
}
}
export function nextTick()
/**
* @internal
*/
export function nextTick(cb, ctx) {
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') { // 当没有回调切当前支持 Promise 时返回 Promise
return new Promise(resolve => {
_resolve = resolve; // Promise 是同步执行的,所有这里 _resolve = resolve 赋值操作在进入微/宏任务前已经执行完毕,在 flushCallbacks 函数遍历中可以使用 _resolve 函数进行 resolve 操作
})
}
}
flushCallbacks 函数,用来主要执行队列中的函数。那么执行的函数是哪里来的?在使用 nextTick 函数时通过 push 向数组中推入的匿名函数。如下代码
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick'); // 错误处理
}
} else if (_resolve) {
_resolve(ctx)
}
})
在代码中可以看到 _resolve 函数,这个是怎么实现的呐?大家都知道代码是同步解析,在遇到 callbacks.push 代码时,会向 callbacks 数组中推入匿名函数,但是此时 _resolve 函数为 undefined, 因为没有执行到 callbacks 数组中的函数所有没事也不会报错,接着执行下面代码
if (!pending) {
pending = true
timerFunc()
}
执行 timerFunc 函数时,由于里面是有微/宏任务,所以先执行下面的同步任务,到同步任务执行完了之后再执行微/宏任务。所以执行到了
if (!cb && typeof Promise !== 'undefined') { // 当没有回调切当前支持 Promise 时返回 Promise
return new Promise(resolve => {
_resolve = resolve; // Promise 是同步执行的,所有这里 _resolve = resolve 赋值操作在进入微/宏任务前已经执行完毕,在 flushCallbacks 函数遍历中可以使用 _resolve 函数进行 resolve 操作
})
}
这个是否根据判断就对 _resolve 进行了赋值,它指向了 resolve 函数。这个时候就可以使用 _resolve 函数进行 resolve 了。
在 flushCallbacks 函数需要着重说一下为什么使用下面的代码进行循环执行
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
而不是下面这种代码进行循环
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
因为如果采用 for (let i = 0; i < callbacks.length; i++) 这种循环方式来执行回调,会造成死循环。比如执行下面代码
nextTick(function(){
console.log('1');
nextTick(function(){
console.log('1-1');
nextTick(function(){
console.log('1-1-1');
});
});
nextTick(function(){
console.log('1-2');
});
});
nextTick(function(){
console.log('2');
});
nextTick(function(){
console.log('3');
});
// 输出 1、2、3、1-1、1-2、1-1-1、1、2、3、1-1 ....
什么原因导致了死循环呐?主要是微/宏任务 、pending 标识和 callbacks 数组一起作用的结果(自己可以通过debugger看一下)。所以使用 callbacks.slice(0); 把数组拷贝一份防止循环
了解完 vue nexttick的实现发现 san.js 和 vue 差不多(比vue简单点),这里粘贴一下看看
var bind = require('./bind');
/**
* 下一个周期要执行的任务列表
*
* @inner
* @type {Array}
*/
var nextTasks = [];
/**
* 执行下一个周期任务的函数
*
* @inner
* @type {Function}
*/
var nextHandler;
/**
* 浏览器是否支持原生Promise
* 对Promise做判断,是为了禁用一些不严谨的Promise的polyfill
*
* @inner
* @type {boolean}
*/
var isNativePromise = typeof Promise === 'function' && /native code/.test(Promise);
/**
* 浏览器是否支持原生setImmediate
*
* @inner
* @type {boolean}
*/
var isNativeSetImmediate = typeof setImmediate === 'function' && /native code/.test(setImmediate);
/**
* 在下一个时间周期运行任务
*
* @inner
* @param {Function} fn 要运行的任务函数
* @param {Object=} thisArg this指向对象
*/
function nextTick(fn, thisArg) {
if (thisArg) {
fn = bind(fn, thisArg);
}
nextTasks.push(fn);
if (nextHandler) { // nextHandler 有值后续不执行
return;
}
nextHandler = function () {
var tasks = nextTasks.slice(0);
nextTasks = [];
nextHandler = null;
for (var i = 0, l = tasks.length; i < l; i++) {
tasks[i]();
}
};
// 非标准方法,但是此方法非常吻合要求。
/* istanbul ignore next */
if (isNativeSetImmediate) {
setImmediate(nextHandler);
}
// 用MessageChannel去做setImmediate的polyfill
// 原理是将新的message事件加入到原有的dom events之后
else if (typeof MessageChannel === 'function') {
var channel = new MessageChannel();
var port = channel.port2;
channel.port1.onmessage = nextHandler;
port.postMessage(1);
}
// for native app
else if (isNativePromise) {
Promise.resolve().then(nextHandler);
}
else {
setTimeout(nextHandler, 0);
}
}
需要注意的是 san 中先判断的是 setImmediate 函数,它是宏任务,然后是 MessageChannel (微任务)、promise.then(微任务)和 setTimeout (宏任务)。这个是和 vue 中的判断有区别的。在vue 中是先微任务后宏任务。