Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue-router(v4.0.6)之 createRouter 方法的实现简析(2) #4

Open
unproductive-wanyicheng opened this issue May 7, 2021 · 0 comments

Comments

@unproductive-wanyicheng
Copy link
Owner

接着看上文未分析完的代码:

// 检测某个导航守卫是否改变了原计划的导航
function checkCanceledNavigation(to, from) {
    if (pendingLocation !== to) {
        return createRouterError(8 /* NAVIGATION_CANCELLED */, {
            from,
            to,
        });
    }
}
// 我们常用的个push和redirect都是调用 pushWithRedirect ,不过参数被格式化了而已
function push(to) {
    return pushWithRedirect(to);
}
function replace(to) {
    return push(assign(locationAsObject(to), { replace: true }));
}
// 待跳转的location检测是否有redirect属性
function handleRedirectRecord(to) {
    // 子路由 最终匹配到最底层的就是最后一个matched对象
    const lastMatched = to.matched[to.matched.length - 1];
    // 存在才有返回值 否则就是undefined
    if (lastMatched && lastMatched.redirect) {
        const { redirect } = lastMatched;
        let newTargetLocation = typeof redirect === 'function' ? redirect(to) : redirect;
        // 格式化待重定向location
        if (typeof newTargetLocation === 'string') {
            newTargetLocation =
                newTargetLocation.indexOf('?') > -1 ||
                    newTargetLocation.indexOf('#') > -1
                    ? (newTargetLocation = locationAsObject(newTargetLocation))
                    : { path: newTargetLocation };
        }
        // 需要提供 下面的参数之一
        if (!('path' in newTargetLocation) &&
            !('name' in newTargetLocation)) {
            warn(`Invalid redirect found:\n${JSON.stringify(newTargetLocation, null, 2)}\n when navigating to "${to.fullPath}". A redirect must contain a name or path. This will break in production.`);
            throw new Error('Invalid redirect');
        }
        // 返回新的目标location
        return assign({
            query: to.query,
            hash: to.hash,
            params: to.params,
        }, newTargetLocation);
    }
}
// 实际执行的push和redirect内部方法
function pushWithRedirect(to, redirectedFrom) {
    // 调用实例的resolve得到匹配信息 同时 pendingLocation 置位
    // targetLocation: 待跳转目标location
    const targetLocation = (pendingLocation = resolve(to));
    // 当前值变成了历史值
    const from = currentRoute.value;
    const data = to.state;
    const force = to.force;
    // to could be a string where `replace` is a function
    const replace = to.replace === true;
    // 上面的 方法 尝试进行一次重定向解析
    const shouldRedirect = handleRedirectRecord(targetLocation);
    // 如果需要的话 重新调用一次 再次调用的时候 不会有redirect字段了
    if (shouldRedirect)
        return pushWithRedirect(assign(locationAsObject(shouldRedirect), {
            state: data,
            force,
            replace,
        }), 
        // keep original redirectedFrom if it exists
        redirectedFrom || targetLocation);
    // if it was a redirect we already called `pushWithRedirect` above
    // 或许是重定向回来的 统一以 toLocation 代表
    const toLocation = targetLocation;
    // 指向原来的
    toLocation.redirectedFrom = redirectedFrom;
    let failure;
    // 非强制更新的场景下 待跳转location和当前又一样
    if (!force && isSameRouteLocation(stringifyQuery$1, from, targetLocation)) {
        failure = createRouterError(16 /* NAVIGATION_DUPLICATED */, { to: toLocation, from });
        // trigger scroll to allow scrolling to the same anchor
        handleScroll(from, from, 
        // this is a push, the only way for it to be triggered from a
        // history.listen is with a redirect, which makes it become a push
        true, 
        // This cannot be the first navigation because the initial location
        // cannot be manually navigated to
        false);
    }
    // 最后的实际调用方法
    // 非错误场景下 调用的其实是 navigate(toLocation, from) 而它返回的是一个promise
    return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
        // 捕获navigate执行过程中的错误
        .catch((error) => isNavigationFailure(error)
        ? error
        : // reject any unknown error
            triggerError(error))
        .then((failure) => {
        // 处理navigate之后的结果
        if (failure) {
            // 无限重定向的错误情况
            if (isNavigationFailure(failure, 2 /* NAVIGATION_GUARD_REDIRECT */)) {
                if (// we are redirecting to the same location we were already at
                    isSameRouteLocation(stringifyQuery$1, resolve(failure.to), toLocation) &&
                    // and we have done it a couple of times
                    redirectedFrom &&
                    // @ts-ignore
                    (redirectedFrom._count = redirectedFrom._count
                        ? // @ts-ignore
                            redirectedFrom._count + 1
                        : 1) > 10) {
                    warn(`Detected an infinite redirection in a navigation guard when going from "${from.fullPath}" to "${toLocation.fullPath}". Aborting to avoid a Stack Overflow. This will break in production if not fixed.`);
                    return Promise.reject(new Error('Infinite redirect in navigation guard'));
                }
                return pushWithRedirect(
                // keep options
                assign(locationAsObject(failure.to), {
                    state: data,
                    force,
                    replace,
                }), 
                // preserve the original redirectedFrom if any
                redirectedFrom || toLocation);
            }
        }
        // 最终成功后的收尾方法
        else {
            // if we fail we don't finalize the navigation
            failure = finalizeNavigation(toLocation, from, true, replace, data);
        }
        // 触发after导航守卫
        triggerAfterEach(toLocation, from, failure);
        return failure;
    });
}

