findxc / blog

88 stars 5 forks source link

redux 源码学习 #52

Open findxc opened 3 years ago

findxc commented 3 years ago

redux 源码只有几百行,加上大量注释一起。如果觉得自己还不够理解 redux ,那就去看看源码吧,redux 实现很简单,看完源码会觉得 redux 真亲切,终于懂它了 😭

redux 是什么

redux 是一个状态管理库。

如果让你去设计一个状态管理库,你会怎么设计?这个库会提供哪些功能?

redux 的设计很简单,主要是三个功能:获取当前状态;更新状态;监听状态变化。

下面是一个很简单的例子:

// reducer: 根据当前 state 和 action ,返回新 state
function reducer(state = { count: 0 }, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 }
    default:
      return state
  }
}

const { createStore } = window.Redux
// 根据 reducer 来创建一个 store
const store = createStore(reducer)
// 注册一个监听器,在 dispatch 后会触发,在监听器里面可以通过 store.getState() 来拿到最新的 state ,比如 react-redux 这个库就是这样来监听 store 值变化然后去更新 react 视图的
store.subscribe(() => {
  console.log('subscribe: ', store.getState())
})

function add() {
  // dispatch 里面会调用 reducer 来更新 state ,更新后会执行所有通过 subscribe 注册的监听器
  store.dispatch({ type: 'INCREMENT' })
}

通过 store.getState() 来获取当前状态,通过 store.dispatch()reducer 来更新状态,通过 store.subscribe() 来监听状态变化。

redux 官网 是这样介绍自己的:A Predictable State Container for JS Apps 。为啥说是 Predictable 呢,因为 reducer 就是一个根据当前 state 和 action 返回新 state 的函数,只要当前 state 和 action 是确定的,那么会返回的新 state 其实就是确定的,所以说是 Predictable 。

createStore

createStore 是 redux 的核心,源码见 https://github.com/reduxjs/redux/blob/v4.1.1/src/createStore.js 。是一个函数,返回值是一个有 dispatch 、subscribe 、getState 等属性的对象,这里面又以 dispatch 最为核心。

dispatch 的代码其实很简单,主要代码如下:

try {
  isDispatching = true
  // dispatch 函数里面会执行 reducer ,而 reducer 的返回值就是新 state
  currentState = currentReducer(currentState, action)
} finally {
  isDispatching = false
}

// 然后再执行所有的监听器,在监听器里面可以通过调用 store.getState() 来获取最新 state
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
  const listener = listeners[i]
  listener()
}

发现没有,redux 的一次状态更新流程就是 dispatch —> reducer —> subscribe 的 listeners 。

combineReducers

在实际使用 redux 时,一般会一个功能模块一个 reducer ,这样就会有多个 reducer ,实际使用时我们需要通过 combineReducers 组装成一个大 reducer 传给 createStore ,比如 const reducer = combineReducers({ reducerA, reducerB })

https://github.com/reduxjs/redux/blob/v4.1.1/src/combineReducers.js#L157 代码中我们可以看出,对于多个 reducer ,一次 dispatch 会依次把所有 reducer 执行一次,那么对于 reducer 很多的项目来说,会不会有性能问题呢?因为一般来说,我们 dispatch 的 action 只会对应某一个 reducer ,其它的 reducer 并没必要执行。

redux 官方文档的 FAQ 里有讨论这个,见 https://redux.js.org/faq/performance#wont-calling-all-my-reducers-for-each-action-be-slow 。大概意思就是 JS 引擎一秒内可以跑特别多函数调用,所以并不会出现性能问题,当然如果你确实很在意这个问题,你也可以使用一些第三方库来优化。

enhancer

createStore 接收的第三个参数是 enhancer ,从 https://github.com/reduxjs/redux/blob/v4.1.1/src/createStore.js#L58 可以知道,当有 enhancer 时,createStore 的返回值是 enhancer(createStore)(reducer, preloadedState) ,这也说明 enhancer 的传入参数是 createStore ,返回值是一个函数,这个函数的传入参数是 reducer 和 preloadedState ,可以理解为返回值是一个增强后的 createStore ,因为本来 createStore 的传入参数也是 reducer 和 preloadedState 。

enhancer 是 redux 提供的来增强 redux 功能的,下面是两个简单的自定义 enhancer 例子:

const sayHiOnDispatch = (createStore) => {
  return (rootReducer, preloadedState, enhancers) => {
    const store = createStore(rootReducer, preloadedState, enhancers)

    function newDispatch(action) {
      const result = store.dispatch(action)
      console.log('Hi!')
      return result
    }

    return { ...store, dispatch: newDispatch }
  }
}

const includeMeaningOfLife = (createStore) => {
  return (rootReducer, preloadedState, enhancers) => {
    const store = createStore(rootReducer, preloadedState, enhancers)

    function newGetState() {
      return {
        ...store.getState(),
        meaningOfLife: 42,
      }
    }

    return { ...store, getState: newGetState }
  }
}

