Спереди написано: Зачем вам изучать исходный код react-router?Почему вы должны понимать весь процесс маршрутизации? Автор лично считает, что изучение react-router поможет нам изучить принцип перехода маршрутизации одностраничного приложения (spa), позволит нам понять весь процесс от history.push до переключения страниц компонентов, так что мы больше не будем маршрутизироваться во время интервью Страшные вопросы по теме, не будем нести чушь, а начнем глубокое путешествие в исходный код react-router.
Правильное понимание react-router
1 Понимание одностраничных приложений
Что такое одностраничное приложение?
Лично одностраничное приложение использует один html для одновременной загрузки js, css и других ресурсов, все страницы находятся под страницей-контейнером, а переключение страниц — это, по сути, переключение компонентов.
2 Предварительное изучение react-router, приоткрывающее завесу принципов маршрутизации
①react-router-domиreact-routerиhistoryКаковы отношения между тремя библиотеками?
historyможно понимать какreact-routerЯдро принципа маршрутизации также является ядром всего принципа маршрутизации, который объединяетpopState,history.pushStateДалее мы объясним принципы и методы базовой реализации маршрутизации.
react-routerможно понимать какreact-router-domядро, которое инкапсулируетRouter,Route,Switchи другие основные компоненты, которые реализуют основные функции от маршрутизации изменений до обновлений компонентов, и их нужно ввести в наш проект только один раз.react-router-domВот и все.
react-router-dom,существуетreact-routerНа основе ядраLinkкомпонентов, а в режиме историиBrowserRouterи в хэш-режимеHashRouterкомпоненты и т.д. так называемыйBrowserRouter和HashRouter,也只不过用了history库中createBrowserHistory和createHashHistory方法
react-router-dom Не будем много говорить, здесь сосредоточимся наreact-router.
②Прийти на небольшую демонстрацию, чтобы попробовать?
import { BrowserRouter as Router, Switch, Route, Redirect,Link } from 'react-router-dom'
import Detail from '../src/page/detail'
import List from '../src/page/list'
import Index from '../src/page/home/index'
const menusList = [
{
name: '首页',
path: '/index'
},
{
name: '列表',
path: '/list'
},
{
name: '详情',
path: '/detail'
},
]
const index = () => {
return <div >
<div >
<Router >
<div>{
/* link 路由跳转 */
menusList.map(router=><Link key={router.path} to={ router.path } >
<span className="routerLink" >{router.name}</span>
</Link>)
}</div>
<Switch>
<Route path={'/index'} component={Index} ></Route>
<Route path={'/list'} component={List} ></Route>
<Route path={'/detail'} component={Detail} ></Route>
{/* 路由不匹配,重定向到/index */}
<Redirect from='/*' to='/index' />
</Switch>
</Router>
</div>
</div>
}
Эффект следующий
Два основных принципа одностраничной реализации
Реализация маршрутизации одностраничных приложений заключается в переключении URL-адреса, прослушивании изменения URL-адреса, отображении различных компонентов страницы.
Основные способыhistoryузор иhashмодель.
1 Принцип режима истории
①Изменить маршрут
history.pushState
history.pushState(state,title,path)
1 state: объект состояния, связанный с указанным URL-адресом. При срабатывании события popstate объект будет передан функции обратного вызова. Может быть заполнен нулем, если не требуется.
2 title: заголовок новой страницы, но в настоящее время все браузеры игнорируют это значение и могут быть заполнены нулем.
3 path: новый URL-адрес, который должен находиться в том же домене, что и текущая страница. Адресная строка вашего браузера отобразит этот адрес.
history.replaceState
history.replaceState(state,title,path)
параметры иpushStateже, этот метод изменяет текущий history записи объектов,history.lengthДлина не меняется.
②Контролировать маршрут
popstate事件
window.addEventListener('popstate',function(e){
/* 监听改变 */
})
тот же документhistoryСрабатывает при изменении объекта popstateсобытиеhistory.pushStateАдрес браузера можно изменить без обновления страницы.Примечание ⚠️: использоватьhistory.pushState()илиhistory.replaceState()не сработаетpopstateсобытие.popstateСобытие будет запускаться только при определенных действиях браузера, таких как нажатие кнопки «Назад», «Вперед» или вызовhistory.back()、history.forward()、history.go()метод.
2 Принцип хэш-режима
①Изменить маршрут
window.location.hash
пройти черезwindow.location.hash Свойство получить и установитьhash ценность.
②Контролировать маршрут
onhashchange
window.addEventListener('hashchange',function(e){
/* 监听改变 */
})
Трое понимают библиотеку истории
react-routerМаршрутизация неотделимаhistoryБиблиотека, история фокусируется на записи состояния истории маршрутизации, и путь изменился, мы должнычто делать,
Используется в режиме историиpopstateОтслеживайте изменения маршрута, используйте в хеш-режимеhashchangeОтслеживайте изменения маршрута.
Далее мы видимBrowserв режимеcreateBrowserHistoryиHashв режимеcreateHashHistoryметод.
1 createBrowserHistory
Работа роутинга в Браузерном режиме, все начинается сcreateBrowserHistoryНачинать. Здесь мы ссылаемся на версию history-4.7.2.В последней версии могут быть некоторые отличия в апи, но принцип тот же.В процессе разбора истории мы ориентируемся наsetState ,push ,handlePopState,listenметод
const PopStateEvent = 'popstate'
const HashChangeEvent = 'hashchange'
/* 这里简化了createBrowserHistory,列出了几个核心api及其作用 */
function createBrowserHistory(){
/* 全局history */
const globalHistory = window.history
/* 处理路由转换,记录了listens信息。 */
const transitionManager = createTransitionManager()
/* 改变location对象,通知组件更新 */
const setState = () => { /* ... */ }
/* 处理当path改变后,处理popstate变化的回调函数 */
const handlePopState = () => { /* ... */ }
/* history.push方法,改变路由,通过全局对象history.pushState改变url, 通知router触发更新,替换组件 */
const push=() => { /*...*/ }
/* 底层应用事件监听器,监听popstate事件 */
const listen=()=>{ /*...*/ }
return {
push,
listen,
/* .... */
}
}
Давайте проанализируем каждый API один за другим и их предыдущие взаимодействия.
const PopStateEvent = 'popstate'
const HashChangeEvent = 'hashchange'
popstateиhashchangeЭто низкоуровневый метод отслеживания изменений маршрута.
①setState
const setState = (nextState) => {
/* 合并信息 */
Object.assign(history, nextState)
history.length = globalHistory.length
/* 通知每一个listens 路由已经发生变化 */
transitionManager.notifyListeners(
history.location,
history.action
)
}
Код прост: унифицировать каждыйtransitionManagerудалосьlistenerСостояние маршрутизации обновлено.
когда связыватьlitener, мы следующиеReact-Routerбудут введены в код.
②слушать
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)
}
}
слушать по существу проходитcheckDOMListenersпараметры1или-1связать / развязатьpopstateсобытие, при изменении маршрута вызывается обработчикhandlePopState.
Далее посмотримpushметод.
③нажать
const push = (path, state) => {
const action = 'PUSH'
/* 1 创建location对象 */
const location = createLocation(path, state, createKey(), history.location)
/* 确定是否能进行路由转换,还在确认的时候又开始了另一个转变 ,可能会造成异常 */
transitionManager.confirmTransitionTo(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
}
})
}
push ( history.push )Процесс примерноСначала сгенерируйте актуальнуюlocationобъект, а затем передатьwindow.history.pushStateМетод изменяет текущий маршрут браузера (т. е. текущий путь) и, наконец, передаетsetStateуведомление о методеReact-RouterОбновите и передайте текущий объект местоположения, поскольку на этот раз URL-адрес изменился, этоhistory.pushStateгенерируется, не запускаетсяpopStateметод, поэтому вам нужно вручнуюsetState, который запускает обновление компонента.
④handlePopState
Наконец, давайте посмотрим, когдаpopStateфункция прослушивания, когдаpathчто происходит, когда он меняется,
/* 我们简化一下handlePopState */
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)
}
})
}
handlePopState Код очень простой, судите самиaction类型为pop,ПотомsetState, перезагрузите компонент.
2 createHashHistory
Режим хеширования аналогичен API истории. Давайте сосредоточимся на том, как отслеживать маршрут в режиме хеширования, иpush , replaceКак изменение метода меняет путь.
Прослушивание изменений хэш-маршрута
const HashChangeEvent = 'hashchange'
const checkDOMListeners = (delta) => {
listenerCount += delta
if (listenerCount === 1) {
addEventListener(window, HashChangeEvent, handleHashChange)
} else if (listenerCount === 0) {
removeEventListener(window, HashChangeEvent, handleHashChange)
}
}
Как было сказано ранее, просто используйтеhashchangeдля отслеживания изменений в маршрутизации хэшей.
изменить хэш-маршрут
/* 对应 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
)
}
существуетhashРежим,history.pushНижний слой называетсяwindow.location.hrefизменить маршрут.history.replaceНижний слой не используетсяwindow.location.replaceИзмените маршрут.
Суммировать
Мы используем изображение, чтобы описать этоhistoryОбщий процесс библиотеки.
Четыре основных API
1 Router-получение изменений местоположения и распространение потоков обновлений
Routerфункция состоит в том, чтобыhistory location Подождите, пока информация о маршрутизации будет передана
Router
/* 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
}}
/>
);
}
}
Суммировать:
Инициализировать прослушивание привязки, маршрутизировать изменения, уведомлять об измененияхlocation, чтобы изменить компонент. Состояние маршрутизации истории реакции сохраняется вReact.ContentМежду контекстами обновляются состояния.
У проекта должен быть кореньRouter, чтобы создать эффект обновления перед переключением компонентов маршрутизации.
Если есть несколькоRouterЭто вызовет, это вызовет переключение маршрутов, страница не обновляется.
2 Switch - сопоставьте правильный уникальный маршрут
Визуализировать текущий компонент в соответствии с потоком обновления маршрутизатора.
/* 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>
);
}
}
Найдите компонент, соответствующий текущему пути, и визуализируйте его. пройти черезpathnameи компонентыpathсоответствовать. Найдите компонент маршрутизатора, соответствующий пути.
matchPath
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);
}
Сопоставьте совпадающие маршруты.
3 Контейнер размещения страницы компонента маршрута
/**
* 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>
);
}
}
Сопоставьте путь и визуализируйте компонент. В качестве контейнера для компонентов маршрутизации фактические компоненты могут быть отображены в соответствии с. пройти черезRouterContext.ConsumeВынуть текущий предыдущий уровеньlocation,matchи другая информация. Передано компоненту страницы в качестве реквизита. Чтобы мы могли получить его в реквизитах в компоненте страницыlocation ,matchи другая информация.
4 Перенаправление - нет подходящего маршрута, затем перенаправление
Компонент перенаправления, если входящий маршрут совпадает, он перенаправит соответствующий маршрут.
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>
);
}
При инициализации выполнить маршрутный переход, при инициализацииmountedвоплощать в жизньpushметод, когда компонент обновляется, еслиlocationне равный. также будет выполнятьhistoryПеренаправление метода.
Пять резюме + анализ процесса
Суммировать
historyПредоставляет основные API-интерфейсы, такие как маршруты мониторинга, методы изменения маршрутов и состояние состояния маршрутизации.
react-routerОбеспечьте маршрутизацию компонентов рендеринга, маршрутизацию уникальных совпадающих компонентов, перенаправление компонентов и другие функциональные компоненты.
Анализ процесса
Когда адресная строка меняет URL-адрес, что происходит с отображением обновления компонента? 😊😊😊Возьмите режим истории в качестве эталона. При изменении URL-адреса сначала запускается история, а затем вызывается прослушиватель событий.popstateсобытие, вызвать функцию обратного вызоваhandlePopState, вызвать следующую историюsetstateметод, сгенерируйте новый объект местоположения, а затем уведомите компонент Router об обновленииlocationи черезпередача контекста контекста,switchЧерез переданный поток обновления выполняется сопоставление соответствующего рендеринга компонента Route, и, наконец,Routeудаление компонентаcontextСодержимое, переданное на страницу рендеринга, отображает обновление.
когда мы звонимhistory.pushметоды, переключение маршрутов, обновление и рендеринг компонентов?
Мы по-прежнему используем режим истории в качестве ссылки, когда мы вызываемhistory.pushметод, сначала вызовите историюpushметод, черезhistory.pushStateизменить текущийurl, а затем вызвать следующую историюsetStateМетод, следующие шаги точно такие же, как описано выше, поэтому я не буду здесь вдаваться в подробности.
Мы используем диаграмму, чтобы представить взаимосвязь между различными компонентами маршрутизации.
Я надеюсь, что друзья, прочитавшие эту статью, смогут понять весь процесс react-router, а логику кода понять несложно. Я проанализировал весь процесс для всех и надеюсь, что студенты смогут взять на себя инициативу посмотреть исходный код и понять весь процесс.На бумаге в конце концов я чувствую себя мелким, и я абсолютно точно знаю, что это дело должно быть сделано.
Написано в конце, спасибо за вашу поддержку и поддержку🌹🌹🌹, если вам это нравится, вы можете поставить лайк автору и подписаться.