React Router — BrowserRouter 源码浅析及原理分析

3,126 阅读5分钟

从例子出发,看源码是如何处理的

ps. 源码均进行了删减,将判断、报错等部分代码移除;文章比较长,有兴趣的同学可以阅读,也可以直接跳到总结直接了解 React Router 的实现原理

例子

import React from 'react';
import {
    BrowserRouter as Router,
    Switch,
    Route,
    Link
} from 'react-router-dom';

export default () => (
    <Router>
        <div>
            <nav>
                <ul>
                    <li>
                        <Link to="/">Home</Link>
                    </li>
                    <li>
                        <Link to="/about">About</Link>
                    </li>
                </ul>
            </nav>
            <Switch>
                <Route path="/about">
                    About
                </Route>
                <Route path="/">
                    Home
                </Route>
            </Switch>
        </div>
    </Router>
)

对应的 React 结构为

下面我们从各个组件的源码解读

BrowserRouter

  • 源码中的写法类似于 ES6 Class 继承的写法,就先不关注这个点了,这边就先将源码直接简化为 class 写法(下同)
  • 这个组件主要是为了传递 history 方法,这是整个路由实现的关键方法
// 简单理解为
class BrowserRouter extends React.Componet {
    constructor(props) {
        super();
        his.props = props;
        this.history = createBrowserHistory(_this.props);
    }

    render() {
        return React.createElement(Router, {
            history: this.history,
            children: this.props.children
        });
    }
}

createBrowserHistory

其中,需要重点关注下 createBrowserHistory 这个函数,它封装了 history 的相关方法

function createBrowserHistory(props) {
  // ... 
  return {
    length: globalHistory.length,
    action: 'POP',
    location: initialLocation,
    createHref: createHref,
    push: push,
    replace: replace,
    go: go,
    goBack: goBack,
    goForward: goForward,
    block: block,
    listen: listen
  };
}

挑几个之后会用到的方法,简单描述下

location

对应的值是 initialLocation,顾名思义,就是初始地址,返回了一个对象,包含路径中的 pathnamehashsearch

 location: initialLocation,

transitionManager

  • setPrompt / confirmTransitionTo
    • setPrompt:给 prompt 赋值
    • confirmTransitionTo:根据 prompt 的值,判断是否劫持默认的回调函数,执行自定义的 getUserConfirmation 方法
  • appendListener / notifyListeners:相当于观察者模式;
    • 调用 appendListener 添加观察者;
    • 调用 notifyListeners 依次触发观察者方法
var transitionManager = createTransitionManager();

function createTransitionManager() {
    var prompt = null;

    function setPrompt(nextPrompt) {
        prompt = nextPrompt;
        return function () {
            if (prompt === nextPrompt) prompt = null;
        };
    }

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

            if (typeof result === 'string') {
                if (typeof getUserConfirmation === 'function') {
                    getUserConfirmation(result, callback);
                } else {
                    callback(true);
                }
            } else {
                callback(result !== false);
            }
        } else {
            callback(true);
        }
    }

    var listeners = [];

    function appendListener(fn) {
        var isActive = true;

        function listener() {
            if (isActive) fn.apply(void 0, arguments);
        }

        listeners.push(listener);
        return function () {
            isActive = false;
            listeners = listeners.filter(function (item) {
                return item !== listener;
            });
        };
    }

    function notifyListeners() {
        for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
            args[_key] = arguments[_key];
        }

        listeners.forEach(function (listener) {
            return listener.apply(void 0, args);
        });
    }

    return {
        setPrompt: setPrompt,
        confirmTransitionTo: confirmTransitionTo,
        appendListener: appendListener,
        notifyListeners: notifyListeners
    };
}

ps. setPrompt 一般只有在服务端渲染或者调用 <Prompt> 组件的时候才会用到,所以在本例中 confirmTransitionTo 只会调用 callback,接下去的说明也只考虑这种情况

setState

  • 合并 historynectState 属性
  • transitionManager.notifyListeners 触发监听者函数
function setState(nextState) {
    _extends(history, nextState);

    history.length = globalHistory.length;
    transitionManager.notifyListeners(history.location, history.action);
}

listen

  • listentransitionManager.appendListener 添加观察者
  • checkDOMListeners: 监听 / 移除 popstate 事件(监听用户在浏览器点击后退、前进,或者在 js 中调用 histroy.back()history.go()history.forward() 等,但监听不到 pushStatereplaceState 方法)
    • needsHashChangeListener:为了兼容 hash 改变不触发 popstate 事件的浏览器,所以需要额外增加了监听 hashchange 事件(本例也暂时不考虑这种情况)
    • handlePopState:相当于执行了 setState 方法
  • unlisten:移除 transitionMagager 中对应的 listener 方法
function listen(listener) {
    var unlisten = transitionManager.appendListener(listener);
    checkDOMListeners(1);
    return function () {
        checkDOMListeners(-1);
        unlisten();
    };
}

// checkDomListeners
var listenerCount = 0;
function checkDOMListeners(delta) {
    listenerCount += delta;
    if (listenerCount === 1 && delta === 1) {
        window.addEventListener('popstate', handlePopState);
        if (needsHashChangeListener) window.addEventListener('hashChange', handleHashChange);
    } else if (listenerCount === 0) {
        window.removeEventListener('popstate', handlePopState);
        if (needsHashChangeListener) window.removeEventListener('hashChange', handleHashChange);
    }
}

function handlePopState(event) {
    // Ignore extraneous popstate events in WebKit.
    if (isExtraneousPopstateEvent(event)) return;
    handlePop(getDOMLocation(event.state));
}

var forceNextPop = false;

function handlePop(location) {
    if (forceNextPop) {
        forceNextPop = false;
        setState();
    } else {
        var action = 'POP';
        transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
            if (ok) {
                setState({
                    action: action,
                    location: location
                });
            } else {
                revertPop(location);   // 本例流程不会走到这一步,所以 forceNextPop 的状态也不会发生变化
            }
        });
    }
}

push / replace

  • 分别对应 history.pushStatehistory.replaceState 方法
  • 再根据是否需要强制刷新,来判断是调用 window.location.href = href 强制刷新,还是调用 setState 方法
function push(path, state) {
    var action = 'PUSH';
    var location = createLocation(path, state, createKey(), history.location);
    transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
        if (!ok) return;
        var href = createHref(location);
        var key = location.key,
            state = location.state;

        if (canUseHistory) {   // 判断是否支持 history API
            globalHistory.pushState({
                key: key,
                state: state
            }, null, href);

            if (forceRefresh) {
                window.location.href = href;
            } else {
                var prevIndex = allKeys.indexOf(history.location.key);
                var nextKeys = allKeys.slice(0, prevIndex + 1);
                nextKeys.push(location.key);
                allKeys = nextKeys;
                setState({
                    action: action,
                    location: location
                });
            }
        } else {
            window.location.href = href;
        }
    });
}

function replace(path, state) {
    var action = 'REPLACE';
    var location = createLocation(path, state, createKey(), history.location);
    transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
        if (!ok) return;
        var href = createHref(location);
        var key = location.key,
            state = location.state;

        if (canUseHistory) {
            globalHistory.replaceState({
                key: key,
                state: state
            }, null, href);

            if (forceRefresh) {
                window.location.replace(href);
            } else {
                var prevIndex = allKeys.indexOf(history.location.key);
                if (prevIndex !== -1) allKeys[prevIndex] = location.key;
                setState({
                    action: action,
                    location: location
                });
            }
        } else {
            window.location.replace(href);
        }
    });
}

回归正题,看 React Route 的组件中,是如何调用上述接口,实现路由的

Router

  • staticContext:这个只有在使用 staticRouter 的时候才会用到,所以此时会调用 history 中的 listen 方法
  • listen:添加观察者方法,当监听到 popState 事件后,就会触发该方法(更新 state 的值);当组件从 DOM 中移除时,该方法会被移除
  • 新建 context 对象,将 state.locationhistory 作为参数进行传递
var context = createNamedContext$1("Router");

class Router extends React.Component {
    constructor(props) {
        super();
        this.props = props;
        this.state = {
            location: this.props.history.location
        }
        this._isMounted = false;
        this._pendingLocation = null;

        if (!this.props.staticContext) {
            this.unlisten = props.history.listen(function (location) {
                if (this._isMounted) {
                    this.setState({
                        location: location
                    });
                } else {
                    this._pendingLocation = location;
                }
            });
        }
    }

    static computeRootMatch(pathname) {
        return {
            path: "/",
            url: "/",
            params: {},
            isExact: pathname === "/"
        };
    }

    componentDidMount() {
        this._isMounted = true;

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

    componentWillUnmount() {
        if (this.unlisten) this.unlisten();
    };

    render() {
        return React.createElement(context.Provider, {
            value: {
                history: this.props.history,
                location: this.state.location,
                match: Router.computeRootMatch(this.state.location.pathname),
                staticContext: this.props.staticContext
            }
        }, React.createElement(historyContext.Provider, {
            children: this.props.children || null,
            value: this.props.history
        }));
    }
}

Link

  • 根据 React 版本,使用 forWardRefref 回调,将 DOM Refs 暴露给父组件
  • 创建 <context.Consumer> 组件,传递 navigate 方法(即 history 中的 pushreplace 方法)
var Link = forwardRef(function (_ref2, forwardedRef) {
    var _ref2$component = _ref2.component,
        component = _ref2$component === void 0 ? LinkAnchor : _ref2$component,
        replace = _ref2.replace,
        to = _ref2.to,
        innerRef = _ref2.innerRef,
        rest = _objectWithoutPropertiesLoose(_ref2, ["component", "replace", "to", "innerRef"]);

    return React.createElement(__RouterContext.Consumer, null, function (context) {
        var history = context.history;
        var location = normalizeToLocation(resolveToLocation(to, context.location), context.location);   // 拼接 to 中的链接地址
        var href = location ? history.createHref(location) : "";

        var props = _extends({}, rest, {
            href: href,
            navigate: function navigate() {
                var location = resolveToLocation(to, context.location);
                var method = replace ? history.replace : history.push;
                method(location);
            }
        }); // React 15 compat


        if (forwardRefShim !== forwardRef) {
            props.ref = forwardedRef || innerRef;
        } else {
            props.innerRef = innerRef;
        }

        return React.createElement(component, props);
    });
});

LinkAnchor

本例中没有在 <Link> 中传递 component 属性,所以使用默认的 <LinkAnchor> 组件

