React-Router 原始碼解析

weixin_34370347發表於2018-08-27

前言

本系列將會根據一個簡單的專案來學習React-Router 原始碼,要到達的目的有如下:

  1. 學會使用React-Router
  2. 在使用的基礎上,分析React-Router 原始碼結構

可以下載專案原始碼,並按照如下步驟,將專案執行起來

git clone git@github.com:bluebrid/react-router-learing.git

npm i

npm start

執行的專案只是一個簡單的React-Router 專案。

我們通過應用,一步步去解析React-Router 原始碼.

查詢入口檔案

在我們clone 專案後,我們先找到入口檔案, 也就是src/index 檔案

ReactDOM.render(
	<BrowserRouter>
		<App />
	</BrowserRouter>
	, document.getElementById('root'));
複製程式碼

其實很簡單,就是React 的入口檔案寫法,ReactDOM.render 去Render 整個頁面,但是我們發現一個問題,Render 的根元件是一個BrowserRouter 元件,檢視其import 路徑import { BrowserRouter } from './react-router/packages/react-router-dom/modules';, 發現其實React-Router 的一個元件,也就是我們學習React-Router 的入口檔案,下面我們就來分析這個元件。

BrowserRouter

檢視原始碼,發現這個元件,只是重新render 了Router元件, 但是傳了一個history 的props. history 是一個獨立的第三方庫,是實現路由的一個關鍵所在,我們後續會深入分析它.

import { createBrowserHistory as createHistory } from "../../../../history/modules";
複製程式碼
history = createHistory(this.props);
複製程式碼
  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
複製程式碼

因為React-Router 是基於history這個庫,來實現對路由變化的監聽,所以我們先對這個庫進行簡單的分析.

history(第三方庫)

  1. 我們檢視modules下面的index.js 的原始碼,可以看出history 暴露出了七個方法:
import createBrowserHistory from "./createBrowserHistory";
import createHashHistory from "./createHashHistory";
import createMemoryHistory from "./createMemoryHistory";
import { createLocation, locationsAreEqual } from "./LocationUtils";
import { parsePath, createPath } from "./PathUtils";
export {
    createBrowserHistory,
    createHashHistory,
    createMemoryHistory,
    createLocation,
    locationsAreEqual,
    parsePath,
    createPath
}
複製程式碼

我們上一節分析BrowserRouter , 其history 引用的是 createBrowserHistory 方法,所以我們接下來主要分析這個方法.

history(createBrowserHistory.js)

如果我們用VS Code 開啟原始碼,我們可以用Ctrl + k Ctrl + 0(數字0) 組合鍵,我們可以檢視這個檔案原始碼結構. 這個檔案暴露出了一個物件, 也就是我們可以用的方法:

  const history = {
    length: globalHistory.length,
    action: "POP",
    location: initialLocation,
    createHref,
    push,
    replace,
    go,
    goBack,
    goForward,
    block,
    listen
  };
複製程式碼

我們接下來我們會分析其中幾個重要的方法

listen

listen 是一個最主要的方法,在Router 元件中有引用,其是實現路由監聽的功能,也就是觀察者 模式.下面我們來分析這個方法:

  const listen = listener => {
    const unlisten = transitionManager.appendListener(listener);
    checkDOMListeners(1);

    return () => {
      checkDOMListeners(-1);
      unlisten();
    };
  };
複製程式碼

其中checkDOMListeners 方法,是真正實現了路由切換的事件監聽:

  //註冊路由監聽事件
  const checkDOMListeners = delta => {
    // debugger
    listenerCount += delta;

    if (listenerCount === 1) {
      window.addEventListener(PopStateEvent, handlePopState);

      if (needsHashChangeListener)
        window.addEventListener(HashChangeEvent, handleHashChange);
    } else if (listenerCount === 0) {
      window.removeEventListener(PopStateEvent, handlePopState);

      if (needsHashChangeListener)
        window.removeEventListener(HashChangeEvent, handleHashChange);
    }
  };
複製程式碼

其中window 監聽了兩種事件: popstatehashchange,這兩個事件都是HTML5中的API,也就是原生的監聽URL變化的事件.

