react-router 源码分析

Published on
发布于·预估阅读16分钟
Authors
  • Name
    willson-wang
    Twitter

全局概览

这里会借助下面这张内部流程图,先有个大致印象,然后后面在逐个进行分析;react-router 5.2.0

image

react-router与react-router-dom是什么关系

react-router是一个monorepo仓库;多个项目放在react-router的packages目录下,分别为react-router、react-router-dom、react-router-config、react-router-native;

react-router是核心方法及组件库被react-router-dom及react-router-native依赖,如提供router、route、redirect、prompt、switch、withRouter、matchPath组件or方法

react-router-dom 浏览器端路由库,提供了BrowserRouter、HashRouter、Link、NavLink组件,供我们直接使用

react-router-native native端路由库

react-router-config 提供静态配置路由的组件

所以我们浏览器端直接引入react-router-dom即可,如果我们使用静态路由配置,可以引入react-router-config or 自己封装一次

BrowserRouter与HashRouter初始化

import {
    BrowserRouter as Router,
    Switch,
    Route
} from "react-router-dom";


<Router>
    <Switch>
        <Route exact path="/">
            <Home />
        </Route>
        <Route path="/about">
            <About />
        </Route>
        <Route path="/dashboard">
            <Dashboard />
        </Route>
    </Switch>
</Router>

class BrowserRouter extends React.Component {
    history = createBrowserHistory(this.props);

    render() {
        return <Router history={this.history} children={this.props.children} />;
    }
}

function createBrowserHistory() {
    const globalHistory = window.history;

    const transitionManager = createTransitionManager();

    let listenerCount = 0;

    function checkDOMListeners(delta) {
        listenerCount += delta;

        if (listenerCount === 1 && delta === 1) {
            window.addEventListener(PopStateEvent, handlePopState);

            if (needsHashChangeListener)
            window.addEventListener(HashChangeEvent, handleHashChange);
        } else if (listenerCount === 0) {
            window.removeEventListener(PopStateEvent, handlePopState);

            if (needsHashChangeListener)
            window.removeEventListener(HashChangeEvent, handleHashChange);
        }
    }

    let isBlocked = false;

    function block(prompt = false) {
        const unblock = transitionManager.setPrompt(prompt);

        if (!isBlocked) {
            checkDOMListeners(1);
            isBlocked = true;
        }

        return () => {
            if (isBlocked) {
            isBlocked = false;
            checkDOMListeners(-1);
            }

            return unblock();
        };
    }

    function listen(listener) {
        const unlisten = transitionManager.appendListener(listener);
        checkDOMListeners(1);

        return () => {
            checkDOMListeners(-1);
            unlisten();
        };
    }

    const history = {
        length: globalHistory.length,
        action: 'POP',
        location: initialLocation,
        block,
        listen
    };

    return history; 

}

BrowserRouter组件内引入router组件,并传入createBrowserHistory返回的history对象;history对象,提供了listen方法,用于注册路由跳转成功之后的回调方法;提供了block方法,用于切换路由时弹出阻止弹窗,确认则继续跳转,取消则不进行跳转;提供location对象,location对象用于描述当前url的所有信息

初始化渲染流程

function createBrowserHistory() {
    function getDOMLocation(historyState) {
        const { key, state } = historyState || {};
        const { pathname, search, hash } = window.location;

        let path = pathname + search + hash;

        if (basename) path = stripBasename(path, basename);

        return createLocation(path, state, key);
    }

    // 获取当前url所在的location对象
    const initialLocation = getDOMLocation(getHistoryState());

    return {
        location: initialLocation,
    }
}

class Router extends React.Component {
    static computeRootMatch(pathname) {
        return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
    }

    constructor(props) {
        super(props);

        this.state = {
            location: props.history.location
        };

        this._isMounted = false;
        this._pendingLocation = null;

        if (!props.staticContext) {
            this.unlisten = props.history.listen(location => {
            // 有redirect组件,且直接触发跳转的话,会先触发这里的回调,但是Router组件可能还没有到mounted阶段,所有不能直接调用this.state来更新location参数
            if (this._isMounted) {
                this.setState({ location });
            } else {
                this._pendingLocation = location;
            }
            });
        }

    }

    componentDidMount() {
        this._isMounted = true;

        if (this._pendingLocation) {
            this.setState({ location: this._pendingLocation });
        }
    }

    componentWillUnmount() {
        if (this.unlisten) {
            this.unlisten();
            this._isMounted = false;
            this._pendingLocation = null;
        }
    }

    render() {
        return (
            <RouterContext.Provider
            value={{
                history: this.props.history,
                location: this.state.location,
                match: Router.computeRootMatch(this.state.location.pathname),
                staticContext: this.props.staticContext
            }}
            >
            <HistoryContext.Provider
                children={this.props.children || null}
                value={this.props.history}
            />
            </RouterContext.Provider>
        );
    }
}

