面试官:讲一下redux怎么处理异步数据流的?

糟了!是要看源码的感受!javascript

  嘛,有点标题党了,其实原问是applyMiddleware的实现细节,不过研究了一下感受最终的处理目的仍是为了应对异步数据流的场景,因此就安排上了。java

准备道具

  emm,我但愿阅读这篇文章的人都能有所收获(带哥能够略过)。所以,会先从一些比较基础的东西开始。git

闭包

  啥是闭包,简单点来说就是你在一个函数里返回了一个函数,在返回的这个函数内,你具备访问包裹它的函数做用域内的变量的能力。github

  通常来讲在咱们声明的函数体内声明变量,只会在函数被调用时在当前函数块的做用域内存在。当函数执行完毕后会垃圾回收。但,若是咱们返回的函数中存在对那个变量的引用,那这个变量便不会在函数调用后被销毁。也基于这一特性,延展出不少闭包的应用,如常见的防抖(debounce)、节流(throttle)函数,它们都是不断对内部的一个定时器进行操做;又如一些递归的缓存结果优化,也是设置了一个内部对象去比对结果来跳过一些冗余的递归场景。redux

// 一个比较常见的节流函数
function throttle(fn, wait) {
	let timeStart = 0;  // 不会被销毁,返回的函数执行时具备访问该变量的能力
	return function (...args) {
		let timeEnd = Date.now();
		if (timeEnd - timeStart > wait) {
			fn.apply(this, args);
			timeStart = timeEnd;
		}
	}
}
复制代码

HOC(高阶函数or组件)与Compose(组合)

  啥是高阶函数,其实跟上面的闭包的操做手段有点像,最终都会再返回一个函数。只不过它会根据你实际需求场景进行一些附加的操做来“加强”传入的原始函数的功能。像React中的一些HOC(高阶组件)的应用其实也是同理,毕竟class也不过是function的语法糖。网上的应用场景也不少,这里不赘述了。主要再提一嘴的是compose函数,它能让咱们在进行多层高阶函数嵌套时,书写代码更为清晰。如咱们有高阶函数A、B、C ,要实现A(B(C(...args)))的效果,若是没有compose,就须要不断地将返回结果赋值,调用。而使用compose,只须要一次赋值let HOC = compose(A, B, C);,而后调用HOC(...args)便可。数组

  瞅瞅compose源码,比较简单,无传参时,返回一个按传入返回的函数;一个入参时,直接返回第一个入参函数;多个则用数组的reduce方法进行迭代,最终返回组合后的结果:promise

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)))
}
复制代码

isPlainObject

  这个工具方法比较简单,就是来判断入参是不是由Object直接构造的且中间没有修改继承关系:缓存

let isObjectLike = obj => {
    return typeof obj === 'object' && obj !== null;
}

let isPlainObject = obj => {
    if (!isObjectLike(obj) || !Object.prototype.toString.call(obj) === '[object Object]') {
        return false;
    }
    if (Object.getPrototypeOf(obj) === null) return true; // Object.prototype 自己
    let proto = obj; // 拷贝指针,移动指针直至原型链顶端
    while (Object.getPrototypeOf(proto) !== null) { // 是否纯粹,若是中间发生继承,则__proto__的最终跨越将不会是1层
        proto = Object.getPrototypeOf(proto);
    }
    return Object.getPrototypeOf(obj) === proto;    
} 
复制代码

庖丁解牛

  在聊applyMiddleware前,咱们有必要先分析一波createStore内作了什么操做,由于他们俩实际上是一个相互成就依赖注入的关系。网络

createStore

function createStore(reducer, preloadedState, enhancer) {
// 略
// return {
// dispatch, // 去改变state的方法 派发 action
// subscribe, // 监听state变化 而后触发回调
// getState, // 访问这个createStore的内部变量currentState 也就是全局那个大state
// replaceReducer, // 传入新的reducer 来替换以前内部的reducer 可能场景是在代码拆分、redux的热加载?
// [$$observable]: observable // symbol属性 返回一个observable方法
// }
}
复制代码

  从源码中的声明能够看到,createStore接收三个参数,第一个是reducer,这个在项目中一般咱们会用combineReducers组合成一个大的reducer传入。这个combineReducers使用频率仍是很高的,先简要看看:session

combineReducers

function combineReducers(reducers) {
        // 略去一些
        return function combination(state = {}, action) {
            const nextState = {}
            for (let i = 0; i < finalReducerKeys.length; i++) {
            const key = finalReducerKeys[i]
            const reducer = finalReducers[key]
            const previousStateForKey = state[key]
            const nextStateForKey = reducer(previousStateForKey, action)
            if (typeof nextStateForKey === 'undefined') {
                const errorMessage = getUndefinedStateErrorMessage(key, action)
                throw new Error(errorMessage)
            }
            nextState[key] = nextStateForKey
            hasChanged = hasChanged || nextStateForKey !== previousStateForKey
            }
            return hasChanged ? nextState : state
        }
    }
	/** * 好比传入的子reducer函数是 * function childA(state = 0, action) { * switch (action.type) { * case 'INCREMENT': * return state + 1 * case 'DECREMENT': * return state - 1 * default: * return state * } * } * 那初始状况下的store.getState() // { childA: 0 } */
