react-router路由原理

247 阅读7分钟

react-router-dom和react-router和history库三者什么关系

  • history 可以理解为react-router的核心,也是整个路由原理的核心,里面集成了popState,history.pushState等底层路由实现的原理方法。
  • react-router可以理解为是react-router-dom的核心,里面封装了Router,Route,Switch等核心组件,实现了从路由的改变到组件的更新的核心功能。
  • react-router-dom,在react-router的核心基础上,添加了用于跳转的Link组件,和histoy模式下的BrowserRouter和hash模式下的HashRouter组件等。所谓BrowserRouter和HashRouter,也只不过用了history库中createBrowserHistorycreateHashHistory方法
// react-router-dom  index.js
export * from "react-router";

export { default as BrowserRouter } from "./BrowserRouter";
export { default as HashRouter } from "./HashRouter";
export { default as Link } from "./Link";
export { default as NavLink } from "./NavLink";

// react-router index.js
export { default as MemoryRouter } from "./MemoryRouter";
export { default as Prompt } from "./Prompt";
export { default as Redirect } from "./Redirect";
export { default as Route } from "./Route";
export { default as Router } from "./Router";
export { default as StaticRouter } from "./StaticRouter";
export { default as Switch } from "./Switch";
export { default as generatePath } from "./generatePath";
export { default as matchPath } from "./matchPath";
export { default as withRouter } from "./withRouter";

import { useHistory, useLocation, useParams, useRouteMatch } from "./hooks.js";
export { useHistory, useLocation, useParams, useRouteMatch };

export { default as __RouterContext } from "./RouterContext";

单页面应用路由实现原理

切换url,监听url变化,从而渲染不同的页面组件。主要的方式有history模式和hash模式。

history模式

history.pushState(state, title, path);
history.replaceState(state, title, path);
// state:一个与指定网址相关的状态对象, popstate 事件触发时,该对象会传入回调函数。如果不需要可填 null。
// title:新页面的标题,但是所有浏览器目前都忽略这个值,可填 null。
// path:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个地址。

// 监听路由
window.addEventListener('popstate',function(e){
    // history.back()、history.forward()、history.go() 触发此监听事件
});

hash模式

window.location.hash = 'xxx'; // 更改hash值

window.addEventListener('hashchange',function (e) {
    /* 监听改变 */
})

history库详细

createBrowserHistory

