Vue nextTick原理分析

this.name = 1;
this.name = 2;
this.name = 3;

为什么只会执行一次?这是nextTick回调和watcher过滤的结果。当数据变化后,将watcher.update函数存放进nextTick的回调数组中,并且会做过滤。通过watcher.id来判断回调数组中是否已经存在这个watcher的更新函数,如果不存在才会push。之后next遍历这个无重复id的回调数组,便会执行更新。
因此当3次修改同一个数据时,仅会有一个watcher.update进入回调数组,从而页面只更新一次。

常见的宏任务

  1. setTimeout
  2. setInterval
  3. setImmediate
  4. script
  5. MessageChannel

常见的微任务

  1. Promise
  2. MutationObserver
  3. process.nextTick

nextTick涉及到的点

  1. 任务队列 callbacks
  2. 任务队列执行函数 flushCallbacks
  3. 控制(宏任务,微任务)注册标记位 pending
  4. 宏任务,微任务

宏任务和微任务的特点

  1. 二者都是异步
  2. 二者会被注册到不同的任务队列中
  3. 宏任务不是一次性就全部清空,而是执行一个宏任务时,然后去清空一次微任务队列

Vue在2.6版本中,去掉了宏任务,仅仅保留下了微任务,Vue内部有两个函数,macroTimerFunc用于注册宏任务,microTimeFunc用于注册微任务。Vue内部借助了setImmediate, MessageChannel, setTimeout来实现macroTimerFunc。

if (setImmediate存在) {
    macroTimerFunc = function() {
        setImmediate(flushCallbacks);
    }
} else if (MessageChannel存在) {
    // messageChannel有两个端口,一个用来绑定回调,一个用来接收传递过来的数据
    var channel = new MessageChannel();
    var port = channel.port2;
    channel.port1.onmessage = flushCallbacks;
    macroTimerFunc = function() {
        port.postMessage(1);
    }
} else {
    macroTimerFunc = function() {
        setTimeout(flushCallbacks, 0);
    }
}

microTimerFunc主要借助Promise来实现。

if (Promise存在) {
    var p = Promise.resolve();
    microTimerFunc = function() {
        p.then(flushCallbacks);
    }
} else {
    microTimerFunc = macroTimerFunc;
}

Vue维护了自己的任务队列,配合宏微任务,主要目的为

  1. 减少了宏微任务注册
  2. 加快异步代码的执行
  3. 避免频繁更新

当我们使用this.$nextTick(cb)是,这个cb回调会被放入到callbacks中,那我们就必须要一个方法来遍历这个callbacks, 来逐个执行其中存放的函数,这个方法就是flushCallbacks。下面我们主要来看看这个flushCallbacks到底做了什么

var callbacks = [];
var pending = false;

function flushCallbacks() {
    pending = false;
    // 浅拷贝
    var copies = callbacks.slice(0);
    for (var i = 0; i < copies.length; i++) {
        copies[i]();
    }
}

nextTick控制所有的异步代码,只注册一个宏微任务,那是如何控制的?

Vue.nextTick = function (cb, ctx) {
    callbacks.push(function() {
        cb && cb.call(ctx);
    });
    // 通过pending来确定是否需要注册宏微任务
    // 当第一次注册的时候,把pending设置为true,表示任务队列已经开始了,同一时期内无需注册了
    // 在任务队列执行完毕之后再把pending设置为false
    if (!pending) {
        pending = true;
        // useMacroTask 用来控制是宏任务还是微任务
        useMacroTask ? macroTimerFunc() : microTimerFunc();
    }
}

那Vue是如何控制多次更新同一个数据,最终实际只触发一次更新呢?这个需要提到watcher了。我们知道Vue中的data经过Object.defineProperty处理之后,会变成响应式的。当数据更新之后,会触发这个数据对应的set函数,而这个set函数通知相关的watcher进行更新,实际上就是调用了watcher.update方法

Watcher.prototype.update = function() {
    // 这里的queueWatcher对于多次赋值,一次更新起到了关键作用
    queueWatcher(this);
}

function queueWatcher(watcher) {
    var id = watcher.id;
    // 如果是同一个watcher,那就不要再次进入宏微任务队列了,直接过滤掉
    if (has[id] == null) {
        has[id] = true;
        // flushing 表示watcher更新队列正在执行更新
        if (!flushing) {
            // queue是更新队列,存放需要更新的watcher
            queue.push(watcher);
        } else {
            var i = queue.length - 1;
            while (i > index && queue[i].id > watcher.id) {
                i--;
            }
            queue.splice(i + 1, 0, watcher);
        }
    }
    // waiting=true表示已经把watcher更新`队列执行函数flushSchedulerQueue`注册到宏微任务上了
    // 当所有的watcher更新完毕,waiting会被重置为false
    if (!waiting) {
        waiting = false;
        // flushSchedulerQueue watcher更新队列的执行函数
        nextTick(flushSchedulerQueue);
    }
}

queueWatcher主要做了两件事情

  1. 处理watcher到更新队列queue
  2. 注册watcher更新队列的执行函数flushSchedulerQueue到宏微任务队列

最后我们来看Vue是如何执行更新队列的

function flushSchedulerQueue() {
    flushing = true;
    var watcher;
    
    // 升序排列watcher更新队列
    // watcher的id越大,表示这个watcher越年轻,实例越是后面生成
    queue.sort((a, b) => {
        return a.id - b.id;
    });
    
    // 遍历watcher更新队列,然后逐个调用watcher更新
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index];
        has[watcher.id] = null;
        watcher.run();
    }
    
    // watcher更新队列完毕,重置状态
    queue.length = 0;
    has = {};
    waiting = flushing = false;
}

事件回调执行的过程中,在JS主线程为空之后,异步代码执行之前,所有通过nextTick注册的异步代码都是宏任务。

function add$1(event, handler) {
    handler = withMacroTask(handler);
    target$1.addEventListener(event, handler);
}
function withMacroTask(fn) {
    return fn._withTask || (fn._withTask = function() {
        useMacroTask = true;
        var res = fn.apply(null, arguments);
        useMacroTask = false;
        return res;
    })
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值