复制代码

  首先combineReducers接收一个对象,里面的key是每个小reducer文件或函数导出的namespacevalue则是与其对应的reducer函数实体。而后它会将这些不一样的reducer函数合并到一个reducer函数中。它会调用每个合并的子reducer,而且会将他们的结果放入一个state中,最后返回一个闭包使咱们能够像操做以前的子reducer同样操做这个大reducer

  preloadedState就是咱们传入的初始state,固然源码中的注释里描述还能够向服务端渲染中的应用注入该值or恢复历史用户的session记录,不过没实践过,就不延展了...

  最后的入参enhancer比较关键,字面理解就是用来加强功能的,先看看部分源码:

if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
}

if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
        throw new Error('Expected the enhancer to be a function.')
    }
    return enhancer(createStore)(reducer, preloadedState)
}
复制代码

  在这里咱们发现其实createStore能够只接收2个参数,当第二个参数为函数时,会自动初始化stateundefined,因此看到一些createStore只传了2个参数不要以为奇怪。

  而后往下看对enhancer函数的调用,这写法一看就是个高阶函数,接收一个方法createStore,而后返回一个函数。如今咱们能够把applyMiddleware抬上来了,这个API也是redux自己提供的惟一用于store enhancer的动做。

applyMiddleware

function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    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.'
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
	const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
复制代码

  咱们注意到applyMiddleware做为enhancer又把createStore这个函数做为参数传入并在内部返回函数中调用了,这其实也是依赖注入的理念。而后咱们发现内部其实将applyMiddleware的入参传入的中间件都执行了一次,传参为getStatedispatch。这里可能初见者比较懵逼,咱们先把早期处理异步action的中间件redux-thunk的源码翻出来看一眼:

redux-thunk

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;
复制代码

  经过代码,咱们能够得知通常middleWare的内部构造都听从一个({ getState, dispatch }) => next => action => {...}的范式,而且导出的时候已经被调用了一次,即返回了一个须要接收getStatedispatch的函数。

  Get到这一点之后,咱们再日后看。经过compose将中间件高阶组合并“加强”传入原store.dispatch的功能,最后再在返回值内解构覆盖原始storedispatch

  因此这个时候,若是我再问applyMiddleware作了什么?应该你们都知道答案了吧,就是加强了原始createStore返回的dispatch的功能。

  那再回到那个如何处理redux中的异步数据流问题?其实核心解决方案就是引入中间件,而中间件最终达成的目的就是加强咱们的原始dispatch方法。仍是以上面的redux-thunkmiddleware来讲,它传入的dispatch就是它内部的next,换言之,调用时,若是action是个普通对象,那就跟往常dispatch没啥差异,正常走reducer更新状态;但若是是个函数,那咱们就要让action本身玩了本身去处理内部的异步逻辑了,好比什么网络请求,当Promiseresolveddispatch一个成功actionrejecteddispatch一个失败action

redux-devtools-extension

  在开发环境中,为了追溯以及定位一些数据流向,咱们会引入redux-devtools-extension,这个模块有2种使用方式,一种是沉浸式,即在开发环境安装对应依赖,而后经过2次加强咱们的applyMiddleWare返回一个传入createStore中的enhancer,好比下面这样的:

import { composeWithDevTools } from 'redux-devtools-extension';

const composeEnhancers = composeWithDevTools(options);
const store = createStore(reducer, /* preloadedState, */ 
composeEnhancers(
  // 一个 enhancer入口 套中套
  applyMiddleware(...middleware),
  // other store enhancers if any
));
复制代码

  又或者是插件扩展式的:

const composeEnhancers = typeof window === 'object' && typeof window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ !== 'undefined' ?
 window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;

 // 剩下操做跟上面同样
复制代码

  更细节定制见官方

收工漫谈

  如今处理异步逻辑的中间件已经很多了,可是原理都是差很少的,只不过说从之前的传function,到PromiseGenerator控制之类的;像前文例子的redux-thunk是比较早的异步中间件了,以后社区中有了更多的方案提供:如redux-promiseredux-sagadvajsredux-observable等等。咱们仍是须要根据实际团队和业务场景使用最适合咱们的方案来组织代码编写。

简单回忆

  1. store自己的dispatch派发action更新数据这个动做是同步的。

  2. 所谓异步action,是经过引入中间件的方案加强dispatch后实现的。具体是applyMiddleware返回dispatch覆盖原始storedispatch,当action为函数时,进行定制的异步场景dispatch派发。

  3. 为什么会采起这种中间件加强的模式,我我的看来一是集中在一个位置方便统一控制处理,另外一个则是减小代码中的冗余判断模板。

相关文章
相关标签/搜索