const PopStateEvent = 'popstate'
const HashChangeEvent = 'hashchange'
/* 这里简化了createBrowserHistory,列出了几个核心api及其作用 */
function createBrowserHistory(){
    /* 全局history  */
    const globalHistory = window.history
    /* 处理路由转换,记录了listens信息。 */
    const transitionManager = createTransitionManager()
    /* 改变location对象,通知组件更新 */
    const setState = (nextState) => {
        /* 合并信息 */
        Object.assign(history, nextState)
        history.length = globalHistory.length
        /* 通知每一个listens 路由已经发生变化 */
        transitionManager.notifyListeners(
            history.location,
            history.action
        )
    }

    /* 处理当path改变后,处理popstate变化的回调函数 */
    const handlePopState = (event)=>{
        /* 获取当前location对象 */
        const location = getDOMLocation(event.state)
        const action = 'POP'

        transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
            if (ok) {
            setState({ action, location })
            } else {
            revertPop(location)
            }
        })
    }
    /* history.push方法,改变路由,通过全局对象history.pushState改变url, 通知router触发更新,替换组件 */
    const push = (path, state) => {
        const action = 'PUSH'
        /* 1 创建location对象 */
        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) {
                /* 改变 url */
                globalHistory.pushState({ key, state }, null, href)
                if (forceRefresh) {
                    window.location.href = href
                } else {
                    /* 改变 react-router location对象, 创建更新环境 */
                    etState({ action, location })
                }
            } else {
                window.location.href = href
            }
        })
    }

    /* 底层应用事件监听器,监听popstate事件 */
    const listen = (listener) => {
        // listen本质通过checkDOMListeners的参数 1 或 -1 来绑定/解绑 popstate 事件,当路由发生改变的时候,调用处理函数handlePopState 。
        /* 添加listen */
        const unlisten = transitionManager.appendListener(listener)
        checkDOMListeners(1)

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

    const go = (n) => {
        globalHistory.go(n);
    }

    const goBack = () => {
        go(-1);
    }

    const goForward = () => {
        go(1);
    }

    function checkDOMListeners(delta) {
        listenerCount += delta;

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

    const history = {
        length: globalHistory.length,
        action: 'POP',
        location: initialLocation,
        createHref: createHref,
        push: push,
        replace: replace,
        go: go,
        goBack: goBack,
        goForward: goForward,
        block: block,
        listen: listen
    };
    return history;
}

createHashHistory

const HashChangeEvent = 'hashchange'
const checkDOMListeners = (delta) => {
    listenerCount += delta
    if (listenerCount === 1) {
        addEventListener(window, HashChangeEvent, handleHashChange)
    } else if (listenerCount === 0) {
        removeEventListener(window, HashChangeEvent, handleHashChange)
    }
}
/* 对应 push 方法 */
const pushHashPath = (path) =>
  window.location.hash = path

/* 对应replace方法 */
const replaceHashPath = (path) => {
    const hashIndex = window.location.href.indexOf('#')

    window.location.replace(
        window.location.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + '#' + path
    )
}

image.png

react-router 核心组件

Router

Router 作用是把 history location 等路由信息 传递下去。初始化绑定listen, 路由变化,通知改变location,改变组件。 react的history路由状态是保存在React.Content上下文之间, 状态更新。一个项目应该有一个根Router , 来产生切换路由组件之前的更新作用。 如果存在多个Router会造成,会造成切换路由,页面不更新的情况。

class Router extends React.Component {
    static computeRootMatch(pathname) {
        return { path: '/', url: '/', params: {}, isExact: pathname === '/' };
    }
    constructor(props) {
        super(props);
        this.state = {
            location: props.history.location
        };
        //记录pending位置
        //如果存在任何<Redirect>,则在构造函数中进行更改
        //在初始渲染时。如果有,它们将在
        //在子组件身上激活,我们可能会
        //在安装<Router>之前获取一个新位置。
        this._isMounted = false;
        this._pendingLocation = null;
        /* 此时的history,是history创建的history对象 */
        if (!props.staticContext) {
        /* 这里判断 componentDidMount 和 history.listen 执行顺序 然后把 location复制 ,防止组件重新渲染 */
            this.unlisten = props.history.listen(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();
    }
    render() {
        return (
            /*  这里可以理解 react.createContext 创建一个 context上下文 ,保存router基本信息。children */
            <RouterContext.Provider
                children={this.props.children || null}
                value={{
                    history: this.props.history,
                    location: this.state.location,
                    match: Router.computeRootMatch(this.state.location.pathname),
                    staticContext: this.props.staticContext
                }}
            />
        );
    }
}

Switch

匹配正确的唯一的路由。根据router更新流,来渲染当前组件。如果不使用Switch,那么所有Route的组件都会被渲染。

/* switch组件 */
class Switch extends React.Component {
  render() {
    return (
        <RouterContext.Consumer>
            {/* 含有 history location 对象的 context */}
            {context => {
                invariant(context, 'You should not use <Switch> outside a <Router>');
                const location = this.props.location || context.location;
                let element, match;
                //我们使用React.Children.forEach而不是React.Children.toArray().find()
                //这里是因为toArray向所有子元素添加了键,我们不希望
                //为呈现相同的两个<Route>s触发卸载/重新装载
                //组件位于不同的URL。
                //这里只需然第一个 含有 match === null 的组件
                React.Children.forEach(this.props.children, child => {
                    if (match == null && React.isValidElement(child)) {
                        element = child;
                        // 子组件 也就是 获取 Route中的 path 或者 rediect 的 from
                        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>
    );
  }
}

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;

        const { regexp, keys } = compilePath(path, {
            end: exact,
            strict,
            sensitive
        });
        const match = regexp.exec(pathname);
        /* 匹配不成功,返回null */
        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);
}

Route

作为路由组件的容器,可以根据将实际的组件渲染出来。通过RouterContext.Consumer 取出当前上一级的location,match等信息。作为prop传递给页面组件。使得我们可以在页面组件中的props中获取location ,match等信息。

class Route extends React.Component {
  render() {
    return (
        <RouterContext.Consumer>
            {context => {
                // computedMatch 为 经过 swich处理后的 path
                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;

                if (Array.isArray(children) && children.length === 0) {
                    children = null;
                }

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

Redirect

重定向组件, 如果没有路由匹配上,会重定向对应的路由。

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;
    }
}
function Redirect({ computedMatch, to, push = false }) {
    return (
        <RouterContext.Consumer>
            {context => {
                const { history, staticContext } = context;
                /* method就是路由跳转方法。 */
                const method = push ? history.push : history.replace;
                /* 找到符合match的location ,格式化location */
                const location = createLocation(
                computedMatch
                    ? typeof to === 'string'
                    ? generatePath(to, computedMatch.params)
                    : {
                        ...to,
                        pathname: generatePath(to.pathname, computedMatch.params)
                        }
                    : to
                )
                /* 初始化的时候进行路由跳转,当初始化的时候,mounted执行push方法,当组件更新的时候,如果location不相等。同样会执行history方法重定向 */
                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>
    );
}

其他API

BrowserRouter

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

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

Link

// 用法
<Link to={ '/detail' }>详情</Link>

// 简易代码
const Link = (props) => {
    const { to, replace, onClick, ...rest } = props;
    return (
        <RouterContext.Consumer>
        {context => {
          const { history } = context;
          const location = normalizeToLocation(
            resolveToLocation(to, context.location),
            context.location
          );
          const href = location ? history.createHref(location) : "";
          const props = {
            ...rest,
            href,
            onClick(event) {
                if (onClick) onClick(event);
                const location = resolveToLocation(to, context.location);
                const method = replace ? history.replace : history.push;
                method(location);
            },
          };
          return <a { ...props } />
        }}
      </RouterContext.Consumer>
    );
};

withRouter

将 history等信息注入到Component组件的props中

import React from "react";
import RouterContext from "./RouterContext";
import hoistStatics from "hoist-non-react-statics";

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>
        );
    };
  return hoistStatics(C, Component);
}

流程分析

image.png

  • 当url改变,首先触发histoy,调用事件监听popstate事件, 触发回调函数handlePopState,触发history下面的setstate方法,产生新的location对象,然后通知Router组件更新location并通过context上下文传递,switch通过传递的更新流,匹配出符合的Route组件渲染,最后有Route组件取出context内容,传递给渲染页面,渲染更新。
  • 当调用history.push方法,首先调用history的push方法,通过history.pushState来改变当前url,接下来触发history下面的setState方法,后续流程和上面一致。