Skip to content

react-router实现原理 #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wingtao opened this issue Nov 26, 2019 · 0 comments
Open

react-router实现原理 #2

wingtao opened this issue Nov 26, 2019 · 0 comments

Comments

@wingtao
Copy link
Owner

wingtao commented Nov 26, 2019

react-router实现原理

整个react-router是基于history实现的,很多方法都是进一步包装的history方法,所以先了解下history。

history

history有三种可用,分别是createBrowserHistory、createHashHistory、createMemoryHistory。三者的主要区别在于:createBrowserHistory利用的是HTML5新加的pushState、replaceState及onPopState事件;createHashHistory是用来兼容旧版浏览器;createMemoryHistory是用来在node等无dom环境的使用。其他共性的方法都封装在一个createHistory里,其他三种都是基于createHistory实现的。

简单说下createBrowserHistory用到的三种方法(这种用的最多),其实其他两种history中的方法作用都一样只是提供的方法不一样而已

pushState

参数包括obj, title, url,其中url必须同域。pushState()方法不会触发页面刷新,只是导致 History 对象发生变化,地址栏会更新URL

replaceState

用法和pushState相同,功能上差异是replaceState是替换当前的url,而pushState是增加一个新的url,当然两者都不会引起页面的刷新。如下:
A路径的页面下,
如果是replaceState(null, 'test', 'B') => 当前地址栏里就会变成B,但页面内容还是A,并且不能再回退了,因为A被替换了,相当于当前浏览记录只有B。
如果是pushState(null, 'test', 'C') => 当前地址栏里也会变成C,但页面也是A,但是能回退,回退的话就是回到A了,相当于当前页面内浏览记录是A->C.

popstate

用于当实际跳转发生时的回调,监听浏览器的前进和回退操作。

history这个库生成的history对象对外提供的api主要如下:

	{
	    transitionTo,
	    push,
	    replace,
	    go,
	    goBack,
	    goForward,
	    createKey,
	    createPath,
	    createHref,
	    createLocation,
	    listenBefore, // createBrowserHistory包装
	    listen, // createBrowserHistory包装
	    registerTransitionHook, // createBrowserHistory包装
	    unregisterTransitionHook // createBrowserHistory包装
	 }

react-router

用法

具体用法可以参考react-router文档
我们一般需要做的事如下:

  1. 静态路由配置
  2. 服务端match(当用到服务端渲染时)
  3. client端match(当用到动态加载时)
  4. Link组件跳转(当用到SPA时)

原理

可以引起url变化的方式有两种,一个是链接跳转,一个是浏览器的前进后退。react-router整个运作流程如下:

  1. Router设置,其中Router主要做的事:设置context,将router、location、history作为context参数下传(实际上动作在RouterContext里完成,RouterContext作为Router子组件出现);另一个是在Router的componentWillMount中注册listener。详述如下:

    Router基于RouterContext

     const Router = React.createClass({
     	...
     	getDefaultProps() {
     	    return {
     	    // 这里将render作为props传进Router,后续Router的render函数会调用该render函数
     	      render(props) {
     	        return <RouterContext {...props} />
     	      }
     	    }
     	  },
     	  ...
     	})
    

    生成context,下传的Context参数会在Link组件中使用

     const RouterContext = React.createClass({
     	...
     	getChildContext() {
     	    let { router, history, location } = this.props
     	    return { history, location, router }
     	  },
     	...
     })
    

    注册listener回调,该回调用会调用setState,以便url更新触发回调后重新render新的component;在注册的动作中同时会注册popState事件监听,该事件回调中会调用transitionTo(该函数完成了url的改变和执行注册的listener回调。

     // Router.js
     const Router = React.createClass({
     	componentWillMount(){
     	
     		this._unlisten = transitionManager.listen((error, state) => {
     	      this.setState(state, this.props.onUpdate)
     	    })
     	}
     })
    

    createTransitionManager.js 中listen实现,是基于history的listen

     function listen(listener) {
     	// 通过此处可以知道每次url更新导致listener回调时都会调用match函数以获取最新的state(包含component)然后再传给回调函数(即setState)使得获取到最新的component
     	return history.listen(function (location) {
     		match(location, function (error, redirectLocation, nextState) {
     			listener(null, nextState);
     		}
     	}
     }
    

    至此,路由的监听操作完成了。

  2. 触发路由改变的有两种方式,一种是Link组件点击跳转,一种是浏览器的前进后退操作。首先看Link。

    Link组件本质上还是一个a标签,但是它组织了默认的跳转操作,并在点击事件回调中执行了route.push操作,该操作本质上会一方面更新地址栏里的url(updateLocation中的pushState或者replaceState),另一方面执行所有注册的listener回调(根据第一点知道回调中会执行match获取最新component进而执行setState)来更新UL。如下:

     	const Link = React.createClass({
     		handleClick(event) {
     	    if (this.props.onClick)
     	      this.props.onClick(event)
     		// 阻止默认跳转
     	    event.preventDefault()
     	
     	    const { to, query, hash, state } = this.props
     	    const location = createLocationDescriptor(to, { query, hash, state })
     	    //执行push,该push的实现在history中
     	    this.context.router.push(location)
     		},
     		render(){
     			return <a {...props} onClick={this.handleClick} />
     		}
     	})
    

    history中push会更新url和UI

     	function push(location) {
     		// transitionTo中会通过finishTransition中的pushState或replaceState来更新URL,同时通过执行回调来获取新的state,进而更新UI
         	transitionTo(
           		createLocation(location, PUSH, createKey())
         	)
       	}
    

    另一个触发路由的方式就是浏览器的前进后退了。在第一点说过,Router中已经做了popState事件的监听了,这个就是用来监听这种浏览器的前进和后退的,当发生前进和后退时,触发popState,执行TranstionTo回调,同样会更新URL和UI。

  3. 整个路由匹配的操作都是在matchRoute里实现的,具体就不讲匹配原理了,也不是这次的重点。

到此为止,整个流程差不多就是这样了,通过这个流程也能看出如何实现SPA的了,这也就是大家为什么说SPA是基于history的新增的这几个api(pushState、replaceState)了

大概流程讲了下,但是还有很多细节的东西没讲,问题是也没看非常精细,看源码太累了;另一个遗憾的点是整个是基于react-router 2.X版本来讲的,后来的版本有了很大的变动,这点也是很伤啊。

作为一个记录作用,写的并不好,如果有想看的可以看看参考文档里的两个,写的比较精炼

参考文档

  1. react-router的实现原理
  2. 前端路由实现与 react-router 源码分析
  3. 看的react-router v2.6源码
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant