React | react-router原理

理解react-router

单页面应用(SPA single page application)

单页面应用是指只有一张Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源,公共资源(js css等)仅需加载一次,常用于PC端应用、购物等网站

react-router-dom react-router history库三者关系

history可以理解为react-router的核心,也就是整个路由的核心,里面集成了popState history.pushState等底层路由实现的原理方法

react-router可以理解为是react-router-dom的核心,里面封装了Router Route Switch等核心组件,实现了从路由的改变到组件的更新的核心功能

一般在项目中只需要引入react-router-dom即可

单页面实现核心原理

history模式原理

改变路由

  • history.pushState
1
history.pushState(state, title, path);
  1. state:一个与指定网址相关的状态对象,popState事件触发,该对象会传入回调函数,如果不需要可以填null
  2. title:新页面的标题,但是所有浏览器目前都忽略这个值,可填null
  3. path:新的网址,必须与当前页面处在同一个域,浏览器的地址栏将显示这个地址
  • history.replaceState
1
history.replaceState()

参数同上,这个方法会修改当前的history记录,history.length的长度不会改变

监听路由

  • popstate
1
2
3
window.addEventListener('popstate', function(e){
/*监听改变*/
})

popstate事件只会在浏览器某些行为下触发,比如点击后退按钮(或者在JS中调用history.back()方法),即在同一文档的history对象出现变化会触发该事件

history.pushState()或者history.replaceState()不会触发popstate事件

hash模式原理

改变路由

  • window.location.hash
1
window.location.hash

通过 window.location.hash属性获取hash值和设置hash值

监听路由

  • onhashchange
1
2
3
window.addEventListener('hashchange', function(e) {
/*监听改变*/
});

history库

react-router路由离不开history库,history专注于记录路由的history状态,以及path改变了我们应该做些什么?在history模式下用popstate监听路由变化,在hash模式下用hashchange监听路由变化

createBrowserHistory

Browser模式下路由的运行,一切都从 createBrowserHistory开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/* popstate 和 hashchangeevent用于监听路由变化的底层方法 */
const PopStateEvent = 'popstate';
const HashChangeEvent = 'hashchange';

/*这里简化了createBrowserHistory,列出了几个核心api极其作用*/
function createBrowserHistory() {
/*全局history*/
const globalHistory = window.history;
/*处理路由转换,记录listens信息*/
const transitionManager = createTransitionManage();
/*
---------------------------------------------------------------------------------------------------------------
(1)setState: 统一每个transitionManager管理的listener路由状态已经更新
---------------------------------------------------------------------------------------------------------------
*/
/*改变location对象,通知组件更新*/
const setState = (nextState) => {
/*合并信息*/
Object.assign(history, nextState);
history.length = globalHistory.length;
/*通知每一个listens路由已经发生了变化*/
transitionManager.notifyListeners(
history.location,
history.action,
);
};

/*
---------------------------------------------------------------------------------------------------------------
(4)handlePopState: 判断action类型为pop然后setState,从而新加载组件
---------------------------------------------------------------------------------------------------------------
*/
/*处理当path改变后,处理popstate变化的回调函数*/
const handlePopState = (event) => {
/* 获取当前location对象 */
const location = getDOMLocation(event.state)
const action = 'POP'

transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (ok) {
setState({ action, location })
} else {
revertPop(location)
}
})
};

/*
---------------------------------------------------------------------------------------------------------------
(3)push:
流程:
1. 生成一个最新的location对象
2. 通过window.history.pushState方法改变浏览器当前路由(当前path)
3. 通过setState方法通知react-router更新,并传递当前的location对象
(由于这次url变化是由history.pushState产生的,并不会触发popState方法,所以需要手动setState,触发组件更新)
---------------------------------------------------------------------------------------------------------------
*/

