实现一个简单的redux模式

698 阅读5分钟

起步

首先需求是根据state来进行视图渲染:

const appState = {
	title: {
		text: '第一次title text',
		color: 'red'
	},
	content: {
		text: '第一次content text',
		color: 'blue'
	}
}
// ...
function renderApp(appState) {
	console.log('app')

	renderTitle(appState.title)
	renderContent(appState.content)
}

function renderTitle(title) {
	console.log('title')

	const titleDOM = document.getElementById('title')
	titleDOM.innerHTML = title.text
	titleDOM.style.color = title.color
}

function renderContent(content) {
	console.log('content')

	const contentDOM = document.getElementById('content')
	contentDOM.innerHTML = content.text
	contentDOM.style.color = content.color
}

如果需要在此基础上对视图修改,只需要对appState进行修改再手动调用渲染就能更新视图上的状态。但是也因此会有很多缺陷,比如说多个其他函数都需要对数据进行操作就会对同一个全局变量进行修改,这样出现问题就会很难定位。

1. 解决外部对同一个state的操作,导致错误很难定位的问题

为了解决这个问题,现在统一规定对state的修改必须调用dispatch函数,它接收一个普通的js对象action,其中type表明想要进行的操作

const dispatch = action => {
	switch (action.type) {
		case 'UPDATE_TITLE_TEXT':
			appState.title.text = action.payload
			break
		case 'UPDATE_TITLE_COLOR':
			appState.title.color = action.payload
			break
		default:
			break
	}
}
// ...
dispatch({ type: 'UPDATE_TITLE_TEXT', payload: '第二次修改的title text' })

这样,当外部函数想要修改state,都需要调用dispatch传入action,出现问题只需要在switch中进行调试。

2. 现在需要将这种模式单独抽离出来给其他项目使用

通过构建createStore函数专门提供dispatch,并且接受一个stateChange专门对state的操作进行描述

const createStore = (state, stateChange) => {
	const dispatch = action => stateChange(state, action)
	return { dispatch }
}
// ...
const stateChange = (state, action) => {
	switch (action.type) {
		case 'UPDATE_TITLE_TEXT':
			state.title.text = action.payload
			break
		case 'UPDATE_TITLE_COLOR':
			state.title.color = action.payload
			break
		default:
			break
	}
}
const store = createStore(appState, stateChange)

这样其他项目只需要通过向createStore传入一个state和一个描述state相关操作的stateChange就也可以使用这样的模式。但有个问题,当dispatch时只是修改了state而并没有渲染数据,所以每次dispatch时都需要手动调用renderApp。我们希望每次dispatch时会自动渲染数据。

3. dispatch时自动渲染数据

我们需要一种方式去“监听”数据的变化,这就用到了观察者模式,通过subscribe将渲染数据的回调保存在listeners数组中,再在dispatch中将listeners数组中的回调依次执行。

const createStore = (state, stateChange) => {
	const listeners = [] // 存储渲染回调
	const getState = () => state // 外部需要获取最新的state传给renderApp
	const dispatch = action => {
		stateChange(state, action)
		listeners.forEach(fn => fn())
	}
	const subscribe = fn => {
		listeners.push(fn)
	}
	return { dispatch, subscribe, getState }
}

这样每次dispatch时都会触发渲染回调,只需要在首次的时候手动调用一次渲染。至此,我们完成了一套dispatch更新数据->渲染数据的自动流程,但是还有一些缺陷,比如只更新state部分字段,但是还是会导致全部数据进行渲染,这样就有很严重的性能损失。

4. 需要每次数据改变返回新的state,以对比前后两次state是否发生改变

为了解决上述问题,就需要对比两次的state的改变,但是之前的变动都是在同一个state上进行修改数值,不能够直接判断。所以我们需要一种方式来判断两次state,为此我们需要在stateChange中返回一份state的拷贝,不变的字段和原state一样而改变的字段就指向一个新的对象。

function renderApp(newState, oldState = {}) {
	if (newState === oldState) return
	console.log('app')

	renderTitle(newState.title, oldState.title)
	renderContent(newState.content, oldState.content)
}

function renderTitle(newTitle, oldTitle = {}) {
	if (newTitle === oldTitle) return
	console.log('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('content')

	const contentDOM = document.getElementById('content')
	contentDOM.innerHTML = newContent.text
	contentDOM.style.color = newContent.color
}
//...
const stateChange = (state, action) => {
	switch (action.type) {
		case 'UPDATE_TITLE_TEXT':
			return {
				...state,
				title: {
					...state.title,
					text: action.payload
				}
			}
		case 'UPDATE_TITLE_COLOR':
			return {
				...state,
				title: {
					...state.title,
					color: action.payload
				}
			}
		default:
			break
	}
}
//...
let oldState = getState()
subscribe(() => {
	renderApp(getState(), oldState)
	oldState = getState()
})

这样将两次的state传入渲染函数就能进行判断状态是否改变从而决定是否重新渲染数据

5. 合并state和stateChange

经过之前几步,现在已经有一个很通用的createStore,使用方式是初始化state和描述状态改变的stateChange,并通过createStore生成的store去调用subscribe订阅渲染数据的回调,然后通过dispatch去返回新的state去触发数据渲染。 进一步优化,其实将state和stateChange放在一起,让stateChange既能够初始化state又能够生成新的state。

const stateChange= (state, action) => {
	if (!state) {
		return {
			title: {
				text: '第一次title text',
				color: 'red'
			},
			content: {
				text: '第一次content text',
				color: 'blue'
			}
		}
	}
	switch (action.type) {
		case 'UPDATE_TITLE_TEXT':
			return {
				...state,
				title: {
					...state.title,
					text: action.payload
				}
			}
		case 'UPDATE_TITLE_COLOR':
			return {
				...state,
				title: {
					...state.title,
					color: action.payload
				}
			}
		default:
			break
	}
}

同时现在需要在createStore中将state进行初始化

const createStore = stateChange=> {
	let state = null
	const listeners = [] // 存储渲染回调
	const getState = () => state // 外部需要获取最新的state传给renderApp
	const dispatch = action => {
		state = reducer(state, action)
		listeners.forEach(fn => fn())
	}
	const subscribe = fn => {
		listeners.push(fn)
	}
	dispatch({}) //初始化state
	return { dispatch, subscribe, getState }
}

最后只需要将stateChange这个名称替换成reducer就实现了一个简单的redux模式。

最后

我们遵循一个发现问题->解决问题这样一个流程实现了redux模式:

  1. 外部可以通过随意修改同一个全局变量state的值来达到重新渲染数据,这种模式在出现问题的时候难以处理
  2. 规定必须通过dispatch并传入一个对修改state的描述对象action来统一修改state,于是就将这种模式抽离成一个专门生成dispatch的createStore
  3. 发现每次dispatch仅仅是改变state并没有触发渲染,每次dispatch都需要手动render很麻烦,就使用观察者模式来对数据变动进行监听并在dispatch时候调用渲染回调
  4. 发现每次dispatch都会将所有state都进行重现渲染十分浪费资源,我们就在stateChange中将每次直接修改state转变为返回新的state的拷贝,这样外部能够通过比对两次state来决定是否渲染数据
  5. 进一步优化,让stateChange既能初始化state又能生成新的state,并在createStore中调用依次dispatch进行state初始化