用原生JS从零到一实现Redux架构
前言
最近利用业余时间阅读了胡子大哈写的《react小书》,从基本的原理讲解了react,redux等等受益颇丰。眼过千遍不如手写一遍,跟着作者的思路以及参考代码可以实现基本的demo,下面根据自己的理解和参考一些资料,用原生js从零开始实现一个redux架构。
一.redux基本概念
经常用react开发的朋友可能很熟悉redux,react-redux,这里告诉大家的是,redux和react-redux并不是一个东西,redux是一种架构模式,2015年,redux出现,将 flux 与函数式编程结合一起,很短时间内就成为了最热门的前端架构。它不关心你使用什么库,可以把它和react,vue或者jquery结合。
二.由一个简单的例子开始
我们从一个简单的例子开始推演,新建一个html页面,代码如下:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>make-redux</title> </head> <body> <div id="app"> <div id="title"></div> <div id="content"></div> </div> <script> // 应用的状态 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); </script> </body> </html>
html内容很简单,我们定义了一个appstate数据对象,包括title和content属性,各自都有text和color,然后定义了renderapp,rendertitle,rendercontent渲染方法,最后执行renderapp(appstate),打开页面:
这些写虽然没有什么问题,但是存在一个比较大的隐患,每个人都可以修改共享状态appstate,在平时的业务开发中也很常见的一个问题是,定义了一个全局变量,其他同事在不知情的情况下可能会被覆盖修改删除掉,带来的问题是函数执行的结果往往是不可预料的,出现问题的时候调试起来非常困难。
那我们如何解决这个问题呢,我们可以提高修改共享数据的门槛,但是不能直接修改,只能修改我允许的某些修改。于是,定义一个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,规定只能修改title的文字与颜色。这样要想知道哪个函数修改了数据,我们直接在dispatch方法里面断点调试就可以了。大大的提高了解决问题的效率。
三.抽离store和实现监控数据变化
上面我们的appstore和dispatch分开的,为了使这种模式更加通用化,我们把他们集中一个地方构建一个函数createstore,用它来生产一个store对象,包含state和dispatch。
function createstore (state, statechanger) { const getstate = () => state; const dispatch = (action) => statechanger(state, action); return { getstate, dispatch } }
我们修改之前的代码如下:
let 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: '换一个标题' }); // 修改标题颜色 store.dispatch({ type: 'update_title_color', color: 'grey' }); // 再次把修改后的数据渲染到页面上 renderapp(store.getstate());
上面代码不难理解:我们用createstore生成了一个store,可以发现,第一个参数state就是我们之前声明的共享数据,第二个statechanger方法就是之前声明的dispatch用于修改数据的方法。
然后我们调用了来两次store.dispatch方法,最后又重新调用了renderapp再重新获取新数据渲染了页面,如下:可以发现title的文字和标题都改变了。
那么问题来了,我们每次dispatch修改数据的时候,都要手动的调用renderapp方法才能使页面得以改变。我们可以把renderapp放到dispatch方法最后,这样的话,我们的createstore不够通用,因为其他的app不一定要执行renderapp方法,这里我们通过一种监听数据变化,然后再重新渲染页面,术语上讲叫做观察者模式。
我们修改createstore如下。
function createstore (state, statechanger) { const listeners = []; // 空的方法数组 // store调用一次subscribe就把传入的listener方法push到方法数组中 const subscribe = (listener) => listeners.push(listener); const getstate = () => state; // 当store调用dispatch的改变数据的时候遍历listeners数组,执行其中每一个方法,到达监听数据重新渲染页面的效果 const dispatch = (action) => { statechanger(state, action); listeners.foreach((listener) => listener()) }; return { getstate, dispatch, subscribe } }
再次修改上一部分的代码如下:
// 首次渲染页面 renderapp(store.getstate()); // 监听数据变化重新渲染页面 store.subscribe(()=>{ renderapp(store.getstate()); }); // 修改标题文本 store.dispatch({ type: 'update_title_text', text: '换一个标题' }); // 修改标题颜色 store.dispatch({ type: 'update_title_color', color: 'grey' });
我们在首次渲染页面后只需要subscribe一次,后面dispatch修改数据,renderapp方法会被重新调用,实现了监听数据自动渲染数据的效果。
三.生成一个共享结构的对象来提高页面的性能
上一节我们每次调用renderapp方法的时候实际上是执行了rendertitle和rendercontent方法,我们两次都是dispatch修改的是title数据,可是rendercontent方法也都被一起执行了,这样执行了不必要的函数,有严重的性能问题,我们可以在几个渲染函数上加上一些log看看实际上是不是这样的
function renderapp (appstate) { console.log('render app...') ... } function rendertitle (title) { console.log('render title...') ... } function rendercontent (content) { console.log('render content...') ... }
浏览器控制台打印如下:
解决方案是:我们在每个渲染函数执行之前对其传入的数据进行一个判断,判断传入的新数据和旧数据是否相同,相同就return不渲染,否则就渲染。
// 渲染函数 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; } ... let oldstate = store.getstate(); // 缓存旧的 state store.subscribe(() => { const newstate = store.getstate(); // 数据可能变化,获取新的 state renderapp(newstate, oldstate); // 把新旧的 state 传进去渲染 oldstate = newstate // 渲染完以后,新的 newstate 变成了旧的 oldstate,等待下一次数据变化重新渲染 })
...
以上代码我们在subscribe的时候先用oldstate缓存旧的state,在dispatch之后执行里面的方法再次获取新的state然后oldstate和newstate传入到renderapp中,之后再用oldstate保存newstate。
好,我们打开浏览器看下效果:
控制台只打印了首次渲染的几行日志,后面两次dispatch数据之后渲染函数都没有执行。这说明oldstate和newstate相等了。
通过断点调试,发现newappstate和oldappstate是相等的。
究其原因,因为对象和数组是引用类型,newstate,oldstate指向同一个state对象地址,在每个渲染函数判断始终相等,就return了。
解决方法:appstate和newstate其实是两个不同的对象,我们利用es6语法来浅复制appstate对象,当执行dispatch方法的时候,用一个新对象覆盖原来title里面内容,其余的属性值保持不变。形成一个共享数据对象,可以参考以下一个demo:
我们修改statechanger,
让它修改数据的时候,并不会直接修改原来的数据 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里面的dispatch方法,执行statechanger(state,action)的返回值来覆盖原来的state,这样在subscribe执行传入的方法在dispatch调用时,newstate就是statechanger()返回的结果。
function createstore (state, statechanger) { ... const dispatch = (action) => { state=statechanger(state, action); listeners.foreach((listener) => listener()) }; return { getstate, dispatch, subscribe } }
再次运行代码打开浏览器:
发现后两次store.dispatch导致的content重新渲染不存在了,优化了性能。
四.通用化reducer
appstate是可以合并到一起的
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 // 没有修改,返回原来的对象 } }
再修改createstore方法:
function createstore (statechanger) { let state = null; const listeners = []; // 空的方法数组 // store调用一次subscribe就把传入的listener方法push到方法数组中 const subscribe = (listener) => listeners.push(listener); const getstate = () => state; // 当store调用dispatch的改变数据的时候遍历listeners数组,执行其中每一个方法,到达监听数据重新渲染页面的效果 const dispatch = (action) => { state=statechanger(state, action); listeners.foreach((listener) => listener()) }; dispatch({}); //初始化state return { getstate, dispatch, subscribe } }
初始化一个局部变量state=null,最后手动调用一次dispatch({})来初始化数据。
statechanger这个函数也可以叫通用的名字:reducer。为什么叫reducer? 参考里面对reducder的讲解;
五:redux总结
以上是根据阅读《react.js小书》再次复盘,通过以上我们由一个简单的例子引入用原生js能大概的从零到一完成了redux,具体的使用步骤如下:
// 定一个 reducer function reducer (state, action) { /* 初始化 state 和 switch case */ } // 生成 store const store = createstore(reducer) // 监听数据变化重新渲染页面 store.subscribe(() => renderapp(store.getstate())) // 首次渲染页面 renderapp(store.getstate()) // 后面可以随意 dispatch 了,页面自动更新 store.dispatch(...)
按照定义reducer->生成store->监听数据变化->dispatch页面自动更新。
下面两幅图也能很好表达出redux的工作流程
使用redux遵循的三大原则:
1.唯一的数据源store
2.保持状态的store只读,不能直接修改应用状态
3.应用状态的修改通过纯函数reducer完成
当然不是每个项目都要使用redux,一些小心共享数据较少的没必要使用redux,视项目大小复杂度而定,具体什么时候使用?引用一句话:当你不确定是否使用redux的时候,那就不要用redux。
项目完整代码地址make-redux
六.写在最后
每一个工具或框架都是在一定的条件下为了解决某种问题产生的,在阅读几遍《react.js》小书之后,终于对react,redux等一些基本原理有了一些了解,深感作为一个coder,不能只cv,记忆一些框架api会用就行,知其然不可,更要知其所以然,这样我们在完成项目才能更好的优化又能,是代码写的更加优雅。有什么错误的地方,敬请指正,技术想要有质的飞跃,就要多学习,多思考,多实践,与君共勉。
参考资料:
2.react进阶之路-徐超
上一篇: Froodog站长的经历记事
下一篇: php smarty模版引擎中的缓存应用