  • 创建 <a> 标签
  • 添加 click 事件,阻止 <a> 的默认跳转
  • 如果没有自定义 onclick 方法的话,则默认使用 <Link> 组件中的 navigate 方法(即 history 中的 pushreplace 方法)
var LinkAnchor = forwardRef(function (_ref, forwardedRef) {
    var innerRef = _ref.innerRef,
        navigate = _ref.navigate,
        _onClick = _ref.onClick,
        rest = _objectWithoutPropertiesLoose(_ref, ["innerRef", "navigate", "onClick"]);

    var target = rest.target;

    var props = _extends({}, rest, {
        onClick: function onClick(event) {
            try {
                if (_onClick) _onClick(event);
            } catch (ex) {
                event.preventDefault();
                throw ex;
            }

            if (!event.defaultPrevented && // onClick prevented default
                event.button === 0 && ( // ignore everything but left clicks
                    !target || target === "_self") && // let browser handle "target=_blank" etc.
                !isModifiedEvent(event) // ignore clicks with modifier keys
            ) {
                event.preventDefault();
                navigate();
            }
        }
    }); // React 15 compat


    if (forwardRefShim !== forwardRef) {
        props.ref = forwardedRef || innerRef;
    } else {
        props.ref = innerRef;
    }
    /* eslint-disable-next-line jsx-a11y/anchor-has-content */


    return React.createElement("a", props);
});

Switch

  • React.Children.forEach:相当于在遍历 <Switch> 的子元素,即所有的 <Route>,取出与路由匹配的 <Route>,克隆该组件进行显示
  • 如果在 <Switch> 组件上定义了 location 变量,则他就会忽略 context 上传递过来的 location 的变化,就只会渲染匹配的 <Route>
class Switch extends React.Component {
    constructor(props) {
        super();
        this.props = props;
    }

    render() {
        return React.createElement(context.Consumer, null, function (context) {
            var location = this.props.location || context.location;
            var element, match;

            React.Children.forEach(this.props.children, function (child) {
                if (match == null && React.isValidElement(child)) {
                    element = child;
                    var path = child.props.path || child.props.from;
                    match = path ? matchPath(location.pathname, _extends({}, child.props, {
                        path: path
                    })) : context.match;
                }
            });

            return match ? React.cloneElement(element, {
                location: location,
                computedMatch: match
            }) : null;
        });
    }
}

Route

显示对应的路由节点

  • 如果不是 <Switch> 组件包装过的话,就需要在组件内部进行路由匹配,判断是否显示
class Route extends React.Component {
    constructor(props) {
        this.props = props;
    }

    render() {
        return React.createElement(context.Consumer, null, function (context$1) {
            var location = _this.props.location || context$1.location;
            var match = _this.props.computedMatch ? _this.props.computedMatch // <Switch> already computed the match for us
                : _this.props.path ? matchPath(location.pathname, _this.props) : context$1.match;

            var props = _extends({}, context$1, {
                location: location,
                match: match
            });

            var _this$props = _this.props,
                children = _this$props.children,
                component = _this$props.component,
                render = _this$props.render; // Preact uses an empty array as children by
            // default, so use null if that's the case.

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

            return React.createElement(context.Provider, {
                value: props
            }, props.match ? children ? typeof children === "function" ? process.env.NODE_ENV !== "production" ? evalChildrenDev(children, props, _this.props.path) : children(props) : children : component ? React.createElement(component, props) : render ? render(props) : null : typeof children === "function" ? process.env.NODE_ENV !== "production" ? evalChildrenDev(children, props, _this.props.path) : children(props) : null);
        });
    };
}

总结

简单概括就是

  • <BrowserRouter> 内部会创建 context 对象,将组件的 state.location 作为 context.Providervalue 值进行传递,表示当前路由;同时也会开启 popState 监听事件,当监听到 popState 事件后,会去更新 state.location 的值
  • <Link> 组件会渲染自定义的 component 组件或者默认的 a 标签,并绑定对应的点击事件, pushStatereplaceState ,同时也会更新 state.location 的值
  • <Switch> 作为 context 的消费者组件,根据 location 的值筛选出与其匹配的 <Route> 组件来显示,当 state.location 的值发生改变后,<Switch> 作为消费者组件就会重新渲染匹配路由的 <Route> 组件