前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue3源码阅读笔记之事件队列

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

原创
作者头像
wanyicheng
修改2021-04-14 10:10:01
1.3K0
修改2021-04-14 10:10:01
举报

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

代码语言:javascript
复制
/**
 * 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; 

 */

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档