React Navigation源代码阅读 :routers/StackRouter.js

import pathToRegexp from 'path-to-regexp';

import NavigationActions from '../NavigationActions';
import createConfigGetter from './createConfigGetter';
import getScreenForRouteName from './getScreenForRouteName';
import StateUtils from '../StateUtils';
import validateRouteConfigMap from './validateRouteConfigMap';
import getScreenConfigDeprecated from './getScreenConfigDeprecated';
import invariant from '../utils/invariant';
import {generateKey} from './KeyGenerator';

/**
 * 检查一个对象是否空对象
 * @param obj
 * @returns {boolean}
 */
function isEmpty(obj) {
    // 如果这个对象是 null, undefined 或者 0, 直接返回 true, 认为是空对象
    if (!obj) return true;

    // 如果这个对象有任何属性,则认为是非空对象
    for (let key in obj) {
        return false;
    }

    // 代码能走到这里,说明这是个空对象
    return true;
}

/**
 * 检测一个指令 action 是否是一个行为跟 push (压栈) 类似,
 * 这样的 action 有两个 : NAVIGATE, PUSH
 * @param action
 * @returns {boolean}
 */
function behavesLikePushAction(action) {
    // 如果 action 是 NAVIGATE 或者PUSH 返回 true, 否则返回 false
    return (
        action.type === NavigationActions.NAVIGATE ||
        action.type === NavigationActions.PUSH
    );
}

/**
 * 导出 栈式路由器 创建函数
 *
 * 概念解释 :
 * 1. 路由配置 : 包含多个路由配置项的一个对象
 * 2. 路由配置项 : 路由名称(字符串) : 路由配置对象(至少包含一个名字为 screen 的属性)
 * 3. 路由配置项中属性 screen 的值可以是一个 开发人员定义的屏幕组件或者也是一个 navigator
 *
 * @param routeConfigs 路由配置
 * @param stackConfig 导航器配置
 * @returns {*} 一个栈式路由器对象
 */
