上一篇文章我們手寫了一個Redux,但是單純的Redux隻是一個狀态機,是沒有UI呈現的,是以一般我們使用的時候都會配合一個UI庫,比如在React中使用Redux就會用到 React-Redux dispatch action state React-Redux
這個庫。這個庫的作用是将Redux的狀态機和React的UI呈現綁定在一起,當你
改變
的時候,會自動更新頁面。本文還是從它的基本使用入手來自己寫一個
,然後替換官方的NPM庫,并保持功能一緻。
本文全部代碼已經上傳GitHub,大家可以拿下來玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/react-redux
基本用法
下面這個簡單的例子是一個計數器,跑起來效果如下:
要實作這個功能,首先我們要在項目裡面添加
react-redux
庫,然後用它提供的
Provider
包裹整個
React
App的根元件:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import store from './store'
import App from './App';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
上面代碼可以看到我們還給
Provider
提供了一個參數
store
,這個參數就是Redux的
createStore
生成的
store
,我們需要調一下這個方法,然後将傳回的
store
傳進去:
import { createStore } from 'redux';
import reducer from './reducer';
let store = createStore(reducer);
export default store;
上面代碼中
createStore
的參數是一個
reducer
,是以我們還要寫個
reducer
:
const initState = {
count: 0
};
function reducer(state = initState, action) {
switch (action.type) {
case 'INCREMENT':
return {...state, count: state.count + 1};
case 'DECREMENT':
return {...state, count: state.count - 1};
case 'RESET':
return {...state, count: 0};
default:
return state;
}
}
export default reducer;
這裡的
reduce
會有一個初始
state
,裡面的
count
是
,同時他還能處理三個
action
,這三個
action
對應的是UI上的三個按鈕,可以對
state
裡面的計數進行加減和重置。到這裡其實我們
React-Redux
的接入和
Redux
資料的組織其實已經完成了,後面如果要用
Redux
裡面的資料的話,隻需要用
connect
API将對應的
state
和方法連接配接到元件裡面就行了,比如我們的計數器元件需要
count
這個狀态和加一,減一,重置這三個
action
,我們用
connect
将它連接配接進去就是這樣:
import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement, reset } from './actions';
function Counter(props) {
const {
count,
incrementHandler,
decrementHandler,
resetHandler
} = props;
return (
<>
<h3>Count: {count}</h3>
<button onClick={incrementHandler}>計數+1</button>
<button onClick={decrementHandler}>計數-1</button>
<button onClick={resetHandler}>重置</button>
</>
);
}
const mapStateToProps = (state) => {
return {
count: state.count
}
}
const mapDispatchToProps = (dispatch) => {
return {
incrementHandler: () => dispatch(increment()),
decrementHandler: () => dispatch(decrement()),
resetHandler: () => dispatch(reset()),
}
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
上面代碼可以看到
connect
是一個高階函數,他的第一階會接收
mapStateToProps
和
mapDispatchToProps
兩個參數,這兩個參數都是函數。
mapStateToProps
可以自定義需要将哪些
state
連接配接到目前元件,這些自定義的
state
可以在元件裡面通過
props
拿到。
mapDispatchToProps
方法會傳入
dispatch
函數,我們可以自定義一些方法,這些方法可以調用
dispatch
去
dispatch action
,進而觸發
state
的更新,這些自定義的方法也可以通過元件的
props
拿到,
connect
的第二階接收的參數是一個元件,我們可以猜測這個函數的作用就是将前面自定義的
state
和方法注入到這個元件裡面,同時要傳回一個新的元件給外部調用,是以
connect
其實也是一個高階元件。
到這裡我們彙總來看下我們都用到了哪些API,這些API就是我們後面要手寫的目标:
: 用來包裹根元件的元件,作用是注入
Provider
的
Redux
。
store
createStore
用來建立
Redux
的核心方法,我們另一篇文章已經手寫過了。
store
:用來将
connect
state
注入給需要的元件,傳回一個新元件,他其實是個高階元件。
dispatch
是以
React-Redux
核心其實就兩個API,而且兩個都是元件,作用還很類似,都是往元件裡面注入參數,
Provider
是往根元件注入
store
,
connect
是往需要的元件注入
state
dispatch
在手寫之前我們先來思考下,為什麼
React-Redux
要設計這兩個API,假如沒有這兩個API,隻用
Redux
可以嗎?當然是可以的!其實我們用
Redux
的目的不就是希望用它将整個應用的狀态都儲存下來,每次操作隻用
dispatch action
去更新狀态,然後UI就自動更新了嗎?那我從根元件開始,每一級都把
store
傳下去不就行了嗎?每個子元件需要讀取狀态的時候,直接用
store.getState()
就行了,更新狀态的時候就
store.dispatch
,這樣其實也能達到目的。但是,如果這樣寫,子元件如果嵌套層數很多,每一級都需要手動傳入
store
,比較醜陋,開發也比較繁瑣,而且如果某個新同學忘了傳
store
,那後面就是一連串的錯誤了。是以最好有個東西能夠将
store
全局的注入元件樹,而不需要一層層作為
props
傳遞,這個東西就是
Provider
!而且如果每個元件都獨立依賴
Redux
會破壞
React
的資料流向,這個我們後面會講到。
React的Context API
React其實提供了一個全局注入變量的API,這就是context api。假如我現在有一個需求是要給我們所有元件傳一個文字顔色的配置,我們的顔色配置在最頂級的元件上,當這個顔色改變的時候,下面所有元件都要自動應用這個顔色。那我們可以使用context api注入這個配置:
先使用 React.createContext
建立一個context
React.createContext
// 我們使用一個單獨的檔案來調用createContext
// 因為這個傳回值會被Provider和Consumer在不同的地方引用
import React from 'react';
const TestContext = React.createContext();
export default TestContext;
使用 Context.Provider
包裹根元件
Context.Provider
建立好了context,如果我們要傳遞變量給某些元件,我們需要在他們的根元件上加上
TestContext.Provider
,然後将變量作為
value
參數傳給
TestContext.Provider
import TestContext from './TestContext';
const setting = {
color: '#d89151'
}
ReactDOM.render(
<TestContext.Provider value={setting}>
<App />
</TestContext.Provider>,
document.getElementById('root')
);
Context.Consumer
接收參數
Context.Consumer
上面我們使用
Context.Provider
将參數傳遞進去了,這樣被
Context.Provider
包裹的所有子元件都可以拿到這個變量,隻是拿這個變量的時候需要使用
Context.Consumer
包裹,比如我們前面的
Counter
元件就可以拿到這個顔色了,隻需要将它傳回的
JSX
用
Context.Consumer
包裹一下就行:
// 注意要引入同一個Context
import TestContext from './TestContext';
// ... 中間省略n行代碼 ...
// 傳回的JSX用Context.Consumer包裹起來
// 注意Context.Consumer裡面是一個方法,這個方法就可以通路到context參數
// 這裡的context也就是前面Provider傳進來的setting,我們可以拿到上面的color變量
return (
<TestContext.Consumer>
{context =>
<>
<h3 style={{color:context.color}}>Count: {count}</h3>
<button onClick={incrementHandler}>計數+1</button>
<button onClick={decrementHandler}>計數-1</button>
<button onClick={resetHandler}>重置</button>
</>
}
</TestContext.Consumer>
);
上面代碼我們通過
context
傳遞了一個全局配置,可以看到我們文字顔色已經變了:
useContext
useContext
除了上面的
Context.Consumer
可以用來接收
context
參數,新版React還有
useContext
這個hook可以接收context參數,使用起來更簡單,比如上面代碼可以這樣寫:
const context = useContext(TestContext);
return (
<>
<h3 style={{color:context.color}}>Count: {count}</h3>
<button onClick={incrementHandler}>計數+1</button>
<button onClick={decrementHandler}>計數-1</button>
<button onClick={resetHandler}>重置</button>
</>
);
是以我們完全可以用
context api
來傳遞
redux store
,現在我們也可以猜測
React-Redux
Provider
其實就是包裝了
Context.Provider
,而傳遞的參數就是
redux store
,而
React-Redux
connect
HOC其實就是包裝的
Context.Consumer
或者
useContext
。我們可以按照這個思路來自己實作下
React-Redux
了。
手寫 Provider
Provider
上面說了
Provider
用了
context api
,是以我們要先建一個
context
檔案,導出需要用的
context
:
// Context.js
import React from 'react';
const ReactReduxContext = React.createContext();
export default ReactReduxContext;
這個檔案很簡單,建立一個
context
再導出就行了,對應的源碼看這裡。
然後将這個
context
應用到我們的
Provider
元件裡面:
import React from 'react';
import ReactReduxContext from './Context';
function Provider(props) {
const {store, children} = props;
// 這是要傳遞的context
const contextValue = { store };
// 傳回ReactReduxContext包裹的元件,傳入contextValue
// 裡面的内容就直接是children,我們不動他
return (
<ReactReduxContext.Provider value={contextValue}>
{children}
</ReactReduxContext.Provider>
)
}
Provider
的元件代碼也不難,直接将傳進來的
store
放到
context
上,然後直接渲染
children
就行,對應的源碼看這裡。
connect
connect
基本功能
其實
connect
才是React-Redux中最難的部分,裡面功能複雜,考慮的因素很多,想要把它搞明白我們需要一層一層的來看,首先我們實作一個隻具有基本功能的
connect
import React, { useContext } from 'react';
import ReactReduxContext from './Context';
// 第一層函數接收mapStateToProps和mapDispatchToProps
function connect(mapStateToProps, mapDispatchToProps) {
// 第二層函數是個高階元件,裡面擷取context
// 然後執行mapStateToProps和mapDispatchToProps
// 再将這個結果組合使用者的參數作為最終參數渲染WrappedComponent
// WrappedComponent就是我們使用connext包裹的自己的元件
return function connectHOC(WrappedComponent) {
function ConnectFunction(props) {
// 複制一份props到wrapperProps
const { ...wrapperProps } = props;
// 擷取context的值
const context = useContext(ReactReduxContext);
const { store } = context; // 解構出store
const state = store.getState(); // 拿到state
// 執行mapStateToProps和mapDispatchToProps
const stateProps = mapStateToProps(state);
const dispatchProps = mapDispatchToProps(store.dispatch);
// 組裝最終的props
const actualChildProps = Object.assign({}, stateProps, dispatchProps, wrapperProps);
// 渲染WrappedComponent
return <WrappedComponent {...actualChildProps}></WrappedComponent>
}
return ConnectFunction;
}
}
export default connect;
觸發更新
用上面的
Provider
connect
替換官方的
react-redux
其實已經可以渲染出頁面了,但是點選按鈕還不會有反應,因為我們雖然通過
dispatch
改變了
store
中的
state
,但是這種改變并沒有觸發我們元件的更新。之前Redux那篇文章講過,可以用
store.subscribe
來監聽
state
的變化并執行回調,我們這裡需要注冊的回調是檢查我們最終給
WrappedComponent
props
有沒有變化,如果有變化就重新渲染
ConnectFunction
,是以這裡我們需要解決兩個問題:
- 當我們
變化的時候檢查最終給到
state
的參數有沒有變化
ConnectFunction
- 如果這個參數有變化,我們需要重新渲染
ConnectFunction
檢查參數變化
要檢查參數的變化,我們需要知道上次渲染的參數和本地渲染的參數,然後拿過來比一下就知道了。為了知道上次渲染的參數,我們可以直接在
ConnectFunction
裡面使用
useRef
将上次渲染的參數記錄下來:
// 記錄上次渲染參數
const lastChildProps = useRef();
useLayoutEffect(() => {
lastChildProps.current = actualChildProps;
}, []);
注意
lastChildProps.current
是在第一次渲染結束後指派,而且需要使用
useLayoutEffect
來保證渲染後立即同步執行。
因為我們檢測參數變化是需要重新計算
actualChildProps
,計算的邏輯其實都是一樣的,我們将這塊計算邏輯抽出來,成為一個單獨的方法
childPropsSelector
function childPropsSelector(store, wrapperProps) {
const state = store.getState(); // 拿到state
// 執行mapStateToProps和mapDispatchToProps
const stateProps = mapStateToProps(state);
const dispatchProps = mapDispatchToProps(store.dispatch);
return Object.assign({}, stateProps, dispatchProps, wrapperProps);
}
然後就是注冊
store
的回調,在裡面來檢測參數是否變了,如果變了就強制更新目前元件,對比兩個對象是否相等,
React-Redux
裡面是采用的
shallowEqual
,也就是淺比較,也就是隻對比一層,如果你
mapStateToProps
傳回了好幾層結構,比如這樣:
{
stateA: {
value: 1
}
}
你去改了
stateA.value
是不會觸發重新渲染的,
React-Redux
這樣設計我想是出于性能考慮,如果是深比較,比如遞歸去比較,比較浪費性能,而且如果有循環引用還可能造成死循環。采用淺比較就需要使用者遵循這種範式,不要傳入多層結構,這點在官方文檔中也有說明。我們這裡直接抄一個它的淺比較:
// shallowEqual.js
function is(x, y) {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y
} else {
return x !== x && y !== y
}
}
export default function shallowEqual(objA, objB) {
if (is(objA, objB)) return true
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false
}
const keysA = Object.keys(objA)
const keysB = Object.keys(objB)
if (keysA.length !== keysB.length) return false
for (let i = 0; i < keysA.length; i++) {
if (
!Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false
}
}
return true
}
在回調裡面檢測參數變化:
// 注冊回調
store.subscribe(() => {
const newChildProps = childPropsSelector(store, wrapperProps);
// 如果參數變了,記錄新的值到lastChildProps上
// 并且強制更新目前元件
if(!shallowEqual(newChildProps, lastChildProps.current)) {
lastChildProps.current = newChildProps;
// 需要一個API來強制更新目前元件
}
});
強制更新
要強制更新目前元件的方法不止一個,如果你是用的
Class
元件,你可以直接
this.setState({})
,老版的
React-Redux
就是這麼幹的。但是新版
React-Redux
用hook重寫了,那我們可以用React提供的
useReducer
useState
hook,
React-Redux
源碼用了
useReducer
,為了跟他保持一緻,我也使用
useReducer
function storeStateUpdatesReducer(count) {
return count + 1;
}
// ConnectFunction裡面
function ConnectFunction(props) {
// ... 前面省略n行代碼 ...
// 使用useReducer觸發強制更新
const [
,
forceComponentUpdateDispatch
] = useReducer(storeStateUpdatesReducer, 0);
// 注冊回調
store.subscribe(() => {
const newChildProps = childPropsSelector(store, wrapperProps);
if(!shallowEqual(newChildProps, lastChildProps.current)) {
lastChildProps.current = newChildProps;
forceComponentUpdateDispatch();
}
});
// ... 後面省略n行代碼 ...
}
connect
這塊代碼主要對應的是源碼中
connectAdvanced
這個類,基本原理和結構跟我們這個都是一樣的,隻是他寫的更靈活,支援使用者傳入自定義的
childPropsSelector
和合并
stateProps, dispatchProps, wrapperProps
的方法。有興趣的朋友可以去看看他的源碼:https://github.com/reduxjs/react-redux/blob/master/src/components/connectAdvanced.js
到這裡其實已經可以用我們自己的
React-Redux
替換官方的了,計數器的功能也是支援了。但是下面還想講一下
React-Redux
是怎麼保證元件的更新順序的,因為源碼中很多代碼都是在處理這個。
保證元件更新順序
前面我們的
Counter
元件使用
connect
連接配接了
redux store
,假如他下面還有個子元件也連接配接到了
redux store
,我們就要考慮他們的回調的執行順序的問題了。我們知道React是單向資料流的,參數都是由父元件傳給子元件的,現在引入了
Redux
,即使父元件和子元件都引用了同一個變量
count
,但是子元件完全可以不從父元件拿這個參數,而是直接從
Redux
拿,這樣就打破了
React
本來的資料流向。在
父->子
這種單向資料流中,如果他們的一個公用變量變化了,肯定是父元件先更新,然後參數傳給子元件再更新,但是在
Redux
裡,資料變成了
Redux -> 父,Redux -> 子
父
與
子
完全可以根據
Redux
的資料進行獨立更新,而不能完全保證父級先更新,子級再更新的流程。是以
React-Redux
花了不少功夫來手動保證這個更新順序,
React-Redux
保證這個更新順序的方案是在
redux store
外,再單獨建立一個監聽者類
Subscription
負責處理所有的
Subscription
變化的回調
state
- 如果目前連接配接
的元件是第一個連接配接
redux
的元件,也就是說他是連接配接
redux
的根元件,他的
redux
回調直接注冊到
state
;同時建立一個
redux store
執行個體
Subscription
通過
subscription
傳遞給子級。
context
的元件不是連接配接
redux
的根元件,也就是說他上面有元件已經注冊到
redux
了,那麼他可以拿到上面通過
redux store
傳下來的
context
,源碼裡面這個變量叫
subscription
,那目前元件的更新回調就注冊到
parentSub
上。同時再建立一個
parentSub
執行個體,替代
Subscription
上的
context
,繼續往下傳,也就是說他的子元件的回調會注冊到目前
subscription
上。
subscription
- 當
變化了,根元件注冊到
state
上的回調會執行更新根元件,同時根元件需要手動執行子元件的回調,子元件回調執行會觸發子元件更新,然後子元件再執行自己
redux store
上注冊的回調,觸發孫子元件更新,孫子元件再調用注冊到自己
subscription
上的回調。。。這樣就實作了從根元件開始,一層一層更新子元件的目的,保證了
subscription
這樣的更新順序。
父->子
Subscription
類
Subscription
是以我們先建立一個
Subscription
類:
export default class Subscription {
constructor(store, parentSub) {
this.store = store
this.parentSub = parentSub
this.listeners = []; // 源碼listeners是用連結清單實作的,我這裡簡單處理,直接數組了
this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
}
// 子元件注冊回調到Subscription上
addNestedSub(listener) {
this.listeners.push(listener)
}
// 執行子元件的回調
notifyNestedSubs() {
const length = this.listeners.length;
for(let i = 0; i < length; i++) {
const callback = this.listeners[i];
callback();
}
}
// 回調函數的包裝
handleChangeWrapper() {
if (this.onStateChange) {
this.onStateChange()
}
}
// 注冊回調的函數
// 如果parentSub有值,就将回調注冊到parentSub上
// 如果parentSub沒值,那目前元件就是根元件,回調注冊到redux store上
trySubscribe() {
this.parentSub
? this.parentSub.addNestedSub(this.handleChangeWrapper)
: this.store.subscribe(this.handleChangeWrapper)
}
}
Subscription
對應的源碼看這裡。
改造 Provider
Provider
然後在我們前面自己實作的
React-Redux
裡面,我們的根元件始終是
Provider
,是以
Provider
需要執行個體化一個
Subscription
并放到
context
上,而且每次
state
更新的時候需要手動調用子元件回調,代碼改造如下:
import React, { useMemo, useEffect } from 'react';
import ReactReduxContext from './Context';
import Subscription from './Subscription';
function Provider(props) {
const {store, children} = props;
// 這是要傳遞的context
// 裡面放入store和subscription執行個體
const contextValue = useMemo(() => {
const subscription = new Subscription(store)
// 注冊回調為通知子元件,這樣就可以開始層級通知了
subscription.onStateChange = subscription.notifyNestedSubs
return {
store,
subscription
}
}, [store])
// 拿到之前的state值
const previousState = useMemo(() => store.getState(), [store])
// 每次contextValue或者previousState變化的時候
// 用notifyNestedSubs通知子元件
useEffect(() => {
const { subscription } = contextValue;
subscription.trySubscribe()
if (previousState !== store.getState()) {
subscription.notifyNestedSubs()
}
}, [contextValue, previousState])
// 傳回ReactReduxContext包裹的元件,傳入contextValue
// 裡面的内容就直接是children,我們不動他
return (
<ReactReduxContext.Provider value={contextValue}>
{children}
</ReactReduxContext.Provider>
)
}
export default Provider;
connect
connect
有了
Subscription
類,
connect
就不能直接注冊到
store
了,而是應該注冊到父級
subscription
上,更新的時候除了更新自己還要通知子元件更新。在渲染包裹的元件時,也不能直接渲染了,而是應該再次使用
Context.Provider
包裹下,傳入修改過的
contextValue
,這個
contextValue
裡面的
subscription
應該替換為自己的。改造後代碼如下:
import React, { useContext, useRef, useLayoutEffect, useReducer } from 'react';
import ReactReduxContext from './Context';
import shallowEqual from './shallowEqual';
import Subscription from './Subscription';
function storeStateUpdatesReducer(count) {
return count + 1;
}
function connect(
mapStateToProps = () => {},
mapDispatchToProps = () => {}
) {
function childPropsSelector(store, wrapperProps) {
const state = store.getState(); // 拿到state
// 執行mapStateToProps和mapDispatchToProps
const stateProps = mapStateToProps(state);
const dispatchProps = mapDispatchToProps(store.dispatch);
return Object.assign({}, stateProps, dispatchProps, wrapperProps);
}
return function connectHOC(WrappedComponent) {
function ConnectFunction(props) {
const { ...wrapperProps } = props;
const contextValue = useContext(ReactReduxContext);
const { store, subscription: parentSub } = contextValue; // 解構出store和parentSub
const actualChildProps = childPropsSelector(store, wrapperProps);
const lastChildProps = useRef();
useLayoutEffect(() => {
lastChildProps.current = actualChildProps;
}, [actualChildProps]);
const [
,
forceComponentUpdateDispatch
] = useReducer(storeStateUpdatesReducer, 0)
// 建立一個subscription執行個體
const subscription = new Subscription(store, parentSub);
// state回調抽出來成為一個方法
const checkForUpdates = () => {
const newChildProps = childPropsSelector(store, wrapperProps);
// 如果參數變了,記錄新的值到lastChildProps上
// 并且強制更新目前元件
if(!shallowEqual(newChildProps, lastChildProps.current)) {
lastChildProps.current = newChildProps;
// 需要一個API來強制更新目前元件
forceComponentUpdateDispatch();
// 然後通知子級更新
subscription.notifyNestedSubs();
}
};
// 使用subscription注冊回調
subscription.onStateChange = checkForUpdates;
subscription.trySubscribe();
// 修改傳給子級的context
// 将subscription替換為自己的
const overriddenContextValue = {
...contextValue,
subscription
}
// 渲染WrappedComponent
// 再次使用ReactReduxContext包裹,傳入修改過的context
return (
<ReactReduxContext.Provider value={overriddenContextValue}>
<WrappedComponent {...actualChildProps} />
</ReactReduxContext.Provider>
)
}
return ConnectFunction;
}
}
export default connect;
到這裡我們的
React-Redux
就完成了,跑起來的效果跟官方的效果一樣,完整代碼已經上傳GitHub了:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/react-redux
下面我們再來總結下
React-Redux
的核心原理。
總結
-
是連接配接React-Redux
React
的庫,同時使用了Redux
React
的API。Redux
-
主要是使用了React-Redux
React
context api
Redux
store
-
的作用是接收Provider
并将它放到Redux store
上傳遞下去。context
-
的作用是從connect
中選取需要的屬性傳遞給包裹的元件。Redux store
-
會自己判斷是否需要更新,判斷的依據是需要的connect
是否已經變化了。state
-
在判斷是否變化的時候使用的是淺比較,也就是隻比較一層,是以在connect
mapStateToProps
中不要反回多層嵌套的對象。mapDispatchToProps
- 為了解決父元件和子元件各自獨立依賴
,破壞了Redux
React
的更新流程,父級->子級
React-Redux
類自己管理了一套通知流程。Subscription
- 隻有連接配接到
最頂級的元件才會直接注冊到Redux
,其他子元件都會注冊到最近父元件的Redux store
執行個體上。subscription
- 通知的時候從根元件開始依次通知自己的子元件,子元件接收到通知的時候,先更新自己再通知自己的子元件。
參考資料
官方文檔:https://react-redux.js.org/
GitHub源碼:https://github.com/reduxjs/react-redux/
文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝啬你的贊和GitHub小星星,你的支援是作者持續創作的動力。
歡迎關注我的公衆号進擊的大前端第一時間擷取高品質原創~
“前端進階知識”系列文章:https://juejin.im/post/5e3ffc85518825494e2772fd
“前端進階知識”系列文章源碼GitHub位址: https://github.com/dennis-jiang/Front-End-Knowledges