一、 redux出現的動機 1. Javascript 需要管理比任何時候都要多的state2. state 在什麼時候,由於什麼原因,如何變化已然不受控制。3. 來自前端開發領域的新需求4. 我們總是將兩個難以理清的概念混淆在一起:變化和非同步。5. Redux 視圖讓state 的變化變得可預測。 ...
一、 redux出現的動機
1. Javascript 需要管理比任何時候都要多的state
2. state 在什麼時候,由於什麼原因,如何變化已然不受控制。
3. 來自前端開發領域的新需求
4. 我們總是將兩個難以理清的概念混淆在一起:變化和非同步。
5. Redux 視圖讓state 的變化變得可預測。
二、 核心概念
1. 想要更新state中的數據,你需要發起一個action,Action就是一個普通的JavaScript 對象用來描述發生了什麼。為了把actin 和state串起來開發一些函數,就是redcer。
三、 三大原則
1. 單一數據源 整個應用的state被存儲在一棵objecttree中, 並且這個 object tree 只 存在於一個唯一的store 中。
2. state 是只讀的,唯一改變state的方法就是觸發action,action 是一個用於描述已發 生事件的普通對象。(確保了視圖和網路請求不能直接修改state,只能表達想要修改的意圖)
3. 使用純函數來執行修改為了描述action如何改變state tree ,你需要編寫reducers。
四、 源碼解析
1. 入口文件index.js
import createStore from './createStore' import combineReducers from './combineReducers' import bindActionCreators from './bindActionCreators' import applyMiddleware from './applyMiddleware' import compose from './compose' import warning from './utils/warning' import __DO_NOT_USE__ActionTypes from './utils/actionTypes' /* * This is a dummy function to check if the function name has been altered by minification. * If the function has been minified and NODE_ENV !== 'production', warn the user. */ // 判斷文件是否被壓縮了 function isCrushed() {} if ( process.env.NODE_ENV !== 'production' && typeof isCrushed.name === 'string' && isCrushed.name !== 'isCrushed' ) { warning( 'You are currently using minified code outside of NODE_ENV === "production". ' + 'This means that you are running a slower development build of Redux. ' + 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 'or setting mode to production in webpack (https://webpack.js.org/concepts/mode/) ' + 'to ensure you have the correct code for your production build.' ) } /* 從入口文件可以看出 redux 對外暴露了5個API。 createStore , combineReducers, bindActionCreators, applyMiddleware, compose, */ export { createStore, combineReducers, bindActionCreators, applyMiddleware, compose, __DO_NOT_USE__ActionTypes }
2. 對外暴露的第一個API createStore => createStore.js
1 import $$observable from 'symbol-observable' 2 3 import ActionTypes from './utils/actionTypes' 4 import isPlainObject from './utils/isPlainObject' 5 6 /** 7 * Creates a Redux store that holds the state tree. 8 * The only way to change the data in the store is to call `dispatch()` on it. 9 * 10 * There should only be a single store in your app. To specify how different 11 * parts of the state tree respond to actions, you may combine several reducers 12 * into a single reducer function by using `combineReducers`. 13 * 14 * @param {Function} reducer A function that returns the next state tree, given 15 * the current state tree and the action to handle. 16 * 17 * @param {any} [preloadedState] The initial state. You may optionally specify it 18 * to hydrate the state from the server in universal apps, or to restore a 19 * previously serialized user session. 20 * If you use `combineReducers` to produce the root reducer function, this must be 21 * an object with the same shape as `combineReducers` keys. 22 * 23 * @param {Function} [enhancer] The store enhancer. You may optionally specify it 24 * to enhance the store with third-party capabilities such as middleware, 25 * time travel, persistence, etc. The only store enhancer that ships with Redux 26 * is `applyMiddleware()`. 27 * 28 * @returns {Store} A Redux store that lets you read the state, dispatch actions 29 * and subscribe to changes. 30 */ 31 /* 32 從源碼上可以看出 createStore 是一個函數。接收三個參數 reducer, preloadeState, enhancer 33 */ 34 export default function createStore(reducer, preloadedState, enhancer) { 35 // 如果 preloadeState 是一個函數 && enhancer未定義preloadeState 和 enhancer交換位置 36 if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { 37 enhancer = preloadedState 38 preloadedState = undefined 39 } 40 // 41 if (typeof enhancer !== 'undefined') { 42 if (typeof enhancer !== 'function') { 43 throw new Error('Expected the enhancer to be a function.') 44 } 45 // 上面兩個判斷是為了確保 enchancer是個函數 46 return enhancer(createStore)(reducer, preloadedState) 47 } 48 49 // reducer必須是 個函數,如果不是個函數給出友好的 提示 50 if (typeof reducer !== 'function') { 51 throw new Error('Expected the reducer to be a function.') 52 } 53 54 let currentReducer = reducer // 把reducer 暫存起來 55 let currentState = preloadedState // 把preloadeState暫存起來 56 let currentListeners = [] 57 let nextListeners = currentListeners 58 let isDispatching = false //判斷是否正處於dispatch中 59 60 // 如果 nextListeners 和 currrentListeners 都指向一個記憶體空間的時候, 深複製一份出來。確保兩個之間不會相互影響。 61 function ensureCanMutateNextListeners() { 62 if (nextListeners === currentListeners) { 63 nextListeners = currentListeners.slice() 64 } 65 } 66 67 /** 68 * Reads the state tree managed by the store. 69 * 70 * @returns {any} The current state tree of your application. 71 */ 72 // 獲取目前的 state的值。 73 function getState() { 74 if (isDispatching) { 75 throw new Error( 76 'You may not call store.getState() while the reducer is executing. ' + 77 'The reducer has already received the state as an argument. ' + 78 'Pass it down from the top reducer instead of reading it from the store.' 79 ) 80 } 81 82 return currentState 83 } 84 85 /** 86 * Adds a change listener. It will be called any time an action is dispatched, 87 * and some part of the state tree may potentially have changed. You may then 88 * call `getState()` to read the current state tree inside the callback. 89 * 90 * You may call `dispatch()` from a change listener, with the following 91 * caveats: 92 * 93 * 1. The subscriptions are snapshotted just before every `dispatch()` call. 94 * If you subscribe or unsubscribe while the listeners are being invoked, this 95 * will not have any effect on the `dispatch()` that is currently in progress. 96 * However, the next `dispatch()` call, whether nested or not, will use a more 97 * recent snapshot of the subscription list. 98 * 99 * 2. The listener should not expect to see all state changes, as the state 100 * might have been updated multiple times during a nested `dispatch()` before 101 * the listener is called. It is, however, guaranteed that all subscribers 102 * registered before the `dispatch()` started will be called with the latest 103 * state by the time it exits. 104 * 105 * @param {Function} listener A callback to be invoked on every dispatch. 106 * @returns {Function} A function to remove this change listener. 107 */ 108 // 經典的訂閱函數 109 function subscribe(listener) { 110 if (typeof listener !== 'function') { 111 throw new Error('Expected the listener to be a function.') 112 } 113 114 if (isDispatching) { 115 throw new Error( 116 'You may not call store.subscribe() while the reducer is executing. ' + 117 'If you would like to be notified after the store has been updated, subscribe from a ' + 118 'component and invoke store.getState() in the callback to access the latest state. ' + 119 'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.' 120 ) 121 } 122 // 閉包的經典應用 每次訂閱一個事件的時候,都有一個內部狀態, 用於後續的取消訂閱 123 let isSubscribed = true 124 // 深複製一份監聽對象 125 ensureCanMutateNextListeners() 126 // 把每個監聽對象都放置於一個數組中,保存下來,(精華之處,對閉包的使用登峰造極) 127 nextListeners.push(listener) 128 // 當註冊一個監聽事件的返回一個函數,調用這個函數可以取消訂閱,具體操作方法就是從監聽的數組中移出掉。 129 return function unsubscribe() { 130 // 防止重覆取消訂閱 131 if (!isSubscribed) { 132 return 133 } 134 135 if (isDispatching) { 136 throw new Error( 137 'You may not unsubscribe from a store listener while the reducer is executing. ' + 138 'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.' 139 ) 140 } 141 // 對應上面那條,防止重覆取消訂閱 142 isSubscribed = false 143 144 ensureCanMutateNextListeners() 145 const index = nextListeners.indexOf(listener) 146 // 刪除數組中某一項的方法 splice 147 nextListeners.splice(index, 1) 148 } 149 } 150 151 /** 152 * Dispatches an action. It is the only way to trigger a state change. 153 * 154 * The `reducer` function, used to create the store, will be called with the 155 * current state tree and the given `action`. Its return value will 156 * be considered the **next** state of the tree, and the change listeners 157 * will be notified. 158 * 159 * The base implementation only supports plain object actions. If you want to 160 * dispatch a Promise, an Observable, a thunk, or something else, you need to 161 * wrap your store creating function into the corresponding middleware. For 162 * example, see the documentation for the `redux-thunk` package. Even the 163 * middleware will eventually dispatch plain object actions using this method. 164 * 165 * @param {Object} action A plain object representing “what changed”. It is 166 * a good idea to keep actions serializable so you can record and replay user 167 * sessions, or use the time travelling `redux-devtools`. An action must have 168 * a `type` property which may not be `undefined`. It is a good idea to use 169 * string constants for action types. 170 * 171 * @returns {Object} For convenience, the same action object you dispatched. 172 * 173 * Note that, if you use a custom middleware, it may wrap `dispatch()` to 174 * return something else (for example, a Promise you can await). 175 */ 176 // 派發一個事件 177 function dispatch(action) { 178 // p、判斷action是否是個對象 179 if (!isPlainObject(action)) { 180 throw new Error( 181 'Actions must be plain objects. ' + 182 'Use custom middleware for async actions.' 183 ) 184 } 185 // 嚴格控制 action 的書寫格式 { type: 'INCREMENT'} 186 if (typeof action.type === 'undefined') { 187 throw new Error( 188 'Actions may not have an undefined "type" property. ' + 189 'Have you misspelled a constant?' 190 ) 191 } 192 193 if (isDispatching) { 194 throw new Error('Reducers may not dispatch actions.') 195 } 196 // isDipatching 也是閉包的經典用法 197 /* 198 觸發 dispatch的時候 把 isDispatching 改為 true。 照應全篇中對 dispatching 正在觸發的時候的判斷 199 finally 執行完畢的時候 置為 false 200 */ 201 try { 202 isDispatching = true 203 // 獲取最新 的state 值。 currentState 經典之處閉包 204 currentState = currentReducer(currentState, action) 205 } finally { 206 isDispatching = false 207 } 208 209 // 對監聽對象從新賦值 其實裡面每個listener都是一個函數。 於subscribe相對應 210 // 當每發生一個dispatch 事件的時候, 都迴圈調用,觸發監聽事件 211 const listeners = (currentListeners = nextListeners) 212 for (let i = 0; i < listeners.length; i++) { 213 const listener = listeners[i] 214 listener() 215 } 216 217 return action 218 } 219 220 /** 221 * Replaces the reducer currently used by the store to calculate the state. 222 * 223 * You might need this if your app implements code splitting and you want to 224 * load some of the reducers dynamically. You might also need this if you 225 * implement a hot reloading mechanism for Redux. 226 * 227 * @param {Function} nextReducer The reducer for the store to use instead. 228 * @returns {void} 229 */ 230 // 替換 reducer 用 新的 reducer替換以前的reducer 參數同樣必須是函數 231 function replaceReducer(nextReducer) { 232 if (typeof nextReducer !== 'function') { 233 throw new Error('Expected the nextReducer to be a function.') 234 } 235 236 currentReducer = nextReducer 237 dispatch({ type: ActionTypes.REPLACE }) 238 } 239 240 /** 241 * Interoperability point for observable/reactive libraries. 242 * @returns {observable} A minimal observable of state changes. 243 * For more information, see the observable proposal: 244 * https://github.com/tc39/proposal-observable 245 */ 246 // 觀察模式 247 function observable() { 248 const outerSubscribe = subscribe 249 return { 250 /** 251 * The minimal observable subscription method. 252 * @param {Object} observer Any object that can be used as an observer. 253 * The observer object should have a `next` method. 254 * @returns {subscription} An object with an `unsubscribe` method that can 255 * be used to unsubscribe the observable from the store, and prevent further 256 * emission of values from the observable. 257 */ 258 subscribe(observer) { 259 if (typeof observer !== 'object' || observer === null) { 260 throw new TypeError('Expected the observer to be an object.') 261 } 262 263 function observeState() { 264 if (observer.next) { 265 observer.next(getState()) 266 } 267 } 268 269 observeState() 270 const unsubscribe = outerSubscribe(observeState) 271 return { unsubscribe } 272 }, 273 274 [$$observable]() { 275 return this 276 } 277 } 278 } 279 280 // When a store is created, an "INIT" action is dispatched so that every 281 // reducer returns their initial state. This effectively populates 282 // the initial state tree. 283 // 觸發一state 樹 284 dispatch({ type: ActionTypes.INIT }) 285 /* 286 由此可以看出 調用 createStore(); 後。對外暴露的方法 287 1. dispatch 288 2. subscribe 289 3. getState 290 4. replaceReducer 291 5.觀察模式 292 const store = createStore(reducer, preloadedState, enchancer); 293 store.dispatch(action); 294 store.getState(); // 為什麼這個方法能夠獲得 state的值。因為 currentState 的閉包實現。 295 store.subscribe(() => console.log(store.getState())); 296 store.replaceReducer(reducer); 297 總結:縱觀createStore方法的實現,其實都是建立在閉包的基礎之上。可謂是把閉包用到了極致。 298 */ 299 return { 300 dispatch, 301 subscribe, 302 getState, 303 replaceReducer, 304 [$$observable]: observable 305 } 306 }
3. combineReducers.js
1 import ActionTypes from './utils/actionTypes' 2 import warning from './utils/warning' 3 import isPlainObject from './utils/isPlainObject' 4 5 function getUndefinedStateErrorMessage(key, action) { 6 const actionType = action && action.type 7 const actionDescription = 8 (actionType && `action "${String(actionType)}"`) || 'an action' 9 10 return ( 11 `Given ${actionDescription}, reducer "${key}" returned undefined. ` + 12 `To ignore an action, you must explicitly return the previous state. ` + 13 `If you want this reducer to hold no value, you can return null instead of undefined.` 14 ) 15 } 16 17 function getUnexpectedStateShapeWarningMessage( 18 inputState, 19 reducers, 20 action, 21 unexpectedKeyCache 22 ) { 23 const reducerKeys = Object.keys(reducers) 24 const argumentName = 25 action && action.type === ActionTypes.INIT 26 ? 'preloadedState argument passed to createStore' 27 : 'previous state received by the reducer' 28 29 if (reducerKeys.length === 0) { 30 return ( 31 'Store does not have a valid reducer. Make sure the argument passed ' + 32 'to combineReducers is an object whose values are reducers.' 33 ) 34 } 35 36 if (!isPlainObject(inputState)) { 37 return ( 38 `The ${argumentName} has unexpected type of "` + 39 {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + 40 `". Expected argument to be an object with the following ` + 41 `keys: "${reducerKeys.join('", "')}"` 42 ) 43 } 44 45 const unexpectedKeys = Object.keys(inputState).filter( 46 key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key] 47 ) 48 49 unexpectedKeys.forEach(key => { 50 unexpectedKeyCache[key] = true 51 }) 52 53 if (action && action.type === ActionTypes.REPLACE) return 54 55 if (unexpectedKeys.length > 0) { 56 return ( 57 `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` + 58 `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` + 59 `Expected to find one of the known reducer keys instead: ` + 60 `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.` 61 ) 62 } 63 } 64 65 function assertReducerShape(reducers) { 66 Object.keys(reducers).forEach(key => { 67 const reducer = reducers[key] 68 const initialState = reducer(undefined, { type: ActionTypes.INIT }) 69 70 if (typeof initialState === 'undefined') { 71 throw new Error( 72 `Reducer "${key}" returned undefined during initialization. ` + 73 `If the state passed to the reducer is undefined, you must ` + 74 `explicitly return the initial state. The initial state may ` + 75 `not be undefined. If you don't want to set a value for this reducer, ` + 76 `you can use null instead of undefined.` 77 ) 78 } 79 80 if ( 81 typeof reducer(undefined, { 82 type: ActionTypes.PROBE_UNKNOWN_ACTION() 83 }) === 'undefined' 84 ) { 85 throw new Error( 86 `Reducer "${key}" returned undefined when probed with a random type. ` + 87 `Don't try to handle ${ 88 ActionTypes.INIT 89 } or other actions in "redux/*" ` + 90 `namespace. They are considered private. Instead, you must return the ` + 91 `current state for any unknown actions, unless it is undefined, ` + 92 `in which case you must return the initial state, regardless of the ` + 93 `action type. The initial state may not be undefined, but can be null.` 94 ) 95 } 96 }) 97 } 98 99 /** 100 * Turns an object whose values are different reducer functions, into a single 101 * reducer function. It will call every child reducer, and gather their results 102 * into a single state object, whose keys correspond to the keys of the passed 103 * reducer functions. 104 * 105 * @param {Object} reducers An object whose values correspond to different 106 * reducer functions that need to be combined into one. One handy way to obtain 107 * it is to use ES6 `import * as reducers` syntax. The reducers may never return 108 * undefined for any action. Instead, they should return their initial state 109 * if the state passed to them was undefined, and the current state for any 110 * unrecognized action. 111 * 112 * @returns {Function} A reducer function that invokes every reducer inside the 113 * passed object, and builds a state object with the same shape. 114 */ 115 116 /* 117 combineReducers 顧名思義就是合併reduces的一個方法。 118 1. 為了項目便於維護與管理我們就需要拆按模塊拆分reducers。 119 2. 而combineReducers就是為瞭解決這個的問題的。 120 121 */ 122 export default function combineReducers(reducers) { // 參數reducers 是一個對象 123 const reducerKeys = Object.keys(reducers) // 獲取reducers的k 124 const finalReducers = {} 125 for (let i = 0; i < reducerKeys.length; i++) { 126 const key = reducerKeys[i] 127 128 if (process.env.NODE_ENV !== 'production') { 129 if (typeof reducers[key] === 'undefined') { 130 warning(`No reducer provided for key "${key}"`) 131 } 132 } 133 134 // 深複製一份reducers出來, 防止後續操作出現不可控因素 135 if (typeof reducers[key] === 'function') { 136 finalReducers[key] = reducers[key] 137 } 138 } 139 const finalReducerKeys = Object.keys(finalReducers) 140 141 let unexpectedKeyCache 142 if (process.env.NODE_ENV !== 'production') { 143 unexpectedKeyCache = {} 144 } 145 146 let shapeAssertionError 147 try { 148 assertReducerShape(finalReducers) 149 } catch (e) { 150 shapeAssertionError = e 151 } 152 // 閉包的運用, 把合併的 reducer保存下來。 153 return function combination(state = {}, action) { 154 if (shapeAssertionError) { 155 throw shapeAssertionError 156 } 157 158 if (process.env.NODE_ENV !== 'production') { 159 const warningMessage = getUnexpectedStateShapeWarningMessage( 160 state, 161 finalReducers, 162 action, 163 unexpectedKeyCache 164 ) 165 if (warningMessage) { 166 warning(warningMessage) 167 } 168 } 169 170 let hasChanged = false 171 const nextState = {} 172 for (let i = 0; i < finalReducerKeys.length; i++) { 173 const key = finalReducerKeys[i] 174 const reducer = finalReducers[key] 175 const previousStateForKey = state[key] 176 // 把合併的時候的key值作為Key值為標準。 在迴圈遍歷的時候取出對應的 reducers 觸發 reducer函數。 177 /* 178 其實對應的createStore.js中的 179 try { 180 isDispatching = true 181 // 獲取最新 的state 值。 currentState 經典之處閉包 182 currentState = currentReducer(currentState, action) 183 } finally { 184 isDispatching = false 185 } 186 */ 187 const nextStateForKey = reducer(previousStateForKey, action) 188 if (typeof nextStateForKey === 'undefined') { 189 const errorMessage = getUndefinedStateErrorMessage(key, action) 190 throw new Error(errorMessage) 191 } 192 nextState[key] = nextStateForKey 193 hasChanged = hasChanged || nextStateForKey !== previousStateForKey 194 } 195 return hasChanged ? nextState : state 196 } 197 } 198 /** 199 * 使用方法 200 * const reducers = combineReducers({ reducer1, reducer2 }); 201 * const store = createStore(reducers, preloadedState, enchancer); 202 */
4. bindActionCreators.js
1 // 主要這個函數 2 function bindActionCreator(actionCreator, dispatch) { 3 return function() { 4 return dispatch(actionCreator.apply(this, arguments)) 5 } 6 } 7 8 /** 9