下面是刚刚用到的一些辅助方法,比较简单


// 判断2个location对象其实是不是一样
// 对各个属性的字段都有值相等的要求
function isSameRouteLocation(stringifyQuery, a, b) {
    let aLastIndex = a.matched.length - 1;
    let bLastIndex = b.matched.length - 1;
    return (aLastIndex > -1 &&
        aLastIndex === bLastIndex &&
        isSameRouteRecord(a.matched[aLastIndex], b.matched[bLastIndex]) &&
        isSameRouteLocationParams(a.params, b.params) &&
        stringifyQuery(a.query) === stringifyQuery(b.query) &&
        a.hash === b.hash);
}
/**
    * Check if two `RouteRecords` are equal. Takes into account aliases: they are
    * considered equal to the `RouteRecord` they are aliasing.
    *
    * @param a - first {@link RouteRecord}
    * @param b - second {@link RouteRecord}
    */
function isSameRouteRecord(a, b) {
    // since the original record has an undefined value for aliasOf
    // but all aliases point to the original record, this will always compare
    // the original record
    return (a.aliasOf || a) === (b.aliasOf || b);
}
function isSameRouteLocationParams(a, b) {
    if (Object.keys(a).length !== Object.keys(b).length)
        return false;
    for (let key in a) {
        if (!isSameRouteLocationParamsValue(a[key], b[key]))
            return false;
    }
    return true;
}
function isSameRouteLocationParamsValue(a, b) {
    return Array.isArray(a)
        ? isEquivalentArray(a, b)
        : Array.isArray(b)
            ? isEquivalentArray(b, a)
            : a === b;
}

const NavigationFailureSymbol = /*#__PURE__*/ PolySymbol('navigation failure' );

// 一些错误信息的标识
const NavigationFailureSymbol = /*#__PURE__*/ PolySymbol('navigation failure' );
/**
    * Enumeration with all possible types for navigation failures. Can be passed to
    * {@link isNavigationFailure} to check for specific failures.
    */
exports.NavigationFailureType = void 0;
(function (NavigationFailureType) {
    /**
        * An aborted navigation is a navigation that failed because a navigation
        * guard returned `false` or called `next(false)`
        */
    NavigationFailureType[NavigationFailureType["aborted"] = 4] = "aborted";
    /**
        * A cancelled navigation is a navigation that failed because a more recent
        * navigation finished started (not necessarily finished).
        */
    NavigationFailureType[NavigationFailureType["cancelled"] = 8] = "cancelled";
    /**
        * A duplicated navigation is a navigation that failed because it was
        * initiated while already being at the exact same location.
        */
    NavigationFailureType[NavigationFailureType["duplicated"] = 16] = "duplicated";
})(exports.NavigationFailureType || (exports.NavigationFailureType = {}));
// DEV only debug messages
const ErrorTypeMessages = {
    [1 /* MATCHER_NOT_FOUND */]({ location, currentLocation }) {
        return `No match for\n ${JSON.stringify(location)}${currentLocation
            ? '\nwhile being at\n' + JSON.stringify(currentLocation)
            : ''}`;
    },
    [2 /* NAVIGATION_GUARD_REDIRECT */]({ from, to, }) {
        return `Redirected from "${from.fullPath}" to "${stringifyRoute(to)}" via a navigation guard.`;
    },
    [4 /* NAVIGATION_ABORTED */]({ from, to }) {
        return `Navigation aborted from "${from.fullPath}" to "${to.fullPath}" via a navigation guard.`;
    },
    [8 /* NAVIGATION_CANCELLED */]({ from, to }) {
        return `Navigation cancelled from "${from.fullPath}" to "${to.fullPath}" with a new navigation.`;
    },
    [16 /* NAVIGATION_DUPLICATED */]({ from, to }) {
        return `Avoided redundant navigation to current location: "${from.fullPath}".`;
    },
};
// 错误信息的一些辅助方法 比较简单
function createRouterError(type, params) {
    {
        return assign(new Error(ErrorTypeMessages[type](params)), {
            type,
            [NavigationFailureSymbol]: true,
        }, params);
    }
}
function isNavigationFailure(error, type) {
    return (error instanceof Error &&
        NavigationFailureSymbol in error &&
        (type == null || !!(error.type & type)));
}
const propertiesToLog = ['params', 'query', 'hash'];
function stringifyRoute(to) {
    if (typeof to === 'string')
        return to;
    if ('path' in to)
        return to.path;
    const location = {};
    for (const key of propertiesToLog) {
        if (key in to)
            location[key] = to[key];
    }
    return JSON.stringify(location, null, 2);
}