// 使用时就是 const store = createStore(rootReducer, sayHiOnDispatch)

compose

当有多个 enhancer 时,需要先用 compose 组装一下,再传给 createStore ,比如:

const composedEnhancer = compose(sayHiOnDispatch, includeMeaningOfLife)
const store = createStore(rootReducer, composedEnhancer)

呃,compose 函数里的 funcs.reduce((a, b) => (...args) => a(b(...args))) 使用很巧妙,但是不太好理解,源码如下:

// compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
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 实现了啥功能。

之前有说过, enhancer 的传入参数是 createStore ,返回值是一个增强后的 createStore 。如果有多个 enhancer ,经过 compose 后,比如 compose(fa, fb, fc) ,那最终其实就是 fc 接收 redux 原本的 createStore ,返回新的 createStore 丢给 fb ,然后 fb 根据这个新的 createStore 又返回一个新的 createStore 丢给 fc ,最终 fc 返回一个新的 createStore ,接收 reducer 来创建 store 。

下面几个代码片段的实际效果是一样的:

const composedEnhancer = compose(sayHiOnDispatch, includeMeaningOfLife)
const store = createStore(rootReducer, composedEnhancer)
const composedEnhancer = compose(sayHiOnDispatch, includeMeaningOfLife)
const newCreateStore = composedEnhancer(createStore)
const store = newCreateStore(rootReducer)
let newCreateStore = includeMeaningOfLife(createStore)
newCreateStore = sayHiOnDispatch(newCreateStore)
const store = newCreateStore(rootReducer)
let newCreateStore = createStore
;[includeMeaningOfLife, sayHiOnDispatch].forEach(func => {
  newCreateStore = func(newCreateStore)
})
const store = newCreateStore(rootReducer)

通过上面这几个代码例子应该就能理解 compose 的目的是啥了,也能理解 compose 源码中的那句注释 compose(f, g, h) 等同于 (...args) => f(g(h(...args))) 的意思了对吧。

但是我还是会觉得 funcs.reduce((a, b) => (...args) => a(b(...args))) 这个实现不太好理解,虽然确实很巧妙,可能看多了就熟了就懂了?😭😭😭

如果是你你会怎么去实现?

Lodash 有个 flow 函数,功能是类似的,但是实现会相对好理解一点,如下:

function flow(...funcs) {
  const length = funcs.length
  let index = length
  while (index--) {
    if (typeof funcs[index] !== 'function') {
      throw new TypeError('Expected a function')
    }
  }
  return function(...args) {
    let index = 0
    let result = length ? funcs[index].apply(this, args) : args[0]
    while (++index < length) {
      // 看,这里就是丢进 result 返回新的 result,下一个函数就会接收这个新的 result
      result = funcs[index].call(this, result)
    }
    return result
  }
}

applyMiddleware

弄懂了 compose 再去看 applyMiddleware 就没问题啦。

因为对 redux 的增强一般是对 dispatch 的增强,所以 redux 提供了一个 applyMiddleware 用来使用一些增强 dispatch 的中间件。

redux-thunk 就是一个增强 dispatch 的中间件,star 贼多 & 代码贼短,源码见 https://github.com/reduxjs/redux-thunk/blob/master/src/index.js 。在看源码之前很好奇为啥最后修改时间是 2019 年这么久没更新了,看了代码就懂了 … 就几行代码,根本没有更新的必要 …

通过 redux-thunk 代码你就能理解它的作用其实就是允许你 dispatch 一个函数(redux 默认是只能 dispatch 一个对象的),然后这个函数的传入参数有 dispatch ,所以你可以在这个函数里面去发请求,当请求返回之后再调用 dispatch 来更新 store 值。

const thunkFunction = (dispatch, getState) => {
  // logic here that can dispatch actions or read state
}

store.dispatch(thunkFunction)

store.getState()

当我们需要知道 store 当前状态值时,我们只能通过 store.getState() 来获取,为什么 redux 不做成直接使用 store.state 这种方式呢?

我个人理解这是对于第三方库很重要的一点,就是库内部的变量不要直接通过属性暴露出去,如果需要暴露就单独提供一个函数去暴露,因为你不知道开发者会如何去使用,比如开发者会不会直接 store.state = xxx 这样来赋值。

有次帮忙改 bug ,项目使用了一个第三方库,然后这个第三方库暴露了一堆属性,很多就类似于 a、b、c 这种很明显是内部用的属性,关键是还被我们项目使用了(其实有专门的函数去获取值的,只是当时项目开发者没注意,就直接用属性去取值了),然后某次迭代的时候这个属性没了就取不到值了 … ,开发者有责任,第三方库其实也可以做得更好。

还是写个总结

只能说看了 redux 源码后觉得真香,因为代码比较清晰比较简单,看了之后会更理解它的思想和工作流程。

当你不懂什么,就去看源码 hhhhhh

Cloudkkk commented 2 years ago

受教了!

Running53 commented 2 years ago

很不错的文章!👍