• 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 操作
        })
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105

    flushCallbacks 函数,用来主要执行队列中的函数。那么执行的函数是哪里来的?在使用 nextTick 函数时通过 push 向数组中推入的匿名函数。如下代码

    let _resolve
    callbacks.push(() => {
        if (cb) {
            try {
                cb.call(ctx)
            } catch (e) {
                handleError(e, ctx, 'nextTick'); // 错误处理
            }
        } else if (_resolve) {
            _resolve(ctx)
        }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在代码中可以看到 _resolve 函数,这个是怎么实现的呐?大家都知道代码是同步解析,在遇到 callbacks.push 代码时,会向 callbacks 数组中推入匿名函数,但是此时 _resolve 函数为 undefined, 因为没有执行到 callbacks 数组中的函数所有没事也不会报错,接着执行下面代码

    if (!pending) {
        pending = true
        timerFunc()
      }
    
    • 1
    • 2
    • 3
    • 4

    执行 timerFunc 函数时,由于里面是有微/宏任务,所以先执行下面的同步任务,到同步任务执行完了之后再执行微/宏任务。所以执行到了

    if (!cb && typeof Promise !== 'undefined') { // 当没有回调切当前支持 Promise 时返回 Promise
        return new Promise(resolve => {
          _resolve = resolve; // Promise 是同步执行的,所有这里 _resolve = resolve 赋值操作在进入微/宏任务前已经执行完毕,在 flushCallbacks 函数遍历中可以使用 _resolve 函数进行 resolve 操作
        })
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这个是否根据判断就对 _resolve 进行了赋值,它指向了 resolve 函数。这个时候就可以使用 _resolve 函数进行 resolve 了。

    在 flushCallbacks 函数需要着重说一下为什么使用下面的代码进行循环执行

    const copies = callbacks.slice(0);
      callbacks.length = 0;
      for (let i = 0; i < copies.length; i++) {
        copies[i]();
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    而不是下面这种代码进行循环

      for (let i = 0; i < callbacks.length; i++) {
        callbacks[i]();
      }
    
    • 1
    • 2
    • 3

    因为如果采用 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 ....
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    什么原因导致了死循环呐?主要是微/宏任务 、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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    需要注意的是 san 中先判断的是 setImmediate 函数,它是宏任务,然后是 MessageChannel (微任务)、promise.then(微任务)和 setTimeout (宏任务)。这个是和 vue 中的判断有区别的。在vue 中是先微任务后宏任务。

  • 相关阅读:
    Java刷题day33
    java实现图片转pdf
    MySql中的like和in走不走索引
    Linux代码初试__进度条
    基于Google‘s FCM实现消息推送
    Linux漏洞SSL/TLS协议信息泄露漏洞(CVE-2016-2183) - 非常危险(7.5分) 解决办法!升级openssl
    图论基础知识 深度优先(Depth First Search, 简称DFS),广度优先(Breathe First Search, 简称DFS)
    java实现获取钉钉的签到记录
    数组扁平化的方法
    电厂数据可视化三维大屏展示平台加强企业安全防范
  • 原文地址:https://blog.csdn.net/qq_42683219/article/details/133356106