最近在复习 React 的过程当中,在网上发现了一篇关于 Redux 手动实现方式的 系列文章 ,收益良多,所以打算在这里汇总整理一下,一方面加深一下对于 Redux 的理解,另一方面也方便以后可以经常回来温习温习,内容有所删减和补充,更多内容可以参考原文
我们都知道,Redux 和 React-Redux 并不是同一个东西,Redux 是一种架构模式(Flux 架构的一种变种),它不关注你到底用什么库,你可以把它应用到 React 和 Vue,甚至跟 jQuery 结合都没有问题,而 React-Redux 就是把 Redux 这种架构模式和 React 结合起来的一个库,就是 Redux 架构在 React 中的体现(关于 React-Redux 的实现我们会在后面进行介绍)
所以在这里我们也就不直接介绍 Redux 当中的 reducers、actions、store 等这些 API 的关系和用法,而是从一个示例开始,一步一步来进行推演
共享状态的修改 我们先从示例的搭建开始,页面结构如下,很简单的两个容器,一个标题,一个内容
1 2 <div id ='title' > </div > <div id ='content' > </div >
下面我们再来添加一些用于渲染的数据和几个渲染函数,它们的作用是将我们的数据渲染到上面的容器当中,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const appState = { title: { text: '标题' , color: 'red' , }, content: { text: '内容' , color: 'blue' } } function renderApp (appState ) { renderTitle(appState.title) renderContent(appState.content) } function renderTitle (title ) { const titleDOM = document .getElementById('title' ) titleDOM.innerHTML = title.text titleDOM.style.color = title.color } function renderContent (content ) { const contentDOM = document .getElementById('content' ) contentDOM.innerHTML = content.text contentDOM.style.color = content.color } renderApp(appState)
逻辑比较简单,我们调用了 renderApp() 方法,它会分别再去调用 rendeTitle() 和 renderContent(),而这两者会把 appState 里面的数据通过原始的 DOM 操作更新到页面上,很明显的页面当中会显现红色的标题和蓝色的内容文字
内容虽然十分简单,但是这里存在一个重大隐患,那就是我们在渲染数据的时候,使用的是一个共享状态 appState,这也就意味着每个人都可以修改它,如果我们在 renderApp(appState) 之前执行了一大堆函数操作,你可能根本不知道它们会对 appState 做什么事情,所以 renderApp(appState) 的结果根本没法得到保障
所以我们来看看如何解决这个问题,针对这种情况我们约定,数据并不能直接去进行修改,如果想要修改,必须显式的声明你想要修改哪些数据,所以我们定义一个函数,叫 dispatch,它专门负责数据的修改
1 2 3 4 5 6 7 8 9 10 11 12 function dispatch (action ) { switch (action.type) { case 'UPDATE_TITLE_TEXT' : appState.title.text = action.text break case 'UPDATE_TITLE_COLOR' : appState.title.color = action.color break default : break } }
所有对数据的操作必须通过 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
任何的模块如果想要修改 appState.title.text,必须大张旗鼓地调用 dispatch
1 2 3 4 5 6 renderApp(appState) dispatch({ type : 'UPDATE_TITLE_TEXT' , text : 'newTitle' }) dispatch({ type : 'UPDATE_TITLE_COLOR' , color : 'green' }) renderApp(appState)
这样一来,我们就不需要担心在 renderApp(appState) 之前的某些函数操作,因为我们规定不能直接修改 appState,它们对 appState 的修改必须只能通过 dispatch,而我们看看 dispatch 的实现可以知道,你只能修改 title.text 和 title.color,对于原来的模块(组件)修改共享数据的方式是可以直接修改的,也就如下图所示
我们很难把控每一根指向 appState 的箭头,appState 里面的东西就无法把控,但现在我们必须通过一个中间人(dispatch),所有的数据修改必须通过它
这样一来我们就不用担心共享数据状态的修改问题了,我们只要把控住 dispatch,所有对 appState 的修改就无所遁形,毕竟只有一根箭头指向 appState 了
监控数据变化 现在我们有了 appState 和 dispatch,所以我们将它们集中起来,起个名字叫做 store,然后构建一个函数 createStore,用来专门生产这种 state 和 dispatch 的集合,这样别的 App 也可以用这种模式了
1 2 3 4 5 function createStore (state, stateChanger ) { const getState = () => state const dispatch = (action ) => stateChanger(state, action) return { getState, dispatch } }
createStore 接受两个参数,一个是表示应用程序状态的 state,另外一个是 stateChanger,它来描述应用程序状态会根据 action 发生什么变化,createStore 会返回一个对象,这个对象包含两个方法 getState 和 dispatch,getState 用于获取 state 数据,其实就是简单地把 state 参数返回,dispatch 用于修改数据,它会把 state 和 action 一并传给 stateChanger
现在我们就可以利用 createStore 来修改数据生成的方式了,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const appState = { title: { text: '标题' , color: 'red' , }, content: { text: '内容' , color: 'blue' } } function stateChanger (state, action ) { switch (action.type) { case 'UPDATE_TITLE_TEXT' : state.title.text = action.text break case 'UPDATE_TITLE_COLOR' : state.title.color = action.color break default : break } } const store = createStore(appState, stateChanger)renderApp(store.getState()) store.dispatch({ type : 'UPDATE_TITLE_TEXT' , text : 'newTitle' }) store.dispatch({ type : 'UPDATE_TITLE_COLOR' , color : 'green' }) renderApp(store.getState())
针对每个不同的 App,我们可以给 createStore 传入初始的数据 appState,和一个描述数据变化的函数 stateChanger,然后生成一个 store,需要修改数据的时候通过 store.dispatch,需要获取数据的时候通过 store.getState
但是此时还存在一些问题,就是我们每次通过 dispatch 去修改数据的时候,都需要手动的调用 renderApp() 才能重新渲染页面,我们来稍微的调整一下,即通过监听的方式,一旦数据有所变化,就会自动的重新渲染页面,所以这里就会用到观察者模式
1 2 3 4 5 6 7 8 9 10 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 数组里面的函数,然后一个个地去调用,这样我们就可以在每当数据变化时候进行重新渲染
1 2 3 4 5 6 7 8 9 const store = createStore(appState, stateChanger)store.subscribe(() => renderApp(store.getState())) renderApp(store.getState()) store.dispatch({ type : 'UPDATE_TITLE_TEXT' , text : 'newTitle' }) store.dispatch({ type : 'UPDATE_TITLE_COLOR' , color : 'green' })
共享数据的性能优化 如果细心观察我们之前的示例,是可以发现其中是有比较严重的性能问题,即每当更新数据的时候就重新渲染整个 App,比如我们之前只是修改了 Title 当中的内容,但是 Content 中的内容也同步会被更新
这里提出的解决方案是,在每个渲染函数执行渲染操作之前先做个判断,判断传入的新数据和旧的数据是不是相同,相同的话就不渲染了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function renderApp (newAppState, oldAppState = {} ) { if (newAppState === oldAppState) return renderTitle(newAppState.title, oldAppState.title) renderContent(newAppState.content, oldAppState.content) } function renderTitle (newTitle, oldTitle = {} ) { if (newTitle === oldTitle) return const titleDOM = document .getElementById('title' ) titleDOM.innerHTML = newTitle.text titleDOM.style.color = newTitle.color } function renderContent (newContent, oldContent = {} ) { if (newContent === oldContent) return const contentDOM = document .getElementById('content' ) contentDOM.innerHTML = newContent.text contentDOM.style.color = newContent.color }
然后我们用一个 oldState 变量保存旧的应用状态,在需要重新渲染的时候把新旧数据传进入去
1 2 3 4 5 6 7 8 9 const store = createStore(appState, stateChanger)let oldState = store.getState() store.subscribe(() => { const newState = store.getState() renderApp(newState, oldState) oldState = newState })
但是仔细观察我们的 state 可以发现,上面的代码根本无法达到我们想要的效果
1 2 3 4 5 6 7 8 9 10 11 12 function stateChanger (state, action ) { switch (action.type) { case 'UPDATE_TITLE_TEXT' : state.title.text = action.text break case 'UPDATE_TITLE_COLOR' : state.title.color = action.color break default : break } }
因为即使我们修改了 state.title.text,但是 state 还是那个 state,每次调用 getState() 的时候返回的还是那个 state,所以我们来调整一下 stateChanger,让它修改数据的时候,并不会直接修改原来的数据 state,而是返回一个新的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function stateChanger (state, action ) { switch (action.type) { case 'UPDATE_TITLE_TEXT' : return { ...state, title: { ...state.title, text: action.text } } case 'UPDATE_TITLE_COLOR' : return { ...state, title: { ...state.title, color: action.color } } default : return state } }
因为 stateChanger 不会修改原来对象了,而是返回对象,所以我们需要修改一下 createStore,让它用每次 stateChanger(state, action) 的调用结果覆盖原来的 state
1 2 3 4 5 6 7 8 9 10 function createStore (state, stateChanger ) { const listeners = [] const subscribe = (listener ) => listeners.push(listener) const getState = () => state const dispatch = (action ) => { state = stateChanger(state, action) listeners.forEach((listener ) => listener()) } return { getState, dispatch, subscribe } }
Reducer 现在我们已经有了一个比较通用的 createStore,但是我们还可以将 appState 和 stateChanger 合并到一起,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function stateChanger (state, action ) { if (!state) { return { title: { text: '标题' , color: 'red' , }, content: { text: '内容' , color: 'blue' } } } switch (action.type) { case 'UPDATE_TITLE_TEXT' : return { ...state, title: { ...state.title, text: action.text } } case 'UPDATE_TITLE_COLOR' : return { ...state, title: { ...state.title, color: action.color } } default : return state } }
stateChanger 现在既充当了获取初始化数据的功能,也充当了生成更新数据的功能,如果有传入 state 就生成更新数据,否则就是初始化数据,这样一来我们就可以优化 createStore 成一个参数,因为 state 和 stateChanger 合并到一起了
1 2 3 4 5 6 7 8 9 10 11 12 function createStore (stateChanger ) { let state = null const listeners = [] const subscribe = (listener ) => listeners.push(listener) const getState = () => state const dispatch = (action ) => { state = stateChanger(state, action) listeners.forEach((listener ) => listener()) } dispatch({}) return { getState, dispatch, subscribe } }
createStore 内部的 state 不再通过参数传入,而是一个局部变量 let state = null,createStore 的最后会手动调用一次 dispatch({}),dispatch 内部会调用 stateChanger,这时候的 state 是 null,所以这次的 dispatch 其实就是初始化数据了,createStore 内部第一次的 dispatch 导致 state 初始化完成,后续外部的 dispatch 就是修改数据的行为了
最后,我们给 stateChanger 起一个比较通用的名字,那就是 reducer,所以我们最终版本的 createStore 如下
1 2 3 4 5 6 7 8 9 10 11 12 function createStore (reducer ) { let state = null const listeners = [] const subscribe = (listener ) => listeners.push(listener) const getState = () => state const dispatch = (action ) => { state = reducer(state, action) listeners.forEach((listener ) => listener()) } dispatch({}) return { getState, dispatch, subscribe } }
createStore 接受一个叫 reducer 的函数作为参数,这个函数规定是一个纯函数,它接受两个参数,一个是 state,一个是 action
如果没有传入 state 或者 state 是 null,那么它就会返回一个初始化的数据
如果有传入 state 的话,就会根据 action 来修改数据(其实是返回一个合并后的新对象)
如果它不能识别你的 action,它就不会产生新的数据,而是(在 default 内部)把 state 原封不动地返回
reducer 是不允许有副作用的,你不能在里面操作 DOM,也不能发 Ajax 请求,更不能直接修改 state,它要做的仅仅只是初始化和计算新的 state
总结 至此,我们的 createStore 可以直接拿来使用了,方式就是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function reducer (state, action ) { } const store = createStore(reducer)store.subscribe(() => renderApp(store.getState())) renderApp(store.getState()) store.dispatch(...)
最终的汇总代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 function renderApp (newAppState, oldAppState = {} ) { if (newAppState === oldAppState) return renderTitle(newAppState.title, oldAppState.title) renderContent(newAppState.content, oldAppState.content) } function renderTitle (newTitle, oldTitle = {} ) { if (newTitle === oldTitle) return const titleDOM = document .getElementById('title' ) titleDOM.innerHTML = newTitle.text titleDOM.style.color = newTitle.color } function renderContent (newContent, oldContent = {} ) { if (newContent === oldContent) return const contentDOM = document .getElementById('content' ) contentDOM.innerHTML = newContent.text contentDOM.style.color = newContent.color } function stateChanger (state, action ) { if (!state) { return { title: { text: '标题' , color: 'red' , }, content: { text: '内容' , color: 'blue' } } } switch (action.type) { case 'UPDATE_TITLE_TEXT' : return { ...state, title: { ...state.title, text: action.text } } case 'UPDATE_TITLE_COLOR' : return { ...state, title: { ...state.title, color: action.color } } default : return state } } function createStore (reducer ) { let state = null const listeners = [] const subscribe = (listener ) => listeners.push(listener) const getState = () => state const dispatch = (action ) => { state = reducer(state, action) listeners.forEach((listener ) => listener()) } dispatch({}) return { getState, dispatch, subscribe } } const store = createStore(stateChanger)let oldState = store.getState()store.subscribe(() => { const newState = store.getState() renderApp(newState, oldState) oldState = newState }) renderApp(store.getState()) store.dispatch({ type : 'UPDATE_TITLE_TEXT' , text : 'newTitle' }) store.dispatch({ type : 'UPDATE_TITLE_COLOR' , color : 'green' })
在上面我们虽然手动实现了 Redux 的整体流程,但是需要注意的是,现在的实现跟 React 一点关系都没有,但是接下来我们会把 React 和 Redux 结合起来,用 Redux 模式帮助我们来管理 React 的应用状态,其实也就是 React-Redux 的实现,关于这部分内容,我们会另起篇幅来进行介绍