专栏首页纸上得来终觉浅Vue3源码阅读笔记之事件队列
原创

Vue3源码阅读笔记之事件队列

总结一下:vue中的事件队列分3种,vue内部实现中主要是把render函数的包裹体effect放到queue队列中。

/**
 * vue中用了事件队列来调度vue内部的一些事件回调以及用户添加的事件,我们详细看下这部分的基础实现 
 */

// then(flushAllCbs)正在执行的标记 标记是否正在刷新任务队列 初始和全部执行完事件后都是false 但是在执行中间某个job的时候又可能会添加新的job到同一队列中 这时候这个标记就起作用了
let isFlushing = false;
// 调用 Promise.then(flushAllCbs) 正式触发下个tick更新队列的标记
let isFlushPending = false;
// 普通事件队列 vue的包裹render函数的effect之类的都放在这里
const queue = [];
// 当前正在执行的job所处queue中的索引
let flushIndex = 0;
// 定义了2种回调事件 pre 和 post
// pre的优先级比queue高 post比queue低
// 2种事件最开始都是被推入到xxxPending队列中 然后被 xxxFlush操作 从pengding队列取出到xxxActive队列 再逐一执行 2者逻辑主体相同
// xxxFlushIndex语义同上
const pendingPreFlushCbs = [];
let activePreFlushCbs = null;
let preFlushIndex = 0;
const pendingPostFlushCbs = [];
let activePostFlushCbs = null;
let postFlushIndex = 0;
// 用于执行p.then的已就绪promise
const resolvedPromise = Promise.resolve();
// p.then 返回的新promise实例
let currentFlushPromise = null;
// pre队列刷新的时候有一种特殊调用情况 带有某个 parentJob 的参数然后刷新pre队列,这个时候在pre队列刷新过程中产生的queue job不与parentJob相同
// vue中用于组件更新的时候 详情见vue组件更新部分源码
let currentPreFlushParentJob = null;
// 在一次tick周期内的刷新过程中 某个job允许最多出现的次数
const RECURSION_LIMIT = 100;
// 把fn推入下个tick执行即可
function nextTick(fn) {
    const p = currentFlushPromise || resolvedPromise;
    return fn ? p.then(this ? fn.bind(this) : fn) : p;
}
// #2768
// Use binary-search to find a suitable position in the queue,
// so that the queue maintains the increasing order of job's id,
// which can prevent the job from being skipped and also can avoid repeated patching.
// 二分查找
function findInsertionIndex(job) {
    // the start index should be `flushIndex + 1`
    // 0 - 10 => 1 - 11 查找范围
    let start = flushIndex + 1;
    let end = queue.length;
    const jobId = getId(job);
    while (start < end) {
        const middle = (start + end) >>> 1;
        const middleJobId = getId(queue[middle]);
        middleJobId < jobId ? (start = middle + 1) : (end = middle);
    }
    return start;
}
// vue中多次使用的用来把render更新effect推入job队列中
function queueJob(job) {
    // the dedupe search uses the startIndex argument of Array.includes()
    // by default the search index includes the current job that is being run
    // so it cannot recursively trigger itself again.
    // if the job is a watch() callback, the search will start with a +1 index to
    // allow it recursively trigger itself - it is the user's responsibility to
    // ensure it doesn't end up in an infinite loop.
    // 1. 队列空 或者 (允许递归则忽略检测当前正在执行的job否则纳入检测范围)
    // 且
    // 2. 不等于 parentJob 这个场景分析见上文 一般情况这个场景都是null 成立 
    if ((!queue.length ||
        !queue.includes(job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex)) &&
        job !== currentPreFlushParentJob) {
        const pos = findInsertionIndex(job);
        // 插入或者推入
        if (pos > -1) {
            queue.splice(pos, 0, job);
        }
        else {
            queue.push(job);
        }
        // 尝试刷新队列
        queueFlush();
    }
}
function queueFlush() {
    // 在一次刷新过程中 又有新的job被插入导致 queueFlush 又执行了 这2个标志就起作用了
    if (!isFlushing && !isFlushPending) {
        isFlushPending = true;
        currentFlushPromise = resolvedPromise.then(flushJobs);
    }
}
// 移除任务
function invalidateJob(job) {
    const i = queue.indexOf(job);
    if (i > -1) {
        queue.splice(i, 1);
    }
}
// 推入cb到某个队列中的公共方法 解析同上文
function queueCb(cb, activeQueue, pendingQueue, index) {
    if (!isArray(cb)) {
        if (!activeQueue ||
            !activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index)) {
            pendingQueue.push(cb);
        }
    }
    else {
        // if cb is an array, it is a component lifecycle hook which can only be
        // triggered by a job, which is already deduped in the main queue, so
        // we can skip duplicate check here to improve perf
        pendingQueue.push(...cb);
    }
    queueFlush();
}
// 2个外部调用方法
function queuePreFlushCb(cb) {
    queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex);
}
function queuePostFlushCb(cb) {
    queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex);
}
// pre队列执行逻辑
function flushPreFlushCbs(seen, parentJob = null) {
    if (pendingPreFlushCbs.length) {
        // 取出队列
        currentPreFlushParentJob = parentJob;
        activePreFlushCbs = [...new Set(pendingPreFlushCbs)];
        pendingPreFlushCbs.length = 0;
        {
            seen = seen || new Map();
        }
        for (preFlushIndex = 0; preFlushIndex < activePreFlushCbs.length; preFlushIndex++) {
            {
                checkRecursiveUpdates(seen, activePreFlushCbs[preFlushIndex]);
            }
            activePreFlushCbs[preFlushIndex]();
        }
        activePreFlushCbs = null;
        preFlushIndex = 0;
        currentPreFlushParentJob = null;
        // recursively flush until it drains
        // pre队列更新过程可能会产生新的pre事件 递归执行
        flushPreFlushCbs(seen, parentJob);
    }
}
function flushPostFlushCbs(seen) {
    if (pendingPostFlushCbs.length) {
        // 取出队列
        const deduped = [...new Set(pendingPostFlushCbs)];
        pendingPostFlushCbs.length = 0;
        // #1947 already has active queue, nested flushPostFlushCbs call
        if (activePostFlushCbs) {
            activePostFlushCbs.push(...deduped);
            return;
        }
        activePostFlushCbs = deduped;
        {
            seen = seen || new Map();
        }
        // 排序
        activePostFlushCbs.sort((a, b) => getId(a) - getId(b));
        for (postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++) {
            {
                checkRecursiveUpdates(seen, activePostFlushCbs[postFlushIndex]);
            }
            activePostFlushCbs[postFlushIndex]();
        }
        activePostFlushCbs = null;
        postFlushIndex = 0;
    }
}
// 获取任务id 通过effect产生的都有id 不然就是无穷大
const getId = (job) => job.id == null ? Infinity : job.id;

