一、基础

jsx

是一种JS和HTML混合的语法,将组件的结构、数据甚至样式都聚合在一起定义组件。
JSX是一种语法糖,最终会通过babeljs转译成createElement语法。

  1. const hello = <h3>Hello JSX</h3>
  2. // 转译成==>
  3. var hello = React.createElement("h3", null, "Hello JSX");

生命周期

image.png
image.png
挂载:componentWillMount()
服务器端和客户端都只调用一次,在初始化渲染执行之前立刻调用。如果在这个方法内调用setState,render() 将会感知到更新后的state,将会执行仅一次,尽管 state 改变了。
react v16.3以后将逐步去掉此api
挂载:componentDidMount()
在初始化渲染执行之后立刻调用一次,仅客户端有效(服务器端不会调用)。在生命周期中的这个时间点,组件拥有一个DOM 展现,你可以通过 this.getDOMNode() 来获取相应 DOM 节点。
如果想和其它JavaScript 框架集成,使用 setTimeout 或者 setInterval 来设置定时器,或者发送 AJAX请求,可以在该方法中执行这些操作。
注意:为了兼容 v0.9,DOM节点作为最后一个参数传入。你依然可以通过this.getDOMNode() 获取 DOM 节点。
更新:componentWillReceiveProps()
componentWillReceiveProps(object nextProps)
在组件接收到新的props 的时候调用。在初始化渲染的时候,该方法不会调用。
用此函数可以作为react 在 prop 传入之后, render() 渲染之前更新 state 的机会。老的 props 可以通过 this.props 获取到。在该函数中调用 this.setState() 将不会引起第二次渲染。
react v16.3以后将逐步去掉此api

  1. componentWillReceiveProps:function(nextProps){
  2. this.setState({
  3. likesIncreasing: nextProps.likeCount> this.props.likeCount
  4. });
  5. }

更新:shouldComponentUpdate()
boolean shouldComponentUpdate(object nextProps, object nextState)
在接收到新的props 或者 state,将要渲染之前调用。该方法在初始化渲染的时候不会调用,在使用 forceUpdate 方法的时候也不会。
如果确定新的props 和 state 不会导致组件更新,则此处应该 返回 false。

  1. shouldComponentUpdate:function(nextProps,nextState) {
  2. return nextProps.id!== this.props.id;
  3. }

更新: componentWillUpdate
componentWillUpdate(object nextProps, object nextState)
在接收到新的props 或者 state 之前立刻调用。在初始化渲染的时候该方法不会被调用。
使用该方法做一些更新之前的准备工作。
注意:

  1. 你不能在此方法中使用 this.setState()。如果需要更新 state 来响应某个 prop 的改变,请使用 componentWillReceiveProps。
  2. react v16.3以后将逐步去掉此api

更新:componentDidUpdate()
componentDidUpdate(object prevProps, object prevState)
在组件的更新已经同步到DOM 中之后立刻被调用。该方法不会在初始化渲染的时候调用。
使用该方法可以在组件更新之后操作DOM 元素。
注意:为了兼容 v0.9,DOM节点会作为最后一个参数传入。如果使用这个方法,你仍然可以使用 this.getDOMNode() 来访问 DOM 节点。

卸载:componentWillUnmount()

