全面剖析 Redux 源码

2,575 阅读12分钟

个人Blog

编程模式

e.g.有一个公式,求连续自然数的平方和:

s = 1² + 2² + 3² + 4² + ... + N²

命令式编程

用命令式是这么解决问题的:

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

虽然说现在的你不会写出这样的代码,但以前肯定写过类似的。别说是同事,就算是自己过半个月回来也要熟悉一会亲手写的逻辑代码。

也称“业务型”编程,指的是用一步步下达命令最终去实现某个功能。行为过程不直观,只关心下一步应该怎么、然后再怎么、最后干什么,却对性能、易读性、复用性漠不关心。

因为需求的差异化、定制化太过严重,依赖于后端交互、并且函数式编程过于抽象,导致无法用函数式编程做到高效率开发,所以现在业务的实现,大多数都偏向于命令式编程。但是也带来很大的一个问题,过于重复,有位大佬(不知道谁)说过:“DRY(Don't Repeat YouSelf)”。最典型的情况莫在于产品让你写若干个后台列表筛选页面,每个页面只是字段不一样而已。有些要筛选框、下拉框、搜索建议、评论等,而有些只要输入框,即使高阶组件面对这种情况也不能做到太多复用效果。

函数式编程

函数式编程是声明式的一种 —— 最经典的Haskell(老读成HaSaKi)。近几年大量库所应用。和生态圈的各类组件中,它们的标签很容易辨认 —— 不可变数据(immutable)、高阶函数(柯里化)、尾递归、惰性序列等... 它最大的特点就是专一、简洁、封装性好。

用函数式编程解决这个问题:

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)

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 会降低性能,这里建立一个空函数在入口处判断警告开发者。 三个条件:

  • 非生产环境。
  • 函数有name , IE 不支持 Function.name。所以先要用 typeof 判断下
  • 但是名称已经被改变 isCrushed。 压缩后isCrushed.name !== 'isCrushed';

这里为什么要用 typeof isCrushed.nametypeof 有容错保护机制,保证不会程序崩溃。

alt

对外暴露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

createStore(reducer:any,preloadedState?:any,enhancer?:middleware),最终返回一个 state tree 实例。可以进行getStatesubscribe 监听和 dispatch 派发。

createStore 接收3个参数

  • reducer: Function。给定当前state tree和要执行的action,返回下一个state tree
  • preloadedState?: any,initial state tree
  • enhancer?:middle, 增强器,若干个中间件可以通过 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)

applyMiddleware源代码

// 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.jsreturn 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)传入............

alt

然后就通过调用此时的dispatch达到一样的效果,上面已经介绍的很详细了,如果不熟悉的话,建议多看几遍。

三番四次扯到中间件,到底是什么东西?

中间件

中间件说起来也不陌生,至于什么是中间件,维基百科的解释大家自行查找,本来只有一个词不懂,看了 Wiki 变成七八个词不懂。

在 JavaScript 里不管是前端还是 Node,都涉及颇广

Ps:Reduxmiddlewarekoa 流程机制不完全一样。具体的区别可以参考 Perkin 的 Redux,Koa,Express之middleware机制对比,本段 koa 内容已隐藏,同学们可选择性去了解。

首先了解下 Reduxmiddleware ,正常流程上来说,和 koa 是一致的,但是如果在某个正在执行的 middleware 里派发 action,那么将会立即“中断” 并且重置当前 dispatch

alt
栗子:

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的栗子,你可以了解或者跳过

借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

alt

上图被称为 洋葱模型,很清晰的表明了一个请求是如何经过中间件最后生成响应。

举一个实际的例子,你每天回到家,假设家门是个中间件,你的卧室门也是个中间件,你是一个请求。那么你必须先进家门,再进卧室的门,你想再出去就必须先出卧室的门,再出家门。需要遵守的是,你必须原路倒序返回。 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       |            |           |
        |          |            |                                 |            |           |
+---------------------------------------------------------------------------------------------------->
        |          |            |                                 |            |           |
        |          |            |                                 |            |           |
        |          |            +---------------------------------+            |           |
        |          +-----------------------------------------------------------+           |
        +----------------------------------------------------------------------------------+

最后再次提示:KoaReduxmiddleware 机制除了特殊状态下是一致的,特殊状态:在某个 middleware 内调用 dispatch

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行测试用例...谨慎打开,但是

alt

subscribe 添加订阅者

可能有些同学不太清楚订阅者模式和监听者模式的区别

