第一次笔记ReactRouter


路由基本原理

第一种,通过监听 哈希切换

通过window事件hashchange监听哈希变化 window.addEventListener('hashchange',()=>{})

通过window.location.hash获取哈希路由

<body>
    <div id="root"></div>
    <ul>
        <li><a href="#/a">/a</a></li>
        <li><a href="#/b">/b</a></li>
    </ul>
    <script>
        window.addEventListener('hashchange',()=>{
            console.log(window.location.hash);
            let pathname = window.location.hash.slice(1);//把最前面的那个#删除 
            root.innerHTML = pathname;
        });

    </script>
</body>

第二种,通过HTML5提供的 window.history对象,包含五个方法修改操作路由pushState replaceState forward back go

history.pushState(state, title, url) (url对应的状态对象,标题,设定的url)

向浏览器历史栈压入一个 路由,并将历史栈指针指向 栈顶路由,state会在onpopstate事件中传给回调函数使用

history.replaceState() 替换历史栈中当前指针指向的位置,不修改指针

window.onpopstate监听函数,给这个参数赋值函数,在这五种情况下触发,浏览器前进 浏览器后退 history.forward(N) history.back() history.go()

注意,pushState()方法 不会被 onpopstate()监听到,所以得自定义监听函数来监听,原理如下

(function (history) {
    let oldPushState = history.pushState;
    history.pushState = function (state, title, pathname) {
        let result = oldPushState.apply(history, arguments); // 本质上是调用原本的 pushState方法,只不过多加了下面一条
         // 手动触发 window.onpushstate 方法,相当于自定义了`pushstate`事件 和 `onpushstate`监听函数
        if (typeof window.onpushstate === 'function') { 
            window.onpushstate(new CustomEvent('pushstate', { detail: { pathname, state } }));
        }
    }
})(history);

6.0以前版本的 react-router原理

从使用开始讲起


ReactDOM.render(
    <Router>
        <div>
          <Route path="/" component={Home} exact/> 
          <Route path="/user" component={User} />
          <Route path="/profile" component={Profile}/>
        </div>
    </Router>
,document.getElementById('root'));
// 注意6.0 可以传标签了,标签可以方便传参 <Route path="/" element={<Home />} />

<Router/> 本质上就是一个provider,作为 类组件 向下传递了一个context.

<Route/> 作为 类组件 接收到context,根据其内的history对象,与自己的path进行匹配,匹配上了就是渲染对应组件.

<Router/> 同时会监听路由,每次路由变动 this.setState({location}),读取路由并修改向下传递的context

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 = {//通过value向下层传递数据
            location:this.state.location,
            history:this.props.history
        }
        return (
            <RouterContext.Provider value={value}>
                {this.props.children}
            </RouterContext.Provider>
        )
    }
}
export default Router;
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} = this.props;
        const match = location.pathname === path;
        let routeProps = {history,location};
        let element=null;
        if(match){
            element= <RouteComponent {...routeProps}/>
        }
        return element;
    }
}
export default Route;

6.0 版本全部用 函数组件 + hooks来实现了,用法也有一定差异

6.0版本 react-router

用法

ReactDOM.render(
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<Home name="lzy" />} />
      <Route path="/user" element={<User />} />
      <Route path="/profile" element={<Profile />} />
    </Routes>
  </BrowserRouter>,
  document.getElementById('root')
);

原理

从外到内 分别是:
BrowserRouter => Router => NavigationContext => LocationContext => Routes => Route

BrowserRouter 负责给Router传递路由信息,监控路由并更新路由信息
navigator (history对象) 子组件可通过NavigationContext,拿到该对象调用函数进行路由跳转
navigationType 当前触发的 路由事件类型 ‘POP’ ‘PUSH’
location 当前的 路由 { pathname: '', state: '' }
children 孩子节点原样传递

Router 负责向Routes传递两个context,NavigationContextLocationContext,并渲染children(也就是Routes)

Routes 负责 拿到LocationContext里的 路由,拿到Route数组 遍历path正则匹配, 渲染element

BrowserRouter简单实现如下

import { createBrowserHistory, createHashHistory } from '../history';
function BrowserRouter({ children }) { //
    let historyRef = React.useRef(null);
    if (historyRef.current === null) {
        historyRef.current = createBrowserHistory();
    }
    let history = historyRef.current; // history对象,包含 location action 及 路由控制函数
    let [state, setState] = React.useState({
        location: history.location
        action: history.action,//POP PUSH 
    });

    // history.listen,内部是 history的跳转方法被调用时,就会notify 这些listener,并传入 { location:{pathname,state}, action }
    // 然后这里调用 setState, 触发更新渲染,然后 history更新, useState 从history拿到新state,创建新setState,等待调用
    React.useLayoutEffect(() => history.listen(({ location, action }) => {
        setState({ location, action });
    }), [history]); // 潜比较history没变化,就一直复用`() =>`这个函数,不会每次都创建新的`() =>`.放入微任务,执行,挂载listener
    return (
        <Router
            navigator={history}
            location={state.location}
            navigationType={state.action}
            children={children}
        />
    )
}