接着往下看:

/**
* Helper to reject and skip all navigation guards if a new navigation happened
* @param to
* @param from
*/
// 发下原目标location已被修改 那剩下的导航守卫们就不需要执行了 promise链可以reject掉了 
function checkCanceledNavigationAndReject(to, from) {
    const error = checkCanceledNavigation(to, from);
    return error ? Promise.reject(error) : Promise.resolve();
}
// TODO: refactor the whole before guards by internally using router.beforeEach
// 导航的实际实现
function navigate(to, from) {
    // 待处理的守卫集合
    let guards;
    // 根据2次路径的对比 把所有的records分3份
    // 待离开 待更新 待进入
    const [leavingRecords, updatingRecords, enteringRecords,] = extractChangingRecords(to, from);
    // all components here have been resolved once because we are leaving
    // 刚解析出来的是待离开 是从父到子的顺序排放的
    // 实际执行需要从子开始离开
    // 从组件中解析解析出 beforeRouteLeave 对应的守卫函数
    guards = extractComponentsGuards(leavingRecords.reverse(), 'beforeRouteLeave', to, from);
    // leavingRecords is already reversed
    // 组件解析过程的时候 把单独注册添加的离开守卫也加入
    for (const record of leavingRecords) {
        record.leaveGuards.forEach(guard => {
            guards.push(guardToPromiseFn(guard, to, from));
        });
    }
    // 添加 检测是否导航目标地址发生了改变 的守卫
    const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(null, to, from);
    guards.push(canceledNavigationCheck);
    // run the queue of per route beforeRouteLeave guards
    // 驱动执行守卫列表
    // 链式调用 promise.then 一个一个执行
    // 以下执行顺序按照文档中描述的执行顺序执行
    /**
        *  1.导航被触发。
        2.在失活的组件里调用 beforeRouteLeave 守卫。
        3.调用全局的 beforeEach 守卫。
        4.在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
        5.在路由配置里调用 beforeEnter。
        6.解析异步路由组件。
        7.在被激活的组件里调用 beforeRouteEnter。
        8.调用全局的 beforeResolve 守卫(2.5+)。
        9.导航被确认。
        10.调用全局的 afterEach 钩子。
        11.触发 DOM 更新。
        12.调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
        */
    // 执行步骤 2
    return (runGuardQueue(guards)
        .then(() => {
        // check global guards beforeEach
        // 执行完后清空守卫集合 添加下一阶段的守卫们
        guards = [];
        for (const guard of beforeGuards.list()) {
            guards.push(guardToPromiseFn(guard, to, from));
        }
        // 每次都需要检查是否改变了目标location
        guards.push(canceledNavigationCheck);
        // 执行步骤 3
        return runGuardQueue(guards);
    })
        .then(() => {
        // check in components beforeRouteUpdate
        guards = extractComponentsGuards(updatingRecords, 'beforeRouteUpdate', to, from);
        for (const record of updatingRecords) {
            record.updateGuards.forEach(guard => {
                guards.push(guardToPromiseFn(guard, to, from));
            });
        }
        guards.push(canceledNavigationCheck);
        // run the queue of per route beforeEnter guards
        // 执行步骤 4
        return runGuardQueue(guards);
    })
        .then(() => {
        // check the route beforeEnter
        guards = [];
        for (const record of to.matched) {
            // do not trigger beforeEnter on reused views
            if (record.beforeEnter && from.matched.indexOf(record) < 0) {
                if (Array.isArray(record.beforeEnter)) {
                    for (const beforeEnter of record.beforeEnter)
                        guards.push(guardToPromiseFn(beforeEnter, to, from));
                }
                else {
                    guards.push(guardToPromiseFn(record.beforeEnter, to, from));
                }
            }
        }
        guards.push(canceledNavigationCheck);
        // run the queue of per route beforeEnter guards
        // 执行步骤 5
        return runGuardQueue(guards);
    })
        .then(() => {
        // NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
        // clear existing enterCallbacks, these are added by extractComponentsGuards
        to.matched.forEach(record => (record.enterCallbacks = {}));
        // check in-component beforeRouteEnter
        // 执行步骤 6
        guards = extractComponentsGuards(enteringRecords, 'beforeRouteEnter', to, from);
        guards.push(canceledNavigationCheck);
        // run the queue of per route beforeEnter guards
        // 执行步骤 7
        return runGuardQueue(guards);
    })
        .then(() => {
        // check global guards beforeResolve
        guards = [];
        for (const guard of beforeResolveGuards.list()) {
            guards.push(guardToPromiseFn(guard, to, from));
        }
        guards.push(canceledNavigationCheck);
        // 执行步骤 8
        return runGuardQueue(guards);
    })
        // catch any navigation canceled
        .catch(err => isNavigationFailure(err, 8 /* NAVIGATION_CANCELLED */)
        ? err
        : Promise.reject(err)));
}
// 执行 全局afterEach钩子
function triggerAfterEach(to, from, failure) {
    // navigation is confirmed, call afterGuards
    // TODO: wrap with error handlers
    for (const guard of afterGuards.list())
        guard(to, from, failure);
}
/**
    * - Cleans up any navigation guards
    * - Changes the url if necessary
    * - Calls the scrollBehavior
    */
