san.js源码解读之工具(util)篇——nexttick函数

vue v2.7.14 nextick 源码解析

在了解 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简单点),这里粘贴一下看看

二、 san.js nexttick 源码分析

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 中是先微任务后宏任务。

你可能感兴趣的:(san.js,javascript,开发语言,ecmascript,san.js)