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')
);
- 先调用一大堆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'))
- 接下来,沿着根组件ReactDOM.render -> mount -> createDOM -> mountClassComponent的调用路径,在mountClassComponent方法中,做了下面几件事:
-
- Router的实例化
- Router的render方法的执行(返回renderVdom)
- 执行createDOM(renderVdom)生成真实DOM
- Router实例化时,其实是实例化HashRouter,HashRouter中调用createHashHistory()返回history对象,这个history对象将来会传给公共的Router组件
- 在执行Router的实例化时,会给上一步创建出来的history对象添加监听,供hash触发change事件时执行,同时在这里还会默认以当前的hash执行一次事件回调
- 紧接着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"
}
}
]
}
})
}
- 拿到上面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
\
- 渲染所有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;