class Route extends React.Component {
    render() {
        return (
            <RouterContext.Consumer>
            {context => {

                const location = this.props.location || context.location;
                const match = this.props.computedMatch
                ? this.props.computedMatch                : this.props.path
                ? matchPath(location.pathname, this.props)
                : context.match;

                const props = { ...context, location, match };

                let { children, component, render } = this.props;


                return (
                    <RouterContext.Provider value={props}>
                        {props.match
                        ? children
                            ? typeof children === "function"
                            ? __DEV__
                                ? evalChildrenDev(children, props, this.props.path)
                                : children(props)
                            : children
                            : component
                            ? React.createElement(component, props)
                            : render
                            ? render(props)
                            : null
                        : typeof children === "function"
                        ? __DEV__
                            ? evalChildrenDev(children, props, this.props.path)
                            : children(props)
                        : null}
                    </RouterContext.Provider>
                );
            }}
            </RouterContext.Consumer>
        );
    }
}

Router组件开始渲染,state内有一个location属性,初始值为当前url所在的location对象,然后Route组件开始渲染,Route组件会优先取switch组件注入的computedMatch属性,如果没有则通过matchPath方法获取,最终根据Route组件传入的children || component || render参数来进行渲染对应的组件;也就是说哪个组件会被渲染,完全取决于Route组件传入的path参数是否与传入的location对象内的pathname是否匹配;最后Router组件内通过history.listen注册一个路由跳转成功回调,在回调函数内通过this.setState重新设置location,从而触发Router组件更新,从而引起子组件Route重新渲染;

history.push or history.replace内部执行流程

function createBrowserHistory() {
    const transitionManager = createTransitionManager()

    function setState(nextState) {
        Object.assign(history, nextState);
        history.length = globalHistory.length;
        transitionManager.notifyListeners(history.location, history.action);
    }

    function push(path, state) {

        const action = 'PUSH';
        const location = createLocation(path, state, createKey(), history.location);

        transitionManager.confirmTransitionTo(
            location,
            action,
            getUserConfirmation,
            ok => {
                if (!ok) return;

                const href = createHref(location);
                const { key, state } = location;

                if (canUseHistory) {
                    globalHistory.pushState({ key, state }, null, href);
                    setState({ action, location });
                } else {
                    window.location.href = href;
                }
            }
        );
    }

    function replace(path, state) {

        const action = 'REPLACE';
        const location = createLocation(path, state, createKey(), history.location);

        transitionManager.confirmTransitionTo(
            location,
            action,
            getUserConfirmation,
            ok => {
                if (!ok) return;

                const href = createHref(location);
                const { key, state } = location;

                if (canUseHistory) {
                    globalHistory.replaceState({ key, state }, null, href);
                    setState({ action, location });
                } else {
                    window.location.replace(href);
                }
            }
        );
    }

    const history = {
        push,
        replace
    }

    return history

}

function createTransitionManager() {
    function confirmTransitionTo(
        location,
        action,
        getUserConfirmation,
        callback
        ) {
        callback(true);
    }

    function notifyListeners(...args) {
        listeners.forEach(listener => listener(...args));
    }

    return {
        confirmTransitionTo,
        notifyListeners
    }
}

push、replace方法内部调用createTransitionManager内的confirmTransitionTo方法,confirmTransitionTo方法内会做拦截相关的处理,后面说,执行拦截操作之后,调用callback,如果为true,则通过history.pushState || history.replaceState进行url的真正跳转;跳转之后执行setState方法,并传入{ action, location }参数;setState内直接调用transitionManager.notifyListeners方法,transitionManager.notifyListeners方法内会执行所有通过listen注册的回调函数,并传入更新后的location参数;最终触发Router组件内注册的监听函数,触发视图更新

浏览器前进后退怎么触发视图更新

function handlePop(location) {
    const action = 'POP';

    transitionManager.confirmTransitionTo(
        location,
        action,
        getUserConfirmation,
        ok => {
            if (ok) {
                setState({ action, location });
            } else {
                revertPop(location);
            }
        }
    );
}

function revertPop(fromLocation) {
    const toLocation = history.location;

    let toIndex = allKeys.indexOf(toLocation.key);

    if (toIndex === -1) toIndex = 0;

    let fromIndex = allKeys.indexOf(fromLocation.key);

    if (fromIndex === -1) fromIndex = 0;

    const delta = toIndex - fromIndex;

    if (delta) {
        go(delta);
    }
}

// history

function handlePopState(event) {
    handlePop(getDOMLocation(event.state));
}

function createBrowserHistory() {
    function checkDOMListeners(delta) {
        listenerCount += delta;

        if (listenerCount === 1 && delta === 1) {
                window.addEventListener(PopStateEvent, handlePopState);
            } else if (listenerCount === 0) {
                window.removeEventListener(PopStateEvent, handlePopState);
        }
    }
}


