十八、「深入React源码」--- 手写实现路由之history

一、实现createBrowserHistory

image.png createBrowserHistory()是基于html5提供的history实现的,最终按照打印的结果作为返回对象。

1. 实现action、push、location

1-1. action

action:表示路径变化的方式。有三个值:

'POP':go/goBack/goForwrd
'PUSH':pushState
'REPLACE':replaceState
复制代码

1-2. location

location表示当前路径。 来自于当前地址栏的路径,因此我们可以通过window.location可以获取到pathstate

1-3. push

push方法有两个入参:pathname路径、nextState新状态。其作用是:添加一个条目并且移动指针到栈顶。首先我们要改变action的值为PUSH

push方法传参的两种方式:

  • url和state分开传 push(url, {...state})
  • 作为一个整体对象传 push({ pathname, state: {...} })

因此在push方法内,我们需要判断入参如果是对象,就分别给pathnamestate赋值,如果不是对象,直接把state的值更新为入参nextState的值。调用原生history的pushState方法跳转路径。

最后把location对象传给监听函数触发更新。

2. 实现路由的动态监听

创建listen方法监听url变化,接收一个变化的回调。首先把这个回调push到数组中保存。最终方法返回一个取消此监听函数函数的方法。return () => listeners = listeners.filter(l !== listener )监听到这次变化后把这次的监听函数从数组中移除。

3. 实现路径跳转的方法

  1. go():借助history原生的go方法,跳几步根据参数决定
  2. goBack():借助history原生的go方法,往后跳一步即可。
  3. goForward():借助history原生的go方法,往后跳一步即可。

最后给popState也添加上监听url变化触发页面刷新。

二、实现createHashHistory

思路与createHashHistory大概一致,最主要的区别是,hash模式下没有原生的history对象。因此push方法我们就不能使用pushState方法。要自己手动实现。

1. 获取当前最新的url

首先把当前url赋值给window.location.hash。然后通过hashChangeHandler方法监听到url的变化。

2. 实现hashChangeHandler

取出当前路径名,给history对象赋值:actionlocationlocation即我们取出的路径名。然后进行判断,如果actionPUSH,说明是push操作,我们就要往历史栈中推入当前条目。随后触发监听回调。

3. 实现路径跳转的方法

go方法操作路径时,action的值为POP。随后把当前索引+1,然后从历史栈中取出新的location,再把新的location中的pathname属性赋值给window上的hash

那么goBack就是go(1)、goForward就是go(-1)

二、代码实现

1. src/history

1-1. history/index.js

export { default as createHashHistory } from "./createHashHistory";
export { default as createBrowserHistory } from "./createBrowserHistory";
复制代码

1-2. history/createBrowserHistory.js

function createBrowserHistory() {
  const globalHistory = window.history;
  let state;
  let listeners = [];

  /**
   * 添加新的路由条目,并移动指针指向栈顶
   * @param {*} pathname 路径
   * @param {*} nextState 新状态
   */
  function push(pathname, nextState) {
    const action = "PUSH";
    // 给puah传参的两种方式:
    // push('/user', {id:1, name:'zs'})
    // push({ pathname: '/user', state: {id:1, name:'zs'} })
    if (typeof pathname === "object") {
      state = pathname.state;
      pathname = pathname.pathname;
    } else {
      state = nextState;
    }
    // 调用原生的history的pushState方法跳转到路径
    globalHistory.pushState(state, null, pathname);
    // 触发监听
    let location = {
      pathname,
      state,
    };
    // notify(action, location);
    notify({ action, location });
  }

  // 触发监听
  function notify(newHistory) {
    // history.action = action
    // history.location = location ==>
    Object.assign(history, newHistory);
    listeners.forEach((listener) => listener(history.location)); // 保证history.location与location保持一致
  }

  // 监听函数
  function listen(listener) {
    listeners.push(listener);
    // 监听方法返回取消此监听的方法
    return () => (listeners = listeners.filter((l) => l !== listener));
  }

  window.addEventListener("popstate", () => {
    let location = {
      pathname: window.location.pathname,
      state: window.location.state,
    };
    notify({ action: "POP", location });
  });

  function go(n) {
    globalHistory.go(n);
  }

  function goBack() {
    globalHistory.go(-1);
  }

  function goForward() {
    globalHistory.go(1);
  }

  // history对象
  const history = {
    action: "POP", // 三个值 pushState-PUSH back、forward-POP、replaceState-PLACE
    push, // 指向pushState
    listen,
    go,
    goBack,
    goForward,
    location: {
      pathname: window.location.pathname,
      state: window.location.state,
    },
  };

  return history;
}

export default createBrowserHistory;
复制代码

1-3. history/createHashHistory.js

/**
 * hash不能使用 浏览器的history对象了
 * @returns
 */
function createHashHistory() {
  let stack = []; // 历史栈 存放路径
  let index = -1; // 栈顶指针 默认是-1
  let action = "POP"; // 动作
  let state; // 最新的状态
  let listeners = []; // 监听函数的数组

  function listen(listener) {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter((item) => item != listener);
    };
  }

  function go(n) {
    action = "POP";
    // 更改栈顶的指针
    index += n;
    // 取出指定索引对应的路径对象
    let nextLocation = stack[index];
    // 取出此location对应的状态
    state = nextLocation.state;
    // 修改hash值-->修改当前的路径
    window.location.hash = nextLocation.pathname;
  }

  let hashChangeHandler = () => {
    // 取出最新的hash值对应的路径  #/user
    let pathname = window.location.hash.slice(1);
    Object.assign(history, { action, location: { pathname, state } });
    if (action === "PUSH") {
      // 说明是调用push方法,需要往历史栈中添加新的条目
      stack[++index] = history.location;
    }
    listeners.forEach((listener) => listener(history.location));
  };

  function push(pathname, nextState) {
    action = "PUSH";
    if (typeof pathname === "object") {
      state = pathname.state;
      pathname = pathname.pathname;
    } else {
      state = nextState;
    }
    window.location.hash = pathname;
  }

  //当hash发生变化的话,会执行回调
  window.addEventListener("hashchange", hashChangeHandler);

  function goBack() {
    go(-1);
  }

  function goForward() {
    go(1);
  }

  const history = {
    action: "POP",
    go,
    goBack,
    goForward,
    push,
    listen,
    location: {},
    location: { pathname: "/", state: undefined },
  };

  if (window.location.hash) {
    //如果初始的情况下,如果hash是有值的
    action = "PUSH";
    hashChangeHandler();
  } else {
    window.location.hash = "/";
  }
  return history;
}

export default createHashHistory;
复制代码

2. src/react-router-dom

2-1. react-router-dom/BrowserRouter.js

import React, { Component } from "react";
import { Router } from "../react-router";
import { createBrowserHistory } from "../history";

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

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

复制代码

2-2. react-router-dom/HashRouter.js

import React, { Component } from "react";
import { Router } from "../react-router";
import { createHashHistory } from "../history";

export default class HashRouter extends Component {
  history = createHashHistory();

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

复制代码

猜你喜欢

转载自juejin.im/post/7041486743572332581