/*history.push方法,改变路由,通过全局对象history.pushState改变url,通知router触发更新,替换组件*/
const push = (path, state) => {
const action = 'PUSH';
/* 1. 创建location对象 */
const loaction = createLocation(path, state, createKey(), history.location);
/* 确定是否能进行路由跳转,还在确认的时候又开始了另一个转变,可能会造成异常 */
transitionManager.confirmTransition(location, action, getUserConfirmation, (ok) => {
if (!ok) {
return;
};
const href = createHref(location)
const { key, state } = location
if (canUseHistory) {
/* 改变 url */
globalHistory.pushState({ key, state }, null, href)
if (forceRefresh) {
window.location.href = href
} else {
/* 改变 react-router location对象, 创建更新环境 */
setState({ action, location })
}
} else {
window.location.href = href
}
});
};


/*
---------------------------------------------------------------------------------------------------------------
(2)listen:
---------------------------------------------------------------------------------------------------------------
*/
/*底层应用事件监听器,监听popstate事件*/
const listen = (listener) => {
/*添加listen*/
const unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);

return () => {
checkDOMListeners(-1);
unlisten();
};
};

/*定义checkDOMListeners*/
const checkDOMListeners = (delta) => {
listenerCount += delta;
if (listenerCount === 1) {
addEventListener(window, PopStateEvent, handlePopState);
if (needsHashChangeListener) addEventListener(window, HashChangeEvent, handleHashChange)
} else if (listenerCount === 0) {
removeEventListener(window, PopStateEvent, handlePopState);
if (needsHashChangeListener) removeEventListener(window, HashChangeEvent, handleHashChange)
};
};
/*
listen本质上是通过checkDOMListeners的参数-1或者1来绑定或者解绑popstate事件,
当路由发生变化的时候,调用处理函数handlePopState
*/


return {
push,
listen,
/*...*/
};
};

createHashHistory

监听哈希路由变化

hashchange监听

1
2
3
4
5
6
7
8
9
const HashChangeEvent = 'hashchange'
const checkDOMListeners = (delta) => {
listenerCount += delta
if (listenerCount === 1) {
addEventListener(window, HashChangeEvent, handleHashChange)
} else if (listenerCount === 0) {
removeEventListener(window, HashChangeEvent, handleHashChange)
}
}

改变哈希路由

history.push底层调用了window.location.href改变路由

history.replace底层调用了window.location.replace改变路由

1
2
3
4
5
6
7
8
9
10
11
12
/* 对应 push 方法 */
const pushHashPath = (path) =>
window.location.hash = path

/* 对应replace方法 */
const replaceHashPath = (path) => {
const hashIndex = window.location.href.indexOf('#')

window.location.replace(
window.location.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + '#' + path
)
}

总结

avatar

核心api

Router :接收location变化,派发更新流

router的作用是把history location等路由信息传递下去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/* Router 作用是把 history location 等路由信息 传递下去  */
class Router extends React.Component {
static computeRootMatch(pathname) {
return { path: '/', url: '/', params: {}, isExact: pathname === '/' };
}
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
//记录pending位置
//如果存在任何<Redirect>,则在构造函数中进行更改
//在初始渲染时。如果有,它们将在
//在子组件身上激活,我们可能会
//在安装<Router>之前获取一个新位置。
this._isMounted = false;
this._pendingLocation = null;
/* 此时的history,是history创建的history对象 */
if (!props.staticContext) {
/* 这里判断 componentDidMount 和 history.listen 执行顺序 然后把 location复制 ,防止组件重新渲染 */
this.unlisten = props.history.listen(location => {
/* 创建监听者 */
if (this._isMounted) {

this.setState({ location });
} else {
this._pendingLocation = location;
}
});
}
}
componentDidMount() {
this._isMounted = true;
if (this._pendingLocation) {
this.setState({ location: this._pendingLocation });
}
}
componentWillUnmount() {
/* 解除监听 */
if (this.unlisten) this.unlisten();
}
render() {
return (
/* 这里可以理解 react.createContext 创建一个 context上下文 ,保存router基本信息。children */
<RouterContext.Provider
children={this.props.children || null}
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
/>
);
}
}

总结:

初始化绑定listen, 路由变化,通知改变 location,改变组件。 react的history路由状态是保存在 React.Content上下文之间, 状态更新。

一个项目应该有一个根 Router , 来产生切换路由组件之前的更新作用。如果存在多个 Router会造成,会造成切换路由,页面不更新的情况。