getDerivedStateFromProps
react v16.3时新增的
getDerivedStateFromProps(nextProps, prevState)

  • 每次接收新的props之后都会返回一个对象作为新的state,返回null则说明不需要更新state
  • 配合componentDidUpdate,可以覆盖componentWillReceiveProps的所有用法
  • 触发时间(v16.4修正):组件每次被rerender的时候,包括在组件构建之后(render之前最后执行),每次获取新的props或state之后。在v16.3版本时,组件state的更新不会触发该生命周期。

    1. // prop更新时重新获取数据
    2. static getDerivedStateFromProps(nextProps, prevState) {
    3. // Store prevId in state so we can compare when props change.
    4. if (nextProps.id !== prevState.prevId) {
    5. return {
    6. externalData: null,
    7. prevId: nextProps.id,
    8. };
    9. }
    10. // No state update necessary
    11. return null;
    12. }
    13. componentDidUpdate(prevProps, prevState) {
    14. if (this.state.externalData === null) {
    15. this._loadAsyncData(this.props.id);
    16. }
    17. }

    getSnapshotBeforeUpdate
    react v16.3时新增的
    getSnapshotBeforeUpdate(prevProps, prevState)

  • 触发时间: update发生的时候,在render之后,在组件dom渲染之前。

  • 返回一个值,作为componentDidUpdate的第三个参数。
  • 配合componentDidUpdate, 可以覆盖componentWillUpdate的所有用法。
  • componentDidUpdate(prevProps, prevState, snapshot)直接获得getSnapshotBeforeUpdate返回的dom属性值
  • 在upate之前获取dom节点,getSnapshotBeforeUpdate(prevProps, prevState)代替componentWillUpdate(nextProps, nextState)

    setState的用法

    ```javascript // 用法1 this.setState({ date: new Date() });

// 用法2 this.setState((prevState, props) => ({ counter: prevState.counter + props.increment }));

// 用法3 this.setState({ date: new Date() }, () => { // 成功设置了date后的回调 // dothing } });

  1. 这样定义方法,不会改变this指向,或者使用第二种也不会改变this指向,或者在constructor时改变this指向:
  2. ```javascript
  3. handleClick = () => {
  4. console.log('this is:', this);
  5. }
  6. <button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
  7. this.handleClick = this.handleClick.bind(this);

事件对象,这里不是原生的事件对象了:
// 需要手动添加事件对象,
// 会隐式添加事件对象e到deleteRow最后面一个参数
元素位于map()方法内时需要设置键属性key。

setState同步用法:

  1. // 方式一
  2. this.setState((nextState) => {
  3. return {
  4. counter: nextState.counter + 1,
  5. }
  6. })
  7. // 方式二
  8. setTimeout(() => {
  9. this.setState({
  10. connter: this.state.counter + 2
  11. })
  12. },0)
  13. // 方式三
  14. componentDidMount() {
  15. dom.addEventListener('click', () => {
  16. this.setState({
  17. counter: this.state.counter + 2
  18. })
  19. })
  20. }

总结:setState只有在合成事件和钩⼦子函数中是异步的,在原⽣生事件和setTimeout、setInterval中都是同步的

context

context 通过组件树提供了一个传递数据的方法,从而避免了在每一个层级手动的传递 props 属性

  1. const ThemeContext = React.createContext('light');
  2. function ThemedButton(props) {
  3. // ThemedButton 组件从 context 接收 theme
  4. return (
  5. <ThemeContext.Consumer>
  6. {theme => <Button {...props} theme={theme} />}
  7. </ThemeContext.Consumer>
  8. );
  9. }
  10. // 中间组件
  11. function Toolbar(props) {
  12. return (
  13. <div>
  14. <ThemedButton />
  15. </div>
  16. );
  17. }
  18. class App extends React.Component {
  19. render() {
  20. return (
  21. <ThemeContext.Provider value="dark">
  22. <Toolbar />
  23. </ThemeContext.Provider>
  24. );
  25. }
  26. }

ref

  1. 使用React.createRef(),不能在函数式组件上使用 ref 属性,因为它们没有实例 ```javascript function MyFunctionalComponent() { return ; }

class Parent extends React.Component { constructor(props) { super(props); this.textInput = React.createRef(); } render() { // 这将 不会 工作! return ( ); } }

  1. 2. 使用 ref 回调函数,在实例的属性中存储对 DOM 节点的引用
  2. ```javascript
  3. class CustomTextInput extends React.Component {
  4. constructor(props) {
  5. super(props);
  6. this.textInput = null;
  7. this.setTextInputRef = element => {
  8. this.textInput = element;
  9. };
  10. this.focusTextInput = () => {
  11. // 直接使用原生 API 使 text 输入框获得焦点
  12. if (this.textInput) this.textInput.focus();
  13. };
  14. }
  15. componentDidMount() {
  16. // 渲染后文本框自动获得焦点
  17. this.focusTextInput();
  18. }
  19. render() {
  20. // 使用 `ref` 的回调将 text 输入框的 DOM 节点存储到 React
  21. // 实例上(比如 this.textInput)
  22. return (
  23. <div>
  24. <input
  25. type="text"
  26. ref={this.setTextInputRef}
  27. />
  28. <input
  29. type="button"
  30. value="Focus the text input"
  31. onClick={this.focusTextInput}
  32. />
  33. </div>
  34. );
  35. }
  36. }
  1. <Modal
  2. visible={showFormData}
  3. width={'12.6rem'}
  4. onCancel={() => (this.refs.Step1Ele as any).onCancel()}
  5. footer={<div />}
  6. title={'基础信息'}
  7. wrapClassName="noFooter"
  8. className='popup-wrap'
  9. >
  10. <Step1
  11. onErr={this.props.onErr}
  12. onOkFromStep2={this.getFormInfoOk}
  13. initData={baseInfo}
  14. ref="Step1Ele"
  15. onCancel={() => this.setState({ showFormData: false })}
  16. />
  17. </Modal>