订阅(subscribe)者模式

redux中就是使用 subscribe (译文订阅) , 打个比方,A告诉B,说你每次吃完饭就通知我一声,我去洗碗,被动去请求得到对方的同意,这是订阅者。B收集订阅者的时候可以去做筛选是否通知A。

监听(listen)者

A不去得到B的同意,每次B吃完饭自动去洗碗,B不管他。最典型的莫过于windowaddEventListener。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更新现有 currentListenernextListener(下回合的[listeners]),
  • subscribe 订阅的事件收集到nextListeners,不影响当前 CurrentListener 的发布更新,
  • 我们得到一个cancelSub:unsubscribe 闭包函数,该函数可以取消订阅
  • 前10次正常发布更新,
  • 在第11次执行的时候,商品数量增加到了11个
  • 逻辑命中 cancelSub:unsubscribe 函数被调用,isSubscribed被标记为0,表示当前事件已经被unSubscribed
  • 再次保存一份快照,nextListener 为下次 dispatch 后的[listeners]
  • nextListener 上将当前 listener 移除。
  • 置空 currentListeners ,清除缓存,防止污染 ensureCanMutateNextListeners 保存快照,使本次处理的listener被重用

replaceReducer 动态注入

源码

// 计算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,在项目工程较小的时候体验不到这种优化,但是如果工程庞大的时候,initialStatedispatch 其实是很耗性能的一件事,几十个 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按需加载

关于路由按需加载reducer,可以参考如下思路,写了个Demo,可以在SandBoxCode上尝试效果,去掉了其他代码,功能简洁,以说明思路和实现功能为主

  • Home、Index等同名文件为新增的 storeviews 关联
  • injectAsyncReducer封装动态替换方法,供 PrivateRoute 调用,
  • reducers.js CombineReducers
  • ProviteRoute 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。最后的效果

observable

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

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

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返回
}

后话

在这里分享一些遇到的问题和技巧

怎么管理大量的reducer、action、constants?

在一般中型项目中通常会遇到这种问题: 代码里存在大量的 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入手,将 reduceraction 合并起来,每个业务单独作为一个 reducer 文件管理。每个 reducer 只针对一个业务。形式有点像“按需加载”。

alt

shop>store 内的负责所有 reduceraction 的创建。每个文件单独负责一块内容业务。

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;
}

这样做的好处是不用在两个文件间来回切换,业务逻辑比较清晰,方便测试。

actions

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';

constants

仍然负责上面所有的常量管理,但只在业务子模块的store 内被引入

reducers

整合该业务模块的所有 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.js

在全局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);

react 如何查看每个组件渲染性能

你可能会用chrome performance的火焰图去查看整个网站的渲染时机和性能,网上教程也一大堆。

alt

  • 紫色的代表style样式计算/layout
  • 黄色代表js操作
  • 蓝色代表html解析
  • 灰色代表其他操作
  • 红色框里代表这个地方存在 强制回流 、long task、FPS低、CPU 占用过多

虽然知道总体性能,但是没有更详细的组件渲染周期,你不知道有哪些组件被多次重渲染,占用主线程过长,是否存在性能。这时候,你可以点击上图左侧的Timings。

通过这个,你能知道那些组件被重渲染哪些被挂载、销毁、重建及更新。合理运用 Time Slicing + Suspense 异步渲染。

Chrome 独有的原生Api requestIdleCallback。可以在告诉浏览器,当你不忙(Cpu占用较低)的时候执行这个回调函数,类似于script标签的async 。 如果要考虑兼容性的话还是用web Worker来做一些优先级较低的任务。

现在 Chrome Mac 版本 React Devtools 也有自己的performance了 官方传送门

why-did-you-update

用React刚开始写的组件基本不合规范,尤其是组件嵌套使用的时候,同级组件更新引起的不必要组件更新,导致无意义的 render ,当然,使用React Hooks的时候这个问题尤其严重,性能可行的情况下视觉看不出来差异,当组件复杂度量级化时候,性能损耗就体现出来了。

只需要在主文件里调用,建议加上环境限制,会有点卡

import React from 'react'
import whyDidYouUpdate from 'why-did-you-update'
if (process.env.NODE_ENV !== 'production') {
  whyDidYouUpdate(React);
}

alt

它会提示你前后值是否相同,是否改变过。是不是很神奇?

其大致原理是将 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等功能,后续会出另一篇文章,持续学习。共勉!

文中所有 源码备注仓库

参考文献