手写react-router1-基本流程

100 阅读6分钟

hash基本使用

浏览器hash有hashchange事件:

<body>
    <ul>
        <li><a href="#/a">/a</a></li>
        <li><a href="#/b">/b</a></li>
    </ul>
    <div id="root"></div>
    <script>
        window.addEventListener('hashchange',()=>{
            console.log(window.location.hash);
            let pathname = window.location.hash.slice(1);
            document.getElementById('root').innerHTML = pathname;
        });
    </script>
</body>

history基本使用

浏览器的history对象有pushState、replaceState等一些方法

还有onpopstate这样一个事件,onpopstate会在在调用浏览器的前进、后退以及执行history.forward、history.back、和history.go触发,因为这些操作有一个共性,即修改了历史堆栈的当前指针

在不改变document的前提下,一旦当前指针改变则会触发onpopstate事件

需要注意:浏览器pushState时不会触发任何事件,所以通常我们需要自己定义onpushstate这样一个事件,并且要对pushState这个方法进行一定改写,让它手动触发我们自定义的onpushstate事件


var historyObj = window.history;
window.onpushstate = (event)=>{
  console.log(event.type,event.detail);
  root.innerHTML = window.location.pathname;//当前的路径
}
//如果当前的历史栈指针发生变化的话会触发popstate事件,执行对应的回调函数
window.addEventListener('popstate',(event)=>{
  console.log(event.type,event.state);
  root.innerHTML = window.location.pathname;//当前的路径
});
;(function(historyObj){
  let oldPushState = historyObj.pushState;
  historyObj.pushState = (state,title,pathname)=>{
    let result = oldPushState.call(historyObj,state,title,pathname);
    //let result = oldPushState(state,title,pathname);
    if(typeof window.onpushstate === 'function'){
      window.onpushstate(new CustomEvent('pushstate',{detail:{pathname,state}}));
    }
    return result;
  }
})(historyObj);
setTimeout(()=>{
  // 调用pushState会修改当前的路径
  historyObj.pushState({page:1},null,'/page1');
},1000);

pushState事件做的事情:

1.修改路径

2.向history历史栈中添加一个条目 路径和状态

react-router有如下几个文件:

react-router-dom/HashRouter.js

react-router-dom/BrowserRouter.js

react-router/index.js

还依赖一个核心库:history

HashRouter的用法:

import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter as Router,Route} from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';

ReactDOM.render(
  <Router>
    <Route path="/" exact={true} component={Home}/>
    <Route path="/user" component={User}/>
    <Route path="/profile" component={Profile}/>
  </Router>,
  document.getElementById('root')
);

BrowserRouter使用的话只需要将上面代码中引入的HashRouter改成BrowserRouter即可:

HashRouter的实现:

import React from 'react';
import {Router} from '../react-router';
import {createHashHistory} from 'history';
class HashRouter extends React.Component{
    history = createHashHistory()//HashRouter的history实例属性会指向用hash实现的历史对象
    render(){
        return (
            <Router history={this.history}>
                {this.props.children}
            </Router>
        )
    }
}
export default HashRouter;

BrowserRouter的实现:

import React from 'react';
import {Router} from '../react-router';
import {createBrowserHistory} from 'history';
class BrowserRouter extends React.Component{
    history = createBrowserHistory()//HashRouter的history实例属性会指向用hash实现的历史对象
    render(){
        return (
            <Router history={this.history}>
                {this.props.children}
            </Router>
        )
    }
}
export default BrowserRouter;

Router的实现:

import React from 'react';
import RouterContext from './RouterContext';
class Router extends React.Component{
    constructor(props){
        super(props);
        this.state = {
            location:props.history.location
        }
        //监听历史对象路径变化,如果路径发生变化的话执行回调
        this.unlisten = props.history.listen((location)=>{
            this.setState({location})
        });
    }
    componentWillUnmount(){
        this.unlisten&&this.unlisten();
    }
    render(){
        let value = {history:this.props.history,location:this.state.location};
        return (
            <RouterContext.Provider value={value}>
                {this.props.children}
            </RouterContext.Provider>
        )
    }
}
export default Router;
import React from 'react';
export default React.createContext({});

Route的实现:

import React from 'react';
import RouterContext from './RouterContext';
class Route extends React.Component{
    static contextType = RouterContext;
    render(){
        const {history,location} = this.context;
        const {path,component:RouteComponent,exact=false} = this.props;
        const match = exact?location.pathname===path:location.pathname.startsWith(path);// /user /user
        const routeProps = {history,location};
        let renderElement=null;// null也一个合法的react渲染节点 代表我们render的返顺值,代表此组件将要渲染的内容
        if(match){
            //React.createElement(RouteComponent,routeProps);
            renderElement = <RouteComponent {...routeProps}/>
        }
        return renderElement
    }
}
export default Route;

history的实现:

createHashHistory:

/**
 * 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];//取出指定索引对应的路径对象
        state= nextLocation.state;//取出此location对应的状态 
        window.location.hash = nextLocation.pathname;//修改hash值 ,从而修改当前的路径
    }
    let hashChangeHandler = ()=>{
        let pathname = window.location.hash.slice(1);//取出最新的hash值对应的路径  #/user
        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;

注:

初始化时,如果浏览器上url的路径是locahost:xxxx这种不带任何path的情况,则hash也是空的,会走到上面代码中的第64行,默认给hash赋值为/,然后会触发hashChangeHandler回调,再遍历listeners依次执行

带路由的组件(Router、Route)整体的执行流程:

hash类路由:

以以下代码为例:

ReactDOM.render(
  <Router>
    <Route path="/" exact={true} component={Home}/>
    <Route path="/user" component={User}/>
    <Route path="/profile" component={Profile}/>
  </Router>,
  document.getElementById('root')
);

babel转化之后为:

ReactDOM.render(
  React.createElement(
    Router,
    null,
    React.createElement(Route, {
      path: "/",
      exact: true,
      component: Home
    }),
    React.createElement(Route, {
      path: "/user",
      component: User
    }),
    React.createElement(Route, {
      path: "/profile",
      component: Profile
    })
  ),
  document.getElementById('root')
);
  1. 先调用一大堆createElement生成vdom,结构如下:
{
  	"type": class BrowserRouter,
    "props":{
        "children":[
            {
              	"type": type: class Route,
                "props":{
                    "path":"/"
                }
            },
            {
              	"type": type: class Route,
                "props":{
                    "path":"/user"
                }
            },
            {
              	"type": type: class Route,
                "props":{
                    "path":"/profile"
                }
            }
        ]
    }
}

所以代码就变成了:

ReactDOM.render({
  	"type": class BrowserRouter,
    "props":{
        "children":[
            {
              	"type": type: class Route,
                "props":{
                    "path":"/"
                }
            },
            {
              	"type": type: class Route,
                "props":{
                    "path":"/user"
                }
            },
            {
              	"type": type: class Route,
                "props":{
                    "path":"/profile"
                }
            }
        ]
    }
}, document.getElementById('root'))
  1. 接下来,沿着根组件ReactDOM.render -> mount -> createDOM -> mountClassComponent的调用路径,在mountClassComponent方法中,做了下面几件事:
    1. Router的实例化
    2. Router的render方法的执行(返回renderVdom)
    3. 执行createDOM(renderVdom)生成真实DOM
  1. Router实例化时,其实是实例化HashRouter,HashRouter中调用createHashHistory()返回history对象,这个history对象将来会传给公共的Router组件
  2. 在执行Router的实例化时,会给上一步创建出来的history对象添加监听,供hash触发change事件时执行,同时在这里还会默认以当前的hash执行一次事件回调
  3. 紧接着Router(更准确来说是HashRouter)的render方法执行,调用createElement生成虚拟DOM树返并回供下一步使用

Router的render方法如下:

    render(){
        let value = {history:this.props.history,location:this.state.location};
        return (
            <RouterContext.Provider value={value}>
                {this.props.children}
            </RouterContext.Provider>
        )
    }

babel转义(这一步在构建完成)之后如下:

render () {
  let value = {
    history: (void 0).props.history,
    location: (void 0).state.location
  };
  return React.createElement(RouterContext.Provider, {
    value: value
  }, props.children);
}

调用React.createElement之后效果如下:

render () {
  let value = {
    history: (void 0).props.history,
    location: (void 0).state.location
  };
  return {
      "type": class Router,
      "props":{
          "history":{
              "action":"POP",
              "location":{
                  "pathname":"/"
              }
          },
          "children":[
              {
                  "type": type: class Route,
                  "props":{
                      "path":"/"
                  }
              },
              {
                  "type": type: class Route,
                  "props":{
                      "path":"/user"
                  }
              },
              {
                  "type": type: class Route,
                  "props":{
                      "path":"/profile"
                  }
              }
          ]
      }
  })
}
  1. 拿到上面HashRouter返回的虚拟DOM树(跨平台的Router组件的实例)后,再调用createDOM -> mountClassComponent,再执行Router的实例化、它的render方法的执行(返回renderVdom)、执行createDOM(renderVdom)生成真实DOM

Router进行实例化时,会给history对象的onChange加监听:

        this.unlisten = props.history.listen((location)=>{
            this.setState({location})
        });

Router的render执行createElement之后返回结果为:

render () {
  let value = {
    history: {action: 'POP', go: ƒ, goBack: ƒ, goForward: ƒ, push: ƒ, …},
    // history: this.props.history,
    location: {pathname: '/', state: undefined}
    // location:this.state.location
  };
  return {
    	"$$typeof": Symbol(react.element),
      "type": {
      	$$typeof: Symbol(react.provider),
        _context: {
        	$$typeof: Symbol(react.context),
          Consumer: {$$typeof: Symbol(react.context), _context: {…}},
          Provider: {$$typeof: Symbol(react.provider), _context: {…}},
          _currentValue: null
        }
      },
      "props":{
          "value":{
            history: {action: 'POP', go: ƒ, goBack: ƒ, goForward: ƒ, push: ƒ, …},
            location: {pathname: '/', state: undefined}
          },
          "children":[
              {
                  "type": type: class Route,
                  "props":{
                      "path":"/"
                  }
              },
              {
                  "type": type: class Route,
                  "props":{
                      "path":"/user"
                  }
              },
              {
                  "type": type: class Route,
                  "props":{
                      "path":"/profile"
                  }
              }
          ]
      }
  })
}

上述renderVdom在执行createDOM时,会先进入 else if(type&&type.$$typeof===REACT_PROVIDER) 这个分支,调用mountProvider方法,在里面会给_context赋值:

type._context._currentValue = props.value;

然后把里面的children取出来,再次传入createDOM方法中继续执行

let renderVdom = props.children;
createDOM(renderVdom)

此处的props.children其实就是一堆由Route组件构成的数组,再进入:

    } else if (vdom.length > 0) {
      dom=document.createDocumentFragment();
      let children = vdom.map(o => createDOM(o)).filter(o => o)
      if (children.length > 0) {
        dom.appendChild(children[0])
      }
    } 

这个分支执行,这里会做如下几件事:

  • 实例化Route
  • 调用render方法返回renderVdom
  • 调用createDOM(renderVdom)返回真实dom

\

  1. 渲染所有Route对象(执行Route的constructor方法),Route对象通常会有多个

\

当由于用户的操作导致hash变化时,createHashHistory中的hashchange的监听函数hashChangeHandler就会执行,Router中的回调也会执行:

        this.unlisten = props.history.listen((location)=>{
            this.setState({location})
        });

state被更新,页面进一步更新

createBrowserHistory实现:

function createBrowserHistory(){
    const globalHistory = window.history;
    let listeners = [];//存放所有的监听函数
    let state;
    function listen(listener){
        listeners.push(listener);
        return ()=>{
            listeners = listeners.filter(item=>item!=listener);
        }
    }
    function go(n){
        globalHistory.go(n);
    }
    window.addEventListener('popstate',()=>{//TODO
        let location = {state:globalHistory.state,pathname:window.location.pathname};
        //当路径改变之后应该让history的监听函数执行,重新刷新组件
        notify({action:"POP",location});
    });
    function goBack(){
        go(-1);
    }
    function goForward(){
        go(1);
    }
    function notify(newState){
        //把newState上的属性赋值到history对象上
        Object.assign(history,newState);
        history.length = globalHistory.length;//路由历史栈中历史条目的长度
        listeners.forEach(listener=>listener(history.location));//通知监听函数执行,参数是新的location
    }
    function push(pathname,nextState){//TODO
        const action = 'PUSH';//action表示是由于什么样的动作引起了路径的变更
        if(typeof pathname === 'object'){
            state = pathname.state;
            pathname = pathname.pathname;
        }else{
            state=nextState;//TODO 
        }
        globalHistory.pushState(state,null,pathname);//我们已经 跳转路径
        let location = {state,pathname};
        notify({action,location});
    }
    const history = {
        action:'POP',
        go,
        goBack,
        goForward,
        push,
        listen,
        location:{pathname:window.location.pathname,state:window.location.state}
    }
    return history;
}
export default createBrowserHistory;