我的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等功能,后续会出另外一篇文章,持续学习。共勉!
文中全部 源码备注仓库
参考文献