<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【React】717- 從零實(shí)現(xiàn) React-Redux

          共 21258字,需瀏覽 43分鐘

           ·

          2020-09-15 19:59



          1. 前言

          在 React 誕生之初,F(xiàn)acebook 宣傳這是一個(gè)用于前端開發(fā)的界面庫,僅僅是一個(gè) View 層。前面我們也介紹過 React 的組件通信,在大型應(yīng)用中,處理好 React 組件通信和狀態(tài)管理就顯得非常重要。為了解決這一問題,F(xiàn)acebook 最先提出了單向數(shù)據(jù)流的 Flux 架構(gòu),彌補(bǔ)了使用 React 開發(fā)大型網(wǎng)站的不足。

          Flux:

          隨后,Dan Abramov 受到 Flux 和函數(shù)式編程語言 Elm 啟發(fā),開發(fā)了 Redux 這個(gè)狀態(tài)管理庫。Redux 源碼非常精簡,實(shí)現(xiàn)也很巧妙,這篇文章將帶你從零手寫一個(gè) Redux 和 react-redux 庫,以及告訴你該如何設(shè)計(jì) Redux 中的 store。在開始前,我已經(jīng)將這篇文章的完整代碼都整理到 GitHub 上,大家可以參考一下。

          Redux:simple-redux

          React-redux:simple-react-redux

          2. 狀態(tài)管理

          2.1 理解數(shù)據(jù)驅(qū)動(dòng)

          在開始講解狀態(tài)管理前,我們先來了解一下現(xiàn)代前端框架都做了些什么。以 Vue 為例子,在剛開始的時(shí)候,Vue 官網(wǎng)首頁寫的賣點(diǎn)是數(shù)據(jù)驅(qū)動(dòng)、組件化、MVVM 等等(現(xiàn)在首頁已經(jīng)改版了)。那么數(shù)據(jù)驅(qū)動(dòng)的意思是什么呢?不管是原生 JS 還是 jQuery,他們都是通過直接修改 DOM 的形式來實(shí)現(xiàn)頁面刷新的。而 Vue/React 之類的框架不是粗暴地直接修改 DOM,而是通過修改 data/state 中的數(shù)據(jù),實(shí)現(xiàn)了組件的重新渲染。也就是說,他們封裝了從數(shù)據(jù)變化到組件渲染這一個(gè)過程。

          原本我們用 jQuery 開發(fā)應(yīng)用,除了要實(shí)現(xiàn)業(yè)務(wù)邏輯,還要操作 DOM 來手動(dòng)實(shí)現(xiàn)頁面的更新。尤其是涉及到渲染列表的時(shí)候,更新起來非常麻煩。

          var ul = document.getElementById("todo-list");$.each(todos, function(index, todo) {    var li = document.createElement('li');    li.innerHTML = todo.content;    li.dataset.id = todo.id;    li.className = "todo-item";    ul.appendChild(li);})

          所以后來出現(xiàn)了 jQuery.tpl 和 Underscore.template 之類的模板,這些讓操作 DOM 變得容易起來,有了數(shù)據(jù)驅(qū)動(dòng)和組件化的雛形,可惜我們還是要手動(dòng)去渲染一遍。

          如果說用純原生 JS 或者 jQuery 開發(fā)頁面是原始農(nóng)耕時(shí)代,那么 React/Vue 等現(xiàn)代化框架則是自動(dòng)化的時(shí)代。有了前端框架之后,我們不需要再去關(guān)注怎么生成和修改 DOM,只需要關(guān)心頁面上的這些數(shù)據(jù)以及流動(dòng)。所以如何管理好這些數(shù)據(jù)流動(dòng)就成了重中之重,這也是我們常說的“狀態(tài)管理”。

          2.2 什么狀態(tài)需要管理?

          前面講了很多例子,可狀態(tài)管理到底要管理什么呢?在我看來,狀態(tài)管理的一般就是這兩種數(shù)據(jù)。

          1. Domain State Domain State 就是服務(wù)端的狀態(tài),這個(gè)一般是指通過網(wǎng)絡(luò)請求來從服務(wù)端獲取到的數(shù)據(jù),比如列表數(shù)據(jù),通常是為了和服務(wù)端數(shù)據(jù)保持一致。
          {    "data": {        "hotels": [            {                "id": "31231231",                "name": "希爾頓",                "price": "1300"            }        ]    }}
          1. UI State UI State 常常和交互相關(guān)。例如模態(tài)框的開關(guān)狀態(tài)、頁面的 loading 狀態(tài)、單(多)選項(xiàng)的選中狀態(tài)等等,這些狀態(tài)常常分散在不同的組件里面。
          {    "isLoading": true,    "isShowModal": false,    "isSelected": false}

          2.3 全局狀態(tài)管理

          我們用 React 寫組件的時(shí)候,如果需要涉及到兄弟組件通信,經(jīng)常需要將狀態(tài)提升到兩者父組件里面。一旦這種組件通信多了起來,數(shù)據(jù)管理就是個(gè)問題。結(jié)合上面的例子,如果想要對應(yīng)用的數(shù)據(jù)流進(jìn)行管理,那是不是可以將所有的狀態(tài)放到頂層組件中呢?將數(shù)據(jù)按照功能或者組件來劃分,將多個(gè)組件共享的數(shù)據(jù)單獨(dú)放置,這樣就形成了一個(gè)大的樹形 store。這里更建議按照功能來劃分。

          這個(gè)大的 store 可以放到頂層組件中維護(hù),也可以放到頂層組件之外來維護(hù),這個(gè)頂層組件我們一般稱之為“容器組件”。容器組件可以將組件依賴的數(shù)據(jù)以及修改數(shù)據(jù)的方法一層層傳給子組件。我們可以將容器組件的 state 按照組件來劃分,現(xiàn)在這個(gè) state 就是整個(gè)應(yīng)用的 store。將修改 state 的方法放到 actions 里面,按照和 state 一樣的結(jié)構(gòu)來組織,最后將其傳入各自對應(yīng)的子組件中。

          class App extends Component {    constructor(props) {        this.state = {            common: {},            headerProps: {},            bodyProps: {                sidebarProps: {},                cardProps: {},                tableProps: {},                modalProps: {}            },            footerProps: {}        }        this.actions = {            header: {                changeHeaderProps: this.changeHeaderProps            },            footer: {                changeFooterProps: this.changeFooterProps            },            body: {                sidebar: {                    changeSiderbarProps: this.changeSiderbarProps                }            }        }    }        changeHeaderProps(props) {        this.setState({            headerProps: props        })    }    changeFooterProps() {}    changeSiderbarProps() {}    ...        render() {        const {            headerProps,            bodyProps,            footerProps        } = this.state;        const {            header,            body,            footer        } = this.actions;        return (            
          ) }}

          我們可以看到,這種方式可以很完美地解決子組件之間的通信問題。只需要修改對應(yīng)的 state 就行了,App 組件會(huì)在 state 變化后重新渲染,子組件接收新的 props 后也跟著渲染。

          這種模式還可以繼續(xù)做一些優(yōu)化,比如結(jié)合 Context 來實(shí)現(xiàn)向深層子組件傳遞數(shù)據(jù)。

          const Context = createContext(null);class App extends Component {    ...    render() {        return (            
          ) }}const Header = () => { // 獲取到 Context 數(shù)據(jù) const context = useContext(Context);}

          如果你已經(jīng)接觸過 Redux 這個(gè)狀態(tài)管理庫,你會(huì)驚奇地發(fā)現(xiàn),如果我們把 App 組件中的 state 移到外面,這不就是 Redux 了嗎?沒錯(cuò),Redux 的核心原理也是這樣,在組件外部維護(hù)一個(gè) store,在 store 修改的時(shí)候會(huì)通知所有被 connect 包裹的組件進(jìn)行更新。這個(gè)例子可以看做 Redux 的一個(gè)雛形。

          3. 實(shí)現(xiàn)一個(gè) Redux

          根據(jù)前面的介紹我們已經(jīng)知道了,Redux 是一個(gè)狀態(tài)管理庫,它并非綁定于 React 使用,你還可以將其和其他框架甚至原生 JS 一起使用,比如這篇文章:如何在非 React 項(xiàng)目中使用 Redux (https://segmentfault.com/a/1190000009963395)

          Redux 工作原理:

          在學(xué)習(xí) Redux 之前需要先理解其工作原理,一般來說流程是這樣的:

          1. 用戶觸發(fā)頁面上的某種操作,通過 dispatch 發(fā)送一個(gè) action。
          2. Redux 接收到這個(gè) action 后通過 reducer 函數(shù)獲取到下一個(gè)狀態(tài)。
          3. 將新狀態(tài)更新進(jìn) store,store 更新后通知頁面重新渲染。

          從這個(gè)流程中不難看出,Redux 的核心就是一個(gè)?發(fā)布-訂閱?模式。一旦 store 發(fā)生了變化就會(huì)通知所有的訂閱者,view 接收到通知之后會(huì)進(jìn)行重新渲染。

          Redux 有三大原則:

          • 單一數(shù)據(jù)源

            前面的那個(gè)例子,最終將所有的狀態(tài)放到了頂層組件的 state 中,這個(gè) state 形成了一棵狀態(tài)樹。在 Redux 中,這個(gè) state 則是 store,一個(gè)應(yīng)用中一般只有一個(gè) store。

          • State 是只讀的

            在 Redux 中,唯一改變 state 的方法是觸發(fā) action,action 描述了這次修改行為的相關(guān)信息。只允許通過 action 修改可以使應(yīng)用中的每個(gè)狀態(tài)修改都很清晰,便于后期的調(diào)試和回放。

          • 通過純函數(shù)來修改

            為了描述 action 使?fàn)顟B(tài)如何修改,需要你編寫 reducer 函數(shù)來修改狀態(tài)。reducer 函數(shù)接收前一次的 state 和 action,返回新的 state。無論被調(diào)用多少次,只要傳入相同的 state 和 action,那么就一定返回同樣的結(jié)果。

          關(guān)于 Redux 的用法,這里不做詳細(xì)講解,建議參考阮一峰老師的《Redux 入門》系列的教程:Redux 入門教程

          3.1 實(shí)現(xiàn) store

          在 Redux 中,store 一般通過 createStore 來創(chuàng)建。

          import { createStore } from 'redux'; const store = createStore(rootReducer, initalStore, middleware);

          先看一下 Redux 中暴露出來的幾個(gè)方法。

          其中 createStore 返回的方法主要有?subscribe、dispatchreplaceReducer、getState。

          createStore?接收三個(gè)參數(shù),分別是 reducers 函數(shù)、初始值 initalStore、中間件 middleware。

          store?上掛載了?getState、dispatch、subscribe?三個(gè)方法。

          getState?是獲取到 store 的方法,可以通過?store.getState()?獲取到?store。

          dispatch?是發(fā)送 action 的方法,它接收一個(gè) action 對象,通知?store?去執(zhí)行 reducer 函數(shù)。

          subscribe?則是一個(gè)監(jiān)聽方法,它可以監(jiān)聽到?store?的變化,所以可以通過?subscribe?將 Redux 和其他框架結(jié)合起來。

          replaceReducer?用來異步注入 reducer 的方法,可以傳入新的 reducer 來代替當(dāng)前的 reducer。

          3.2 實(shí)現(xiàn) getState

          store 的實(shí)現(xiàn)原理比較簡單,就是根據(jù)傳入的初始值來創(chuàng)建一個(gè)對象。利用閉包的特性來保留這個(gè) store,允許通過 getState 來獲取到 store。之所以通過 getState 來獲取 store 是為了獲取到當(dāng)前 store 的快照,這樣便于打印日志以對比前后兩次 store 變化,方便調(diào)試。

          const createStore = (reducers, initialState, enhancer) => {    let store = initialState;    const getState = () => store;    return {        getState    }}

          當(dāng)然,現(xiàn)在這個(gè) store 實(shí)現(xiàn)的比較簡單,畢竟 createStore 還有兩個(gè)參數(shù)沒用到呢。先別急,這倆參數(shù)后面會(huì)用到的。

          3.3 實(shí)現(xiàn) subscribe && unsubscribe

          既然 Redux 本質(zhì)上是一個(gè)?發(fā)布-訂閱?模式,那么就一定會(huì)有一個(gè)監(jiān)聽方法,類似 jQuery 中的?$.on,在 Redux 中提供了監(jiān)聽和解除監(jiān)聽的兩個(gè)方法。實(shí)現(xiàn)方式也比較簡單,使用一個(gè)數(shù)組來保存所有監(jiān)聽的方法。

          const createStore = (...) => {    ...    let listeners = [];    const subscribe = (listener) => {        listeners.push(listener);    }    const unsubscribe = (listener) => {        const index = listeners.indexOf(listener)        listeners.splice(index, 1)    }}

          3.4 實(shí)現(xiàn) dispatch

          dispatch 和 action 是息息相關(guān)的,只有通過 dispatch 才能發(fā)送 action。而發(fā)送 action 之后才會(huì)執(zhí)行 subscribe 監(jiān)聽到的那些方法。所以 dispatch 做的事情就是將 action 傳給 reducer 函數(shù),將執(zhí)行后的結(jié)果設(shè)置為新的 store,然后執(zhí)行 listeners 中的方法。

          const createStore = (reducers, initialState) => {    ...    let store = initialState;    const dispatch = (action) => {        store = reducers(store, action);        listeners.forEach(listener => listener())    }}

          這樣就行了嗎?當(dāng)然還不夠。如果有多個(gè) action 同時(shí)發(fā)送,這樣很難說清楚最后的 store 到底是什么樣的,所以需要加鎖。在 Redux 中 dispatch 執(zhí)行后的返回值也是當(dāng)前的 action。

          const createStore = (reducers, initialState) => {    ...    let store = initialState;    let isDispatch = false;    const dispatch = (action) => {        if (isDispatch) return action        // dispatch必須一個(gè)個(gè)來        isDispatch = true        store = reducers(store, action);        isDispatch = false        listeners.forEach(listener => listener())        return action;    }}

          至此為止,Redux 工作流程的原理就已經(jīng)實(shí)現(xiàn)了。但你可能還會(huì)有很多疑問,如果沒有傳 initialState,那么 store 的默認(rèn)值是什么呢?如果傳入了中間件,那么又是什么工作原理呢?

          3.5 實(shí)現(xiàn) combineReducers

          在剛開始接觸 Redux 的 store 的時(shí)候,我們都會(huì)有一種疑問,store 的結(jié)構(gòu)究竟是怎么定的?combineReducers 會(huì)揭開這個(gè)謎底?,F(xiàn)在來分析 createStore 接收的第一個(gè)參數(shù),這個(gè)參數(shù)有兩種形式,一種直接是一個(gè) reducer 函數(shù),另一個(gè)是用 combineReducers 把多個(gè) reducer 函數(shù)合并到一起。

          可以猜測 combineReducers 是一個(gè)高階函數(shù),接收一個(gè)對象作為參數(shù),返回了一個(gè)新的函數(shù)。這個(gè)新的函數(shù)應(yīng)當(dāng)和普通的 reducer 函數(shù)傳參保持一致。

          const combineReducers = (reducers) => {    return function combination(state = {}, action) {    }}

          那么 combineReducers 做了什么工作呢?主要是下面幾步:

          1. 收集所有傳入的 reducer 函數(shù)
          2. 在 dispatch 中執(zhí)行 combination 函數(shù)時(shí),遍歷執(zhí)行所有 reducer 函數(shù)。如果某個(gè) reducer 函數(shù)返回了新的 state,那么 combination 就返回這個(gè) state,否則就返回傳入的 state。
          const combineReducers = reducers => {    const finalReducers = {},        nativeKeys = Object.keys    // 收集所有的 reducer 函數(shù)    nativeKeys(reducers).forEach(reducerKey => {        if(typeof reducers[reducerKey] === "function") {            finalReducers[reducerKey] = reducers[reducerKey]        }    })    return function combination(state, action) {        let hasChanged = false;        const store = {};        // 遍歷執(zhí)行 reducer 函數(shù)        nativeKeys(finalReducers).forEach(key => {            const reducer = finalReducers[key];            // 很明顯,store 的 key 來源于 reducers 的 key 值            const nextState = reducer(state[key], action)            store[key] = nextState            hasChanged = hasChanged || nextState !== state[key];        })        return hasChanged ? nextState : state;    }}

          細(xì)心的童鞋一定會(huì)發(fā)現(xiàn),每次調(diào)用 dispatch 都會(huì)執(zhí)行這個(gè) combination 的話,那豈不是不管我發(fā)送什么類型的 action,所有的 reducer 函數(shù)都會(huì)被執(zhí)行一遍?如果 reducer 函數(shù)很多,那這個(gè)執(zhí)行效率不會(huì)很低嗎?但不執(zhí)行貌似又無法完全匹配到?switch...case?中的?action.type。如果能通過鍵值對的形式來匹配?action.type?和 reducer 是不是效率更高一些?類似這樣:

          // reduxconst count = (state = 0, action) => {    switch(action.type) {        case 'increment':            return state + action.payload;        case 'decrement':            return state - action.payload;        default:            return state;    }}// 改進(jìn)后的const count = {    state: 0, // 初始 state    reducers: {        increment: (state, payload) => state + payload,        decrement: (state, payload) => state - payload    }}

          這樣每次發(fā)送新的 action 的時(shí)候,可以直接用?reducers?下面的 key 值來匹配了,無需進(jìn)行暴力的遍歷。天啊,你實(shí)在太聰明了。小聲告訴你,社區(qū)中一些類 Redux 的方案就是這樣做的。以 rematch 和 relite 為例:rematch:

          import { init, dispatch } from "@rematch/core";import delay from "./makeMeWait";
          const count = { state: 0, reducers: { increment: (state, payload) => state + payload, decrement: (state, payload) => state - payload }, effects: { async incrementAsync(payload) { await delay(); this.increment(payload); } }};
          const store = init({ models: { count }});
          dispatch.count.incrementAsync(1);

          relite:

          const increment = (state, payload) => {    state.count = state.count + payload;    return state;}const decrement = (state, payload) => {    state.count = state.count - payload;    return state;}

          3.6 中間件 和 Store Enhancer

          考慮到這樣的情況,我想要打印每次 action 的相關(guān)信息以及 store 前后的變化,那我只能到每個(gè) dispatch 處手動(dòng)打印信息,這樣繁瑣且重復(fù)。createStore 中提供的第三個(gè)參數(shù),可以實(shí)現(xiàn)對 dispatch 函數(shù)的增強(qiáng),我們稱之為?Store Enhancer。?Store Enhancer?是一個(gè)高階函數(shù),它的結(jié)構(gòu)一般是這樣的:

          const enhancer = () => {    return (createStore) => (reducer, initState, enhancer) => {        ...    }}

          enhancer?接收 createStore 作為參數(shù),最后返回的是一個(gè)加強(qiáng)版的?store,本質(zhì)上是對 dispatch 函數(shù)進(jìn)行了擴(kuò)展。logger:

          const logger = () => {    return (createStore) => (reducer, initState, enhancer) => {        const store = createStore(reducer, initState, enhancer);        const dispatch = (action) => {            console.log(`action=${JSON.stringify(action)}`);            const result = store.dispatch(action);            const state = store.getState();            console.log(`state=${JSON.stringify(state)}`);            return result;        }        return {            ...state,            dispatch        }    }}

          createStore 中如何使用呢?一般在參數(shù)的時(shí)候,會(huì)直接返回。

          const createStore = (reducer, initialState, enhancer) => {    if (enhancer && typeof enhancer === "function") {        return enhancer(createStore)(reducer, initialState)    }}

          如果你有看過 applyMiddleware 的源碼,會(huì)發(fā)現(xiàn)這兩者實(shí)現(xiàn)方式很相似。applyMiddleware 本質(zhì)上就是一個(gè)?Store Enhancer。

          3.7 實(shí)現(xiàn) applyMiddleware

          在創(chuàng)建 store 的時(shí)候,經(jīng)常會(huì)使用很多中間件,通過 applyMiddleware 將多個(gè)中間件注入到 store 之中。

          const store = createStore(reducers, initialStore, applyMiddleware(thunk, logger, reselect));

          applyMiddleware 的實(shí)現(xiàn)類似上面的?Store Enhancer。由于多個(gè)中間件可以串行使用,因此最終會(huì)像洋蔥模型一樣,action 傳遞需要經(jīng)過一個(gè)個(gè)中間件處理,所以中間件做的事情就是增強(qiáng) dispatch 的能力,將 action 傳遞給下一個(gè)中間件。那么關(guān)鍵就是將新的 store 和 dispatch 函數(shù)傳給下一個(gè)中間件。

          來看一下 applyMiddleware 的源碼實(shí)現(xiàn):

          const applyMiddleware = (...middlewares) => {    return (createStore) => (reducer, initState, enhancer) => {        const store = createStore(reducer, initState, enhancer)        const middlewareAPI = {            getState: store.getState,            dispatch: (action) => dispatch(action)        }        let chain = middlewares.map(middleware => middleware(middlewareAPI))        store.dispatch = compose(...chain)(store.dispatch)        return {          ...store,          dispatch        }      }}

          這里用到了一個(gè) compose 函數(shù),compose 函數(shù)類似管道,可以將多個(gè)函數(shù)組合起來。compose(m1, m2)(dispatch)?等價(jià)于?m1(m2(dispatch))。使用 reduce 函數(shù)可以實(shí)現(xiàn)函數(shù)組合。

          const compose = (...funcs) => {    if (!funcs) {        return args => args    }    if (funcs.length === 1) {        return funcs[0]    }    return funcs.reduce((f1, f2) => (...args) => f1(f2(...args)))}

          再來看一下 redux-logger 中間件的精簡實(shí)現(xiàn),會(huì)發(fā)現(xiàn)兩者恰好能匹配到一起。

          function logger(middlewareAPI) {  return function (next) { // next 即 dispatch    return function (action) {      console.log('dispatch 前:', middlewareAPI.getState());      var returnValue = next(action);      console.log('dispatch 后:', middlewareAPI.getState(), '\n');      return returnValue;    };  };}

          至此為止,Redux 的基本原理就很清晰了,最后整理一個(gè)精簡版的 Redux 源碼實(shí)現(xiàn)。

          // 這里需要對參數(shù)為0或1的情況進(jìn)行判斷const compose = (...funcs) => {    if (!funcs) {        return args => args    }    if (funcs.length === 1) {        return funcs[0]    }    return funcs.reduce((f1, f2) => (...args) => f1(f2(...args)))}
          const bindActionCreator = (action, dispatch) => { return (...args) => dispatch(action(...args))}
          const createStore = (reducer, initState, enhancer) => { if (!enhancer && typeof initState === "function") { enhancer = initState initState = null } if (enhancer && typeof enhancer === "function") { return enhancer(createStore)(reducer, initState) } let store = initState, listeners = [], isDispatch = false; const getState = () => store const dispatch = (action) => { if (isDispatch) return action // dispatch必須一個(gè)個(gè)來 isDispatch = true store = reducer(store, action) isDispatch = false listeners.forEach(listener => listener()) return action } const subscribe = (listener) => { if (typeof listener === "function") { listeners.push(listener) } return () => unsubscribe(listener) } const unsubscribe = (listener) => { const index = listeners.indexOf(listener) listeners.splice(index, 1) } return { getState, dispatch, subscribe, unsubscribe }}
          const applyMiddleware = (...middlewares) => { return (createStore) => (reducer, initState, enhancer) => { const store = createStore(reducer, initState, enhancer); const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } let chain = middlewares.map(middleware => middleware(middlewareAPI)) store.dispatch = compose(...chain)(store.dispatch) return { ...store } }}
          const combineReducers = reducers => { const finalReducers = {}, nativeKeys = Object.keys nativeKeys(reducers).forEach(reducerKey => { if(typeof reducers[reducerKey] === "function") { finalReducers[reducerKey] = reducers[reducerKey] } }) return (state, action) => { const store = {} nativeKeys(finalReducers).forEach(key => { const reducer = finalReducers[key] const nextState = reducer(state[key], action) store[key] = nextState }) return store }}

          4. 實(shí)現(xiàn)一個(gè) react-redux

          如果想要將 Redux 結(jié)合 React 使用的話,通??梢允褂?react-redux 這個(gè)庫。看過前面 Redux 的原理后,相信你也知道 react-redux 是如何實(shí)現(xiàn)的了吧。react-redux 一共提供了兩個(gè) API,分別是 connect 和 Provider,前者是一個(gè) React 高階組件,后者是一個(gè)普通的 React 組件。react-redux 實(shí)現(xiàn)了一個(gè)簡單的發(fā)布-訂閱庫,來監(jiān)聽當(dāng)前 store 的變化。兩者的作用如下:

          1. Provider:將 store 通過 Context 傳給后代組件,注冊對 store 的監(jiān)聽。
          2. connect:一旦 store 變化就會(huì)執(zhí)行 mapStateToProps 和 mapDispatchToProps 獲取最新的 props 后,將其傳給子組件。

          使用方式:

          // ProviderReactDOM.render({    ,    document.getElementById('app')})// connect@connect(mapStateToProps, mapDispatchToProps)class App extends Component {}

          4.1 實(shí)現(xiàn) Provider

          先來實(shí)現(xiàn)簡單的 Provider,已知 Provider 會(huì)使用 Context 來傳遞 store,所以 Provider 直接通過?Context.Provider?將 store 給子組件。

          // Context.jsconst ReactReduxContext = createContext(null);
          // Provider.jsconst Provider = ({ store, children }) => { return ( {children} )}

          Provider 里面還需要一個(gè)發(fā)布-訂閱器

          class Subscription {    constructor(store) {        this.store = store;        this.listeners = [this.handleChangeWrapper];    }    notify = () => {        this.listeners.forEach(listener => {            listener()        });    }    addListener(listener) {        this.listeners.push(listener);    }    // 監(jiān)聽 store    trySubscribe() {        this.unsubscribe = this.store.subscribe(this.notify);    }    // onStateChange 需要在組件中設(shè)置    handleChangeWrapper = () => {        if (this.onStateChange) {          this.onStateChange()        }    }    unsubscribe() {        this.listeners = null;        this.unsubscribe();    }}

          將 Provider 和 Subscription 結(jié)合到一起,在 useEffect 里面注冊監(jiān)聽。

          // Provider.jsconst Provider = ({ store, children }) => {    const contextValue = useMemo(() => {        const subscription = new Subscription(store);        return {            store,            subscription        }    }, [store]);    // 監(jiān)聽 store 變化    useEffect(() => {        const { subscription } = contextValue;        subscription.trySubscribe();        return () => {            subscription.unsubscribe();        }    }, [contextValue]);    return (                    {children}            )}

          4.2 實(shí)現(xiàn) connect

          再來看 connect 的實(shí)現(xiàn),這里主要有三步:

          1. 使用 useContext 獲取到傳入的 store 和 subscription。
          2. 對 subscription 添加一個(gè) listener,這個(gè) listener 的作用就是一旦 store 變化就重新渲染組件。
          3. store 變化之后,執(zhí)行 mapStateToProps 和 mapDispatchToProps 兩個(gè)函數(shù),將其和傳入的 props 進(jìn)行合并,最終傳給 WrappedComponent。

          先來實(shí)現(xiàn)簡單的獲取 Context。

          const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {    return function Connect(props) {        const { store, subscription } = useContext(ReactReduxContext);        return     }}

          接下來就要來實(shí)現(xiàn)如何在 store 變化的時(shí)候更新這個(gè)組件。我們都知道在 React 中想實(shí)現(xiàn)更新組件只有手動(dòng)設(shè)置 state 和調(diào)用 forceUpdate 兩種方法,這里使用 useState 每次設(shè)置一個(gè) count 來觸發(fā)更新。

          const connect = (mapStateToProps, mapDispatchToProps) => {    return (WrappedComponent) => {        return (props) => {            const { store, subscription } = useContext(ReactReduxContext);            const [count, setCount] = useState(0)            useEffect(() => {                subscription.onStateChange = () => setCount(count + 1)            }, [count])            const newProps = useMemo(() => {                const stateProps = mapStateToProps(store.getState()),                    dispatchProps = mapDispatchToProps(store.dispatch);                return {                    ...stateProps,                    ...dispatchProps,                    ...props                }            }, [props, store, count])            return         }    }}

          react-redux 的原理和上面比較類似,這里只作為學(xué)習(xí)原理的一個(gè)例子,不建議用到生產(chǎn)環(huán)境中。

          5. 如何設(shè)計(jì) store

          在開發(fā)中,如果想要查看當(dāng)前頁面的 store 結(jié)構(gòu),可以使用 [Redux-DevTools][14] 或者 [React Developer Tools][15] 這兩個(gè) chrome 插件來查看。前者一般用于開發(fā)環(huán)境中,可以將 store 及其變化可視化展示出來。后者主要用于 React,也可以查看 store。關(guān)于 Redux 中 store 如何設(shè)計(jì)對初學(xué)者來說一直都是難題,在我看來這不僅是 Redux 的問題,在任何前端 store 設(shè)計(jì)中應(yīng)該都是一樣的。

          5.1 store 設(shè)計(jì)誤區(qū)

          這里以知乎的問題頁 store 設(shè)計(jì)為例。在開始之前,先安裝 React Developer Tools,在 RDT 的 Tab 選中根節(jié)點(diǎn)。

          然后在 Console 里面輸入?$r.state.store.getState(),將 store 打印出來。

          可以看到 store 中有一個(gè) entities 屬性,這個(gè)屬性中分別有 users、questions、answer 等等。

          這是一個(gè)問題頁,自然包括問題、回答、回答下面的評論 等等。

          一般情況下,這里應(yīng)該是當(dāng)進(jìn)入頁面的時(shí)候,根據(jù) question_id 來分批從后端獲取到所有的回答。點(diǎn)開評論的時(shí)候,會(huì)根據(jù) answer_id 來分批從后端獲取到所有的評論。所以你可能會(huì)想到 store 結(jié)構(gòu)應(yīng)當(dāng)這樣設(shè)計(jì),就像俄羅斯套娃一樣,一層套著一套。

          {    questions: [        {            content: 'LOL中哪個(gè)英雄最能表達(dá)出你對刺客的想象?',            question_id: '1',            answers: [                {                       answer_id: '1-1',                    content: '我就是來提名一個(gè)已經(jīng)式微的英雄的。沒錯(cuò),就是提莫隊(duì)長...'                    comments: [                        {                              comment_id: '1-1-1',                            content: '言語精煉,每一句話都是一幅畫面,一組鏡頭'                        }                    ]                }            ]        }    ]}

          看圖可以更直觀感受數(shù)據(jù)結(jié)構(gòu):

          這是初學(xué)者經(jīng)常進(jìn)入的一個(gè)誤區(qū),按照 API 來設(shè)計(jì) store 結(jié)構(gòu),這種方法是錯(cuò)誤的。以評論區(qū)回復(fù)為例子,如何將評論和回復(fù)的評論關(guān)聯(lián)起來呢?也許你會(huì)想,把回復(fù)的評論當(dāng)做評論的子評論不就行了嗎?

          {    comments: [        {            comment_id: '1-1-1',            content: '言語精煉,每一句話都是一幅畫面,一組鏡頭',            children: [                {                    comment_id: '1-1-2',                    content: '我感覺是好多畫面,一部電影。。。'                }            ]        },        {            comment_id: '1-1-2',            content: '我感覺是好多畫面,一部電影。。。'        }    ]}

          這樣挺好的,滿足了我們的需求,但 children 中的評論和 comments 中的評論數(shù)據(jù)亢余了。

          5.2 扁平化 store

          聰明的你一定會(huì)想到,如果 children 中只保存?comment_id?不就好了嗎?展示的時(shí)候只要根據(jù)?comment_id?從 comments 中查詢就行了。這就是設(shè)計(jì) store 的精髓所在了。我們可以將 store 當(dāng)做一個(gè)數(shù)據(jù)庫,store 中的狀態(tài)按照領(lǐng)域(domain)來劃分成一張張數(shù)據(jù)表。不同的數(shù)據(jù)表之間以主鍵來關(guān)聯(lián)。因此上面的 store 可以設(shè)計(jì)成三張表,分別是 questions、answers、comments,以它們的 id 作為 key,增加一個(gè)新的字段來關(guān)聯(lián)子級。

          {    questions: {        '1': {            id: '1',            content: 'LOL中哪個(gè)英雄最能表達(dá)出你對刺客的想象?',            answers: ['1-1']        }    },    answers: {        '1-1': {            id: '1-1',            content: '我就是來提名一個(gè)已經(jīng)式微的英雄的。沒錯(cuò),就是提莫隊(duì)長...',            comments: ['1-1-1', '1-1-2']        }    },    comments: {        '1-1-1': {            id: '1-1-1',            content: '言語精煉,每一句話都是一幅畫面,一組鏡頭',            children: ['1-1-2']        },        '1-1-2': {            id: '1-1-2',            content: '我感覺是好多畫面,一部電影。。。'        }    }}

          你會(huì)發(fā)現(xiàn)數(shù)據(jù)結(jié)構(gòu)變得非常扁平化,避免了數(shù)據(jù)亢余以及嵌套過深的問題。在查找的時(shí)候也可以直接通過 id 來查找,避免了通過索引來查找某一具體項(xiàng)。

          6. 推薦閱讀

          • 解析Twitter前端架構(gòu) 學(xué)習(xí)復(fù)雜場景數(shù)據(jù)設(shè)計(jì)
          • JSON數(shù)據(jù)范式化(normalizr)
          • React+Redux打造“NEWS EARLY”單頁應(yīng)用




          1. JavaScript 重溫系列(22篇全)
          2. ECMAScript 重溫系列(10篇全)
          3. JavaScript設(shè)計(jì)模式 重溫系列(9篇全)
          4.?正則 / 框架 / 算法等 重溫系列(16篇全)
          5.?Webpack4 入門(上)||?Webpack4 入門(下)
          6.?MobX 入門(上)?||??MobX 入門(下)
          7.?70+篇原創(chuàng)系列匯總

          回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~

          點(diǎn)擊“閱讀原文”查看70+篇原創(chuàng)文章

          點(diǎn)這,與大家一起分享本文吧~
          瀏覽 58
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美一级视频在线观看 | 欧美在线一区二区三区 | 欧美国产精品一区二区 | 免费视频日一下 | 91精品日产一二三区乱码 |