JavaScript 的事件循环

vue官网深入响应式原理章节中,作者写到Vue是异步执行DOM更新的。官网的原话是“只要观察到数据变化,Vue将开启一个队列,并缓存在同一事件循环中发生的所有数据改变。...... 然后在下一个的事件循环tick中,Vue刷新队列并执行实际(已去重的)的工作。”看了官网的介绍还是有点懵圈,所以看了下vue数据响应的部分源代码。这里只分析涉及事件循环部分的代码。下一篇文章中会详细分析数据响应部分的源码。

源码(v2.3.4)

// 每次数据改变的时候会执行update函数
Watcher.prototype.update = function update() {
    if (this.lazy) {
        this.dirty = true;
    } else if (this.sync) {
        this.run();
    } else {
        // 这个函数做的一部分事情就是缓存watcher,可以理解为‘缓存在同一事件循环中发生的所有数据改变’
        queueWatcher(this);
    }
}

复制代码

这里只分析异步处理数据变化,即queueWatcher函数。这个函数的代码如下:

function queueWatcher(watcher) {
    var id = watcher.id;
    // 忽略相同的watcher
    if (has(id) == null) {
        has[id] = true;
        if (!flushing) {
            queue.push(watcher);
        } else {
            // 如果已经在flushing,则将watcher添加到最近执行的watcher的后面,会立即执行。
            var i = queue.length - 1;
            while(i > index && queue[i].id > watcher.id) {
                i--;
            }
            queue.splice(i + 1, 0, watcher);
        }
        if (!waiting) {
            waiting = true;
            // nextTick函数作用是将flushSchedulerQueue函数的执行放在下一个事件循环中。
            nextTick(flushSchedulerQueue);
        }
    }
}
复制代码

我们来看一下nextTick函数,这个函数的作用即是将传入的回调函数放在下一次事件循环中执行。

// Defer a task to execute it asynchronously. 
var nextTick = (function() {
    var callbacks = [];
    var pending = false;
    var timerFunc;
    
    function nextTickHandler() {
        pending = false;
        var copies = callbacks.slice(0);
        callbacks.length = 0;
        for (var i = 0; i < copies.length; i++) {
            copies[i]();
        }
    }
    
    // 这里是实现下次事件循环中执行的关键,后续会介绍 Promise、MutationObserver、setTimeout
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
        var p = Promise.resolve();
        var logError = function (err) { console.error(err); };
        timerFunc = function() {
            p.then(nextHandler).catch(logError);
            if (isIOS) { setTimeout(noop); }
        };
    } else if (typeof MutationObserver !== 'undefined' && (
        isNative(MutationObserver) ||
        // PhantomJS and iOS 7.x
        MutationObserver.toString() === '[object MutationObserverConstructor]'
    )) {
        var counter = 1;
        var observer = new MutationObserver(nextTickHandler);
        var textNode = document.createTextNode(String(counter));
        observer.observe(textNode, {
          characterData: true
        });
        timerFunc = function () {
          counter = (counter + 1) % 2;
          textNode.data = String(counter);
        };
    } else {
        timerFunc = function() {
            setTimeout(nextTickHandler, 0);
        }
    }
    return function queueNextTick(cb, ctx) {
        var _resolve;
        callbacks.push(function() {
            if (cb) {
                try{
                    cb.call(ctx);
                }catch(err) {
                    handleError(e, ctx, 'nextTick');
                }
            } else if (_resolve) {
                _resolve(ctx);
            }
        });
        if (!pending) {
            pending = true;
            timerFunc();
        }
        // nextTick返回Promise对象,实现了nextTick().then(cb) 调用的功能
        if (!cb && typeof Promise !== 'undefined') {
            return new Promise(function(resolve, reject) {
                // 这里对_resolve赋值,将在callbacks数组里的函数执行时调用。
                _resolve = resolve;
            })
        }
    }
})();
复制代码

nextTick函数里通过timerFunc函数调用nextTickHandler循环执行callbacks队列里的函数。关键在timerFunc函数,引用官网的话就是“Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MutationObserver,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替”。那么为什么这三种方式可以将nextTickHandler推迟到下次事件循环中执行呢?

这里涉及到JavaScript引擎的事件循环(Event Loop)了。

事件循环(Event Loop)

JavaScript只在一个线程上运行,也就是说同时只能执行一个任务,其他的任务需要等待当前任务完成后才能执行。这样带来的问题是,当执行比较耗时的任务时,会阻塞后续任务的执行,造成页面卡顿。为了防止一些非常耗时操作(比如I/O操作、ajax)阻塞后续代码执行,JavaScript采用了事件的机制。比如当执行ajax时,原本任务会继续执行,当等到ajax返回数据后,再执行ajax里指定的回调函数。一些I/O的操作也是类似。

消息队列,JavaScript引擎提供消息队列用于维护各类事件消息(各种事件,ajax状态改变事件,promise,setTimeout)。新的消息进入队列后,会自动排在队列的尾端。当执行栈上的任务执行完成,就会取出消息队列里的第一个消息,并且将对应的回调函数放入执行栈中执行。一旦执行栈中的任务全部执行完成,会再次取出消息队列里的第一条消息,执行回调函数,不断循环。

Wikipedia对Event Loop的定义是:“Event Loop是一个程序结构,用于等待和发送消息和事件。”个人的理解是当产生一个新的消息时,会将消息推入消息队列;在执行栈执行完当前任务时,会发送消息队列里的第一条消息,执行对应的回调函数。

所有的任务分为两种,一种是同步任务,一种是异步任务。

同步任务指的是,在JavaScript运行线程上排队执行的任务;异步任务指的是,不进入JavaScript运行线程,而是进入“任务队列(task queue)”的任务,只有当可以执行时,才进入线程的执行栈中执行。

虽然JavaScript只有一个线程来执行,但是并行的还有其他线程(比如,处理定时器的线程、处理用户输入的线程、处理网络通信的线程等等)。这些线程通过向任务队列添加任务,实现与JavaScript引擎线程通信。

Promise.then、MutationObserver和setTimeout,都是异步任务。经过以上的分析应该比较清楚异步任务的执行时机了。回调函数会在执行线程的任务完成后,再推入到执行栈中执行,也就是在会下一个事件循环中执行。

当然Promise.then和MutationObserver与setTimeout任务也是有区别的,Promise.then和MutationObserver产生的是“microTask”,setTimeout产生的是“task”。这两者是有差异的,后续会比较两者的差异。

转载于:https://juejin.im/post/5a77cfb65188257a654cb3e0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值