// 主刷新任务队列逻辑
function flushJobs(seen) {
    
    // 标志置位
    isFlushPending = false;
    isFlushing = true;
    {
        seen = seen || new Map();
    }
    // 一次刷新中先执行pre队列
    flushPreFlushCbs(seen);
    // Sort queue before flush.
    // This ensures that:
    // 1. Components are updated from parent to child. (because parent is always
    //    created before the child so its render effect will have smaller
    //    priority number)
    // 2. If a component is unmounted during a parent component's update,
    //    its update can be skipped.
    // 按照job创建的时间也就是id来排序处理 原因见上面的原文注释
    queue.sort((a, b) => getId(a) - getId(b));
    try {
        for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
            const job = queue[flushIndex];
            if (job) {
                if (true) {
                    checkRecursiveUpdates(seen, job);
                }
                // 执行
                callWithErrorHandling(job, null, 14 /* SCHEDULER */);
            }
        }
    }
    finally {
        flushIndex = 0;
        queue.length = 0;
        // 最后处理post队列
        flushPostFlushCbs(seen);
        isFlushing = false;
        currentFlushPromise = null;
        // some postFlushCb queued jobs!
        // keep flushing until it drains.
        // queue刷新过程中如果又推入了新的任务或者post cb 再次执行
        if (queue.length || pendingPostFlushCbs.length) {
            flushJobs(seen);
        }
    }
}
// 检查函数出现的次数
function checkRecursiveUpdates(seen, fn) {
    if (!seen.has(fn)) {
        seen.set(fn, 1);
    }
    else {
        const count = seen.get(fn);
        if (count > RECURSION_LIMIT) {
            throw new Error(`Maximum recursive updates exceeded. ` +
                `This means you have a reactive effect that is mutating its own ` +
                `dependencies and thus recursively triggering itself. Possible sources ` +
                `include component template, render function, updated hook or ` +
                `watcher source function.`);
        }
        else {
            seen.set(fn, count + 1);
        }
    }
}

