渲染模块6: 调度机制实现

本文详细介绍了Vue3中渲染调度器scheduler的实现原理,以及nextTick API的创建过程。通过事件循环EventLoop的概念,将组件更新任务放入微任务队列,确保在所有同步任务完成后执行,优化了组件渲染效率。文章提供了完整的代码示例,展示了如何从头构建这一机制。
摘要由CSDN通过智能技术生成

源码位置:
vue-next/packages/runtime-core/src/scheduler.ts

其实本文中的调度机制,即 scheduler,是为了解决组件渲染时的一个渲染任务的调度问题,或者说调用时机的问题,属于是很典型的 “可以没有,但有了会更好”,但是考虑到有 nextTick 这么一个很好用的 API,且实现起来非常简单,因此顺带实现一下

为什么要调度

在前面实现的 component 的更新方法 instance.update 存在一个问题,就是 setup 中每一次 trigger 都会重新渲染一次组件,换句话说就是每次修改 instance.ctx 中的数据,都会重新渲染一次组件,如下例子

const Comp = {
    setup() {
        const a = ref(0);
        const add = () => {
            a.value++;
            a.value++;
        };
        return { a, add };
    },
    render(ctx) {
        // 这里打印一下
        console.log('update......');

        return h('div', null, [
            h('div', null, ctx.a.value),
            h('button', { onClick: ctx.add }, 'add'),
        ]);
    },
};

render(h(Comp), document.body);

页面效果如下,一开始是个 0,然后挂载时打印了一次 “update…”,到这里一切都符合预期

在这里插入图片描述

而我点击一下 add 按钮,页面数据会加 2,不过注意看控制台,“update…” 输出了两次

在这里插入图片描述

再点一下也是一样

在这里插入图片描述

以上这个例子说明什么呢,说明组件更新时,每次触发 trigger 都会重新渲染一次组件,而在 setup 函数中经常会进行大量的数据更新操作,因此会产生大量重复的组件更新。
我们最希望的,是 setup 完全执行完成之后,再来更新组件,也因此有了 “任务调度” 这个概念,这个概念简单来说就是先把任务存起来,在该执行的时候再来执行。

分析一下

知道了 scheduler 做的事情,又该怎么实现呢,其实个人感觉这里还是比较容易能联想到使用一个什么东西先把要执行的 update 函数存起来,然后到之后再执行,不知道大家看到 scheduler 会不会觉得眼熟,而且在组件渲染的实现中,我们用一个 effect 监视更新和挂载的方法,这里已经明示的很明显了,scheduler 调度机制其实主要就是靠这个实现的

可是话说回来,就算知道了可以靠这个实现也没什么用,因为核心思路还没有出来,以下就来分析一下该怎么做

再一次明确我们的目标,就是让 setup 执行完成之后再来更新组件,那么我们想要将 update 函数存起来的话,最合适的就是用一个任务队列了,因为先进先出嘛,顺序不会出现问题,而存进去之后我们就有操作的空间了,因为只需要控制出队的时机就可以控制 update 执行的时机,接着问题就来到了要在什么时候出队呢?

想到这里其实老实说我没思路了想不出来,因此就看了源码,接下来就直接从分析变成解释了

vue3 的调度机制是基于事件循环 EventLoop 实现的,将更新函数放在微任务中,这样就可以利用 js 本身的机制来完成这一需求,关于 EventLoop 不在这里过多赘述,是非常非常非常重要的概念

写一下

上面开了上帝模式一顿看,现在写起来还是比较轻松的,也很容易总结出我们需要做的事情

  • 将更新任务入队
  • 将更新任务放在微任务中执行
  • 还原到入队前的样子

以下分步骤一步一步来

入队

入队操作没什么好说的,非常简单,不过顺带提一嘴,在这里可以做一个简单的小过滤,将重复的更新任务去掉,如下

const queue = [];

const queueJob = job => {
    // 入队,顺便去重
    if (!queue.length || !queue.includes(job)) {
        queue.push(job);
        // 下一步
        queueFlush();
    }
}

执行

上一步中留了 queueFlush(),这里直接接上开始执行微任务,在此之前需要解释一下,我们执行时需要标注当前是否有一个微任务正在执行,因此需要一个 isFlushPending 变量用来标注,接着用一个 currentFlushPromise 记录当前正在执行的微任务,其他的应该没什么需要解释的了,都挺简单的

let isFlushPending = false;
let currentFlushPromise = null;
const resolvePromise = Promise.resolve();

const queueFlush = () => {
    if (!isFlushPending) {
        isFlushPending = true;
        currentFlushPromise = resolvePromise.then(flushJobs);
    }
}

const flushJobs = () => {
    // job 里面有可能是用户代码,可能出错
    // 因此用 try-catch 包一下
    try {
        for (const job of queue) {
            job();
        }
    } finally {
        // ......
    }
}

还原

以上步骤完成后,可能还会存在下一轮更新任务,因此需要将更改的变量全部还原,而 flushJobs 中也留了个 finally 的坑,直接塞进去就行了

// 前略
finally {
    isFlushPending = false;
    queue.length = 0;
    currentFlushPromise = null;
}

快装上试试

以上就基本完成了,只需要在 component 的更新函数后面加上这么个 scheduler 即可

instance.update = effect(() => {
        if (!instance.isMounted) {
            // mount...
        } else {
           // update...
        }
    }, queueJob);

还有个 nextTick

建立在现有基础上,nextTick 就很好实现了,只要把 currentFlushPromise 给他用就可以了,如下

const nextTick = fn => {
    const p = currentFlushPromise || resolvePromise;
    return fn ? p.then(fn) : p;
}

以上判断 fn 是否存在是为了兼容 async-await 写法,老实说我没用过这种写法,但是就这么一行就顺便加上了

跑跑看

还是上面那么个例子

const Comp = {
    setup() {
        const a = ref(0);
        const add = () => {
            a.value++;
            a.value++;
        };
        return { a, add };
    },
    render(ctx) {
        // 这里打印一下
        console.log('update......');

        return h('div', null, [
            h('div', null, ctx.a.value),
            h('button', { onClick: ctx.add }, 'add'),
        ]);
    },
};

render(h(Comp), document.body);

刚开始一样

在这里插入图片描述

但现在开始就不一样了,我点了一下,只打印了一句 update....

在这里插入图片描述

再来一次也是一样

在这里插入图片描述

总结

在 js 中的事件循环 EventLoop 真的是一个非常非常重要的概念,而本文 scheduler 的实现可以说完全基于 EventLoop,简单来说就是把更新任务放到微任务中执行,而由于 EventLoop 的存在,更新任务就会在所有同步任务完成之后再来执行,也就达到了我们的目的

以下是完整代码

const queue = [];
let isFlushPending = false;
let currentFlushPromise = null;
const resolvePromise = Promise.resolve();

const queueJob = job => {
    if (!queue.length || !queue.includes(job)) {
        queue.push(job);
        queueFlush();
    }
}

const queueFlush = () => {
    if (!isFlushPending) {
        isFlushPending = true;
        currentFlushPromise = resolvePromise.then(flushJobs);
    }
}

const flushJobs = () => {
    try {
        for (const job of queue) {
            job();
        }
    } finally {
        isFlushPending = false;
        queue.length = 0;
        currentFlushPromise = null;
    }
}

const nextTick = fn => {
    const p = currentFlushPromise || resolvePromise;
    return fn ? p.then(fn) : p;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值