// 最终改变url和当前路由值的方法实现
function finalizeNavigation(toLocation, from, isPush, replace, data) {
    // a more recent navigation took place
    // 再次检查
    const error = checkCanceledNavigation(toLocation, from);
    if (error)
        return error;
    // only consider as push if it's not the first navigation
    const isFirstNavigation = from === START_LOCATION_NORMALIZED;
    const state = !isBrowser ? {} : history.state;
    // change URL only if the user did a push/replace and if it's not the initial navigation because
    // it's just reflecting the url
    // 用户调用的引起的push、redirect调用
    if (isPush) {
        // on the initial navigation, we want to reuse the scroll position from
        // history state if it exists
        // 使用history的push 修改浏览器url
        if (replace || isFirstNavigation)
            routerHistory.replace(toLocation.fullPath, assign({
                scroll: isFirstNavigation && state && state.scroll,
            }, data));
        else
            routerHistory.push(toLocation.fullPath, data);
    }
    // accept current navigation
    // 最终改变了我们的响应式对象 currentRoute 的 value 而这个操作将会引起dom视图的更新 也就是 执行步骤11
    currentRoute.value = toLocation;
    // 处理滚动情况因为滚到到曾经访问过的路径的话 可能需要滚动到历史位置
    handleScroll(toLocation, from, isPush, isFirstNavigation);
    // 标记ready 首次才有意义
    markAsReady();
}
// 逐个执行守卫 promise.then
function runGuardQueue(guards) {
    return guards.reduce((promise, guard) => promise.then(() => guard()), Promise.resolve());
}
// 对比2个路径 解析3种守卫出来
function extractChangingRecords(to, from) {
    const leavingRecords = [];
    const updatingRecords = [];
    const enteringRecords = [];
    // 取最大值
    // 注意顺序 按照matched数组 从父到子 从前往后正常解析的
    const len = Math.max(from.matched.length, to.matched.length);
    for (let i = 0; i < len; i++) {
        const recordFrom = from.matched[i];
        if (recordFrom) {
            if (to.matched.find(record => isSameRouteRecord(record, recordFrom)))
                updatingRecords.push(recordFrom);
            else
                leavingRecords.push(recordFrom);
        }
        const recordTo = to.matched[i];
        if (recordTo) {
            // the type doesn't matter because we are comparing per reference
            if (!from.matched.find(record => isSameRouteRecord(record, recordTo))) {
                enteringRecords.push(recordTo);
            }
        }
    }
    return [leavingRecords, updatingRecords, enteringRecords];
}

