一文读懂 react 您所在的位置:网站首页 typeof原理 一文读懂 react

一文读懂 react

#一文读懂 react| 来源: 网络整理| 查看: 265

react-router 是 react 生态的重要组成部分,我们用它来管理 URL,实现页面组件切换。本篇我们深入 react-router 源码,搞懂它的工作方式:

文中你将看到:

react-router 相关库的实现由哪些部分组成 Router、Route等组件是如何互相配合,实现规则配置和路由解析的。 在组件中,我们是如何通过 withRouter 和 hooks 拿到路由信息的。 history 做了哪些事,如何统一 browser history 和 hash history 的 api。 整体结构

开发中我们通常不直接依赖核心的 react-router,而是把所有 API、组件从 react-router-dom 导出使用。此外还有我们不直接接触的 history 库,共同构成了完整的 router 功能,它们之间关系如下图:

react-router 实现了 router 的最核心能力。提供了Router、Route等组件,以及配套方法、hook。这部分只和 react 有关,和宿主无关,像 在初始化的时候,必须手动传入宿主对 history api 的实现。 react-router-dom 则是 react-router 在浏览器宿主下的更上一层封装。把浏览器 history api 传入Router 并初始化成 BrowserRouter、HashRouter,补充了 Link 这样给浏览器直接用的组件。同时把 react-router 透传导出,减少开发者依赖。 history 库给 browser、hash 两种 history 统一了 api,补充了订阅的能力,最终规范成 react-router 需要的接口供 react-router-dom 调用。

这就是三个关键模块间的关系。

react-router Router:基于 Context 的全局状态下发

router 是一个 “Provider-Consumer” 模型,你在最外层给个Router,在内部任意位置都可以用Route接到数据。很显然用了 React.Context。

import RouterContext from "./RouterContext.js"; import HistoryContext from "./HistoryContext.js"; class Router extends React.Component { render() { {this.props.children} } } 复制代码

这里下发了两个 Context:RouterContext、HistoryContext,都是隔壁模块导入的单例,所以一个项目中只能用一套 react-router。那这两个有什么不同?各自下发了哪些状态?

RouterContext

RouterContext 下发一个对象,主要包含三个信息:

history: this.props.history location: this.state.location match: Router.computeRootMatch(this.state.location.pathname)

其中,history 来自 history 库提供的统一 API,包括 history 的读取、操作、订阅等。

location 来自 Router 的一个状态,Router 会在 mount 的时候监听 history,并在改变时更新 location:

componentDidMount() { this.props.history.listen(location => { this.setState({ location }); } } 复制代码

match 用来描述当前 Route 对 URL 的匹配信息。Router 来自 Class 的静态方法,写死了根路由的信息。

static computeRootMatch(pathname) { return { path: "/", url: "/", params: {}, isExact: pathname === "/" }; } 复制代码 HistoryContext

HistoryContext 更简单,直接下发了 history:value={this.props.history}。但是 HistoryContext 为什么要单独给呢?

总结下来,Router 的结构如下图:

Route:路由递归

Route 用来匹配路由,特性如下:

Route 会根据当前路由匹配规则渲染对应组件(children 或 component 或 render) Route 可以继续嵌套 Route,每层嵌套都会获得当前层级的 router 信息,比如根据 path 解析出的 params 根据当前路由匹配规则渲染对应组件

首先 Route 要判断自己是否匹配:

class Route extends React.Component { return {context => { const location = this.props.location || context.location; const match = this.props.path ? matchPath(location.pathname, this.props) : context.match; return ( {props.match ? `/* 渲染路由组件 */` : null} ); }} }} 复制代码 如果 Route 有 props.path,则看 location.pathname 符不符合 this.props.path 的规则;如果没有,就是匹配的,用消费到的 match matchPath 就是个匹配方法,得到 match 对象或 null。 对于匹配的再渲染子节点,这个很简单,找到渲染方法执行就好: // Route 的 render 函数 children 渲染部分 let { children, component, render } = this.props; props.match ? children ? typeof children === "function" ? children(props) : children : component ? React.createElement(component, props) : render ? render(props) : null 复制代码 Route 嵌套的实现

为了实现 Route 套 Route,::Route 每次渲染都会重建一个 RouterContext.Provider,把值更新为当前 Route 下计算后的 router 信息::。

