最近在复习 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
的实现,关于这部分内容,我们会另起篇幅来进行介绍