// 总结:在一次tick中,pre队列在queue job之前执行,post总是在queue job之后执行。
/**
 * 看下vue中如何使用这几个队列的:
 * 1. render函数包裹体effect的调度函数 其实就是推入 queue job
 * function createDevEffectOptions(instance) {
        return {
            scheduler: queueJob,
            allowRecurse: true,
            onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc, e) : void 0,
            onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg, e) : void 0
        };
    }

    2. 实例的 $forceUpdate 方法就是调用组件实例的effect函数 也是推入queue job 
    const publicPropertiesMap = extend(Object.create(null), {
        $: i => i,
        $el: i => i.vnode.el,
        $data: i => i.data,
        $props: i => (shallowReadonly(i.props) ),
        $attrs: i => (shallowReadonly(i.attrs) ),
        $slots: i => (shallowReadonly(i.slots) ),
        $refs: i => (shallowReadonly(i.refs) ),
        $parent: i => getPublicInstance(i.parent),
        $root: i => getPublicInstance(i.root),
        $emit: i => i.emit,
        $options: i => (resolveMergedOptions(i) ),
        $forceUpdate: i => () => queueJob(i.update),
        $nextTick: i => nextTick.bind(i.proxy),
        $watch: i => (instanceWatch.bind(i) )
    });

    3. watch的选择 默认是pre
    job.allowRecurse = !!cb;
    let scheduler;
    if (flush === 'sync') {
        scheduler = job;
    }
    else if (flush === 'post') {
        scheduler = () => queuePostRenderEffect(job, instance && instance.suspense);
    }
    else {
        // default: 'pre'
        scheduler = () => {
            if (!instance || instance.isMounted) {
                queuePreFlushCb(job);
            }
            else {
                // with 'pre' option, the first call must happen before
                // the component is mounted so it is called synchronously.
                job();
            }
        };
    }

    4. suspense 组件内使用
    // no pending parent suspense, flush all jobs
    if (!hasUnresolvedAncestor) {
        queuePostFlushCb(effects);
    }

    5. pre队列在组件更新之前执行
    const updateComponentPreRender = (instance, nextVNode, optimized) => {
        nextVNode.component = instance;
        const prevProps = instance.vnode.props;
        instance.vnode = nextVNode;
        instance.next = null;
        updateProps(instance, nextVNode.props, prevProps, optimized);
        updateSlots(instance, nextVNode.children);
        // props update may have triggered pre-flush watchers.
        // flush them before the render update.
        flushPreFlushCbs(undefined, instance.update);
    };

    6. 高级API render函数执行完成dom视图更新 后
    flushPostFlushCbs();
    container._vnode = vnode; 

 */

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Vue3源码阅读笔记之异步组件

    wanyicheng
  • Vue3源码阅读笔记之$emit实现

    总结:组件实例上的 emit 方法其实就是调用props中从父组件传进来的一个箭头函数。

    wanyicheng
  • Vue3源码阅读笔记之vnode定义

    wanyicheng
  • Vue3源码阅读笔记之组件是如何实现

    wanyicheng
  • Vue3源码阅读笔记之数据响应式

    总结:Vue3中的数据响应式实现是一个较为独立的实现,适合单独分析学习哈。上文是删除了部分支线逻辑的版本,只保留了主线逻辑,大家如果想看完整的实现,还是建议去读...

    wanyicheng
  • Vue3源码阅读笔记之整体执行顺序简介(1)

    从Vue官网得到源码(https://unpkg.com/vue@next),拷贝到本地文件,然后创建如下html:

    wanyicheng
  • Vue3源码阅读笔记之整体执行顺序简介(2)

    可以看到,目前只是直接对组件实例的data做了一次代理,handlers在普通对象情况下为 baseHandlers

    wanyicheng
  • python源码阅读笔记之GC

    哒呵呵
  • 写给初中级前端的高级进阶指南(JS、TS、Vue、React、性能、学习规划)

    我曾经一度很迷茫,在学了Vue、React的实战开发和应用以后,好像遇到了一些瓶颈,不知道该怎样继续深入下去。相信这也是很多一两年经验的前端工程师所遇到共同问题...

    ssh_晨曦时梦见兮

扫码关注云+社区

领取腾讯云代金券