Skip to content

Instantly share code, notes, and snippets.

@mutongwu
Last active October 11, 2018 08:43
Show Gist options
  • Save mutongwu/544cf3e8c1a4480b493f1c44d77a0b87 to your computer and use it in GitHub Desktop.
Save mutongwu/544cf3e8c1a4480b493f1c44d77a0b87 to your computer and use it in GitHub Desktop.
redux_monkey_patch

理解 redux 的middleware 中间件 机制: https://redux.js.org/advanced/middleware

假如对 store.dispatch 函数,做一些调整,比如 log、errorCatch 的事情,通常我们会想到 monkey-patch:

function addLog(store){
	var next = store.dispatch;

	store.dispatch = function(action){
		console.log('action before');
		next(action)
		console.log('action after');
		return action;
	}
}

function catchError(store) {
	var next = store.dispatch;
	
	store.dispatch = function(action){
		console.log('action before');
		next(action)
		console.log('action after');
		return action;
	}
}

addLog(store);
catchError(store);

看起来不错。但是,如果有多个 middleware需要处理,我们会考虑有一个 applayMiddleware 的函数,来帮助处理. 我们先把 middleware函数稍微修改一下,之前是直接赋值store.dispatch,我们可以直接返回新的dispatch 函数:

function addLog(store){
	var next = store.dispatch;
	
	return  function(action){
		console.log('action before');
		next(action)
		console.log('action after');
		return action;
	}
}

function catchError(store) {
	var next = store.dispatch;
	
	return function(action){
		console.log('action before');
		next(action)
		console.log('action after');
		return action;
	}
}

如此一来,applayMiddleware 就可以拿到最新的 dispatch 函数,重新赋值 给 store.dispatch。

function applayMiddleware(store, fns){
	var next = store.dispatch;
	fns.forEach( (fn) => {
		store.dispatch = fn(store);
	})
}

来到这里,我们会想,既然每一次都要重新赋值 store.dispatch,何不干脆把store.dispatch 函数做为参数,传递给 middleware ?

function addLog(next){	
	return  function(action){
		console.log('action before');
		next(action)
		console.log('action after');
		return action;
	}
}

function catchError(next) {
	return function(action){
		console.log('action before');
		next(action)
		console.log('action after');
		return action;
	}
}


function applayMiddleware(store, fns){
	var next = store.dispatch;
	fns.forEach( (fn) => {
		next = fn(next);
	})
	store.dispatch = next;
}

嗯嗯,事实上,来到这里,我们已经掌握了 middleware机制的诀窍:传递进来一个旧的函数,我包装一下,返回一个新的函数。

看一下我们的 middleware函数的结构:

function addLog(dispatch){
	return function(action){
		// xxxx
	}
}

跟 redux 好像还差一层 ?人家是:

function addLog(store){
	return function(dispatch){
		return function(action) {
			// xxxx
		}
	}
}

呃,为什么要再包装一层呢?因为redux 的 store还有 getState 等方法,中间件可能会需要,所以传递了 store,就把 middleware 再封装一层了。 因此,按照这种写法,我们的代码变成为:

 function addLog(store){
	return  function(next){
		return  function(action){
			console.log('action before');
			next(action)
			console.log('action after');
			return action;
		}
	}
}
function catchError(store){
	return function (next) {
		return function(action){
			try{
				next(action)
			}catch(e){
				doSomeLog(e)
			}
			return action;
		}
	}
}


function applyMiddleware(store, fns){
	var next = store.dispatch;
	fns.forEach( (fn) => {
		next = fn(store)(next); // 这里要执行2次。
	});
	store.dispatch = next;
}

这时候,我们的middle 函数也是:addLog = store => dispatch => action => { return dispatch(action)} 这种结构啦啦啦。 到此,我们其实已经完成 redux 的中间件核心思路了。 其它的,是 redux 在 函数调用技巧(比如:用一个compose 函数),和具体的实现了。

我们不妨看看 redux 的applyMiddleware 的源码:

export default 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)
    }
	// 这里先执行一次,就是最外层的 store 函数的执行。
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
	// chain 现在是2层结构了,传递 dispatch 函数给它
    dispatch = compose(...chain)(store.dispatch)

	// 最后,返回一个 "重写" 了dispatch 的 store。
    return {
      ...store,
      dispatch
    }
  }
}


再说一句,compose 函数,其实是自右向左的一个循环,每一个函数的返回(新的 dispatch),作为下一个函数的参数。————这是当然了,我们每一次都要重写 dispatch 嘛!

/**
 * Composes single-argument functions from right to left. The rightmost
 * function can take multiple arguments as it provides the signature for
 * the resulting composite function.
 *
 * @param {...Function} funcs The functions to compose.
 * @returns {Function} A function obtained by composing the argument functions
 * from right to left. For example, compose(f, g, h) is identical to doing
 * (...args) => f(g(h(...args))).
 */

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment