redux是一个前端架构
,经常和react一起使用。你要用react.js基本上都要用到redux和react-redux,但这两者并不是一个东西!
- redux是一个前端框架,你可以把它用到react、vue,设置jquery。
- react-redux是把redux这个前端架构结合到react形成的库,就是
redux架构
在react中的体现。
话不多说,我们来从头手写一个redux框架。
用create-react-app
新建一个项目myRedux
,修改public/index.html
里的body
结构为:
<body>
<div id='title'></div>
<div id='content'></div>
</body>
把src/index.js
里的代码替换为如下代码,代表我们的应用状态:
const appState = {
title: {
text: 'React.js 小书',
color: 'red',
},
content: {
text: 'React.js 小书内容',
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
方法会调用renderTitle
和renderContent
,而这两个方法会把appState
的数据渲染到页面上。
运行截图贴在这里
一. 手写dispatch
这是一个很简单的页面,但是存在着严重的隐患:我们渲染页面时,用到了一个共享数据appState,每个人都在哪里都可以修改它。如果在渲染之前做了一系列其他操作:
loadDataFromServer()
doSomethingUnexpected()
doSomthingMore()
// ...
renderApp(appState)
你根本无法知道这些方法会对renderApp做什么修改。这种任何地方都可以对共享数据进行修改的写法,会给debug/开发造成极大的难度。
有过一定开发经验的朋友看到这里,一定会忍不住对修改共享数据的操作做一个收口,让所有对共享数据修改的操作统一收口在一起。
原来各个模块可以直接修改appState,如下图:
而现在要写更改appState,必须通过dispatch,如下图:
我们定义一个收口函数dispatch
,让它专门负责共享数据的修改:
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
在switch
里会去识别这个type字段
,然后对appState
进行相应的修改。
上面的dispatch只能识别两种操作:一种是'UPDATE_TITLE_TEXT'
会用action
的text
字段去更新appState.title.text
;一种是'UPDATE_TITLE_COLOR'
会用action
的color
字段去更新appState.title.color
。可以看到,action里除了type字段,其他都是可以自定义的。
二. 手写store
上面我们有了appState
和dispatch
。
现在我们进一步整合,把它们都集中在一个地方,并给这个地方起名叫store
,然后创建一个createStore
方法来构建store
。
function createStore (state, stateChanger) {
const getState = () => state
const dispatch = (action) => stateChanger(state, action)
return { getState, dispatch }
}
createStore
接受两个参数,一个表示app的状态state,一个用来修改state。
现在,我们来修改原来的代码:
let appState = {
title: {
text: 'React.js 小书',
color: 'red',
},
content: {
text: 'React.js 小书内容',
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: '《React.js 小书》' }) // 修改标题文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改标题颜色
renderApp(store.getState()) // 把新的数据渲染到页面上
针对不同的app,我们给createStore
传入初始状态appState
和用于描述appState
变化的stateChanger
。需要修改的数据的时候通过store.dispatch
,需要获取数据的时候通过store.getState
。
监控数据的变化
上面的代码有一个问题,在dispatch
修改数据的时候,其实只是数据发生了变化,并没有调用renderApp
方法,页面上的内容是不会变化的。然而,我们又不能每次dispatch
的时候又renderApp
,我们希望用一种通用的监听数据变化的方式,然后重新渲染页面。
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方法,可以用该方法给数组push一个渲染函数
,每当dispatch
的时候,listeners
里的渲染函数都会被调用。这样我们就可以在数据变化的时候重新渲染页面:
const store = createStore(appState, stateChanger)
store.subscribe(() => renderApp(store.getState()))
renderApp(store.getState()) // 首次渲染页面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 小书》' }) // 修改标题文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改标题颜色
三. 手写reducer
细心的朋友会发现,前面的代码有着严重的性能问题。我们在每个渲染函数前打下log:
function renderApp (appState) {
console.log('render app...')
renderTitle(appState.title)
renderContent(appState.content)
}
function renderTitle (title) {
console.log('render title...')
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = title.text
titleDOM.style.color = title.color
}
function renderContent (content) {
console.log('render content...')
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = content.text
contentDOM.style.color = content.color
}
我们接下来dispatch两个action,来修改title的color和text:
const store = createStore(appState, stateChanger)
store.subscribe(() => renderApp(store.getState())) // 监听数据变化
renderApp(store.getState()) // 首次渲染页面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 小书》' }) // 修改标题文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改标题颜色
可以在控制台看到:
整个页面都刷新了三遍!第一次是页面初始化,后面两次修改title的状态的操作,把整个页面都刷新了!
但其实,我们只希望让跟修改的数据(title)有关系的组件刷新。
这里给出一个解决方案:
每次渲染之前,先对新数据和旧数据做一下比较,把不一样的部分进行渲染。
function renderApp (newAppState, oldAppState = {}) { // 防止 oldAppState 没有传入,所以加了默认参数 oldAppState = {}
if (newAppState === oldAppState) return // 数据没有变化就不渲染了
console.log('render app...')
renderTitle(newAppState.title, oldAppState.title)
renderContent(newAppState.content, oldAppState.content)
}
function renderTitle (newTitle, oldTitle = {}) {
if (newTitle === oldTitle) return // 数据没有变化就不渲染了
console.log('render title...')
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = newTitle.text
titleDOM.style.color = newTitle.color
}
function renderContent (newContent, oldContent = {}) {
if (newContent === oldContent) return // 数据没有变化就不渲染了
console.log('render content...')
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = newContent.text
contentDOM.style.color = newContent.color
}
我们修改下stateChanger
,让它接收到action
后不是直接去修改state
,而是重新生成一个对象。
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
。
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 }
}
此时,我们再去执行刚才的代码:
我们就这样成功优化了页面性能,每次只刷新了修改过的数据对应的组件。
此时,我们的stateChanger还有没有优化的空间了?
其实,可以把appState和stateChanger合并到一起:
function stateChanger (state, action) {
if (!state) {
return {
title: {
text: 'React.js 小书',
color: 'red',
},
content: {
text: 'React.js 小书内容',
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
}
}
这样createStore就只有了一个参数:
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({}) // 初始化 state
return { getState, dispatch, subscribe }
}
此时,这个stateChanger
,就是redux
的reducer
了!
四. 总结
此时,我们已经完整地手写了一个redux框架。
redux的核心就是发明了store,通过dispatch一个action来更改store里的值。
五. 参考
https://imweb.io/topic/59f5a7fdb72024f03c7f49bc
http://huziketang.mangojuice.top/books/react/lesson35