class Route extends React.Component { render() { return {context => { //... return ( {props.match ? `/* 渲染路由组件 */` : null} ); }} }} 复制代码

总结如图:

matchPath 方法细节

最后打开 matchPath 方法看细节:

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); 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); } 复制代码 组件取值

组件可能需要哪些值?

history:方便直接操作 history api match:路由相关信息,特别是匹配到的参数

可能通过哪些方式拿到?

如果是 Route 子节点,Route 渲染时会当成 props 带进去:const props = { …context, location, match }; 此外,每个 Route 都下发了 context 数据。value 也是上面的 props。后代组件都可以消费。

消费 Context 的方式显然更通用,因此 react-router 的消费实现大多用这种方式。

withRouter()

在没 hook 前,withRouter 是我们取 route 信息的主要方式。它是个简单的 HOC:

import RouterContext from "./RouterContext.js"; function withRouter(Component) { const C = props => { return ( {context => } ); }; // ... return hoistStatics(C, Component); } 复制代码 传入子组件,返回高阶组件 C C 在子组件外层包了 RouterContext.Consumer,把 context 带进去 hooks

有 hook 后,react-router 提供了几个 hook,也都是基于 useContext 来做的。

import RouterContext from "./RouterContext.js"; import HistoryContext from "./HistoryContext.js"; export function useHistory() { return useContext(HistoryContext); } export function useLocation() { return useContext(RouterContext).location; } export function useParams() { const match = useContext(RouterContext).match; return match ? match.params : {}; } export function useRouteMatch(path) { const location = useLocation(); const match = useContext(RouterContext).match; return path ? matchPath(location.pathname, path) : match; } 复制代码 其他路由组件

react-router 还提供了一些其他组件来丰富调用方式,举个 Switch 的例子看看。

class Switch extends React.Component { render() { return ( {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; }} ); } } 复制代码 在 Route 外面先消费一下 RouterContext,只渲染第一个匹配到的子组件 用 React.Children.forEach 遍历子组件 从 context 取 location.pathname,从各子组件 child.props.path 取 path,提前调用 matchPath 匹配 react-router-dom 组件: BrowserRouter 和 HashRouter

只是在用不同的 history 调 Router:

import { Router } from "react-router"; import { createHashHistory, createBrowserHistory } from "history"; class HashRouter extends React.Component { history = createHashHistory(this.props); render() { return ; } } class BrowserRouter extends React.Component { history = createBrowserHistory(this.props); render() { return ; } } 复制代码 history 接口 export interface History { readonly action: Action; readonly location: Location; createHref(to: To): string; push(to: To, state?: any): void; replace(to: To, state?: any): void; go(delta: number): void; back(): void; forward(): void; listen(listener: Listener): () => void; block(blocker: Blocker): () => void; } 复制代码

主要几个信息:

当前 location push、replace、go、back、forward:history 操作 listen:history 事件订阅 实现思路

如图:

模块划分:

history 依赖 window.history API:用于同步的增删改查浏览器路由状态 history 依赖 event listener 监听浏览器路由事件:用于处理自身状态和回调 转接层:实现 window.history.state 和自身 location 数据的双向转换 location:history 维护的 location 数据,全局唯一 订阅池:自己实现了回调池子,通过 push、call 方法添加订阅和修改 路由方法:间接调用 window.history 的方法

主要调用逻辑:

初始化(执行 createBrowserHistory):创建location、订阅池、方法;从 window.location、window.history 计算当前 location。 浏览器事件触发:重新计算当前 location;调订阅池 call 方法处理所有回调 push、replace 方法被调用:根据调用入参生成新 location,并转为 history state,调 window.history 方法;调订阅池 call 方法处理所有回调 createBrowserHistory 和 createHashHistory 的差异

两个方法向外暴露的接口完全一样,为了抹平差异,实现上做了如下两点适配:

1、location 属性计算

createBrowserHistory 下,location 中的 pathname, search, hash 直接来自于 window.location。

createHashHistory 下则都是从#后的 hash 中解析出来的,比如 hash 部分是 #/a/b?c=1#/d,解析出 {hash: '#/d', search: '?c=1', pathname: '/a/b'}。

2、event listener 事件监听

createBrowserHistory 只需监听 popstate,而 createHashHistory 还要监听 hashchange,而且这里要判断下前后 location是否相等,因为 hashchange 可能是无效的。

小结 react-router 整体分为三个部分:react-router 模块实现核心能力;react-router-dom 封装了 react-router,创建浏览器环境下的对应组件;history 在浏览器实现了两种路由下 react-router 需要的 history 接口。 router 状态传递靠 React.Context 能力实现。 Router就是整个配置的 Provider; Route 进行 Consume、匹配计算、重新 Provide;withRouter、hook 等 Consume 获取路由状态。 history 依赖浏览器 history 接口和事件监听,内部保存 location 数据,自己实现了事件订阅池,最终封装了统一的 api 供Router用。


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有