我的Bloghtml
e.g.有一個公式,求連續天然數的平方和:前端
s = 1² + 2² + 3² + 4² + ... + N²
複製代碼
用命令式是這麼解決問題的:react
function squares(arr){
var i, sum = 0, squares = []
for(i = 0; i < arr.length; i++){
squares.push(arr[i] * arr[i])
}
for(i = 0; i < squares.length; i++){
sum += squares[i]
}
return sum
}
console.log(squares([1, 2, 3, 4, 5])) //55
複製代碼
雖說如今的你不會寫出這樣的代碼,但之前確定寫過相似的。別說是同事,就算是本身過半個月回來也要熟悉一會親手寫的邏輯代碼。webpack
也稱「業務型」編程,指的是用一步步下達命令最終去實現某個功能。行爲過程不直觀,只關心下一步應該怎麼、而後再怎麼、最後幹什麼,卻對性能、易讀性、複用性不聞不問。git
由於需求的差別化、定製化太過嚴重,依賴於後端交互、而且函數式編程過於抽象,致使沒法用函數式編程作到高效率開發,因此如今業務的實現,大多數都偏向於命令式編程。可是也帶來很大的一個問題,過於重複,有位大佬(不知道誰)說過:「DRY(Don't Repeat YouSelf)」。最典型的狀況莫在於產品讓你寫若干個後臺列表篩選頁面,每一個頁面只是字段不同而已。有些要篩選框、下拉框、搜索建議、評論等,而有些只要輸入框,即便高階組件面對這種狀況也不能作到太多複用效果。github
函數式編程是聲明式的一種 —— 最經典的Haskell(老讀成HaSaKi)。近幾年大量庫所應用。和生態圈的各種組件中,它們的標籤很容易辨認 —— 不可變數據(immutable)、高階函數(柯里化)、尾遞歸、惰性序列等... 它最大的特色就是專注、簡潔、封裝性好。web
用函數式編程解決這個問題:chrome
function squares(arr){
return arr.map(d=>Math.pow(d,2))
.reduce((p,n)=>p+n,0)
}
console.log(squares([1,2,3,4,5])) //55
複製代碼
它不只可讀性更高,並且更加簡潔,在這裏,咱們不用去關心for循環和索引,咱們只關心兩件事:編程
1.取出每一個數字計算平方(map,Math.pow)redux
2.累加(reduce)
屬於稀有動物,有點像初中數學的命題推論和 Node
裏的 asset
斷言,經過一系列事實和規則,利用數理邏輯來推導或論證結論。但並不適合理論上的教學,因此沒有被普遍採用。
函數式編程關心數據是如何被處理的,相似於自動流水線。
而命令式編程關心的是怎麼去作?就像是手工,先這樣作,再這樣作,而後再這樣,若是這樣,就這樣作 ...
邏輯式編程是經過必定的規則和數據,推導出結論,相似於asset,使用極少
他們幾個有什麼區別?這個問題對於一個非專出身有點難以理解。
函數式編程關心數據的映射,命令式編程關心解決問題的步驟。
瞭解到這裏,相信大概的概念你也能領悟到。 引入主題,redux是函數式編程很好的一門不扯皮了,咱們開始幹正事
// src/redux/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.'
)
}
export {
createStore,
combineReducers,
bindActionCreators,
applyMiddleware,
compose,
__DO_NOT_USE__ActionTypes
}
複製代碼
首先會判斷在非生產環境下 isCrushed
表示在生產環境下壓縮,由於使用壓縮事後的 redux
會下降性能,這裏創建一個空函數在入口處判斷警告開發者。 三個條件:
typeof
判斷下isCrushed
。 壓縮後isCrushed.name !== 'isCrushed'
;這裏爲何要用 typeof isCrushed.name
,typeof
有容錯保護機制,保證不會程序崩潰。
對外暴露5個經常使用的API。 __DO_NOT_USE__ActionTypes
。顧名思義不要用這裏面的幾個ActionTypes。可是隨機數的方法爲何不用symbol防止重命名有待思考。
// src/redux/utils/actionTypes.js
// 生成隨機數,大概輸出sqrt(36*(7-1)) = 46656次後看到重複,通常程序事件觸發不到這個次數
const randomString = () =>
Math.random()
.toString(36)
.substring(7)
.split('')
.join('.')
const ActionTypes = {
INIT: `@@redux/INIT${randomString()}`, //用來redux內部發送一個默認的dispatch, initialState
REPLACE: `@@redux/REPLACE${randomString()}`, // store.replaceReducers替換當前reducer觸發的內部Actions
PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}
複製代碼
而PROBE_UNKNOWN_ACTION
則是redux內部隨機檢測combineReducers
合併全部reducer
默認狀況下觸發任何Action判斷是否返回了相同的數據。
createStore(reducer:any,preloadedState?:any,enhancer?:middleware),最終返回一個
state tree
實例。能夠進行getState
,subscribe
監聽和dispatch
派發。
createStore
接收3個參數
state tree
和要執行的action
,返回下一個state tree
。initial state tree
。applymiddleware
產生一個加強器enhancer
,多個加強器能夠經過 compose
函數合併成一個加強器。// src/redux/createStore.js
export default function createStore(reducer, preloadedState, enhancer) {
if (
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
// 檢測是否傳入了多個compose函數,拋出錯誤,提示強制組合成一個enhancer
throw new Error(
'It looks like you are passing several store enhancers to ' +
'createStore(). This is not supported. Instead, compose them ' +
'together to a single function.'
)
}
// 直接傳enhancer的狀況
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
// 校驗enhancer
throw new Error('Expected the enhancer to be a function.')
}
// 返回建立加強後的store
return enhancer(createStore)(reducer, preloadedState)
}
if (typeof reducer !== 'function') {
// 校驗reducer
throw new Error('Expected the reducer to be a function.')
}
//60 --------------
}
複製代碼
60行暫停一會,return enhancer(createStore)(reducer, preloadedState)
。若是傳入了 enhancer
加強器的狀態
// src/store/index.js
const logger = store => next => action => {
console.log('logger before', store.getState())
const returnValue = next(action)
console.log('logger after', store.getState())
return returnValue
}
export default function configStore(preloadedState){
const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const logEnhancer = applyMiddleware(logger);// 應用中間件生成的加強器
const store = createStore(
ShopState,
preloadedState,
composeEnhancer(logEnhancer) // compose能夠將多個加強器合併成一個加強器Plus
)
return store;
}
複製代碼
最終建立store後的狀態樣子應該是
// enhancer = composeEnhancer(applyMiddleware(logger)))
enhancer(createStore)(reducer, preloadedState)
||
\||/
\/
composeEnhancer(applyMiddleware(logger)))(createStore)(reducer, preloadedState)
複製代碼
看起來是否是很複雜,沒事,咱們一步一步來,先看下compose
函數作了什麼。
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製代碼
很精簡,首先檢查是否有加強器的狀況,若是沒有就返回一個空函數,若是有一個就返回該函數,只有多個的纔會產生compose
。這裏的compose
代碼其實只有一行,經過迭代器生成組合迭代函數。
funcs.reduce((a, b) => (...args) => a(b(...args)))
複製代碼
其餘都是作兼容。最終會將compose(f,g,h...)
轉化成compose(f(g(h(...))))
。
不一樣於柯里化,compose參數無限收集一次性執行,而科裏化是預先設置參數長度等待執行。並且
compose(f(g(h(...))))
等價於compose(h(g(f(...))))
,咱們來看個Demo
const a = str => str + 'a'
const b = str => str + 'b'
const c = str => str + 'c'
const compose = (...funcs) => {
return funcs.reduce((a,b)=>(...args)=>a(b(...args)))
}
compose(a,b,c)('開始迭代了') // 開始迭代了cba
複製代碼
compose的入參如今只有一個,直接返回自身,能夠被忽略,咱們能夠試試傳入多個 enhancer
。
const enhancer = applyMiddleware(logger)
compose(enhancer,enhancer,enhancer) // 先後將會打印6次logger
複製代碼
瞭解完了compose
,咱們再看applyMiddleware(logger)
// src/redux/applyMiddleware.js
import compose from './compose'
export default function applyMiddleware(...middlewares) {
// 接受若干個中間件參數
// 返回一個enhancer加強器函數,enhancer的參數是一個createStore函數。等待被enhancer(createStore)
return createStore => (...args) => {
// 先建立store,或者說,建立已經被前者加強過的store
const store = createStore(...args)
// 若是尚未改造完成,就先被調用直接拋出錯誤
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
// 暫存改造前的store
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 遍歷中間件 call(oldStore),改造store,獲得改造後的store數組
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 組合中間件,將改造前的dispatch傳入,每一箇中間件都將獲得一個改造/加強事後的dispatch。
dispatch = compose(...chain)(store.dispatch)
// 最終返回一個增強後的createStore()函數
return {
...store,
dispatch
}
}
}
複製代碼
能實現錯誤也是一種學習。有時候這種錯誤反而能帶來一些更直觀的感覺,知道緣由,在可見的將來徹底能夠去避免。上面拋出錯誤的狀況只有一種
function middleware(store) {
// 監聽路由的時候 dispatch(action),因爲當前還未改造完,會拋錯
history.listen(location => { store.dispatch(updateLocation(location)) })
return next => action => {
if (action.type !== TRANSITION) {
return next(action)
}
const { method, arg } = action
history[method](arg)
}
}
複製代碼
當在map middlewares的期間,dispatch
將要在下一步應用,可是目前沒應用的時候,經過其餘方法去調用了原生 dispatch
的某個方法,這樣很容易形成混淆,由於改變的是同一個 store
,在你 middlewares
數量多的時候,你很難去找到緣由到底爲何數據不符合預期。
核心方法是 dispatch = compose(...chain)(store.dispatch)
,如今看是否是與上面Demo的 compose(a,b,c)('開始迭代了')
看起來如出一轍?咱們繼續把上面的邏輯捋一遍。假如咱們有兩個中間件,被applyMiddleware應用,
// src/store/index.js
const logger = store => next => action => { // 打印日誌
console.log('logger before', store.getState())
const returnValue = next(action)
console.log('logger after', store.getState())
return returnValue
}
const handlerPrice = store => next => action => { // 給每次新增的商品價格補小數位
console.log('action: ', action);
action = {
...action,
data:{
...action.data,
shopPrice:action.data.shopPrice + '.00'
}
}
const returnValue = next(action)
return returnValue
}
export default function configStore(){
const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
ShopState,
undefined,
composeEnhancer(applyMiddleware(logger,handlerPrice))) ------ enhancer
return store;
}
複製代碼
enhancer
最終會返回一個加強函數,咱們再看一遍applyMiddleware
的源碼,得出applyMiddleware(logger,handlerPrice)
執行後將會獲得一個加強器。
const logger = store => next => action => { console.log(store); next(action) }
const handlerPrice = store => next => action => { console.log(store); next(action) }
middlewares = [logger, handlerPrice]
enhancer = (createStore) => (reducer, preloadedState, enhancer) => {
// 初始化store
var store = createStore(reducer, preloadedState, enhancer)
// 保存初始化的dispatch指針
var dispatch = store.dispatch
var chain = []
// 暫存改造前的store
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
// 將store傳入,等待 logger(store) 返回的 next => action => next(action)
// 經過閉包,每一箇中間件獲得的都是同一個store即middlewareAPI。這樣就保證了數據的迭代變化
chain = [logger, handlerPrice].map(middleware => middleware(middlewareAPI))
/* 每次middleware(middlewareAPI) 應用中間件,都至關於 logger(store)一次,store也隨之改變,返回兩個next形參函數 * [next => action => { console.log(store); next(action) },// logger * next => action => { console.log(store); next(action) }] // handlerPrice * 隨之兩個中間件等待被compose, 每一個均可以單獨訪問next/dispatch先後的store */
dispatch = compose(...chain)(store.dispatch)
// 先將全部的中間件compose合併,而後將store.dispatch做爲next形數傳入,獲得每一個action => store.dispatch(action)
// 也就行上文的 next(action) === store.dispatch(action)
// 最終拋出一個compose後的加強dispatch與store
// 返回改造後的store
return {
...store,
dispatch
}
}
複製代碼
實現邏輯是經過next(action)
處理和傳遞 action
直到 redux
原生的 dispatch
接收處理。
咱們回到以前的 src/redux/createStore.js
的 return enhancer(createStore)(reducer, preloadedState)
,若是看不懂的話這裏能夠分解成兩步
1.const enhancedCreateStore = enhancer(createStore) //----加強的createStore函數
2.return enhancedCreateStore(reducer, preloadedState)
複製代碼
此時將createStore
傳入,enhancer(createStore)
後獲得一個enhancedCreateStore()
生成器。
也就是上文中的 {...store,dispatch}
。
enhancerStore = (reducer, preloadedState, enhancer) =>{
// ... 省略若干代碼
return {
...store,
dispatch
}
}
複製代碼
此時執行第2步再將enhancerStore(reducer, preloadedState)
傳入............
而後就經過調用此時的dispatch達到同樣的效果,上面已經介紹的很詳細了,若是不熟悉的話,建議多看幾遍。
三番四次扯到中間件,究竟是什麼東西?
中間件提及來也不陌生,至於什麼是中間件,維基百科的解釋你們自行查找,原本只有一個詞不懂,看了 Wiki 變成七八個詞不懂。
在 JavaScript 裏無論是前端仍是 Node,都涉及頗廣
Ps:
Redux
的middleware
與koa
流程機制不徹底同樣。具體的區別能夠參考 Perkin 的 Redux,Koa,Express之middleware機制對比,本段koa
內容已隱藏,同窗們可選擇性去了解。
首先了解下 Redux
的 middleware
,正常流程上來講,和 koa
是一致的,可是若是在某個正在執行的 middleware
裏派發 action
,那麼將會當即「中斷」 而且重置當前 dispatch
const logger = store =>{
return next => action => {
console.log(1)
next(action)
console.log(2)
}
}
const handlerPrice = store => next => action => {
console.log(3)
// 禁止直接調用原生store.dispatch,在知道反作用的狀況下加條件執行,不然程序將崩潰
// 若是你想派發其餘的任務,可使用next(),此時next等價於dispatch
store.dispatch({type: 'anything' })
next(action)
console.log(4)
}
const enhancer = applyMiddleware(logger, handlerPrice)
const store = createStore(
ShopState,
null,
composeEnhancer(enhancer,handlerPrice))
// 結果無限循環的1和3
1
3
1
3
...
複製代碼
這是怎麼作到的?咱們來看,在 store.dispatch({type: 'anything' })
的時候,此時的 store
表面子上看仍是原生的,但實際上 store === middlewareAPI // false
,Why ?
// src/redux/applyMiddleware.js
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
// 暫存改造前的store
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args) //--- 保存了dispatch的引用
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch) // --- dispatch被改變
複製代碼
dispatch
最後的引用就是 compose(...chain)(store.dispatch)
,換句話說 store.dispatch
就是一次 middleWare Loop ...
這樣就能解釋上面的代碼了,store.dispatch({type:'anything'})
其實就是從頭又調了一遍中間件...
借Koa代碼一閱,在 SandBoxCode 上手動嘗試
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next)=>{
console.log(1)
await next();
console.log(2)
});
app.use(async (ctx, next) => {
console.log(3)
await next();
console.log(4)
})
app.use(async (ctx, next) => {
console.log(5)
})
app.listen(3000);
複製代碼
1
3
5
4
2
複製代碼
上圖被稱爲 洋蔥模型
,很清晰的代表了一個請求是如何通過中間件最後生成響應。
舉一個實際的例子,你天天回到家,假設家門是個中間件,你的臥室門也是個中間件,你是一個請求。那麼你必須先進家門,再進臥室的門,你想再出去就必須先出臥室的門,再出家門。須要遵照的是,你必須原路倒序返回
。 A->B->C->B->A。不能瞎蹦躂跳窗戶出去(若是你家是一樓能夠走後門當我沒說)
那麼再看上面的的例子就很是簡單了。koa
經過 use
方法添加中間件,每一個 async
函數就是你的要通過的門,而 next()
就表示你進門的動做。這不一樣於JavaScript執行機制中棧,更像是
+----------------------------------------------------------------------------------+
| |
| middleware 1 |
| |
| +--------------------------next()---------------------------+ |
| | | |
| | middleware 2 | |
| | | |
| | +-------------next()--------------+ | |
| | | middleware 3 | | |
| action | action | | action | action |
| 001 | 002 | | 005 | 006 |
| | | action action | | |
| | | 003 next() 004 | | |
| | | | | |
+---------------------------------------------------------------------------------------------------->
| | | | | |
| | | | | |
| | +---------------------------------+ | |
| +-----------------------------------------------------------+ |
+----------------------------------------------------------------------------------+
複製代碼
最後再次提示:
Koa
與Redux
的middleware
機制除了特殊狀態下是一致的,特殊狀態:在某個middleware
內調用dispatch
回到主題,咱們看61行以後的
let currentReducer = reducer // 當前reducer對象
let currentState = preloadedState // 當前state對象
let currentListeners = [] // 當前的listeners訂閱者集合, 使用subscribe進行訂閱
let nextListeners = currentListeners // currentListeners 備份
let isDispatching = false // dispatch狀態
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
// @returns {any} ,獲取state惟一方法,若是當前正在dispatch,就拋出一個錯誤,告訴
function getState() {
if (isDispatching) {
throw new Error(
'You may not call store.getState() while the reducer is executing. ' +
'The reducer has already received the state as an argument. ' +
'Pass it down from the top reducer instead of reading it from the store.'
)
}
return currentState
}
複製代碼
這裏的錯誤也很直觀了,懂一些邏輯或英語的人基本都能明白,很無語的是這種錯誤開發過程當中基本沒人遇到過,可是在18年末不少用chrome redux擴展程序的人遭了殃。緣由應該是在初始化 的時候沒有排除在INIT階段的 dispatching===true
就直接去取數據,這裏的報錯復現只要在dispatch
的時候去調用一次 getState()
就好了。
// src/App.js
const addShop = async () => {
dispatch({
type:'ADD_SHOP',
data:{
...newShop,
fn:()=> getState() // -----添加函數準備在dispatch的期間去執行它
}
})
}
// src/store/index
//...other
case 'ADD_SHOP': //添加商品
newState = {
...newState,
shopList:newState.shopList.concat(action.data)
}
action.data.fn() //----- 在這裏執行
複製代碼
或者異步去中間件獲取也會獲得這個錯誤。先來分析何時 isDispatching === true
。
function dispatch(action) {
// dispatch只接受一個普通對象
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
// action type爲有效參數
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
// 若是當前正在dispatch,拋出警告,可能不會被派發出去,由於store尚未被change完成
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
// INIT 和 dispatch 都會觸發這一步
// 將當前的 reducer 和 state 以及 action 執行以達到更新State的目的
currentState = currentReducer(currentState, action)
} finally {
// 不管結果如何,先結束dispatching狀態,防止阻塞下個任務
isDispatching = false
}
// 更新訂閱者,通知遍歷更新核心數據
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener() // 將下文的subscribe收集的訂閱者通知更新
}
return action // 將 action 返回,在react-redux中要用到
}
// ... other 省略100行
dispatch({ type: ActionTypes.INIT }) //INIT store 會觸發dispatch
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
複製代碼
固然是在 dispatch
的時候,這是觸發 state change
的惟一方法。首先會經過遞歸原型鏈頂層是否爲null
來區分普通對象。
export default function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false
let proto = obj
// 遞歸對象的原型 終點是否爲null
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(obj) === proto
}
複製代碼
這種檢測方式和 lodash
幾乎差很少,爲何不直接用toString.call呢?緣由我認爲toString的雖然可行,可是隱患太多,react想讓開發者以字面量的方式建立Action,杜絕以new方式去建立action,就好比下面這種建立方式
var obj = {}
Object.getPrototypeOf(obj) === Object.prototype // true
Object.getPrototypeOf(Object.prototype) === null // true
Object.prototype.toString.call({}) // [object Object]
// but
Object.prototype.toString.call(new function Person(){}) // [object Object]
複製代碼
看起來也沒有多難,可是咱們看下redux倉庫isPlainObject的測試用例
import expect from 'expect'
import isPlainObject from '../../src/utils/isPlainObject'
import vm from 'vm'
describe('isPlainObject', () => {
it('returns true only if plain object', () => {
function Test() {
this.prop = 1
}
const sandbox = { fromAnotherRealm: false }
// vm.runInNewContext (沙箱) 能夠在Node環境中建立新的上下文環境運行一段 js
vm.runInNewContext('fromAnotherRealm = {}', sandbox)
expect(isPlainObject(sandbox.fromAnotherRealm)).toBe(true)
expect(isPlainObject(new Test())).toBe(false) // ---
expect(isPlainObject(new Date())).toBe(false)
expect(isPlainObject([1, 2, 3])).toBe(false)
expect(isPlainObject(null)).toBe(false)
expect(isPlainObject()).toBe(false)
expect(isPlainObject({ x: 1, y: 2 })).toBe(true)
})
})
複製代碼
還有iframe、代碼並不是只在一個環境下運行,因此要考慮到比較多的因素,而lodash的考慮的因素更多——2.6w行測試用例...謹慎打開,可是
可能有些同窗不太清楚訂閱者模式和監聽者模式的區別
redux中就是使用 subscribe
(譯文訂閱) , 打個比方,A告訴B,說你每次吃完飯就通知我一聲,我去洗碗,被動去請求獲得對方的贊成,這是訂閱者。B收集訂閱者的時候能夠去作篩選是否通知A。
A不去獲得B的贊成,每次B吃完飯自動去洗碗,B無論他。最典型的莫過於window
的addEventListener
。B沒法拒絕,只能經過A主動解綁。
function subscribe(listener) {
// 校驗訂閱函數
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
// 若是當前派發的時候添加訂閱者,拋出一個錯誤,由於可能已經有部分action已經dispatch掉。不能保證通知到該listener
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
)
}
// ...other
}
複製代碼
要復現這個問題只須要阻塞 dispatch
函數中的 currentState = await currentReducer(currentState, action)
,不改源碼你能夠經過上文的方法也能作到
// App.js
const addShop = () => {
dispatch({
type:'ADD_SHOP',
data:{
...newShop, // ...商品數據
fn:() => subscribe(() => { // -----添加函數準備在dispatch的期間去執行它
console.log('我如今要再添加監聽者') // Error
})
}
})
}
複製代碼
而後在reducer
change state 的時候去執行它,
// src/store/reducer.js
export default (state = ShopState, action)=>{
let newState = {...state}
switch(action.type){
case 'ADD_SHOP': //添加商品
newState = {
...newState,
shopList:newState.shopList.concat(action.data)
}
action.data.fn() //---- 執行,報錯
break
default:
break
}
return newState
}
複製代碼
或者在中間件裏調用 subscribe
添加訂閱者也能達到相同的效果
固然,經過返回的函數你能夠取消訂閱
function subscribe(listen){
// ...other
let isSubscribed = true // 訂閱標記
ensureCanMutateNextListeners() // nextListener先拷貝currentListeners保存一次快照
nextListeners.push(listener) // 收集這次訂閱者,將在下次 dispatch 後更新該listener
return function unsubscribe() {
if (!isSubscribed) { // 屢次解綁,已經解綁就沒有必要再往下走了
return
}
// 一樣,在dispatch的時候,禁止 unsubscribed 當前listener
if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. ' +
'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
)
}
isSubscribed = false // 標記爲已經 unSubscribed
// 每次unsubscribe都要深拷貝一次 currentListeners 好讓nextListener拿到最新的 [listener] ,
ensureCanMutateNextListeners() // 再次保存一份快照,
// 再對 nextListeners(也就是下次dispatch) 取消訂閱當前listener。
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null // 防止污染 `ensureCanMutateNextListeners` 保存快照,使本次處理掉的listener被重用
}
}
function ensureCanMutateNextListeners() {
// 在 subscribe 和 unsubscribe 的時候,都會執行
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice() // 只有相同狀況才保存快照
}
}
複製代碼
什麼是快照,假如如今有3個listener [A,B,C]
, 遍歷執行,當執行到B
的時候(此時下標爲1),B
的內部觸發了unsubscribe
取消訂閱者B
,致使變成了[A,C]
,而此時下標再次變爲2的時候,本來應該是C
的下標此時變成了1,致使跳過C未執行。快照的做用是深拷貝當前listener,在深拷貝的listener上作事件subscribe與unSubscribe。不影響當前執行隊列
// 因此在dispatch的時候,須要明確將要發佈哪些listener
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
複製代碼
每次 dispatch()
調用以前都會保存一份快照。當你在正在調用監聽器 listener
的時候訂閱 subscribe
或者去掉訂閱 unsubscribe
,都會對當前隊列[A,B,C]
沒有任何影響,你影響的只有下次 dispatch
後的listener。
currentListeners
爲當前的listener
,nextListeners
爲下次dispatch
後才發佈的訂閱者集合
咱們模擬下使用場景
const cancelSub = subscribe(()=>{
if(getState().shopList.length>10) cancelSub() // 商品數量超過10個的時候,放棄訂閱更新
})
複製代碼
首先,假設目前有0個商品,
ensureCanMutateNextListeners
更新現有 currentListener
給 nextListener
(下回合的[listeners]
),subscribe
訂閱的事件收集到nextListeners
,不影響當前 CurrentListener
的發佈更新,cancelSub:unsubscribe
閉包函數,該函數能夠取消訂閱cancelSub:unsubscribe
函數被調用,isSubscribed
被標記爲0,表示當前事件已經被unSubscribed
。nextListener
爲下次 dispatch
後的[listeners]
。nextListener
上將當前 listener
移除。ensureCanMutateNextListeners
保存快照,使本次處理的listener被重用// 計算reducer,動態注入
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
currentReducer = nextReducer
// This action has a similiar effect to ActionTypes.INIT.
// Any reducers that existed in both the new and old rootReducer
// will receive the previous state. This effectively populates
// the new state tree with any relevant data from the old one.
dispatch({ type: ActionTypes.REPLACE })
}
複製代碼
結合路由能作到按需加載reducers,在項目工程較小的時候體驗不到這種優化,可是若是工程龐大的時候,initialState
和 dispatch
實際上是很耗性能的一件事,幾十個 Reducer
包含了成百上千個 switch
,難道一個個去case?
多個dispatch
上千次case 的情景你能夠想象一下。無從下手的性能優化或許能夠在這上面幫你一把。今天就帶你瞭解一下 reducer
的「按需加載」,官方稱它爲動態注入。
一般用來配合 webpack 實現 HMR hot module replacement
// src/store/reducers.js
import { combineReducers } from 'redux'
import { connectRouter } from 'connect-react-router'
import userReducer from '@/store/user/reducer'
const rootReducer = history => combineReducers({
...userReducer,
router:connectRouter(history)
})
export default rootReducer
// src/store/index.js
import RootReducer from './reducers'
export default function configureStore(preloadState){
const store = createStore(RootReducer,preloadState,enhancer)
if(module.hot){
module.hot.accpet('./reducers',()=>{ // 熱替換 reducers.js
const hotRoot = RootReducer(history) // require語法引入則須要加.default
store.replaceReducer(hotRoot)
})
}
return store
}
複製代碼
關於路由按需加載reducer,能夠參考以下思路,寫了個Demo,能夠在SandBoxCode上嘗試效果,去掉了其餘代碼,功能簡潔,以說明思路和實現功能爲主
store
和 views
關聯injectAsyncReducer
封裝動態替換方法,供 PrivateRoute
調用,reducers.js
CombineReducersProviteRoute
Code Spliting 與 執行生成 AsyncReducers
替換動做// src/store/reducer.js 合併Reducers
import { combineReducers } from 'redux';
import publicState from 'store/Public';
export default function createReducer(asyncReducers) {
return combineReducers({
public: publicState,
...asyncReducers // 異步Reducer
});
}
複製代碼
// src/store/index.js
import { createStore } from '../redux/index.js';
import createReducer from './reducers';
export default function configStore(initialState) {
const store = createStore(createReducer(),initialState);
store.asyncReducers = {}; // 隔離防止對store其餘屬性的修改
// 動態替換方法
function injectAsyncReducer(store, name, asyncReducer) {
store.asyncReducers[name] = asyncReducer;
store.replaceReducer(createReducer(store.asyncReducers));
}
return {
store,
injectAsyncReducer
};
}
複製代碼
// src/router/PrivateRoute.js
import React, { lazy, Suspense } from 'react';
import loadable from '@loadable/component'; // Code-spliting 也可使用Suspense+lazy
import { Route, Switch } from 'react-router-dom';
const PrivateRoute = (props) => {
const { injectAsyncReducer, store } = props;
const withReducer = async (name) => {
// 規定views和store關聯文件首字母大寫
const componentDirName = name.replace(/^\S/, s => s.toUpperCase());
const reducer = await import(`../store/${componentDirName}/index`);// 引入reducer
injectAsyncReducer(store, name, reducer.default);// 替換操做
return import(`../views/${componentDirName}`); // 返回組件
};
return (
<Suspense fallback={<div>loading...</div>}>
<Switch>
<Route {...props} exact path='/' name='main' component={lazy(() => withReducer('main'))} />
<Route {...props} exact path='/home' name='home' component={lazy(() => withReducer('home'))}/>
<Route {...props} exact path='/user' name='user' component={lazy(() => withReducer('user'))}/>
<Route {...props} exact path='/shopList' name='shopList' component={lazy(() => withReducer('shopList'))}/>
</Switch>
</Suspense>
);
};
export default PrivateRoute;
複製代碼
這只是一個按需提供reducer的demo。最後的效果
function observable() {
const outerSubscribe = subscribe;
return {
subscribe(observer) {
if (typeof observer !== 'object' || observer === null) {
throw new TypeError('Expected the observer to be an object.');
}
function observeState() {
if (observer.next) {
observer.next(getState()); // 將數據同步返回
}
}
observeState();
const unsubscribe = outerSubscribe(observeState);
return { unsubscribe }; // 解綁事件
},
[$$observable]() { // 經過symbol-observable建立全局惟一的觀察者
return this;
}
};
}
複製代碼
這個方法是爲Rxjs準備的, 用來觀察對象作出相應的響應處理。
observable本來在ReactiveX中,一個觀察者(Observer)訂閱一個可觀察對象(Observable)。觀察者對Observable發射的數據或數據序列做出響應。這種模式能夠極大地簡化併發操做,由於它建立了一個處於待命狀態的觀察者哨兵,在將來某個時刻響應Observable的通知,不須要阻塞等待Observable發射數據。
在實際業務中並未使用到,若是有興趣的能夠參考
至此,createStore.js完結,大哥大都走過了,還有幾個小菜雞你還怕麼?
combineReducers
用來將若干個reducer合併成一個reducers,使用方式:
combineReducers({
key:(state = {}, action)=>{
return state
},
post:(state = {}, action)=>{
return state
}
})
複製代碼
176行源碼碼大半部分全都是用來校驗數據、拋錯。
首當其衝是兩個輔助函數,用來 「友好」 的拋出提示信息
function getUndefinedStateErrorMessage(key, action) {
// 若是任意一個 reducer 返回的state undefined 會踩到這個雷
const actionType = action && action.type;
const actionDescription =
(actionType && `action "${String(actionType)}"`) || 'an action';
// 即便沒有值應該返回null,而不要返回undefined
return (
`Given ${actionDescription}, reducer "${key}" returned undefined. ` +
`To ignore an action, you must explicitly return the previous state. ` +
`If you want this reducer to hold no value, you can return null instead of undefined.`
);
}
function getUnexpectedStateShapeWarningMessage( inputState, reducers, action, unexpectedKeyCache ) {
const reducerKeys = Object.keys(reducers);
// 辨認這次操做來源是來自內部初始化仍是外部調用,大部分都是後者
const argumentName = action && action.type === ActionTypes.INIT
? 'preloadedState argument passed to createStore'
: 'previous state received by the reducer';
if (reducerKeys.length === 0) { // 合併成空的reducers也會報錯
return (
'Store does not have a valid reducer. Make sure the argument passed ' +
'to combineReducers is an object whose values are reducers.'
);
}
if (!isPlainObject(inputState)) { // state必須是個普通對象
return (
`The ${argumentName} has unexpected type of "` +
{}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
`". Expected argument to be an object with the following ` +
`keys: "${reducerKeys.join('", "')}"`
);
}
// 過濾 state 與 finalReducers(也就是combineReducer定義時的有效 reducers),
// 拿到 state 多餘的key值,好比 combineReducer 合併2個,但最後返回了3個對象
const unexpectedKeys = Object.keys(inputState).filter(
key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
);
// 標記警告這個值
unexpectedKeys.forEach(key => {
unexpectedKeyCache[key] = true;
});
// 辨別來源,replaceReducers表示設置這次替代Reducer,能夠被忽略
if (action && action.type === ActionTypes.REPLACE) {
return
;
}
// 告訴你有什麼值是多出來的,會被忽略掉
if (unexpectedKeys.length > 0) {
return (
`Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
`"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
`Expected to find one of the known reducer keys instead: ` +
`"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
);
}
}
複製代碼
還有一個輔助函數 assertReducerShape
用來判斷初始化和隨機狀態下返回的是否是 undefined
。
function assertReducerShape(reducers) {
Object.keys(reducers).forEach(key => {
// 遍歷 reducer
const reducer = reducers[key];
// 初始化該 reducer,獲得一個state值
const initialState = reducer(undefined, { type: ActionTypes.INIT });
// 因此通常reducer寫法都是 export default (state={},action)=>{ return state}
// 若是針對INIT有返回值,其餘狀態沒有仍然是個隱患
// 再次傳入一個隨機的 action ,二次校驗。判斷是否爲 undefined
const unknown = reducer(undefined, { type: ActionTypes.PROBE_UNKNOWN_ACTION() });
// 初始化狀態下 state 爲 undefined => 踩雷
if (typeof initialState === 'undefined') {
throw new Error(
`Reducer "${key}" returned undefined during initialization. ` +
`If the state passed to the reducer is undefined, you must ` +
`explicitly return the initial state. The initial state may ` +
`not be undefined. If you don't want to set a value for this reducer, ` +
`you can use null instead of undefined.`
);
}
// 隨機狀態下 爲 undefined => 踩雷
if (typeof unknown === 'undefined') {
throw new Error(
`Reducer "${key}" returned undefined when probed with a random type. ` +
`Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
`namespace. They are considered private. Instead, you must return the ` +
`current state for any unknown actions, unless it is undefined, ` +
`in which case you must return the initial state, regardless of the ` +
`action type. The initial state may not be undefined, but can be null.`
);
}
});
}
複製代碼
輔助打野都解決了,切輸出吧。
export default function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers);
const finalReducers = {};// 收集有效的reducer
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
if (process.env.NODE_ENV !== 'production') {
if (typeof reducers[key] === 'undefined') {
// 這個reducerKey 的 reducer是 undefined
warning(`No reducer provided for key "${key}"`);
}
}
if (typeof reducers[key] === 'function') {
// reducer必須是函數,無效的數據不會被合併進來
finalReducers[key] = reducers[key];
}
}
// 全部可用reducer
const finalReducerKeys = Object.keys(finalReducers);
// This is used to make sure we don't warn about the same
// keys multiple times.
let unexpectedKeyCache; // 配合getUnexpectedStateShapeWarningMessage輔助函數過濾掉多出來的值
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {};
}
let shapeAssertionError;
try {
assertReducerShape(finalReducers);//校驗reducers是否都是有效數據
} catch (e) {
shapeAssertionError = e; // 任何雷都接着
}
// 返回一個合併後的 reducers 函數,與普通的 reducer 同樣
return function combination(state = {}, action) {
if (shapeAssertionError) {
throw shapeAssertionError;
}
if (process.env.NODE_ENV !== 'production') {
// 開發環境下校驗有哪些值是多出來的
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
);
if (warningMessage) {
warning(warningMessage);
}
}
let hasChanged = false; // mark值是否被改變
const nextState = {};
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]; // reducerKey
const reducer = finalReducers[key]; // 對應的 reducer
const previousStateForKey = state[key]; // 改變以前的 state
// 對每一個reducer 作 dispatch,拿到 state 返回值
const nextStateForKey = reducer(previousStateForKey, action);
if (typeof nextStateForKey === 'undefined') { // 若是state是undefined就準備搞事情
const errorMessage = getUndefinedStateErrorMessage(key, action);
throw new Error(errorMessage);
}
nextState[key] = nextStateForKey; // 收錄這個reducer
// 檢測是否被改變過
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
// 若是沒有值被改變,就返回原先的值,避免性能損耗
return hasChanged ? nextState : state;
};
}
複製代碼
因爲這部分較於簡單,就直接過吧。
bindActionCreators
由父組件申明,傳遞給子組件直接使用,讓子組件感覺不到redux的存在,當成普通方法調用。
// 以import * 傳入的
import * as TodoActionCreators from './ActionCreators'
const todoAction = bindActionCreators(TodoActionCreators, dispatch) //綁定TodoActionCreators上全部的action
// 普通狀態
import { addTodoItem, removeTodoItem } from './ActionCreators'
const todoAction = bindActionCreators({ addTodoItem, removeTodoItem }, dispatch)
// 調用方法
todoAction.addTodoItem(args) //直接調用
todoAction.removeTodoItem(args)
複製代碼
翻到源碼,除去註釋就只有30行不到
function bindActionCreator(actionCreator, dispatch) {
// 用apply將action進行this顯示綁定
return function() {
return dispatch(actionCreator.apply(this, arguments));
};
}
export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
// 若是是函數直接綁定this
return bindActionCreator(actionCreators, dispatch);
}
if (typeof actionCreators !== 'object' || actionCreators === null) { // 校驗 action
throw new Error(
`bindActionCreators expected an object or a function, instead received ${ actionCreators === null ? 'null' : typeof actionCreators }. ` +
`Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
);
}
const boundActionCreators = {};
// 若是是以import * as actions 方式引入的
for (const key in actionCreators) {
const actionCreator = actionCreators[key];
if (typeof actionCreator === 'function') {
// 就遍歷成一個普通對象,其action繼續處理this顯示綁定
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
}
}
return boundActionCreators; // 將綁定後的actions返回
}
複製代碼
在這裏分享一些遇到的問題和技巧
在通常中型項目中一般會遇到這種問題: 代碼裏存在大量的 constants
常量和 actions
冗餘代碼
而後又跑到 shop/reducers
又定義一遍,這點量仍是少的,要是遇到大型項目就蛋疼了,reducer
action
constants
三個文件來回切,兩個屏幕都不夠切的。雖然能夠用 import * as types
方式所有引入,可是在業務組件裏仍是得這樣寫
import bindActionCreators from '../redux/bindActionCreators';
import * as shop from 'store/ShopList/actionCreators'; // 全部的action
function mapDispatchToProps(dispatch) {
return bindActionCreators(shop, dispatch);
}
複製代碼
優雅是要靠犧牲可讀性換來的,問題迴歸到本質,爲何要這麼作呢? 明確分工?利於查找?統一管理?協同規範?跟隨主流?
只要利於開發,利於維護,利於協同就夠了。
因此從業務關聯的reducer入手,將 reducer
和 action
合併起來,每一個業務單獨做爲一個 reducer 文件管理。每一個 reducer
只針對一個業務。形式有點像「按需加載」。
shop>store
內的負責全部 reducer
及 action
的建立。每一個文件單獨負責一塊內容業務。
import {
ADD_SHOP_BEGIN,
ADD_SHOP_FAIL,
ADD_SHOP_SUCCESS,
ADD_SHOP_FINALLY
} from '../constants';
export const addShopBegin = (payload) => ({
type: ADD_SHOP_BEGIN,
payload
});
export const addShopSuccess = (payload) => ({
type: ADD_SHOP_SUCCESS,
payload
});
export const addShopFail = (payload) => ({
type: ADD_SHOP_FAIL,
payload
});
export const addShopFinally = (payload) => ({
type: ADD_SHOP_FINALLY,
payload
});
export function reducer (state = { }, action) {
let newState = { ...state };
switch (action.type) {
case ADD_SHOP_BEGIN:
newState = {
...newState,
hasLoaded: !newState.hasLoaded
};
// begin doSomething
break;
case ADD_SHOP_SUCCESS:
// successful doSomething
break;
case ADD_SHOP_FAIL:
// failed doSomething
break;
case ADD_SHOP_FINALLY:
// whether doSomething
break;
default:
break;
}
return newState;
}
複製代碼
這樣作的好處是不用在兩個文件間來回切換,業務邏輯比較清晰,方便測試。
shop
模塊子業務的 actions.js
則負責整合全部的 action
導出。
export { addShopBegin, addShopSuccess, addShopFail, addShopFinally } from './store/add';
export { deleteShopBegin, deleteShopSuccess, deleteShopFail, deleteShopFinally } from './store/delete';
export { changeShopBegin, changeShopSuccess, changeShopFail, changeShopFinally } from './store/change';
export { searchShopBegin, searchShopSuccess, searchShopFail, searchShopFinally } from './store/search';
複製代碼
仍然負責上面全部的常量管理,但只在業務子模塊的store
內被引入
整合該業務模塊的全部 reducer
,建立核心 reducer
進行遍歷,這裏核心的一點是怎麼去遍歷全部的reducer。上代碼
import { reducer as addShop } from './store/add';
import { reducer as removeShop } from './store/delete';
import { reducer as changeShop } from './store/change';
import { reducer as searchShop } from './store/search';
const shopReducer = [ // 整合reducer
addShop,
removeShop,
changeShop,
searchShop
];
let initialState = {
hasLoaded: false
};
export default (state = initialState, action) => {
let newState = { ...state };
// 對全部reducer進行迭代。相似於compose
return shopReducer.reduce((preReducer, nextReducer) => {
return nextReducer(preReducer, action)
, newState);
};
複製代碼
在全局store內的reducers直接引用就能夠了
import { combineReducers } from '../redux';
import { connectRouter } from 'connected-react-router';
import history from 'router/history';
import publicState from 'store/Public';
import shopOperation from './Shop/reducers';
export default function createReducer(asyncReducers) {
return combineReducers({
router: connectRouter(history),
shop: shopOperation,
public: publicState,
...asyncReducers// 異步Reducer
});
}
複製代碼
業務組件內和正常調用便可。
import React from 'react';
import { addShopBegin } from 'store/Shop/actions';
import { connect } from 'react-redux';
import { bindActionCreators } from '../redux/index';
const Home = (props) => {
const { changeLoaded } = props;
return (
<div> <h1>Home Page</h1> <button onClick={() => changeLoaded(false)}>changeLoaded</button> </div>
);
};
function mapDispatchToProps(dispatch) {
return bindActionCreators({ changeLoaded: addShopBegin }, dispatch);
}
export default connect(null, mapDispatchToProps)(Home);
複製代碼
你可能會用chrome performance的火焰圖去查看整個網站的渲染時機和性能,網上教程也一大堆。
雖然知道整體性能,可是沒有更詳細的組件渲染週期,你不知道有哪些組件被屢次重渲染,佔用主線程過長,是否存在性能。這時候,你能夠點擊上圖左側的Timings。
經過這個,你能知道那些組件被重渲染哪些被掛載、銷燬、重建及更新。合理運用 Time Slicing + Suspense 異步渲染。
Chrome 獨有的原生Api
requestIdleCallback
。能夠在告訴瀏覽器,當你不忙(Cpu佔用較低)的時候執行這個回調函數,相似於script標籤的async 。 若是要考慮兼容性的話仍是用web Worker來作一些優先級較低的任務。
如今 Chrome Mac 版本 React Devtools 也有本身的performance了 官方傳送門
用React剛開始寫的組件基本不合規範,尤爲是組件嵌套使用的時候,同級組件更新引發的沒必要要組件更新,致使無心義的 render
,固然,使用React Hooks的時候這個問題尤爲嚴重,性能可行的狀況下視覺看不出來差別,當組件複雜度量級化時候,性能損耗就體現出來了。
只須要在主文件裏調用,建議加上環境限制,會有點卡
import React from 'react'
import whyDidYouUpdate from 'why-did-you-update'
if (process.env.NODE_ENV !== 'production') {
whyDidYouUpdate(React);
}
複製代碼
它會提示你先後值是否相同,是否改變過。是否是很神奇?
其大體原理是將
React.Component.prototype.componentDidUpdate
覆蓋爲一個新的函數,在其中進行了每次渲染先後的props
的深度比較,並將結果以友好直觀的方式呈現給用戶。但它有一個明顯的缺陷——若是某一組件定義了componentDidUpdate
方法,why-did-you-update
就失效了。參考文獻
拿到結果,分析緣由,合理使用 memo/PureComponent
優化純組件,將組件進一步細分。 useMemo/reselect
緩存計算結果。對於一些能夠異步加載的組件可使用 React.lazy
或 @loadable/component
code Spliting 。 避免沒必要要的 render
性能損耗。
這也是 Immutable
於是誕生的一點,經過不可變數據結構,避免了數據流被更改無所謂的觸發changed。
至此 Redux 源碼完整版刨析完畢。
因爲 react-redux
增長了hooks等功能,後續會出另外一篇文章,持續學習。共勉!
文中全部 源碼備註倉庫
參考文獻