分析事件監聽的回撥函式handlePopState ,其最終是聽過setState 來出發路由監聽者,

  const setState = nextState => {
    Object.assign(history, nextState);

    history.length = globalHistory.length;

    transitionManager.notifyListeners(history.location, history.action);
  };
複製程式碼

其中notifyListeners 會呼叫所有的listen 的回撥函式,從而達到通知監聽路由變化的監聽者

在下面的Router 元件的componentWillMount 生命週期中就呼叫了history.listen呼叫,從而達到當路由變化, 會去呼叫setState 方法, 從而去Render 對應的路由元件。

Router

  1. 我先檢視render 方法
  render() {
    const { children } = this.props;
    return children ? React.Children.only(children) : null;
  }
複製程式碼

很簡單,只是將chiildren 給render 出來

  1. 我們接下來分析這個元件的所有的生命週期函式
  • componentWillMount
  componentWillMount() {
  const { children, history } = this.props;
  this.unlisten = history.listen(() => {
    this.setState({
      match: this.computeMatch(history.location.pathname)
    });
  });
}
複製程式碼

在這個方法中,註冊了對history 路由變更的監聽,並且在監聽後去變更狀態

  • componentWillUnmount
  componentWillUnmount() {
  this.unlisten();
}
複製程式碼

當元件解除安裝時,登出監聽.

由此分析,Router最主要的功能就是去註冊監聽history 路由的變更,然後重新render 元件。

分析到此,我們發現跟React-Router已經斷開了聯絡,因為後面所有的事情都是去render children, 我們接下來繼續返回到index.js檔案中:

ReactDOM.render(
  <BrowserRouter>
  	<App />
  </BrowserRouter>
  , document.getElementById('root'));
複製程式碼

我們發現children 就是<App/>元件了,我們去檢視APP的程式碼,還是先檢視render

class App extends Component {

render() {

  return (
    <div>
     <nav className="navbar navbar">     
      <ul className="nav navbar-nav">
        <li><Link to="/">Homes</Link></li>
        <li><Link to="/category">Category</Link></li>
        <li><Link to="/products">Products</Link></li>
        <li><Link to="/admin">Admin area</Link></li>
      </ul>
     </nav>
    
     <Switch>
      <Route path="/login"  render={(props) => <Login {...props} />} />
      <Route exact path="/" component={Home}/>
      <Route path="/category" component={Category}/>
      <PrivateRoute path='/admin' component = {Admin} />
      <Route path="/products" component={Products}/>
     </Switch>
    </div>
  );
}
}
複製程式碼

非常顯眼的是:Switch 元件,經檢視是React-Router 的一個元件,我們接下來就分析Switch元件

Switch

  render() {
  const { route } = this.context.router;
  const { children } = this.props;
  const location = this.props.location || route.location;

  let match, child;
  React.Children.forEach(children, element => {
    if (match == null && React.isValidElement(element)) {
      const {
        path: pathProp,
        exact,
        strict,
        sensitive,
        from
      } = element.props;
      const path = pathProp || from;

      child = element;
      match = matchPath(
        location.pathname,
        { path, exact, strict, sensitive },
        route.match
      );
    }
  });
  return match
    ? React.cloneElement(child, { location, computedMatch: match })
    : null;
}
複製程式碼

其中最明顯的一塊程式碼就是: React.Children.forEach , 去遍歷Switch 下面的所有的children, 然後根據path 去匹配對應的children, 然後將匹配到的children render 出來。

而Switch 的所有的Children 是一個Route 元件,我們接下來就要分析這個元件的原始碼

Switch 的主要功能就是根據path 匹配上對應的children, 然後去Render 一個元素React.cloneElement(child, { location, computedMatch: match })

Route

從app.js 中,發現 Route 使用方式是<Route exact path="/" component={Home}/>

render() {
  const { match } = this.state;
  const { children, component, render } = this.props;
  const { history, route, staticContext } = this.context.router;
  const location = this.props.location || route.location;
  const props = { match, location, history, staticContext };
  
  if (component) return match ? React.createElement(component, props) : null;

  if (render) return match ? render(props) : null;

  if (typeof children === "function") return children(props);

  if (children && !isEmptyChildren(children))
    return React.Children.only(children);

  return null;
}
複製程式碼

