Vue.js 内部运行机制(六)---- 批量异步更新策略及 nextTick 原理

31 篇文章 0 订阅
6 篇文章 1 订阅

之前我们学到了 Vue 更新数据是如何更新视图的。

简单回顾

数据更新(setter)-> 通知依赖收集集合(Dep) -> 调用所有观察者(Watcher) -> 比对节点树(patch) -> 视图

在更新视图这一步,使用异步更新策略

为什么呢?引用小册中的例子,下面有一个这样的 Vue 组件

<template>
  <div>
    <div>{{number}}</div>
    <div @click="handleClick">click</div>
  </div>
</template>
<script>
export default {
    data () {
        return {
            number: 0
        };
    },
    methods: {
        handleClick () {
            for(let i = 0; i < 1000; i++) {
                this.number++;
            }
        }
    }
}
</script>

在for循环中,我们连续更改了1000次绑定数据 number ,如果使用同步更新,则需要1000次的 patch ,也就是1000次的 Diff ,1000次更新,这就很可怕了。

所以,在 Vue 里使用异步更新的方法,每次触发某个数据的 setter 方法后,对应的 Watcher 对象其实会被 push 进一个队列 queue 中,在下一个 tick(代表一次异步) 的时候将这个队列 queue 全部拿出来 run( Watcher 对象的一个方法,用来触发 patch 操作) 一遍。

nextTick

在 Vue 里,实现了一个 nextTick 函数,主要用来异步操作,参数为一个 callback 函数,会被存放在 callback 队列中,在下一个 tick 时触发队列中的所有 callback 事件。

在 Vue 源码中,使用 setImmediate、MessageChannel、setTimeout 来实现 macroTimerFunc(nextTick 中使用的异步方法),使用 Promise 来实现 microTimerFunc ,感兴趣可以看看 next-tick 。下面我们同样用 setTimeout 来举例。

/* cb 函数集合、 pending 是一个标记位,代表一个等待的状态 */
let callbacks = [];
let pending = false;

/**
    * 异步钩子函数
    * 目的是在当前调用栈执行完毕以后(不一定立即)才会去执行这个事件
    */
function nextTick(cb) {
    callbacks.push(cb);
    
    /* 第一次执行时,设定异步执行器 timeout 当前执行栈执行完,调用 flushCallbacks */
    if (!pending) {
        pending = true;
        setTimeout(flushCallbacks, 0);
    }
}

/* cb 集合执行函数 */
function flushCallbacks() {
    pending = false;
    /* 生成 cb 集合副本 copies */
    const copies = callbacks.slice(0);
    /* cb 集合长度赋值为0 */
    callbacks.length = 0;
    for (let i = 0; i < copies.length; i++) {
        copies[i]();
    }
}

注意:Vue文档中说明,在DOM更新之后立即执行callback函数,可以使用Vue.$nextTick(),我理解是应该是将这些callback函数,放在dom更新的函数后面

重写Watcher

在前面说到,连续更新1000次 number ,不可能连续渲染视图1000次,因此 有一个 queue 队列来收集过滤 Watcher ,同一个 Watcher 在同一个 tick 的时候只执行一次。

/* 用来给 Watcher 作唯一标识 */
let uid = 0;

/* 观察者 */
class Watcher {
    constructor() {
        /* 标识 */
        this.id = ++uid;
    }

    /* 调用 queueWatcher 将 Watcher 推入过滤队列 */
    update() {
        console.log('watch' + this.id + ' update');
        queueWatcher(this);
    }

    /* 更新视图函数 在函数里触发 patch  */
    run() {
        console.log('watch' + this.id + '视图更新啦~');
    }
}

注意:  update 方法,在修改数据后由 Dep 对象来调用,而 run 函数才是真正触发 patch 函数更新视图的方法

queueWatcher

用来存放并过滤相同 Watcher 的函数,第一次调用时,会将调用执行 Watcher 队列的函数推入 nextTick 函数,达到异步更新的效果。

/* 用来区分当前 Watcher 是否已存放的 map  */
let has = {};
/* 存取 Watcher 的队列 */
let queue = [];
/* 标识位,标记是否已经向 nextTick 传递了 flushSchedulerQueue 方法 */
let waiting = false;

function queueWatcher(watcher) {
    const id = watcher.id;
    /* 相同的 Watcher 不会被重复传入 */
    if (has[id] == null) {
        has[id] = true;
        queue.push(watcher);

        /* 第一次被调用,将 flushSchedulerQueue 函数推入 nextTick */
        if (!waiting) {
            waiting = true;
            nextTick(flushSchedulerQueue);
        }
    }
}

flushSchedulerQueue

用来调用所有队列中的 Watcher.run() 函数,触发 patch 函数。

/* 用来调用所有队列中的 Watcher.run() 函数,触发 patch 函数 */
function flushSchedulerQueue() {
    let watcher, id;

    for (index = 0; index < queue.length; index++) {
        watcher = queue[index];
        id = watcher.id;
        /* 将区分 map 归为原始值 */
        has[id] = null;
        watcher.run();
    }

    waiting = false;
}

注:代码参考《批量异步更新策略及 nextTick 原理》

参考文章

《批量异步更新策略及 nextTick 原理》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值