Router简简单单,如下

/**
 * 路由容器
 * @param {*} children 儿子 
 * @param {*} navigator 历史对象,其实就是history
 * @param {*} location 地址对象 {pathname:"当前路径"}
 * @returns 
 */
function Router({ children, navigator, location }) {
    return (
        <NavigationContext.Provider value={{ navigator }}>
            <LocationContext.Provider value={{ location }}>
                {children}
            </LocationContext.Provider>
        </NavigationContext.Provider>
    )
}

Routes 就是 拿到LocationContext里的路由,拿到Route里的elementpath数组, 正则选择渲染哪个element

继承window.history对象,监听操作路由

<BrowserRouter/> 里面 history 提供了路由的信息, 由createBrowserHistory()函数创建

原理 就是是拿到window.history 再进行个性化的修改,提供给子组件使用.

<HashRouter/>同理, 与<BrowserRouter/>内部函数属性一模一样,这样做抹平了差异,想用哪个随时替换,子组件调用的方法都是那几个.

<HashRouter/> 没有栈,内部模拟了 浏览器的history栈.

自定义的history对象
state就是更新页面要传递的参数
push(pathname,state)时 要更新页面 要用到传入的新state, onpopstate时 更新页面也要用到 以前push传入的 state

const history = {
    action: 'POP',
    go,
    goBack,
    goForward,
    push, // 只有这个方法action是`PUSH`
    listen,
    location: { pathname: window.location.pathname, state: window.location.state }
}

createBrowserHistory()
采用了 发布/订阅 模式

function createBrowserHistory() {
    let globalHistory = window.;
    let state;
    const history = {
        action: 'POP',
        go,
        goBack,
        goForward,
        push, // 只有这个方法action是`PUSH`
        listen,
        location: { pathname: window.location.pathname, state: window.location.state }
    }
    let listeners = [];//存放所有的监听函数

    function go(N) {
        globalHistory.go(N);
    }
    function goBack() {
        globalHistory.back();
    }
    function goForward() {
        globalHistory.forward();
    }
    function push(pathname, nextState) { // pathname 路径名,可能是字符串,也可能是{pathname,state}
        const action = 'PUSH';
        if (typeof pathname === 'object') {
            state = pathname.state;
            pathname = pathname.pathname;
        } else {
            state = nextState;
        }
        globalHistory.pushState(state, null, pathname);
        notify({ location: { pathname, state }, action }); // pushState 和 onpopstate 都调用notify,解决了pushState 没有被监听的问题.
    }
    window.onpopstate = () => { // 这个事件的state 历史路由栈中,以前push进去的
        let location = { pathname: window.location.pathname, state: globalHistory.state }
        notify({ location, action: 'POP' });
    }

    function notify(newState) {
        //把newState上的属性都拷贝到history上
        Object.assign(history, newState);//newState {location,action}
        history.length = globalHistory.length;
        listeners.forEach(listener => listener({ location: history.location, action: history.action }));
    }

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

路径参数的原理

<Route path="/post/:id" element={<Post />} />

Routes 内遍历children时,会拿到 Route内的path,根据path拼好正则. 正则中使用了分组

然后拿正则去匹配当前 loaction下的 pathname,查看路由是否匹配 并 拼好路径参数

匹配的结果 match对象会 作为props的一部分 传给 Post(props)

function Post(props) {
    console.log(props)
    props = {
        match: {
            params: {id:'100'},
            path: "/post/:id",
            pathname: "/post/100"
        }
    }
}

Link 实际上就是一个组件,从historyContext 拿到了history,调用了push方法

useNavigate()返回的navigate(),就是history.push()

// 使用方式 <Link to="/">首页</Link>
export function Link({ to, ...rest }) {
    let navigate = useNavigate();// navigate  history
    function handleClick(event) {
        event.preventDefault();
        navigate(to);
    }
    return (
        <a {...rest} href={to} onClick={handleClick} />
    )
}

export function useNavigate() {
    let { navigator } = React.useContext(NavigationContext);
    let navigate = React.useCallback((to) => { // 用hooks缓存起来,防止每次都创建一个新的
        navigator.push(to);
    }, [navigator]);
    return navigate; // 实际上就是 拿到context,返回history对象
}

NavLink 内部就是用了Link组件,会给使用者的className``style,传一个isActive变量代表 当前标签的路由是否激活

<NavLink
    to="/"
    end
    style={({ isActive }) => isActive ? activeStyle : {}}
    className={({ isActive }) => isActive ? 'active' : ''}
>首页</NavLink>

Navigate 是一个组件,但是不渲染任何DOM,直接拿到history,调用push. 这里是跳转在 宏任务

// 效果:放最后一个Route,没匹配到路由就跳回/
<Route path="/home" element={<Navigate to="/" />} />

export function Navigate({ to }) {
    let navigate = useNavigate();
    React.useEffect(() => {
        navigate(to);
    });
    return null;
}

文章作者: 罗紫宇
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 罗紫宇 !
  目录