從render 方法可以知道,其中有三個重要props, 決定了怎麼去render 一個路由。

  1. component (直接傳遞一個元件, 然後去render 元件)
  2. render (render 是一個方法, 通過方法去render 這個元件)
  3. children (如果children 是一個方法, 則執行這個方法, 如果只是一個子元素,則直接render 這個元素)

在render元件的時候,都會將props 傳遞給子元件

props = {match, location, history, staticContext} 這些屬性在元件中會有很大的用途

使用方式

從上面的程式碼可以發現Route的使用方式有四種:

  1. <Route exact path="/" component={Home}/> 直接傳遞一個元件
  2. <Route path="/login" render={(props) => <Login {...props} />} /> 使用render 方法
  3. <Route path="/category"> <Category/><Route/>
  4. <Route path="/category" children={(props) => <Category {...props} />} /> 跟render 使用方式一樣

props(引數)

上面我們已經分析了render 方法,我們現在需要分析props, 因為理解了render 方法,也就是知道這個元件的實現原理, 理解了props, 就會理解這個元件可以傳遞哪些屬性,從而可以達到更好的使用Route元件.

  static propTypes = {
    computedMatch: PropTypes.object, // private, from <Switch>
    path: PropTypes.string,
    exact: PropTypes.bool,
    strict: PropTypes.bool,
    sensitive: PropTypes.bool,
    component: PropTypes.func,
    render: PropTypes.func,
    children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
    location: PropTypes.object
  };
複製程式碼

下面我們一一來分析這些屬性的用途:

  1. path 很簡單,就是一個字串型別,也就是我們這個路由要匹配的的URL路徑.
  2. component, 居然是個func 型別, 我們上面分析render 方法,發現conponent 傳遞的是一個元件.其實React component 其實就是一個function .不管是Class component 或者是一個函式式元件,其實說白了都是function(typeof App === 'function')
  3. render, 上面在render 方法也已經分析,其實是通過一個function 來render 一個元件
  4. children 上面render 方法也已經分析了
  5. computedMatch 是從Switch 傳遞過來的,就是Switch 元件已經找到對應的match.這個也是Switch 元件的主要功能, 就是用Swtich 包裹所有的Route 元件,在Switch 中已經查詢到對應的Route元件了, 不用將Switch 下面的所有的Route 去Render一遍了。也是效能提升的一個方式。

上面我們已經分析了我們使用過的四個props, 我們接下來分析我們沒有使用過的幾個props,但是其實在特殊環境中是很有作用:

  1. exact 從字面意義上理解是“精確的”,也就是要精確去匹配路徑, 舉個例子:
     <Switch>
        <Route path="/login"  render={(props) => <Login {...props} />} />
        <Route path="/" component={Home}/>
        <Route path="/category" children={(props) => <Category {...props} />} />
        <PrivateRoute path='/admin' component = {Admin} />
        <Route path="/products" component={Products}/>
       </Switch>
複製程式碼

上面"/" 路徑會匹配所有的路徑如果:/login /category ...., 但是我們需要"/" 只匹配 Home , 我們需要變更如:<Route exact path="/" component={Home}/>

  1. strict 從字面上理解是“嚴格的”,也就是嚴格模式匹配,對後面的"/" 也是需要匹配上的, 如: <Route strict path="/one/" component={About}/> 只會匹配/one/ 或者/one/two 不會匹配 /one
  2. sensitive 用來設定path 匹配是否區分大小寫,如果設定了這個值,是區分大小寫的,如: <Route sensitive path="/one" component={About}/> 只會匹配/one 不會匹配/One or /ONe
  3. location
  4. computedMatch, 是一個私有的屬性,我們不會使用

Route 元件最主要的功能,是匹配URL 然後render出元件, 其中匹配的邏輯是是其核心的功能,我們後續會分析其匹配的過程,原始碼.

Link

TODO......

相關文章