miniVue3的简单实现-异步更新机制

1.为什么vue会采用异步更新

vue框架对于数据的改变是敏感的,其能具体准确知道是哪一个数据改变, 所以vue是能对更新粒度进行控制,在vue1中是细粒度的更新(dom小范围更新),所以内存占用高,性能消耗大。 在vue2以后改变为中等力度的更新,定位在组件级别,一个组件包含大量状态,在一个事件循环周期内,不管组件的状态改变多少个或一个状态改变多少次,都只进行一次界面更新,也就是只执行一次组件的渲染函数,异步去处理。在vue2中组件更新是利用watcher函数,而在vue3中是利用effect副作用函数。
effect函数分为三种:
  1. 组件的effect函数,用于组件的更新和渲染。当状态改变时,effect函数会被推入微队列,并去重,所以组件内不管状态改变多少次都会只执行一次。
  2. 计算属性的effect函数,惰性函数,只有当依赖的值改变时才会执行,否则一直使用上次计算的结果,具有缓存效果。
  3. watch的effect函数,用于监测某状态的改变,然后做相应逻辑处理。

2.vue3异步队列实现逻辑

  1. vue实现异步更新是利用微任务,当状态发生改变后,将组件effect渲染函数推入任务队列,当到微任务执行时,执行任务队列中收集的effect函数,改变dom,执行完微任务后,浏览器渲染线程对改变后的dom进行渲染。
  2. vue3的异步更新任务队列分为三种 1. 更新的异步队列queue 2. 异步队列queue之前的前置更新任务队列pendingPreFlushCbs 3. 异步队列queue之后的后置更新任务队列pendingPostFlushCbs。在微任务执行时,依次去执行pendingPreFlushCbs、queue、pendingPostFlushCbs的队列任务
  3. 每次向pendingPreFlushCbs、queue、pendingPostFlushCbs的队列添加任务后,都需要执行queueFlush函数,去创建微任务,如果已有微任务就利用现有微任务。

vue3 异步任务代码实现

1.初始化各种状态和创建任务队列及微任务
// 标识是否正在执行微任务函数
let isFlushing = false;
// 标识是否正在等待执行微任务函数
let isFlushPending = false;

// 异步更新队列
const queue = [];
// 当前队列任务序列
let flushIndex = 0;


// 异步更新前置队列
let pendingPreFlushCbs = [];
let activePreFlushCbs = null;
// 前置任务执行的序列
let preflushIndex = 0;

// 异步更新后置队列
let pendingPostFlushCbs = [];
// 异步更新后置队列现在正在执行的队列,因为执行过程中可能会加入新的任务,所以要区分
let activePostFlushCbs = null;
// 后置任务队列执行的序列
let postflushIndex = 0;


// 添加微任务的方法
let resolvedPromise = Promise.resolve();
// 代表正在执行的promise
let currentFlushPromise;
 

创建queue、pendingPreFlushCbs、pendingPostFlushCbs队列,用flushIndex、preflushIndex、postflushIndex记录异步任务队列、异步任务前置队列、异步队列后置队列的执行位置,isFlushing用于记录任务队列是否正在执行,isFlushPending用于标识任务队列是否在等待执行。resolvedPromise是一个promise用于将函数推入微任务队列中。

2.创建相应函数提供给用户向queue、pendingPreFlushCbs、pendingPostFlushCbs队列中推入任务
// ..........
// 省略前面代码

// 异步任务队列加入任务处理
export const queueJob = (job) => {
    // 只有当当前的任务不在queue队列中时才加入队列,做到任务去重,防止重复执行
    if (!queue.length || !queue.includes(job)) {
        queue.push(job);
        //执行微任务
        queueFlush();
    }
}
// 抽离的处理prev和post的公共逻辑
// activeQueue指正在执行的任务,pendingQueue等待执行的任务队列
const queueCb = (job, activeQueue, pendingQueue) => {

    if (!activeQueue || !activeQueue.includes(job)) {
        pendingQueue.push(job)
        // 执行微任务
        queueFlush()
    }
}
// 前置任务队列加入任务处理
export const queuePreFlushCb = (job) => {
    queueCb(job, activePreFlushCbs, pendingPreFlushCbs)
}
// 后置任务队列处理
export const queuePostFlushCb = (job) => {
    queueCb(job, activePostFlushCbs, pendingPostFlushCbs)
}
 
  1. 执行queueJob向queue任务队列中加入任务时,需要判断queue中是否存在当前要加入的任务,任务不在队列中则将任务推入queueJob队列,达到去重的效果,所以在一个事件循环周期内,不管一个组件内的多少个状态改变或一个状态改变多少次都只执行一次界面渲染(一个组件内的状态对应的组件effect渲染函数都是同一个)。

2.pendingPreFlushCbs、pendingPostFlushCbs任务队列执行时会用activePreFlushCbs、activePostFlushCbs对任务队列进行拷贝,然后清空pendingPreFlushCbs、pendingPostFlushCbs队列。所以当用于执行queuePreFlushCb或queuePostFlushCb推入任务时, 需要判断activePreFlushCbs、activePostFlushCbs是否存在当前任务,如果当前推入队列的不存在于activePreFlushCbs、activePostFlushCbs中, 就将当前任务存入pendingPreFlushCbs或pendingPostFlushCbs中.

