摘要:到主菜了,先看它的一看,我們應該有個猜測,這貨是個高階函數。可能有點繞,但就是這么一個個高階函數組成的,后面會詳細說。定義了一個處理函數和高階函數執行次的方法,這個方法比上面的復雜在于它需要檢測參數是否訂閱了。
注意:文章很長,只想了解邏輯而不深入的,可以直接跳到總結部分。初識
首先,從它暴露對外的API開始
ReactReduxContext /* 提供了 React.createContext(null) */ Provider /* 一個儲存數據的組件,渲染了ContextProvider,內部調用redux中store.subscribe 訂閱數據,每當redux中的數據變動,比較新值與舊值,判斷是否重新渲染 */ connect /* 一個高階組件,第一階傳入對數據處理方法,第二階傳入要渲染的組件 內部處理了: 1. 對參數的檢查 2. 對傳入的數據處理方法進行處理 (沒傳怎么處理,傳了提供什么參數,傳的類型不同怎么處理,結果如何比較等等) 3. 靜態方法轉移 4. 對渲染組件的傳遞(傳遞給connectAdvanced) */ connectAdvanced /* 保存每一次執行的數據,執行connect定義的方案和邏輯,新舊數據對比(全等對比),渲染組件 這里作為公開API,如果我們去使用,那么connect里面的邏輯就需要我們自定義了。 */
現在對它的大概工作范圍有了解后,我們可以開始沿著執行順序分析。
抽絲 Provider我們使用時,當寫完了redux的reducer, action, bindActionCreators, combineReducers, createStore這一系列內容后,
我們得到了一個store
會先使用
這時,Provider組件開始工作
componentDidMount() { this._isMounted = true this.subscribe() }
第一次加載,需要執行subscribe
subscribe是什么呢,就是對redux的store執行subscribe一個自定義函數,
這樣,每當數據變動,這個函數便會執行
subscribe() { const { store } = this.props // redux 的 store 訂閱 // 訂閱后,每當state改變 則自動執行這個函數 this.unsubscribe = store.subscribe(() => { // store.getState() 獲取最新的 state const newStoreState = store.getState() // 組件未加載,取消 if (!this._isMounted) { return } // 比較state是否相等,全等的不更新 this.setState(providerState => { if (providerState.storeState === newStoreState) { return null } return { storeState: newStoreState } }) }) /* ... */ }
看到嗎,這個自定義函數非常簡單,每次收到數據,進行全等比較,不等則更新數據。
這個組件的另2個生命周期函數:
componentWillUnmount() { if (this.unsubscribe) this.unsubscribe() this._isMounted = false } componentDidUpdate(prevProps) { // 比較store是否相等,如果相等則跳過 if (this.props.store !== prevProps.store) { // 取消訂閱之前的,再訂閱現在的(因為數據(store)不同了) if (this.unsubscribe) this.unsubscribe() this.subscribe() } }
這2段的意思就是,每當數據變了,就取消上一次數據的訂閱,在訂閱本次的數據,
當要銷毀組件,取消訂閱。
一段題外話(可跳過):
這個邏輯用Hooks的useEffect簡直完美匹配!
useEffect(()=>{ subscribe() return ()=>{ unSubscribe() } },props.data)這段的意思就是,當props.data發生改變,執行unSubscribe(),再執行subscribe()。
邏輯完全一致有沒有!
最后的render:
這里Context就是React.createContext(null)
{this.props.children}
到這里我稱為react-redux的第一階段。
一個小總結,第一階段就做了1件事:
定義了Provider組件,內部訂閱了store。
connect到主菜了,先看它的export
export default createConnect()
一看,我們應該有個猜測,這貨createConnect是個高階函數。
看看它的參數吧。
export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {}) { /* ... */ }
題外話:一個編寫默認對象內部含有默認值的方法
function a({x=1,y=2}={}){} a() // x:1,y:2 a({}) // x:1,y:2 a({x:2,z:5}) //x:2,y:2
這里先說明一下它的參數,后面讀起來會很順。
connectHOC: 一個重要組件,用于執行已確定的邏輯,渲染最終組件,后面會詳細說。 mapStateToPropsFactories: 對 mapStateToProps 這個傳入的參數的類型選擇一個合適的方法。 mapDispatchToPropsFactories: 對 mapDispatchToProps 這個傳入的參數的類型選擇一個合適的方法。 mergePropsFactories: 對 mergeProps 這個傳入的參數的類型選擇一個合適的方法。 selectorFactory: 以上3個只是簡單的返回另一個合適的處理方法,它則執行這些處理方法,并且對結果定義了如何比較的邏輯。
可能有點繞,但react-redux就是這么一個個高階函數組成的,selectorFactory后面會詳細說。
首先我們再次確定這3個名字很長,實際很簡單的函數(源碼這里不放了)
mapStateToPropsFactories
mapDispatchToPropsFactories
mergePropsFactories
它們只是判斷了參數是否存在,是什么類型,并且返回一個合適的處理方法,它們并沒有任何處理邏輯。
舉個例子:
const MyComponent=connect((state)=>state.articles})
這里我只定義了mapStateToProps,并且是個function,那么mapStateToPropsFactories就會返回一個
處理function的方法。
我沒有定義mapDispatchToProps,那么mapDispatchToPropsFactories檢測不到參數,
則會提供一個默認值dispatch => ({ dispatch }),返回一個處理非function(object)的方法。
那么處理邏輯是誰定義呢?
wrapMapToPropswrapMapToProps.js這個文件內部做了以下事情:
定義了一個處理object的方法(簡單的返回即可,因為最終目的就是要object)。
定義了一個處理函數和高階函數(執行2次)的方法,這個方法比上面的復雜在于它需要檢測參數是否訂閱了ownProps。
檢測方法很簡單,就是檢查參數的length(這里dependsOnOwnProps是上一次檢查的結果,如果存在則不需要再次檢查)
export function getDependsOnOwnProps(mapToProps) { return mapToProps.dependsOnOwnProps !== null && mapToProps.dependsOnOwnProps !== undefined ? Boolean(mapToProps.dependsOnOwnProps) : mapToProps.length !== 1 }
回到connect,繼續往下看
export function createConnect({ /* 上面所講的參數 */ } = {}) { return function connect( mapStateToProps, mapDispatchToProps, mergeProps, { pure = true, areStatesEqual = strictEqual, areOwnPropsEqual = shallowEqual, areStatePropsEqual = shallowEqual, areMergedPropsEqual = shallowEqual, ...extraOptions } = {} ) { /* ... */ } }
已經到了我們傳遞參數的地方,前3個參數意思就不解釋了,最后的參數options
areStatesEqual = strictEqual, // ===比較 areOwnPropsEqual = shallowEqual, // 淺比較 areStatePropsEqual = shallowEqual, // 淺比較 areMergedPropsEqual = shallowEqual, // 淺比較
它們用在selectorFactory這個比較數據結果的方法內部。
繼續往下看
export function createConnect({ /* 上面已講 */ } = {}) { return function connect( /* 上面已講 */ ) { const initMapStateToProps = match( mapStateToProps, mapStateToPropsFactories, "mapStateToProps" ) const initMapDispatchToProps = match( mapDispatchToProps, mapDispatchToPropsFactories, "mapDispatchToProps" ) const initMergeProps = match(mergeProps, mergePropsFactories, "mergeProps")
這里定義了3個變量(函數),match的作用是什么?
以mapStateToProps舉例來說,
因為上面也說了,mapStateToPropsFactories里面有多個方法,需要找到一個適合mapStateToProps的,
match就是干這事了。
match方法內部遍歷mapStateToPropsFactories所有的處理方法,任何一個方法能夠匹配參數mapStateToProps,便被match捕獲返回,
如果一個都找不到則報錯提示參數配置錯誤。
現在這3個變量定義明確了,都是對應的參數的合適的處理方法。
至此,我們已經完成了第二階段,
做個小總結,第二階段做了哪些事:
connect接收了對參數處理方案(3個...Factories)。
connect接收了參數的結果比較方案(selectFactory)
connect接收了參數(mapStateToProps,mapDispatchToProps,mergeProps,options)。
定義了比較方案(4個are...Equal,其實就是全等比較和淺比較)。
前2個階段都是定義階段,接下來需要我們傳入自定義組件,也就是最后一個階段
connect(...)(Component)
接著看connect源碼
export function createConnect({ /* 上面已講 */ } = {}) { return function connect( /* 上面已講 */ ) { /* 上面已講 */ return connectHOC(selectorFactory, { // 方法名稱,用在錯誤提示信息 methodName: "connect", // 最終渲染的組件名稱 getDisplayName: name => `Connect(${name})`, shouldHandleStateChanges: Boolean(mapStateToProps), // 以下是傳遞給 selectFactory initMapStateToProps, initMapDispatchToProps, initMergeProps, pure, areStatesEqual, areOwnPropsEqual, areStatePropsEqual, areMergedPropsEqual, // any extra options args can override defaults of connect or connectAdvanced ...extraOptions }) } }
這里執行了connectHOC(),傳遞了上面已經講過的參數,而connectHOC = connectAdvanced
因此我們進入最后一個對外API,connectAdvanced
connectAdvancedconnectAdvanced函數,之前也提過,就是一個執行、組件渲染和組件更新的地方。
它里面沒有什么新概念,都是將我們上面講到的參數進行調用,最后根據結果進行渲染新組件。
還是從源碼開始
export default function connectAdvanced( selectorFactory, { // 執行后作用于connect這個HOC組件名稱 getDisplayName = name => `ConnectAdvanced(${name})`, // 用于錯誤提示 methodName = "connectAdvanced", // 有REMOVED標志,這里不關注 renderCountProp = undefined, // 確定connect這個HOC是否訂閱state變動,好像已經沒有用到了 shouldHandleStateChanges = true, // 有REMOVED標志,這里不關注 storeKey = "store", // 有REMOVED標志,這里不關注 withRef = false, // 是否通過 forwardRef 暴露出傳入的Component的DOM forwardRef = false, // React的createContext context = ReactReduxContext, // 其余的(比較方法,參數處理方法等)將會傳遞給上面的 selectFactory ...connectOptions } = {} ) { /* ... */ }
參數也沒什么特別的,有一個forwardRef作用就是能獲取到我們傳入的Component的DOM。
這里也不深入。
接著看
export default function connectAdvanced( /* 上面已講 */ ) { /* ...對參數的一些驗證和提示哪些參數已經作廢... */ // 定義Context const Context = context return function wrapWithConnect(WrappedComponent) { /* ...檢查 WrappedComponent 是否符合要求... */ /* ...獲取傳入的WrappedComponent的名稱... */ /* ...通過WrappedComponent的名稱計算出當前HOC的名稱... */ /* ...獲取一些上面的參數(沒有新的參數,都是之前見過的)... */ // Component就是React.Component let OuterBaseComponent = Component let FinalWrappedComponent = WrappedComponent // 是否純組件 if (pure) { OuterBaseComponent = PureComponent } /* 定義 makeDerivedPropsSelector 方法,作用后面講 */ /* 定義 makeChildElementSelector 方法,作用后面講 */ /* 定義 Connect 組件,作用后面講 */ Connect.WrappedComponent = WrappedComponent Connect.displayName = displayName /* ...如果是forWardRef 為true的情況,此處不深入... */ // 靜態方法轉換 return hoistStatics(Connect, WrappedComponent) } }
這一段特別長,因此我將不太重要的直接用注釋說明了它們在做什么,具體代碼就不放了(不重要)。
并且定義了3個新東西,makeDerivedPropsSelector,makeChildElementSelector,Connect。
先看最后一句hoistStatics就是hoist-non-react-statics,它的作用是將組件WrappedComponent的所有非React
靜態方法傳遞到Connect內部。
那么最終它還是返回了一個Connect組件。
Connect組件這個組件已經是我們寫了完整connect(...)(Component)的返回值了,所以能確定,只要調用
因此它的功能就是確定是否重復更新組件和確定到底更新什么?
看一個組件,從constructor看起
class Connect extends OuterBaseComponent { constructor(props) { super(props) /* ...提示一些無用的參數...*/ this.selectDerivedProps = makeDerivedPropsSelector() this.selectChildElement = makeChildElementSelector() this.renderWrappedComponent = this.renderWrappedComponent.bind(this) } /* ... */ }
綁定了一個方法,看名字是render的意思,先不管它。
執行了2個函數。
Connect組件還沒完,這里先放著,我們先看makeDerivedPropsSelector和makeChildElementSelector
makeDerivedPropsSelectorfunction makeDerivedPropsSelector() { // 閉包儲存上一次的執行結果 let lastProps let lastState let lastDerivedProps let lastStore let sourceSelector return function selectDerivedProps(state, props, store) { // props和state都和之前相等 直接返回上一次的結果 if (pure && lastProps === props && lastState === state) { return lastDerivedProps } // 當前store和lastStore不等,更新lastStore if (store !== lastStore) { lastStore = store // 終于調用 selectorFactory 了 sourceSelector = selectorFactory( store.dispatch, selectorFactoryOptions ) } // 更新數據 lastProps = props lastState = state // 返回的就是最終的包含所有相應的 state 和 props 的結果 const nextProps = sourceSelector(state, props) // 最終的比較 if (lastDerivedProps === nextProps) { return lastDerivedProps } lastDerivedProps = nextProps return lastDerivedProps } }
大概的說,makeDerivedPropsSelector的執行,先判斷了當前傳入的props(組件的props)和state(redux傳入的state)
跟以前的是否全等,如果全等就不需要更新了;
如果不等,則調用了高階函數selectFactory,并且獲得最終數據,最后再判斷最終數據和之前的最終數據是否全等。
為什么第一次判斷了,還要判斷第二次,而且都是===判斷?
因為第一次獲取的state是redux傳入的,是整個APP的所有數據,它們不等說明有組件更新了,但不確定是否是當前組件;
第二次比較的是當前組件的最新數據和以前數據對比。
現在,我們知道selectFactory的作用是獲取當前組件的的最新數據,深入源碼看看。
selectFactoryexport default function finalPropsSelectorFactory( // redux store的store.dispatch dispatch, // 3種已經確定了的處理方法 { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options } ) { // 返回一個針對用戶傳入的類型的解析函數 // 例如 mapStateToProps 如果是function,那么就返回proxy,proxy可以判斷是否需要ownProps,并且對高階函數的 mapStateToProps 進行2次處理, // 最終確保返回一個plainObject,否則報錯 const mapStateToProps = initMapStateToProps(dispatch, options) const mapDispatchToProps = initMapDispatchToProps(dispatch, options) const mergeProps = initMergeProps(dispatch, options) if (process.env.NODE_ENV !== "production") { verifySubselectors( mapStateToProps, mapDispatchToProps, mergeProps, options.displayName ) } const selectorFactory = options.pure ? pureFinalPropsSelectorFactory : impureFinalPropsSelectorFactory // 默認pure問題true,因此執行 pureFinalPropsSelectorFactory(...) return selectorFactory( mapStateToProps, mapDispatchToProps, mergeProps, dispatch, options ) }
參數就不說了,看注釋。
以下3個,到底返回了什么,源碼在wrapMapToProps.js,上面也說過這個文件內部做了什么事情。
const mapStateToProps = initMapStateToProps(dispatch, options) const mapDispatchToProps = initMapDispatchToProps(dispatch, options) const mergeProps = initMergeProps(dispatch, options)
這3個調用返回的一個函數,名字叫proxy,這個proxy一旦調用,
就能返回經過mapStateToProps, mapDispatchToProps, mergeProps這3個參數處理過后的數據(plainObject)。
接下來:
const selectorFactory = options.pure ? pureFinalPropsSelectorFactory : impureFinalPropsSelectorFactory // 默認pure問題true,因此執行 pureFinalPropsSelectorFactory(...) return selectorFactory( mapStateToProps, mapDispatchToProps, mergeProps, dispatch, options )
返回了selectorFactory的調用值,也就是pureFinalPropsSelectorFactory(pure默認為true)。
看pureFinalPropsSelectorFactory,它的代碼不少,但邏輯很明了,大方向就是對比數據。
這里關鍵的如何比較不列代碼,只用注釋講明白它的邏輯。
export function pureFinalPropsSelectorFactory( // 接受3個proxy方法 mapStateToProps, mapDispatchToProps, mergeProps, dispatch, // 接受3個比較方法 { areStatesEqual, areOwnPropsEqual, areStatePropsEqual } ) { /* ...定義變量保存之前的數據(閉包)... */ function handleFirstCall(firstState, firstOwnProps) { /* ...定義第一次執行數據比較的方法,也就是簡單的賦值給上面定義的閉包變量... */ } function handleNewPropsAndNewState() { /* 當state和props都有變動時的處理方法 */ } function handleNewProps() { /* 當state無變動,props有變動時的處理方法 */ } function handleNewState() { /* 當state有變動,props無變動時的處理方法 */ } // 后續數據比較的方法 function handleSubsequentCalls(nextState, nextOwnProps) { // 淺比較 const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps) // 全等比較 const stateChanged = !areStatesEqual(nextState, state) // 更新數據 state = nextState ownProps = nextOwnProps // 當發生不相等的3種情況(關鍵) if (propsChanged && stateChanged) return handleNewPropsAndNewState() if (propsChanged) return handleNewProps() if (stateChanged) return handleNewState() // 比較都相等,直接返回舊值 return mergedProps } return function pureFinalPropsSelector(nextState, nextOwnProps) { return hasRunAtLeastOnce ? handleSubsequentCalls(nextState, nextOwnProps) : handleFirstCall(nextState, nextOwnProps) } }
上面的閉包變量儲存了上一次的數據,關鍵點就是當和這一次的數據比較后,如果處理更新。
react-redux將它分為3種情況
state和props都相等。
state相等,props不等。
state不等,props相等。
第一種:state和props都相等
mapStateToProps(proxy):
不管是否訂閱ownProps,執行mapStateToProps, 因為state有變動。
mapDispatchToProps(proxy):
只有訂閱了ownProps,才會執行mapDispatchToProps,因為state變動與mapDispatchToProps無影響。
mergedProps(proxy):
必定執行,將所有結果合并。
第二種:state相等,props不等
mapStateToProps(proxy):
只有訂閱了ownProps,才會執行mapStateToProps, 因為state無變動。
mapDispatchToProps(proxy):
只有訂閱了ownProps,才會執行mapDispatchToProps,因為state變動與mapDispatchToProps無影響。
mergedProps(proxy):
必定執行,將所有結果合并。
第三種:state不等,props相等
mapStateToProps(proxy):
不管是否訂閱ownProps,執行mapStateToProps, 因為state有變動。
注意,這里結果需要淺比較判斷
因為如果沒有淺比較檢查,而兩者剛好淺比較相等,
那么最后也會認為返回一個新的props,也就是相當于重復更新了。
之所以第一個state和props都有變動的不需要淺比較檢查,
是因為如果props變了,則必須要更新組件。
mapDispatchToProps(proxy):
不會執行,因為它只關注props。
mergedProps(proxy):
只有上面淺比較不等,才會執行。
makeDerivedPropsSelector的總結:
通過閉包管理數據,并且通過淺比較和全等比較判斷是否需要更新組件數據。
makeChildElementSelectormakeChildElementSelector也是一個高階函數,儲存了之前的數據和組件,并且判斷與當前的判斷。
這里是最終渲染組件的地方,因為需要判斷一下剛才最終給出的數據是否需要去更新組件。
2個邏輯:
數據與之前不等(===),更新組件。
forWardRef屬性值與之前不等,更新組件。
否則,返回舊組件(不更新)。
繼續回到Connect組件。
之后就是render了
render() { // React的createContext const ContextToUse = this.props.context || Context return ({this.renderWrappedComponent} ) }
Context.Consumer內部必須是一個函數,這個函數的參數就是Context.Provider的value,也就是redux的store。
renderWrappedComponent最后一個函數:renderWrappedComponent
renderWrappedComponent(value) { /* ...驗證參數有效性... */ // 這里 storeState=store.getState() const { storeState, store } = value // 傳入自定義組件的props let wrapperProps = this.props let forwardedRef if (forwardRef) { wrapperProps = this.props.wrapperProps forwardedRef = this.props.forwardedRef } // 上面已經講了,返回最終數據 let derivedProps = this.selectDerivedProps( storeState, wrapperProps, store ) // 返回最終渲染的自定義組件 return this.selectChildElement(derivedProps, forwardedRef) }
總算結束了,可能有點混亂,做個總結吧。
總結我把react-redux的執行流程分為3個階段,分別對應我們的代碼編寫(搭配導圖閱讀)
一張導圖:
第一階段:
對應的用戶代碼:
執行內容有:
定義了Provider組件,這個組件內部訂閱了redux的store,保證當store發生變動,會立刻執行更新。
第二階段:
對應的用戶代碼:
connect(mapStateToProps,mapDispatchToProps,mergeProps,options)
執行內容有:
connect接收了參數(mapStateToProps,mapDispatchToProps,mergeProps,options)。
connect接收了對參數如何處理方案(3個...Factories)。
connect接收了參數的結果比較方案(selectFactory)
定義了比較方案(4個are...Equal,其實就是全等比較和淺比較)。
第三階段:
對應的用戶代碼:
let newComponent=connect(...)(Component)
執行內容有:
接受自定義組件(Component)。
創建一個Connect組件。
將Component的非React靜態方法轉移到Connect。
獲取Provider傳入的數據(redux的整個數據),利用閉包保存數據,用于和未來數據做比較。
當比較(===)有變動,執行上一階段傳入的參數,獲取當前組件真正的數據。
利用閉包保存當前組件真正的數據,用于和未來作比較。
通過全等和淺比較,處理state變動和props變動的邏輯,判斷返回新數據還是舊數據。
利用閉包保存渲染的組件,通過上面返回的最終數據,判斷需要返回新組件還是就組件。
邏輯理順了,還是很好理解的。
其中第三階段就是對外APIconnectAdvanced的執行內容。
此處查看更多前端源碼閱讀內容。
或許哪一天,我們需要設計一個專用的數據管理系統,那么就利用好connectAdvanced,
我們要做的就是編寫一個自定義第二階段的邏輯體系。
感謝閱讀!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/108708.html
摘要:不只為組件提供中的數據及擴展方法,它還為定義的組件添加了一系列事件操作,這些事件的核心點就是,然后可以在自己定義的組件內獲得。行為功能是對目的功能和有用行為的一種抽象。下一個中間件函數通常由名為的變量來表示。 redux 這個是好久之前寫的,一直忘記粘過來,里面有一些是寫作格式是我自己定義的,所以和segmentfault的markdown語法有出入,圖片也不能加載,所以原文效果可以在...
摘要:下面會從淺到深,淡淡在閱讀源碼過程中自己的理解。分拆子頁面后,每一個子頁面對應一個文件。總結上面就是最早版本的源碼,很簡潔的使用了等其目的也很簡單簡化相關生態的繁瑣邏輯參考源碼地址 ??dva的思想還是很不錯的,大大提升了開發效率,dva集成了Redux以及Redux的中間件Redux-saga,以及React-router等等。得益于Redux的狀態管理,以及Redux-saga中...
摘要:下面會從淺到深,淡淡在閱讀源碼過程中自己的理解。分拆子頁面后,每一個子頁面對應一個文件。總結上面就是最早版本的源碼,很簡潔的使用了等其目的也很簡單簡化相關生態的繁瑣邏輯參考源碼地址 ??dva的思想還是很不錯的,大大提升了開發效率,dva集成了Redux以及Redux的中間件Redux-saga,以及React-router等等。得益于Redux的狀態管理,以及Redux-saga中...
摘要:下面會從淺到深,淡淡在閱讀源碼過程中自己的理解。分拆子頁面后,每一個子頁面對應一個文件。總結上面就是最早版本的源碼,很簡潔的使用了等其目的也很簡單簡化相關生態的繁瑣邏輯參考源碼地址 ??dva的思想還是很不錯的,大大提升了開發效率,dva集成了Redux以及Redux的中間件Redux-saga,以及React-router等等。得益于Redux的狀態管理,以及Redux-saga中...
摘要:閱讀深入淺出和本書值得記錄的地方源碼第二章設計高質量的組件檢查雖然能夠在開發階段發現代碼中的問題,但是放在產品環境中就不大合適現有的就具有這個功能,可以通過安裝,但是應該確保只在發布產品代碼時使用它。 閱讀深入淺出react和redux本書值得記錄的地方 github源碼:https://github.com/mocheng/react-and-redux 第二章 設計高質量的 Rea...
閱讀 3557·2021-08-02 13:41
閱讀 2390·2019-08-30 15:56
閱讀 1519·2019-08-30 11:17
閱讀 1174·2019-08-29 15:18
閱讀 580·2019-08-29 11:10
閱讀 2671·2019-08-26 13:52
閱讀 508·2019-08-26 13:22
閱讀 2948·2019-08-23 15:41