defaultProps

  1. // 方式一
  2. Greeting.defaultProps = {
  3. name: 'Mary'
  4. };
  5. // 方式二
  6. var Greeting = createReactClass({
  7. getDefaultProps: function() {
  8. return {
  9. name: 'Mary'
  10. };
  11. },

porps校验

  1. import PropTypes from "prop-types";
  2. export default class StatefulTabSelect extends Component {
  3. static propTypes = {
  4. initialValue: PropTypes.string,
  5. value: PropTypes.string,
  6. options: PropTypes.array,
  7. onChange: PropTypes.func
  8. };
  9. state = { value: null };
  10. static defaultProps = {
  11. value: null,
  12. options: [],
  13. onChange: () => {}
  14. };
  15. }

createPortal

  1. import ReactDOM from "react-dom";
  2. export default class PortalSample extends React.PureComponent {
  3. state = { visible: false };
  4. renderButton() {
  5. return (
  6. <Button type="primary" onClick={() => this.setState({ visible: true })}>
  7. 打开对话框
  8. </Button>
  9. );
  10. }
  11. renderDialog() {
  12. return (
  13. <div className="portal-sample">
  14. <div>这是一个对话框!</div>
  15. <br />
  16. <Button
  17. type="primary"
  18. onClick={() => this.setState({ visible: false })}
  19. >
  20. 关闭对话框
  21. </Button>
  22. </div>
  23. );
  24. }
  25. render() {
  26. if (!this.state.visible) return this.renderButton();
  27. return ReactDOM.createPortal(
  28. this.renderDialog(),
  29. document.getElementById("dialog-container"), // dialog-container为页面上任意位置的html元素
  30. );
  31. }
  32. }

二、进阶

suspense

利用 componentDidCatch 来实现同步形式的异步处理
功能:

  1. Suspense 解决异步操作的问题;
  2. 有了 Supsense 之后,依赖 AJAX 的组件也可以是函数形式,不需要是 class。

    性能优化

  • 手动控制shouldComponentUpdate是否渲染,返回true就重新渲染,false不重新渲染
  • React.PureComponent做浅比较,不用写shouldComponentUpdate,遇到复杂数据时要使用不可突变的数据结构immutable.js

    高阶组件

    高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件
    const EnhancedComponent = higherOrderComponent(WrappedComponent);

    forceUpdate

    this.$forceUpdate(callback). 里面调用了diff算法,比较新节点和旧节点;不相同时更新节点

    diff算法

    diff比较原则:
  1. 同级节点比较。
  2. 节点元素不同时(如div和p),直接生成不同的树形结构。相同时,生成相识的树形结构。
  3. 对于同一级的一组子节点,通过唯一的key进行区分。

    合成事件

    jsx 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了document上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。在 v17 改成了 app 容器上。这样更利于一个 html 下存在多个应用(微前端)。
  • 冒泡阶段:开发者正常给 React 绑定的事件比如 onClick,onChange,默认会在模拟冒泡阶段执行。
  • 捕获阶段:如果想要在捕获阶段执行可以将事件后面加上 Capture 后缀,比如 onClickCapture,onChangeCapture。

    1. export default function Index(){
    2. const handleClick=()=>{ console.log('模拟冒泡阶段执行') }
    3. const handleClickCapture = ()=>{ console.log('模拟捕获阶段执行') }
    4. return <div>
    5. <button onClick={ handleClick } onClickCapture={ handleClickCapture } >点击</button>
    6. </div>
    7. }
  • 绑定事件并不是一次性绑定所有事件,比如发现了 onClick 事件,就会绑定 click 事件,比如发现 onChange 事件,会绑定 [blur,change ,focus ,keydown,keyup] 多个事件。

  • React 事件合成的概念:React 应用中,元素绑定的事件并不是原生事件,而是React 合成的事件,比如 onClick 是由 click 合成,onChange 是由 blur ,change ,focus 等多个事件合成。

另外冒泡到document上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用event.stopPropagation是无效的,而应该调用event.preventDefault。
优点:

  • 合成事件首先抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力
  • 对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。

    useEffect和useLayoutEffect的区别

    useEffect和useLayoutEffect的执行时机不一样,前者被异步调度,当页面渲染完成后再去执行,不会阻塞页面渲染。 后者是在commit阶段新的DOM准备完成,但还未渲染到屏幕之前,同步执行。
    https://www.neroht.com/article-detail/31

    userReducer模拟实现

    1. function useReducer (reducer, initialState) {
    2. const [state, setState] = useState(initialState);
    3. function dispatch(action) {
    4. const nextState = reducer(state, action);
    5. setState(nextState)
    6. }
    7. return [state, dispatch];
    8. }

    react-hooks和class的区别

    设计模式来看:
  1. 类组件的根基是OOP(面向对象编程),有继承、属性、内部状态;
  2. 函数组件的根基是FP(函数式编程),能够更好的拆分组合,使关注点分离;
  3. 组合高于继承;

用法来看:

  1. 不用写class,
  2. 不用关心this指向
  3. 不用关心生命周期
  4. 状态共享

    三、react周边

    redux

    用一个普通对象来描述应用的 state,当你想要更新 state 中的数据时,你需要发起一个 action 。
  • action: 派发分发的意思;action 动作 描述一下你想干什么,动作是一个普通的JS对象,只有一个属性是必须的。type,其它属性随意
  • dispatch: 接收新的动作后,通过 才状态 和新动作计算出新状态

connect
是一个高阶组件函数,
用法:connect(state=>state.counter,actions)(Counter)
let mapStateToProps = state=>state.counter; //把仓库中的状态树映射为当前组件的属性对象

  1. 把store的dispatch方法转换成一个当前组件的属性对象

    1. let mapDispatchToProps = dispatch = ({increment:()=>dispatch(action.increment)})
  2. 直接把actionCreator放在这。这也是

    1. let mapDispatchToProps = actions;<br />combineReducers() 将多个 reducer 合并成为一个reducer<br />**Reducer**<br />reducer是纯函数,永远不要在reducer里做这些操作:
  • 修改传入的参数
  • 执行有副作用的操作,如API请求和路由跳转
  • 调用非纯函数,如Date.now()或Math.random()

    react-router

    1.1 React-router简介

    react-router包含3个库,react-router、react-router-dom和react-router-native。react-router提供最
    基本的路由功能,实际使用的时候我们不会直接安装react-router,而是根据应用运行的环境选择安装
    react-router-dom(在浏览器中使用)或react-router-native(在rn中使用)。react-router-dom和
    react-router-native都依赖react-router,所以在安装时,react-router也会自动安装,创建web应用,
    使用 yarn add react-router-dom

    1.2 React-router相关组件

    React Router 的组件通常分为三种:

React - 图3

  • 路由器组件:

: 使用 HTML5 提供的 history API ( pushState , replaceState 和 popstate 事件)来保 持 UI 和 URL 的同步;
: 使用 URL 的 hash 部分(即 window.location.hash )来保持 UI 和 URL 的同步;
: 把 URL 的历史记录保存在内存中的 (不读取、不写入地址栏)。在测试和非浏览器环境中很有用,如React Native。

  • 路由匹配组件: ,路由匹配组件通过匹配 path,渲染对应组件。
  • 导航组件: ,导航组件起到类似 a 标签跳转页面的作用。

react-router中奉行一切皆组件的思想,路由器-Router、链接-Link、路由-Route、独占-Switch、重定向-Redirect都以组件形式存在

1.3 基本使用

  1. import React, { Component } from "react";
  2. import { BrowserRouter, Link, Route } from "react-router-dom";
  3. import HomePage from "./HomePage";
  4. import UserPage from "./UserPage";
  5. export default class RouterPage extends Component {
  6. render() {
  7. return (
  8. <div>
  9. <h1>RouterPage</h1>
  10. <BrowserRouter>
  11. <nav>
  12. <Link to="/">首页</Link>
  13. <Link to="/user">用户中心</Link>
  14. </nav>
  15. {/* 根路由要添加exact,实现精确匹配 */}
  16. <Route exact path="/" component={HomePage} />
  17. <Route path="/user" component={UserPage} />
  18. </BrowserRouter>
  19. </div>
  20. );
  21. }
  22. }

1.4 动态路由

  1. // 定义路由
  2. <Route path="/product/:id" component={Product} />
  3. // 添加导航链接
  4. <Link to={"/product/123"}>搜索</Link>
  5. // 创建组件并获取参数
  6. function Product({location, match}) {
  7. console.log("match", match); //sy-log
  8. const {id} = match.params;
  9. return <h1>Product-{id}</h1>;
  10. }

1.5 嵌套路由

Route组件嵌套在其他页面组件中就产生了嵌套关系

  1. export default function App(props) {
  2. return (
  3. <div>
  4. <Router>
  5. <Link to="/">首页</Link>
  6. <Link to="/product/123">搜索</Link>
  7. <Switch>
  8. <Route exact path="/" component={HomePage} />
  9. <Route path="/product/:id" component={Product} />
  10. <Route component={_404Page} />
  11. </Switch>
  12. </Router>
  13. </div>
  14. );
  15. }
  16. function Product({match}) {
  17. console.log("match", match); //sy-log
  18. const {params, url} = match;
  19. const {id} = params;
  20. return (
  21. <div>
  22. <h1>Search-{id}</h1>
  23. <Link to={url + "/detail"}>详情</Link>
  24. <Route path={url + "/detail"} component={Detail} />
  25. </div>
  26. );
  27. }
  28. function Detail({match}) {
  29. return (
  30. <div>
  31. <h1>detail</h1>
  32. </div>
  33. );
  34. }

1.6 路由拦截

  1. @withRouter
  2. class Product extends Component {
  3. constructor(props) {
  4. super(props);
  5. this.state = {confirm: true};
  6. }
  7. render() {
  8. console.log("Product", this.props); //sy-log
  9. return (
  10. <div>
  11. <h3>Product</h3>
  12. <Link to="/">go home</Link>
  13. <Prompt
  14. when={this.state.confirm}
  15. // message="Are you sure you want to leave?"
  16. message={location => {
  17. return "Are you sure you want to leave-fun";
  18. }}
  19. />
  20. </div>
  21. );
  22. }
  23. }

2.1 如何实现一个前端路由

前端路由要实现的两个功能:监听记录路由变化,匹配路由变化并渲染内容

2.2 Hash 实现

通过 hashChange 事件就能直接监听到路由 hash 的变化,并根据匹配到的 hash 的不同来渲染不同的内容。

  1. <body>
  2. <a href="#/home">Home</a>
  3. <a href="#/user">User</a>
  4. <a href="#/about">About</a>
  5. <div id="view"></div>
  6. </body>
  7. <script>
  8. // onHashChange事件回调, 匹配路由的改变并渲染对应内容
  9. function onHashChange() {
  10. const view = document.getElementById('view')
  11. switch (location.hash) {
  12. case '#/home':
  13. view.innerHTML = 'Home';
  14. break;
  15. case '#/user':
  16. view.innerHTML = 'User';
  17. break;
  18. case '#/about':
  19. view.innerHTML = 'About';
  20. break;
  21. }
  22. }
  23. // 绑定hash变化事件,监听路由变化
  24. window.addEventListener('hashchange', onHashChange);
  25. </script>

2.3 History 实现

history 模式相较于 hash 最直接的区别就是跳转的路由不带 # 号,所以我们尝试直接拿掉 #号:

  1. <body>
  2. <a href="/home">Home</a>
  3. <a href="/user">User</a>
  4. <a href="/about">About</a>
  5. <div id="view"></div>
  6. </body>

点击 a 标签,会看到页面发生跳转,并提示找不到跳转页面,这也是意料之中的行为,因为 a 标签的默认行为就是跳转页面,我们在跳转的路径下没有对应的网页文件,就会提示错误。那么对于这种非 hash 的路由变化,我们应该怎么处理呢?大体上,我们可以通过以下三步来实现 history 模式下的路由:
1.拦截a标签 的点击事件,阻止它的默认跳转行为
2.使用 H5 的 history API 更新 URL
3.监听和匹配路由改变以更新页面
在开始写代码之前,我们有必要先了解一下 H5 的几个 history API 的基本用法。其实 window.history 这个全局对象在 HTML4 的时代就已经存在,只不过那时我们只能调用 back()、go()等几个方法来操作浏览器的前进后退等基础行为,而 H5 新引入的 pushState()和 replaceState()及 popstate事件 ,能够让我们在不刷新页面的前提下,修改 URL,并监听到 URL 的变化,为 history 路由的实现提供了基础能力。

  1. // 几个 H5 history API 的用法
  2. History.pushState(state, title [, url])
  3. // 往历史堆栈的顶部添加一个状态,方法接收三个参数:一个状态对象, 一个标题, 和一个(可选的)URL
  4. // 简单来说,pushState能更新当前 url,并且不引起页面刷新
  5. History.replaceState(stateObj, title[, url]);
  6. // 修改当前历史记录实体,方法入参同上
  7. // 用法和 pushState类似,区别在于 pushState 是往页面栈顶新增一个记录,
  8. // 而 replaceState 则是修改当前记录
  9. window.onpopstate
  10. // 当活动历史记录条目更改时,将触发popstate事件
  11. // 需要注意的是,pushState 和 replaceState 对 url 的修改都不会触发onpopstate,
  12. // 它只会在浏览器某些行为下触发, 比如点击后退、前进按钮、a标签点击等

详细的参数介绍和用法读者们可以进一步查阅 MDN,这里只介绍和路由实现相关的要点以及基本用法。了解了这几个 API 以后,我们就能按照我们上面的三步来实现我们的 history 路由:

  1. <body>
  2. <a href="/home">Home</a>
  3. <a href="/user">User</a>
  4. <a href="/about">About</a>
  5. <div id="view"></div>
  6. </body>
  7. <script>
  8. // 重写所有 a 标签事件
  9. const elements = document.querySelectorAll('a[href]')
  10. elements.forEach(el => el.addEventListener('click', (e) => {
  11. e.preventDefault() // 阻止默认点击事件
  12. const test = el.getAttribute('href')
  13. history.pushState(null, null, el.getAttribute('href'))
  14. // 修改当前url(前两个参数分别是 state 和 title,这里暂时不需要用到
  15. onPopState()
  16. // 由于pushState不会触发onpopstate事件, 所以我们需要手动触发事件
  17. }))
  18. // onpopstate事件回调, 匹配路由的改变并渲染对应内容, 和 hash 模式基本相同
  19. function onPopState() {
  20. const view = document.querySelector('#view')
  21. switch (location.pathname) {
  22. case '/home':
  23. view.innerHTML = 'Home';
  24. break;
  25. case '/user':
  26. view.innerHTML = 'User';
  27. break;
  28. case '/about':
  29. view.innerHTML = 'About';
  30. break;
  31. }
  32. }
  33. // 绑定onpopstate事件, 当页面路由发生更改时(如前进后退),将触发popstate事件
  34. window.addEventListener('popstate', onPopState);
  35. </script>

Tips:history 模式的代码无法通过直接打开 html 文件的形式在本地运行,在切换路由的时候,将会提示: Uncaught SecurityError: A history state object with URL ‘file://xxx.html’ cannot be created in a document with origin ‘null’. 这是由于 pushState 的 url 必须与当前的 url 同源,而 file:// 形式打开的页面没有 origin ,导致报错。如果想正常运行体验,可以使用http-server为文件启动一个本地服务。

History 模式的实现代码也比较简单,我们通过重写 a 标签的点击事件,阻止了默认的页面跳转行为,并通过 history API 无刷新地改变 url,最后渲染对应路由的内容。到这里,我们基本上了解了hash 和history 两种前端路由模式的区别和实现原理,总的来说,两者实现的原理虽然不同,但目标基本一致,都是在不刷新页面的前提下,监听匹配路由的变化,并根据路由匹配渲染页面内容。既然我们能够如此简单地实现前端路由,那么 React Router 的优势又体现在哪,它的实现能给我们带来哪些启发和借鉴呢。

3.1 React Router 源码实现

640.webp