Switch: 匹配正确的唯一的路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

/* switch组件 */
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{/* 含有 history location 对象的 context */}
{context => {
invariant(context, 'You should not use <Switch> outside a <Router>');
const location = this.props.location || context.location;
let element, match;
//我们使用React.Children.forEach而不是React.Children.toArray().find()
//这里是因为toArray向所有子元素添加了键,我们不希望
//为呈现相同的两个<Route>s触发卸载/重新装载
//组件位于不同的URL。
//这里只需然第一个 含有 match === null 的组件
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
// 子组件 也就是 获取 Route中的 path 或者 rediect 的 from
const path = child.props.path || child.props.from;
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});
return match
? React.cloneElement(element, { location, computedMatch: match })
: null;
}}
</RouterContext.Consumer>
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function matchPath(pathname, options = {}) {
if (typeof options === "string" || Array.isArray(options)) {
options = { path: options };
}

const { path, exact = false, strict = false, sensitive = false } = options;

const paths = [].concat(path);

return paths.reduce((matched, path) => {
if (!path && path !== "") return null;
if (matched) return matched;

const { regexp, keys } = compilePath(path, {
end: exact,
strict,
sensitive
});
const match = regexp.exec(pathname);
/* 匹配不成功,返回null */
if (!match) return null;

const [url, ...values] = match;
const isExact = pathname === url;

if (exact && !isExact) return null;

return {
path, // the path used to match
url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
isExact, // whether or not we matched exactly
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
}, null);
}

Route:组件页面承载容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* The public API for matching a single path and rendering.
*/
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
/* router / route 会给予警告警告 */
invariant(context, "You should not use <Route> outside a <Router>");
// computedMatch 为 经过 swich处理后的 path
const location = this.props.location || context.location;
const match = this.props.computedMatch
? this.props.computedMatch // <Switch> already computed the match for us
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;
const props = { ...context, location, match };
let { children, component, render } = this.props;

if (Array.isArray(children) && children.length === 0) {
children = null;
}

return (
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}

Redirect:重定向

  1. 初始化页面跳转
  2. 组件更新的时候location不相等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function Redirect({ computedMatch, to, push = false }) {
return (
<RouterContext.Consumer>
{context => {
const { history, staticContext } = context;
/* method就是路由跳转方法。 */
const method = push ? history.push : history.replace;
/* 找到符合match的location ,格式化location */
const location = createLocation(
computedMatch
? typeof to === 'string'
? generatePath(to, computedMatch.params)
: {
...to,
pathname: generatePath(to.pathname, computedMatch.params)
}
: to
)
/* 初始化的时候进行路由跳转,当初始化的时候,mounted执行push方法,当组件更新的时候,如果location不相等。同样会执行history方法重定向 */
return (
<Lifecycle
onMount={() => {
method(location);
}}
onUpdate={(self, prevProps) => {
const prevLocation = createLocation(prevProps.to);
if (
!locationsAreEqual(prevLocation, {
...location,
key: prevLocation.key
})
) {
method(location);
}
}}
to={to}
/>
);
}}
</RouterContext.Consumer>
);
};

总结

  1. history提供了核心api,如监听路由,更改路由的方法,已经保存路由状态state
  2. react-router提供路由渲染组件,路由唯一性匹配组件,重定向组件等功能组件

流程分析:

1. 当地址栏改变url,组件的更新渲染都经历了什么?

拿history模式做参考。当url改变,首先触发histoy,调用事件监听 popstate事件, 触发回调函数 handlePopState,触发history下面的 setstate方法,产生新的location对象,然后通知Router组件更新 location并通过context上下文传递,switch通过传递的更新流,匹配出符合的Route组件渲染,最后有 Route组件取出 context内容,传递给渲染页面,渲染更新

2.当我们调用 history.push方法,切换路由,组件的更新渲染又都经历了什么呢?

当我们调用 history.push方法,首先调用history的 push方法,通过 history.pushState来改变当前 url,接下来触发history下面的 setState方法,接下来的步骤就和上面一模一样了

各个路由组件之间的关系

avatar