3. 每次用户向任务队列推入任务后都会执行queueFlush 去尝试创建微任务,等待任务队列执行
// 执行queue队列
const queueFlush = function () {
    // 只有没有执行微任务去刷新任务队列,才会向微任务添加任务
    // 如果是正在执行的微任务,就直接用当前微任务在flushJobs处理中,直接把后加入的执行掉
    if (!isFlushing && !isFlushPending) {
        isFlushPending = true;
        currentFlushPromise = resolvedPromise.then(flushJobs)
    }
}
 

当isFlushing 和 isFlushPending 都为false时,说明当前还没有微任务,就需要创建微任务,然后将flushJobs推入微任务队列等待执行。然后把isFlushPending设置为true,当用户在此次事件循环周期内再向任务队列推入任务时,就不需要创建新的微任务,直接推入任务队列即可

4. 任务队列执行函数flushJobs 实现
  1. 执行flushJobs函数时首先改变状态,标识当前正在执行任务队列
const flushJobs = () => {
    isFlushing = true;
    isFlushPending = false;
 }
 
  1. 先执行flushPostFlushCbs函数,清空前置任务队列
const flushJobs = () => {
    isFlushing = true;
    isFlushPending = false;
    
    // 执行前置队列任务
    flushPreFlushCbs();
 }
 // 刷新前置队列
const flushPreFlushCbs = () => {
    // 将需要执行的pendingPostFlushCbs去重被赋值给activePreFlushCbs,然后清空pendingPostFlushCbs
    if (pendingPreFlushCbs.length) {

        activePreFlushCbs = [...new Set(pendingPreFlushCbs)];
        pendingPreFlushCbs.length = 0;


        for (preflushIndex; preflushIndex < activePreFlushCbs.length; preflushIndex++) {
            const task = activePreFlushCbs[preflushIndex];
            task();
        }
        activePreFlushCbs = null;
        preflushIndex = 0;
        // 直到清空前置任务队列, 再往下执行异步更新队列任务
        flushPreFlushCbs();
    }
}
 

flushPreFlushCbs函数首先判断前置队列是否存在任务,如果存在,需要将pendingPreFlushCbs内的任务拷贝给activePreFlushCbs,然后清空pendingPreFlushCbs队列(在微任务执行清空任务队列时,可能还会有任务被加入队列,所以需要拷贝当前任务,清空pendingPreFlushCbs队列,重新收集),然后顺序执行activePreFlushCbs内的任务,最后递归执行flushPreFlushCbs直到pendingPreFlushCbs清空所有任务

3.清空queue任务队列

const flushJobs = () => {
    isFlushing = true;
    isFlushPending = false;
    // 执行前置队列任务
    flushPreFlushCbs();

    /* 
        *执行异步队列任务
        * 因为异步队列任务大多是组件渲染函数,所以要先父组件后子组件执行
        * 父组件先创建effect的id小于子组件id 所以从小到大排列及为先父 后子执行顺序
        * 所以此处对queue进行sort排序
        * 
    */
    queue.sort((a, b) => getId(a) - getId(b));
    try {
        // 循环一次执行queue队列任务
        for (flushIndex; flushIndex < queue.length; flushIndex++) {
            callWithErrorHandling(queue[flushIndex]);
        }
    } finally {
        flushIndex = 0;
        queue.length = 0;

    }
}
// 用于比较大小排序
const getId = (job) => {
    return job.id == null ? Infinity : job.id;
}
 

首先需要对queue队列进行从小到大排序,因为queue收集的是组件的effect渲染函数, 组件渲染需要从父到子按顺序执行,父组件的effect函数先于子组件创建,所以父组件的effect函数id较小,按从小到大排序会保证父组件执行先于子组件

  1. 清空pendingPostFlushCbs任务队列
const flushJobs = () => {
    //.........
     //.........
      //.........
    try {

    } finally {
         //.........
         //.........
        // 执行后置队列任务
        flushPostFlushCbs()
        // 只要异步队列和后置任务队列还有任务,就一直去执行,到清空队列为止
        if (queue.length || pendingPostFlushCbs.length) {
            flushJobs();
        }
        currentFlushPromise = null;
    }


}
// 刷新后置队列
const flushPostFlushCbs = () => {
    let deps = [...new Set(pendingPostFlushCbs)];
    pendingPostFlushCbs.length = 0;
   
    if (activePostFlushCbs) {
        return activePostFlushCbs.push(...deps);
    }
    activePostFlushCbs = deps;
    // 排序
    activePostFlushCbs.sort((a, b) => getId(a) - getId(b));
    // 执行
    for (postflushIndex = 0; postflushIndex < activePostFlushCbs.length; postflushIndex++) {
        const task = activePostFlushCbs[postflushIndex];
        task();
    }
    activePostFlushCbs = null;
    postflushIndex = 0;
}
 

后置任务队列清空后,会判断queue、pendingPostFlushCbs队列是否还存在任务,如果存在则递归执行flushJobs直到清空队列为止

3.github地址

代码链接 miniVue3文件夹为vue3的简单实现代码

原创文章,作者:我心飞翔,如若转载,请注明出处:https://www.pipipi.net/12564.html

发表评论

登录后才能评论