Redux
是一种架构模式(Flux
架构的一种变种),它不关注你到底用什么库,你可以把它应用到 React
和 Vue
,甚至跟 jQuery
结合都没有问题。而 React-redux
就是把 Redux
这种架构模式和 React.js
结合起来的一个库,就是 Redux
架构在 React.js
中的体现。
See the Pen
修改共享状态 by whjin (@whjin)
on CodePen.
很简单,renderApp
会调用 rendeTitle
和 renderContent
,而这两者会把 appState
里面的数据通过原始的 DOM
操作更新到页面上。
这是一个很简单的 App
,但是它存在一个重大的隐患,我们渲染数据的时候,使用的是一个共享状态 appState
,每个人都可以修改它。
renderApp(appState)
之前执行了一大堆函数操作,你根本不知道它们会对 appState
做什么事情,renderApp(appState)
的结果根本没法得到保障。一个可以被不同模块任意修改共享的数据状态就是魔鬼,一旦数据可以任意修改,所有对共享状态的操作都是不可预料的。
但不同的模块(组件)之间确实需要共享数据,这些模块(组件)还可能需要修改这些共享数据,就像上一节的“主题色”状态(themeColor
)。这里的矛盾就是:“模块(组件)之间需要共享数据”,和“数据可能被任意修改导致不可预料的结果”之间的矛盾。
所有对数据的操作必须通过 dispatch
函数。它接受一个参数 action
,这个 action
是一个普通的 JavaScript
对象,里面必须包含一个 type
字段来声明你到底想干什么。dispatch
在 swtich
里面会识别这个 type
字段,能够识别出来的操作才会执行对 appState
的修改。
上面的 dispatch
它只能识别两种操作,一种是 UPDATE_TITLE_TEXT
它会用 action
的 text
字段去更新 appState.title.text
;一种是 UPDATE_TITLE_COLOR
,它会用 action
的 color
字段去更新 appState.title.color
。可以看到,action
里面除了 type
字段是必须的以外,其他字段都是可以自定义的。
任何的模块如果想要修改 appState.title.text
,必须大张旗鼓地调用 dispatch
。
现在我们把它们集中到一个地方,给这个地方起个名字叫做 store
,然后构建一个函数 createStore
,用来专门生产这种 state
和 dispatch
的集合,这样别的 App
也可以用这种模式了。
createStore
接受两个参数,一个是表示应用程序状态的 state
;另外一个是 stateChanger
,它来描述应用程序状态会根据 action
发生什么变化,其实就是相当于本节开头的 dispatch
代码里面的内容。
createStore
会返回一个对象,这个对象包含两个方法 getState
和 dispatch
。
getState
用于获取 state
数据,其实就是简单地把 state
参数返回。
dispatch
用于修改数据,和以前一样会接受 action
,然后它会把 state
和 action
一并传给 stateChanger
,那么 stateChanger
就可以根据 action
来修改 state
了。
针对每个不同的 App
,我们可以给 createStore
传入初始的数据 appState
,和一个描述数据变化的函数 stateChanger
,然后生成一个 store
。需要修改数据的时候通过 store.dispatch
,需要获取数据的时候通过 store.getState
。
我们每次通过 dispatch
修改数据的时候,其实只是数据发生了变化,如果我们不手动调用 renderApp
,页面上的内容是不会发生变化的。但是我们总不能每次 dispatch
的时候都手动调用一下 renderApp
,我们肯定希望数据变化的时候程序能够智能一点地自动重新渲染数据,而不是手动调用。
你说这好办,往 dispatch
里面加 renderApp
就好了,但是这样 createStore
就不够通用了。我们希望用一种通用的方式“监听”数据变化,然后重新渲染页面,这里要用到观察者模式。
function createStore(state, stateChanger) {
const listeners = [];
const subscribe = (listener) => listeners.push(listener);
const getState = () => state;
const dispatch = (action) => {
stateChanger(state, action);
listeners.forEach((listener) => listener())
};
return {getState, dispatch, subscribe}
}
我们在 createStore
里面定义了一个数组 listeners
,还有一个新的方法 subscribe
,可以通过 store.subscribe(listener)
的方式给 subscribe
传入一个监听函数,这个函数会被 push
到数组当中。
我们修改了 dispatch
,每次当它被调用的时候,除了会调用 stateChanger
进行数据的修改,还会遍历 listeners
数组里面的函数,然后一个个地去调用。相当于我们可以通过 subscribe
传入数据变化的监听函数,每当 dispatch
的时候,监听函数就会被调用,这样我们就可以在每当数据变化时候进行重新渲染。
我们只需要 subscribe
一次,后面不管如何 dispatch
进行修改数据,renderApp
函数都会被重新调用,页面就会被重新渲染。这样的订阅模式还有好处就是,以后我们还可以拿同一块数据来渲染别的页面,这时 dispatch
导致的变化也会让每个页面都重新渲染。
See the Pen
Redux-抽离 store 和监控数据变化 by whjin (@whjin)
on CodePen.
总结
现在我们有了一个比较通用的 createStore
,它可以产生一种我们新定义的数据类型 store
,通过 store.getState
我们获取共享状态,而且我们约定只能通过 store.dispatch
修改共享状态。store
也允许我们通过 store.subscribe
监听数据数据状态被修改了,并且进行后续的例如重新渲染页面的操作。
一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。
纯函数很严格,也就是说你几乎除了计算数据以外什么都不能干,计算的时候还不能依赖除了函数参数以外的数据。
我们新建一个 appState
,新建 appState.title
,新建 appState.title.text
。
appState
和 newAppState
其实是两个不同的对象,因为对象浅复制const appState2={...appState}
的缘故,其实它们里面的属性 content
指向的是同一个对象;但是因为 title
被一个新的对象覆盖了,所以它们的 title
属性指向的对象是不同的。
我们每次修改某些数据的时候,都不会碰原来的数据,而是把需要修改数据路径上的对象都 copy
一个出来。
修改数据的时候就把修改路径都复制一遍,但是保持其他内容不变,最后的所有对象具有某些不变共享的结构(例如上面三个对象都共享 content
对象)。大多数情况下我们可以保持 50%
以上的内容具有共享结构,这种操作具有非常优良的特性,我们可以用它来优化上面的渲染性能。
优化性能
我们修改 stateChanger
,让它修改数据的时候,并不会直接修改原来的数据 state
,而是产生上述的共享结构的对象。
每次需要修改的时候都会产生新的对象,并且返回。而如果没有修改(在 default
语句中)则返回原来的 state
对象。
因为 stateChanger
不会修改原来对象了,而是返回对象,所以我们需要修改一下 createStore
。让它用每次 stateChanger(state, action)
的调用结果覆盖原来的 state
。
stateChanger
现在既充当了获取初始化数据的功能,也充当了生成更新数据的功能。
如果有传入 state
就生成更新数据,否则就是初始化数据。这样我们可以优化 createStore
成一个参数,因为 state
和 stateChanger
合并到一起了。
createStore
内部的 state
不再通过参数传入,而是一个局部变量 let state = null
。createStore
的最后会手动调用一次 dispatch({})
,dispatch
内部会调用 stateChanger
,这时候的 state
是 null
,所以这次的 dispatch
其实就是初始化数据了。createStore
内部第一次的 dispatch
导致 state
初始化完成,后续外部的 dispatch
就是修改数据的行为了。
这是一个最终形态的 createStore
,它接受的参数叫 reducer
,reducer
是一个函数,细心的朋友会发现,它其实是一个纯函数。
reducer
createStore
接受一个叫 reducer
的函数作为参数,这个函数规定是一个纯函数,它接受两个参数,一个是 state
,一个是 action
。
如果没有传入 state
或者 state
是 null
,那么它就会返回一个初始化的数据。如果有传入 state
的话,就会根据 action
来“修改“数据,但其实它没有、也规定不能修改 state
,而是要通过上节所说的把修改路径的对象都复制一遍,然后产生一个新的对象返回。如果它不能识别你的 action
,它就不会产生新的数据,而是(在 default
内部)把 state
原封不动地返回。
reducer
是不允许有副作用的。你不能在里面操作 DOM
,也不能发 Ajax
请求,更不能直接修改 state
,它要做的仅仅是 —— 初始化和计算新的 state
。
你必须通过 dispatch
执行某些允许的修改操作,而且必须大张旗鼓的在 action
里面声明。
这种模式挺好用的,我们就把它抽象出来一个 createStore
,它可以产生 store
,里面包含 getState
和 dispatch
函数,方便我们使用。
后来发现每次修改数据都需要手动重新渲染非常麻烦,我们希望自动重新渲染视图。所以后来加入了订阅者模式,可以通过 store.subscribe
订阅数据修改事件,每次数据更新的时候自动重新渲染视图。
我们优化了 stateChanger
为 reducer
,定义了 reducer
只能是纯函数,功能就是负责初始化 state
,和根据 state
和 action
计算具有共享结构的新的 state
。
原文链接:React.js 小书