// hash

function handleHashChange() {
    const path = getHashPath();
    const encodedPath = encodePath(path);

    if (path !== encodedPath) {
        // Ensure we always have a properly-encoded hash.
        replaceHashPath(encodedPath);
    } else {
        const location = getDOMLocation();
        const prevLocation = history.location;

        if (!forceNextPop && locationsAreEqual(prevLocation, location)) return; // A hashchange doesn't always == location change.

        if (ignorePath === createPath(location)) return; // Ignore this change; we already setState in push/replace.

        ignorePath = null;

        handlePop(location);
    }
}

function createHashHistory() {
    function checkDOMListeners(delta) {
        listenerCount += delta;

        if (listenerCount === 1 && delta === 1) {
            window.addEventListener(HashChangeEvent, handleHashChange);
        } else if (listenerCount === 0) {
            window.removeEventListener(HashChangeEvent, handleHashChange);
        }
    }
}

不论是createBrowserHistory还是createHashHistory,都是通过history对象暴露listen or block方法内去注册对应的url变更事件回调,并在回调内通过transitionManager.confirmTransitionTo方法来达到跳转url并更新location的目的;这里需要注意的hash模式下,通过push、replace方法进行跳转之后,会触发hashChange事件,所有在push及replace的时候会给变量ignorePath赋值为当前的location对象,然后在handleHashChange事件回调内会做一次if (ignorePath === createPath(location)) return的判断,避免重复跳转

react-router跳转拦截

function confirmTransitionTo(
    location,
    action,
    getUserConfirmation,
    callback
  ) {
    if (prompt != null) {
        const result =
        typeof prompt === 'function' ? prompt(location, action) : prompt;

        if (typeof result === 'string') {
        if (typeof getUserConfirmation === 'function') {
            getUserConfirmation(result, callback);
        } else {
            warning(
            false,
            'A history needs a getUserConfirmation function in order to use a prompt message'
            );

            callback(true);
        }
        } else {
        // Return false from a transition hook to cancel the transition.
        callback(result !== false);
        }
    } else {
        callback(true);
    }
}

history对象暴露出了block方法,我们可以通过block方法注册路由跳转拦截信息,及通过getUserConfirmation方法提供拦截弹窗,当我通过了block注册了拦截信息之后,每次调用confirmTransitionTo方法,confirmTransitionTo内先判断有没有设置block,如果有,在判断有没有设置getUserConfirmation方法,如果有则调用getUserConfirmation方法,并把callback传入getUserConfirmation方法,在getUserConfirmation方法内调用callback,并传入true or false;如果没有设置block则直接调用callback并传入true;我们可以设置block来做跳转之前的确认操作场景

route组件及switch如何渲染

function matchPath(pathname, options = {}) {
    if (typeof options === "string" || Array.isArray(options)) {
        options = { path: options };
    }

    const { path, exact = false, strict = false, sensitive = false } = options;

    const paths = [].concat(path);

    return paths.reduce((matched, path) => {
        if (!path && path !== "") return null;
        if (matched) return matched;

        // 利用Route组件传入的path构建一个path的正则表达式
        // keys 是用来匹配:id params形式的参数
        const { regexp, keys } = compilePath(path, {
            end: exact,
            strict,
            sensitive
        });

        // 然后利用regexp去匹配location传入的pathname,如果匹配上了,说明当前Route需要展示,没有匹配上则返回null
        const match = regexp.exec(pathname);

        if (!match) return null;

        const [url, ...values] = match;
        const isExact = pathname === url;

        if (exact && !isExact) return null;

        return {
            path, // the path used to match
            url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
            isExact, // whether or not we matched exactly
            params: keys.reduce((memo, key, index) => {
            memo[key.name] = values[index];
            return memo;
            }, {})
        };
    }, null);
}

class Route extends React.Component {
    render() {
        return (
            <RouterContext.Consumer>
            {context => {

                const location = this.props.location || context.location;
                // 先判断父组件是非有传入computedMatch属性,如果有则直接使用,没有则判断是非有传入path,有则利用matchPath返回匹配对象
                // 
                const match = this.props.computedMatch
                ? this.props.computedMatch // <Switch> already computed the match for us
                : this.props.path
                ? matchPath(location.pathname, this.props)
                : context.match;

                const props = { ...context, location, match };

                let { children, component, render } = this.props;

                return (
                    <RouterContext.Provider value={props}>
                        {props.match
                        ? children
                            ? typeof children === "function"
                            ? __DEV__
                                ? evalChildrenDev(children, props, this.props.path)
                                : children(props)
                            : children
                            : component
                            ? React.createElement(component, props)
                            : render
                            ? render(props)
                            : null
                        : typeof children === "function"
                        ? __DEV__
                            ? evalChildrenDev(children, props, this.props.path)
                            : children(props)
                        : null}
                    </RouterContext.Provider>
                );
            }}
            </RouterContext.Consumer>
        );
    }
}

class Switch extends React.Component {
    render() {
        return (
            <RouterContext.Consumer>
            {context => {
                const location = this.props.location || context.location;

                let element, match;

                React.Children.forEach(this.props.children, child => {
                    if (match == null && React.isValidElement(child)) {
                        element = child;

                        const path = child.props.path || child.props.from;

                        match = path
                        ? matchPath(location.pathname, { ...child.props, path })
                        : context.match;
                    }
                });

                return match
                ? React.cloneElement(element, { location, computedMatch: match })
                : null;
            }}
            </RouterContext.Consumer>
        );
    }
}

先说matched,matched是通过当前Route传入的path参数,然后利用path及其它几个相关参数,生成一个路径正则,然后利用这个路径正则去匹配location对象内的pathname属性,如果返回有匹配项,说明当前Route是与当前url pathname相匹配的路由组件,然后进行展示;如果没有返回匹配项,则直接返回null,表示当前Route组件没有被匹配到;

没有switch组件的时候,只要与当前location.pathname匹配的Route组件都会被渲染出来;而有switch组件的时候,则会在switch组件内先遍历一次switch组件的一级子元素(注意这里的一级子元素及Route组件),从上到下然后找出第一个匹配项,然后传入computedMatch属性;

从这里看出react-router 4.x之后为什么称之为动态路由就是因为,每次location更新的时候,都会执行所有的Route组件,然后通过Route组件的path与location.pathname进行匹配,来最终决定渲染哪个Route组件,相当于pathname渲染什么,完全可以通过动态控制传入Route的path来进行控制;此时Router及Route组件完全就是React组件,包括生命周期与内部状态

redirect组件如何渲染

function Redirect({ computedMatch, to, push = false }) {
    return (
        <RouterContext.Consumer>
            {context => {

            const { history, staticContext } = context;

            const method = push ? history.push : history.replace;
            const location = createLocation(
                computedMatch
                ? typeof to === "string"
                    ? generatePath(to, computedMatch.params)
                    : {
                        ...to,
                        pathname: generatePath(to.pathname, computedMatch.params)
                    }
                : to
            );

            return (
                <Lifecycle
                    onMount={() => {
                        method(location);
                    }}
                    onUpdate={(self, prevProps) => {
                        const prevLocation = createLocation(prevProps.to);
                        if (
                        !locationsAreEqual(prevLocation, {
                            ...location,
                            key: prevLocation.key
                        })
                        ) {
                        method(location);
                        }
                    }}
                    to={to}
                />
            );
            }}
        </RouterContext.Consumer>
    );
}

class Lifecycle extends React.Component {
    componentDidMount() {
        if (this.props.onMount) this.props.onMount.call(this, this);
    }

    componentDidUpdate(prevProps) {
        if (this.props.onUpdate) this.props.onUpdate.call(this, this, prevProps);
    }

    componentWillUnmount() {
        if (this.props.onUnmount) this.props.onUnmount.call(this, this);
    }

    render() {
        return null;
    }
}

redirect组件完全就是根据computedMatch or to属性转换成对应的location对象,然后通过history.replace or history.push 在componentDidMount钩子内进行对应路由跳转;

withRouter组件如何渲染

function withRouter(Component) {
    const displayName = `withRouter(${Component.displayName || Component.name})`;
    const C = props => {
        const { wrappedComponentRef, ...remainingProps } = props;

        return (
            <RouterContext.Consumer>
            {context => {
                return (
                <Component
                    {...remainingProps}
                    {...context}
                    ref={wrappedComponentRef}
                />
                );
            }}
            </RouterContext.Consumer>
        );
    };

    C.displayName = displayName;
    C.WrappedComponent = Component;

    return hoistStatics(C, Component);
}

withRouter就是一个高阶组件,为我们不在route下的组件提供history,location等属性

在看全局

在看这张图,是不是清楚了react-router内部的运行机制了;简而言之react-router;分为两个部分,第一部分history部分,第二个部分与react结合部分

history部分提供统一的跳转方法及返回路由对象,允许设置导航成功回调函数、执行路由跳转拦截

react结合部分则是,在Router组件内,通过设置导航监听函数,然后在监听函数内通过调用this.setState(location),从而达到更新视图的目的;同时在Router组件内通过context,来为子组件提供history、location等属性

image

react-router与vue-router对比

其实二者的思路差不多是一致的,分为两个部分,一个部分是history导航部分,一部分是与对应框架结合部分,只不过react抽离了单独的history库,而vue-router则没有;同时vue-router有更丰富的路由钩子,而react-router没有;