export default (routeConfigs, stackConfig = {}) => {
    // Fail fast on invalid route definitions
    // 检查路由配置,如果有问题直接抛出错误提示错误不再继续
    // 什么样的路由配置认为是没问题的 ?
    // 1. 必须有配置项
    // 2. 每个配置项必须包含屏幕组件,也就是属性 screen 或者 属性方法 getScreen
    // 3. 每个配置项中 screen 或者 getScreen 二者最多存在一个,不能两个同时设置
    // 4. 如果是属性 screen, 其类型必须是 string 或者 function
    validateRouteConfigMap(routeConfigs);


    // 用于记录子路由的路由器信息:
    // 1. 如果某个子路由是一个普通 React 组件,记录其路由器属性为 null
    // 2. 如果某个子路由也是一个 navigator/router, 记录器路由器属性为该子路由的 router 属性
    const childRouters = {};
    // 用于记录子路由名称
    const routeNames = Object.keys(routeConfigs);

    // Loop through routes and find child routers
    routeNames.forEach(routeName => {
        const screen = getScreenForRouteName(routeConfigs, routeName);
        if (screen && screen.router) {
            // 属性 screen 本身也是 navigator/router 的情况
            // 如果 screen 中有 router 属性,说明它也是一个 navigator/router
            // If it has a router it's a navigator.
            childRouters[routeName] = screen.router;
        } else {
            // 属性 screen 是普通 React 组件的情况
            // 如果 screen 中没有 router 属性,认为它是一个普通 React 组件
            // If it doesn't have router it's an ordinary React component.
            childRouters[routeName] = null;
        }
    });

    // 从导航器参数中获取初始路由参数属性
    const {initialRouteParams} = stackConfig;

    // 确定当前路由器的初始路由名称:
    // 使用指定的初始路由名称,如果没有指定初始路由名称,使用第一个路由配置项的路由作为初始路由
    const initialRouteName = stackConfig.initialRouteName || routeNames[0];

    // 记录初始路由对应的路由器信息,null或者一个嵌套的 navigator/router
    const initialChildRouter = childRouters[initialRouteName];
    const pathsByRouteNames = {...stackConfig.paths} || {};
    let paths = [];

    /**
     * 基于 action 构建初始导航状态
     * @param action
     * @returns {{key: string, isTransitioning: boolean, index: number, routes: *[]}}
     */
    function getInitialState(action) {
        let route = {};
        const childRouter = childRouters[action.routeName];

        // This is a push-like action, and childRouter will be a router or null if we are responsible for this routeName
        if (behavesLikePushAction(action) && childRouter !== undefined) {
            // 1. 如果 action 是一个 push-like action 并且 action 目标路由由该路由器负责处理的情况
            // (如果 action 对应的路由不是由该路由器负责,则 childRouter 这里应该是 undefined)
            let childState = {};
            // The router is null for normal leaf routes
            // 1. 如果 childRouter 为 null , 表示目标路由是一个普通 React 组件,也就是一个一般功能屏幕;
            // 2. 如果 childRouter 不为 null, 则从上面的逻辑可知它一定是另外一个 navigator (StackNavigator, TabNavigator
            // 或者其他什么类型的 navigator)
            if (childRouter !== null) {
                // childRouter 是另外一个 navigator 的情况( navigator 嵌套的情况)
                // 备注 :
                // 1. 这里可以将一个 navigator 理解成一棵树,
                // 2. 分支节点是一个嵌套的 navigator, 叶子节点是普通 screen/scene 组件,
                // 3. 根节点或者分支节点上 navigator 可能是 StackNavigator,TabNavigator或者其他什么类型的 navigator
                // 构造 childAction, 如果指定了 action.action 使用它,如果没指定,应用初始状态
                const childAction =
                    action.action || NavigationActions.init({params: action.params});
                // 获取分支节点路由器上应用 childAction 的状态
                childState = childRouter.getStateForAction(childAction);
            }
            // 各种计算和准备都做完了,现在构造初始状态对象并返回
            return {
                key: 'StackRouterRoot',// 注意这里的 Stack,表明这是一个 栈式路由器,
                isTransitioning: false,
                index: 0,// index 属性用于记录当前路由,也就是当前屏幕,初始为 0
                routes: [
                    {
                        params: action.params,
                        ...childState,
                        key: action.key || generateKey(),
                        routeName: action.routeName,
                    },
                ],
            };
        }

        //  2. 非 (action 是一个 push-like action 并且 action 目标路由由该路由器负责处理的情况) 的情况
        if (initialChildRouter) {
            // 如果初始路由对应的也是一个路由器,则计算出在它上面派发 navigate 到路由 initialRouteName
            // 的 action 后它的状态,记录到 route 变量
            route = initialChildRouter.getStateForAction(
                NavigationActions.navigate({
                    routeName: initialRouteName,
                    params: initialRouteParams,
                })
            );

            // 代码能走到这个分支的实际举例 :
            // 顶层路由器是一个 StackNavigator, 顶层路由器的初始路由或者第一个子路由是一个嵌套的TabNavigator,
            // 那么应用刚启动时,代码逻辑就会走这个分支
        }

        // 现在对 route 变量的内容做一个总结 :
        // 1. 如果 initialChildRouter 是一个路由器, route 已经记录了由它计算出的目标信息,
        // 2. 如果 initialChildRouter 为 null , route 保持为 {}
        const params = (route.params || action.params || initialRouteParams) && {
            ...(route.params || {}),
            ...(action.params || {}),
            ...(initialRouteParams || {}),
        };
        const {initialRouteKey} = stackConfig;
        route = {
            ...route,
            ...(params ? {params} : {}),
            routeName: initialRouteName,
            key: action.key || (initialRouteKey || generateKey()),
        };
        return {
            key: 'StackRouterRoot',
            isTransitioning: false,
            index: 0,// index 属性用于记录当前路由,也就是当前屏幕,初始为 0
            routes: [route],
        };
    }

    // Build paths for each route
    routeNames.forEach(routeName => {
        let pathPattern =
            pathsByRouteNames[routeName] || routeConfigs[routeName].path;
        let matchExact = !!pathPattern && !childRouters[routeName];
        if (pathPattern === undefined) {
            pathPattern = routeName;
        }
        const keys = [];
        let re, toPath, priority;
        if (typeof pathPattern === 'string') {
            // pathPattern may be either a string or a regexp object according to path-to-regexp docs.
            re = pathToRegexp(pathPattern, keys);
            toPath = pathToRegexp.compile(pathPattern);
            priority = 0;
        } else {
            // for wildcard match
            re = pathToRegexp('*', keys);
            toPath = () => '';
            matchExact = true;
            priority = -1;
        }
        if (!matchExact) {
            const wildcardRe = pathToRegexp(`${pathPattern}/*`, keys);
            re = new RegExp(`(?:${re.source})|(?:${wildcardRe.source})`);
        }
        pathsByRouteNames[routeName] = {re, keys, toPath, priority};
    });

    paths = Object.entries(pathsByRouteNames);
    paths.sort((a: [string, *], b: [string, *]) => b[1].priority - a[1].priority);

    // 构建 StackRouter 对象并返回
    return {
        /**
         * 查找指定导航状态 state 当前路由对应的屏幕(screen)组件(React Component)
         * 注意 : 如果当前路由也是一个navigator/router,会从它里面获取相应的屏幕组件
         * @param state
         * @returns {*} 目标路由对应的screen组件,找不到则抛出错误
         */
        getComponentForState(state) {
            const activeChildRoute = state.routes[state.index];
            const {routeName} = activeChildRoute;
            if (childRouters[routeName]) {
                return childRouters[routeName].getComponentForState(activeChildRoute);
            }
            return getScreenForRouteName(routeConfigs, routeName);
        },

        /**
         * 获取指定名称的路由对应的屏幕(screen)组件(React Component)
         * 注意 : 本方法只招直接子路由,不递归查找
         * @param routeName 目标路由名称
         * @returns {*} 目标路由对应的screen组件,找不到则抛出错误
         */
        getComponentForRouteName(routeName) {
            return getScreenForRouteName(routeConfigs, routeName);
        },

        /**
         * 基于当前导航状态 state, 执行 action, 计算执行后的导航状态并返回
         * @param action 待执行 action
         * @param state 当前状态
         * @returns {*} 在状态 state 上执行 action 之后的 状态
         */
        getStateForAction(action, state) {
            // Set up the initial state if needed
            if (!state) {
                // 如果没有提供 state 参数,或者 state 参数为 null, 则直接构建初始状态并返回初始状态
                return getInitialState(action);
            }

            // Check if the focused child scene wants to handle the action, as long as
            // it is not a reset to the root stack
            if (action.type !== NavigationActions.RESET || action.key !== null) {
                const keyIndex = action.key
                    ? StateUtils.indexOf(state, action.key)
                    : -1;
                const childIndex = keyIndex >= 0 ? keyIndex : state.index;
                const childRoute = state.routes[childIndex];
                invariant(
                    childRoute,
                    `StateUtils erroneously thought index ${childIndex} exists`
                );
                const childRouter = childRouters[childRoute.routeName];
                if (childRouter) {
                    // 如果当前状态中的当前路由是一个 navigator/router, 先尝试让它看看能不能处理
                    // 待执行的 action
                    const route = childRouter.getStateForAction(action, childRoute);
                    // 这里 route 分三种情况 :
                    // 1. null : 表明 childRouter 可以处理该 action , 但是处理完之后什么都不变,所以需要直接返回当前 state
                    // 2. 一个新的路由状态对象 : 表明 childRouter 可以处理该 action ,并且有了相应的输出(可能有状态变化,也可能没有)
                    // 3. undefined : 表明 childRouter 处理不了该 action ,
                    // 以上 1,2 两种情况都算是当前函数任务完成了,接下来需要返回,而3这种情况表明该函数的任务
                    // 还未被完成,代码逻辑继续
                    if (route === null) {
                        return state;
                    }
                    if (route && route !== childRoute) {
                        // 如果 route 存在并且和当前 childRoute 不同,说明当前路由(嵌套的
                        // 路由器)能处理指定 action ,那么就将这个状态更新到导航状态对象
                        // state 中相应的位置
                        return StateUtils.replaceAt(state, childRoute.key, route);
                    }
                }
            }

            // 代码走到这里,说明待执行的 action 并没有被处理 : 当前子路由不是一个嵌套路由器,
            // 或者当前子路由是一个嵌套路由器但是它处理不了待执行的 action, 所以下面继续处理
            // 待执行的 action

            // Handle explicit push navigation action. This must happen after the
            // focused child router has had a chance to handle the action.
            if (
                behavesLikePushAction(action) &&
                childRouters[action.routeName] !== undefined
            ) {
                // 如果待执行 action 是 NAVIGATE/PUSH,并且目标路由是当前路由器对象的子路由屏幕组件,
                // 则代码会走到这个分支
                const childRouter = childRouters[action.routeName];
                let route;

                invariant(
                    action.type !== NavigationActions.PUSH || action.key == null,
                    'StackRouter does not support key on the push action'
                );

                // With the navigate action, the key may be provided for pushing, or to navigate back to the key
                if (action.key) {
                    // 如果 action 中提供了 key,则这个信息可以被用来检测目标路由屏幕是否已经存在于导航栈中,如果存在于
                    // 导航栈中,则会将导航栈中该路由屏幕之上的路由屏幕都扔掉,并更新路由状态中当前路由屏幕的索引属性 index
                    // 为目标路由屏幕在导航栈中的索引,同时更新状态中的其他一些参数,并返回新状态,此函数任务结束
                    const lastRouteIndex = state.routes.findIndex(
                        r => r.key === action.key
                    );
                    if (lastRouteIndex !== -1) {
                        // If index is unchanged and params are not being set, leave state identity intact
                        if (state.index === lastRouteIndex && !action.params) {
                            return state;
                        }

                        // Remove the now unused routes at the tail of the routes array
                        const routes = state.routes.slice(0, lastRouteIndex + 1);

                        // Apply params if provided, otherwise leave route identity intact
                        if (action.params) {
                            const route = state.routes.find(r => r.key === action.key);
                            routes[lastRouteIndex] = {
                                ...route,
                                params: {
                                    ...route.params,
                                    ...action.params,
                                },
                            };
                        }
                        // Return state with new index. Change isTransitioning only if index has changed
                        return {
                            ...state,
                            isTransitioning:
                                state.index !== lastRouteIndex
                                    ? action.immediate !== true
                                    : undefined,
                            index: lastRouteIndex,
                            routes,
                        };
                    }
                }

                if (childRouter) {
                    // 如果目标路由(当前路由器的子路由)不是一个屏幕组件,而是一个嵌套navigator/router,
                    // 则更新该嵌套路由器到其初始状态,并将该子路由器状态记录新的路由栈帧到 route
                    const childAction =
                        action.action || NavigationActions.init({params: action.params});
                    route = {
                        params: action.params,
                        // merge the child state in this order to allow params override
                        ...childRouter.getStateForAction(childAction),
                        routeName: action.routeName,
                        key: action.key || generateKey(),
                    };
                } else {
                    //  如果目标路由(当前路由器的子路由)是一个屏幕组件,基于此构建新的路由栈帧 route
                    route = {
                        params: action.params,
                        routeName: action.routeName,
                        key: action.key || generateKey(),
                    };
                }
                // 在当前路由状态中压入新的路由栈帧并返回它,当前函数任务结束
                return {
                    ...StateUtils.push(state, route),
                    isTransitioning: action.immediate !== true,
                };
            } else if (
                action.type === NavigationActions.PUSH &&
                childRouters[action.routeName] === undefined
            ) {
                // 如果待执行 action 是 PUSH,但目标路由不是当前路由器对象的子路由,
                // 则代码会走到这个分支:什么都不做,返回原状态
                // If we've made it this far with a push action, we return the
                // state with a new identity to prevent the action from bubbling
                // back up.
                return {
                    ...state,
                };
            }

            // Handle navigation to other child routers that are not yet pushed
            if (behavesLikePushAction(action)) {
                const childRouterNames = Object.keys(childRouters);
                for (let i = 0; i < childRouterNames.length; i++) {
                    const childRouterName = childRouterNames[i];
                    const childRouter = childRouters[childRouterName];
                    if (childRouter) {
                        // For each child router, start with a blank state
                        const initChildRoute = childRouter.getStateForAction(
                            NavigationActions.init()
                        );
                        // Then check to see if the router handles our navigate action
                        const navigatedChildRoute = childRouter.getStateForAction(
                            action,
                            initChildRoute
                        );
                        let routeToPush = null;
                        if (navigatedChildRoute === null) {
                            // Push the route if the router has 'handled' the action and returned null
                            routeToPush = initChildRoute;
                        } else if (navigatedChildRoute !== initChildRoute) {
                            // Push the route if the state has changed in response to this navigation
                            routeToPush = navigatedChildRoute;
                        }
                        if (routeToPush) {
                            const route = {
                                ...routeToPush,
                                routeName: childRouterName,
                                key: action.key || generateKey(),
                            };
                            return StateUtils.push(state, route);
                        }
                    }
                }
            }

            // Handle pop-to-top behavior. Make sure this happens after children have had a chance to handle the action, so that the inner stack pops to top first.
            if (action.type === NavigationActions.POP_TO_TOP) {
                // Refuse to handle pop to top if a key is given that doesn't correspond
                // to this router
                if (action.key && state.key !== action.key) {
                    return state;
                }

                // If we're already at the top, then we return the state with a new
                // identity so that the action is handled by this router.
                if (state.index === 0) {
                    return {
                        ...state,
                    };
                } else {
                    return {
                        ...state,
                        isTransitioning: action.immediate !== true,
                        index: 0,
                        routes: [state.routes[0]],
                    };
                }
                return state;
            }

            // Handle replace action
            if (action.type === NavigationActions.REPLACE) {
                const routeIndex = state.routes.findIndex(r => r.key === action.key);
                // Only replace if the key matches one of our routes
                if (routeIndex !== -1) {
                    const childRouter = childRouters[action.routeName];
                    let childState = {};
                    if (childRouter) {
                        const childAction =
                            action.action ||
                            NavigationActions.init({params: action.params});
                        childState = childRouter.getStateForAction(childAction);
                    }
                    const routes = [...state.routes];
                    routes[routeIndex] = {
                        params: action.params,
                        // merge the child state in this order to allow params override
                        ...childState,
                        routeName: action.routeName,
                        key: action.newKey || generateKey(),
                    };
                    return {...state, routes};
                }
            }

            // Update transitioning state
            if (
                action.type === NavigationActions.COMPLETE_TRANSITION &&
                (action.key == null || action.key === state.key) &&
                state.isTransitioning
            ) {
                return {
                    ...state,
                    isTransitioning: false,
                };
            }

            if (action.type === NavigationActions.SET_PARAMS) {
                const key = action.key;
                const lastRoute = state.routes.find(route => route.key === key);
                if (lastRoute) {
                    const params = {
                        ...lastRoute.params,
                        ...action.params,
                    };
                    const routes = [...state.routes];
                    routes[state.routes.indexOf(lastRoute)] = {
                        ...lastRoute,
                        params,
                    };
                    return {
                        ...state,
                        routes,
                    };
                }
            }

            if (action.type === NavigationActions.RESET) {
                // Only handle reset actions that are unspecified or match this state key
                if (action.key != null && action.key != state.key) {
                    // Deliberately use != instead of !== so we can match null with
                    // undefined on either the state or the action
                    return state;
                }
                const newStackActions = action.actions;

                return {
                    ...state,
                    routes: newStackActions.map(newStackAction => {
                        const router = childRouters[newStackAction.routeName];

                        let childState = {};

                        if (router) {
                            const childAction =
                                newStackAction.action ||
                                NavigationActions.init({params: newStackAction.params});

                            childState = router.getStateForAction(childAction);
                        }

                        return {
                            params: newStackAction.params,
                            ...childState,
                            routeName: newStackAction.routeName,
                            key: newStackAction.key || generateKey(),
                        };
                    }),
                    index: action.index,
                };
            }

            if (
                action.type === NavigationActions.BACK ||
                action.type === NavigationActions.POP
            ) {
                const {key, n, immediate} = action;
                let backRouteIndex = state.index;
                if (action.type === NavigationActions.POP && n != null) {
                    // determine the index to go back *from*. In this case, n=1 means to go
                    // back from state.index, as if it were a normal "BACK" action
                    backRouteIndex = Math.max(1, state.index - n + 1);
                } else if (key) {
                    const backRoute = state.routes.find(route => route.key === key);
                    backRouteIndex = state.routes.indexOf(backRoute);
                }

                if (backRouteIndex > 0) {
                    return {
                        ...state,
                        routes: state.routes.slice(0, backRouteIndex),
                        index: backRouteIndex - 1,
                        isTransitioning: immediate !== true,
                    };
                } else if (
                    backRouteIndex === 0 &&
                    action.type === NavigationActions.POP
                ) {
                    return {
                        ...state,
                    };
                }
            }
            return state;
        },

        getPathAndParamsForState(state) {
            const route = state.routes[state.index];
            const routeName = route.routeName;
            const screen = getScreenForRouteName(routeConfigs, routeName);
            const subPath = pathsByRouteNames[routeName].toPath(route.params);
            let path = subPath;
            let params = route.params;
            if (screen && screen.router) {
                const stateRoute = route;
                // If it has a router it's a navigator.
                // If it doesn't have router it's an ordinary React component.
                const child = screen.router.getPathAndParamsForState(stateRoute);
                path = subPath ? `${subPath}/${child.path}` : child.path;
                params = child.params ? {...params, ...child.params} : params;
            }
            return {
                path,
                params,
            };
        },

        getActionForPathAndParams(pathToResolve, inputParams) {
            // If the path is empty (null or empty string)
            // just return the initial route action
            if (!pathToResolve) {
                return NavigationActions.navigate({
                    routeName: initialRouteName,
                });
            }

            const [pathNameToResolve, queryString] = pathToResolve.split('?');

            // Attempt to match `pathNameToResolve` with a route in this router's
            // routeConfigs
            let matchedRouteName;
            let pathMatch;
            let pathMatchKeys;

            // eslint-disable-next-line no-restricted-syntax
            for (const [routeName, path] of paths) {
                const {re, keys} = path;
                pathMatch = re.exec(pathNameToResolve);
                if (pathMatch && pathMatch.length) {
                    pathMatchKeys = keys;
                    matchedRouteName = routeName;
                    break;
                }
            }

            // We didn't match -- return null
            if (!matchedRouteName) {
                // If the path is empty (null or empty string)
                // just return the initial route action
                if (!pathToResolve) {
                    return NavigationActions.navigate({
                        routeName: initialRouteName,
                    });
                }
                return null;
            }

            // Determine nested actions:
            // If our matched route for this router is a child router,
            // get the action for the path AFTER the matched path for this
            // router
            let nestedAction;
            let nestedQueryString = queryString ? '?' + queryString : '';
            if (childRouters[matchedRouteName]) {
                nestedAction = childRouters[matchedRouteName].getActionForPathAndParams(
                    pathMatch.slice(pathMatchKeys.length).join('/') + nestedQueryString
                );
                if (!nestedAction) {
                    return null;
                }
            }

            // reduce the items of the query string. any query params may
            // be overridden by path params
            const queryParams = !isEmpty(inputParams)
                ? inputParams
                : (queryString || '').split('&').reduce((result, item) => {
                    if (item !== '') {
                        const nextResult = result || {};
                        const [key, value] = item.split('=');
                        nextResult[key] = value;
                        return nextResult;
                    }
                    return result;
                }, null);

            // reduce the matched pieces of the path into the params
            // of the route. `params` is null if there are no params.
            const params = pathMatch.slice(1).reduce((result, matchResult, i) => {
                const key = pathMatchKeys[i];
                if (key.asterisk || !key) {
                    return result;
                }
                const nextResult = result || {};
                const paramName = key.name;

                let decodedMatchResult;
                try {
                    decodedMatchResult = decodeURIComponent(matchResult);
                } catch (e) {
                    // ignore `URIError: malformed URI`
                }

                nextResult[paramName] = decodedMatchResult || matchResult;
                return nextResult;
            }, queryParams);

            return NavigationActions.navigate({
                routeName: matchedRouteName,
                ...(params ? {params} : {}),
                ...(nestedAction ? {action: nestedAction} : {}),
            });
        },

        getScreenOptions: createConfigGetter(
            routeConfigs,
            stackConfig.navigationOptions
        ),

        getScreenConfig: getScreenConfigDeprecated,
    };
};

猜你喜欢

转载自blog.csdn.net/andy_zhang2007/article/details/80285965