然后还有一个 setupListeners 方法:

let removeHistoryListener;
// attach listener to history to trigger navigations
function setupListeners() {
    // 利用history的监听的popstate事件来触发
    // 也就是说 当我们通过点击浏览器的前进回退按钮的时候 我们也需要手动触发我们的导航内部逻辑
    removeHistoryListener = routerHistory.listen((to, _from, info) => {
        // cannot be a redirect route because it was in history
        let toLocation = resolve(to);
        // due to dynamic routing, and to hash history with manual navigation
        // (manually changing the url or calling history.hash = '#/somewhere'),
        // there could be a redirect record in history
        const shouldRedirect = handleRedirectRecord(toLocation);
        if (shouldRedirect) {
            pushWithRedirect(assign(shouldRedirect, { replace: true }), toLocation).catch(noop);
            return;
        }
        pendingLocation = toLocation;
        const from = currentRoute.value;
        // 分析同上面
        // TODO: should be moved to web history?
        // 记录当时的滚动信息
        if (isBrowser) {
            saveScrollPosition(getScrollKey(from.fullPath, info.delta), computeScrollPosition());
        }
        // 响应浏览器popstate事件 我们手动驱动导航逻辑触发去更新当前route且更新dom组件视图
        navigate(toLocation, from)
            .catch((error) => {
            if (isNavigationFailure(error, 4 /* NAVIGATION_ABORTED */ | 8 /* NAVIGATION_CANCELLED */)) {
                return error;
            }
            if (isNavigationFailure(error, 2 /* NAVIGATION_GUARD_REDIRECT */)) {
                // Here we could call if (info.delta) routerHistory.go(-info.delta,
                // false) but this is bug prone as we have no way to wait the
                // navigation to be finished before calling pushWithRedirect. Using
                // a setTimeout of 16ms seems to work but there is not guarantee for
                // it to work on every browser. So Instead we do not restore the
                // history entry and trigger a new navigation as requested by the
                // navigation guard.
                // the error is already handled by router.push we just want to avoid
                // logging the error
                pushWithRedirect(error.to, toLocation
                // avoid an uncaught rejection, let push call triggerError
                ).catch(noop);
                // avoid the then branch
                return Promise.reject();
            }
            // do not restore history on unknown direction
            if (info.delta)
                routerHistory.go(-info.delta, false);
            // unrecognized error, transfer to the global handler
            return triggerError(error);
        })
            .then((failure) => {
            failure =
                failure ||
                    finalizeNavigation(
                    // after navigation, all matched components are resolved
                    toLocation, from, false);
            // revert the navigation
            if (failure && info.delta)
                routerHistory.go(-info.delta, false);
            triggerAfterEach(toLocation, from, failure);
        })
            .catch(noop);
    });
}
// Initialization and Errors
let readyHandlers = useCallbacks();
let errorHandlers = useCallbacks();
let ready;
/**
    * Trigger errorHandlers added via onError and throws the error as well
    * @param error - error to throw
    * @returns the error as a rejected promise
    */
// 触发用户设置的err钩子
function triggerError(error) {
    markAsReady(error);
    errorHandlers.list().forEach(handler => handler(error));
    return Promise.reject(error);
}
// 感觉这个作用就是延迟1个tick而已...
function isReady() {
    if (ready && currentRoute.value !== START_LOCATION_NORMALIZED)
        return Promise.resolve();
    return new Promise((resolve, reject) => {
        readyHandlers.add([resolve, reject]);
    });
}
/**
    * Mark the router as ready, resolving the promised returned by isReady(). Can
    * only be called once, otherwise does nothing.
    * @param err - optional error
    */
// 只调用一次的标记已经ready 初始化完成
function markAsReady(err) {
    if (ready)
        return;
    ready = true;
    // 设置一个监听事件
    setupListeners();
    // 触发钩子
    readyHandlers
        .list()
        .forEach(([resolve, reject]) => (err ? reject(err) : resolve()));
    readyHandlers.reset();
}
// 简单的调用history的方法
const go = (delta) => routerHistory.go(delta);

滚动有关的代码以及从组件中抽取钩子的代码,下篇再分析;
总结一下:目前分析到的push等实例方法,展示了如何使用resolve方法来解析路径,然后构造对应的守卫数组,按照指定顺序执行,然后更新响应式对象的值,
更新浏览器url;同时也利用history的popstate事件来监听浏览器的url的改变,再次手动驱动我们的代码执行;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant