写点什么

自己手写一个 redux

  • 2022-11-16
    浙江
  • 本文字数:5148 字

    阅读完需:约 17 分钟

提起 Redux 我们想到最多的应该就是 React-redux 这个库,可是实际上 Redux 和 React-redux 并不是同一个东西, Redux 是一种架构模式,源于 Flux。 React-redux 是 Redux 思想与 React 结合的一种具体实现。


在我们使用 React 的时候,常常会遇到组件深层次嵌套且需要值传递的情况,如果使用 props 进行值的传递,显然是非常痛苦的。为了解决这个问题,React 为我们提供了原生的 context API,但我们用的最多的解决方案却是使用 React-redux 这个基于 context API 封装的库。


本文并不介绍 React-redux 的具体用法,而是通过一个小例子,来了解下什么是 redux。


好了,现在我们言归正传,来实现我们自己的 redux。

一、最初

首先,我们用 creat-react-app 来创建一个项目,删除 src 下冗余部分,只保留 index.js,并修改 index.html 的 DOM 结构:


# index.html<div id="root">  <div id="head"></div>  <div id="body"></div></div>
复制代码


我们在 index.js 中创建一个对象,用它来储存、管理我们整个应用的数据状态,并用渲染函数把数据渲染在页面:


const appState = {  head: {    text: '我是头部',    color: 'red'  },  body: {    text: '我是body',    color: 'green'  }}
function renderHead (state){ const head = document.getElementById('head') head.innerText = state.head.text; head.style.color = state.head.color;}function renderBody (state){ const body = document.getElementById('body') body.innerText = state.body.text; body.style.color = state.body.color;}function renderApp (state){ renderHead(state); renderBody(state);}renderApp(appState);
复制代码


此时运行代码,打开页面,我们可以看到,在 head 中已经出现了红色字体的‘我是头部’,在 body 中出现了绿色字体的‘我是 body’。


如果我们把 head 和 body 看作是 root 中的两个组件,那么我们已经实现了一个全局唯一的 state 。这个 state 是全局共享的,随处可调用的。


我们可以修改 head 的渲染函数,来看下效果:


function renderHead (state){  const head = document.getElementById('head')  head.innerText = state.head.text + '--' + state.body.text;  head.style.color = state.head.color;  state.body.text = '我是经过 head 修改后的 body';}
复制代码


我们看到,在 head 渲染函数中,我们不仅可以取用 body 属性的值,还可以改变他的值。这样就存在一个严重的问题,因为 state 是全局共用的,一旦在一个地方改变了 state 的值,那么,所有用到这个值的组件都将受到影响,而且这个改变是不可预期的,显然给我们的代码调试增加了难度系数,这样的结果是我们不愿意看到的!

二、dispatch

现在看来,在我们面前出现了一个矛盾:我们需要数据共享,但共享数据被任意的修改又会造成不可预期的问题!


为了解决这个矛盾,我们需要一个管家,专门来管理共享数据的状态,任何对共享数据的操作都要通过他来完成,这样,就避免了随意修改共享数据带来的不可预期的危害!我们重新定义一个函数,用这个函数充当我们的管家,来对我们的共享数据进行管理:


function dispatch(state, action) {  switch (action.type) {    case 'HEAD_COLOR':      state.head.color = action.color      break    case 'BODY_TEXT':      state.body.text = action.text      break    default:      break  }}
复制代码


我们来重新修改 head 的渲染函数:


function renderHead (state){  const head = document.getElementById('head')  head.innerText = state.head.text + '--' + state.body.text;  head.style.color = state.head.color;  dispatch(state, { type: 'BODY_TEXT', text: '我是 head 经过调用 dispatch 修改后的 body' })}
复制代码


参考 前端手写面试题详细解答


dispatch 函数接收两个参数,一个是需要修改的 state ,另一个是修改的值。这时,虽然我们依旧修改了 state ,但是通过 dispatch 函数,我们使这种改变变得可控,因为任何改变 state 的行为,我们都可以在 dispatch 中找到改变的源头。这样,我们似乎已经解决了之前的矛盾,我们创建了一个全局的共享数据,而且严格的把控了任何改变这个数据的行为。


然而,在一个文件中,我们既要保存 state, 还要维护管家函数 dispatch,随着应用的越来越复杂,这个文件势必会变得冗长繁杂,难以维护。


现在,我们把 state 和 dispatch 单独抽离出来:


  • 用一个文件单独保存 state

  • 用另一个文件单独保存 dispatch 中修改 state 的对照关系 changeState

  • 最后再用一个文件,把他们结合起来,生成全局唯一的 store


这样,不仅使单个文件变得更加精简,而且在其他的应用中,我们也可以很方便的复用我们这套方法,只需要传入不同应用的 state 和修改 state 的对应逻辑 stateChange,就可以放心的通过调用 dispatch 方法,对数据进行各种操作了:


# 改变我们的目录结构,新增 redux 文件夹+ src++ redux--- state.js // 储存应用数据状态--- storeChange.js //  维护一套修改 store 的逻辑,只负责计算,返回新的 store--- createStore.js // 结合 state 和 stateChange , 创建 store ,方便任何应用引用 --index.js 
## 修改后的各个文件
# state.js -- 全局状态export const state = { head: { text: '我是头部', color: 'red' }, body: { text: '我是body', color: 'green' }}
# storeChange.js -- 只负责计算,修改 storeexport const storeChange = (store, action) => { switch (action.type) { case 'HEAD_COLOR': store.head.color = action.color break case 'BODY_TEXT': store.body.text = action.text break default: break }}
# createStore.js -- 创建全局 storeexport const createStore = (state, storeChange) => { const store = state || {}; const dispatch = (action) => storeChange(store, action); return { store, dispatch }}
# index.js import { state } from './redux/state.js';import { storeChange } from './redux/storeChange.js';import { createStore } from './redux/createStore.js';const { store, dispatch } = createStore(state, storeChange)
function renderHead (state){ const head = document.getElementById('head') head.innerText = state.text; head.style.color = state.color;}function renderBody (state){ const body = document.getElementById('body') body.innerText = state.text; body.style.color = state.color;}
function renderApp (store){ renderHead(store.head); renderBody(store.body);}// 首次渲染renderApp(store);
复制代码


通过以上的文件拆分,我们看到,不仅使单个文件更加精简,文件的职能也更加明确:


  • 在 state 中,我们只保存我们的共享数据

  • 在 storeChange 中,我们来维护改变 store 的对应逻辑,计算出新的 store

  • 在 createStore 中,我们创建 store

  • 在 index.js 中,我们只需要关心相应的业务逻辑

三、subscribe

一切似乎都那么美好,可是当我们在首次渲染后调用 dispatch 修改 store 时,我们发现,虽然数据被改变了,可是页面并没有刷新,只有在 dispatch 改变数据后,重新调用 renderApp() 才能实现页面的刷新。


// 首次渲染renderApp(store);dispatch({ type: 'BODY_TEXT', text: '我是调用 dispatch 修改的 body' }) // 修改数据后,页面并没有自动刷新renderApp(store);  // 重新调用 renderApp 页面刷新
复制代码


这样,显然并不能达到我们的预期,我们并不想在每次改变数据后手动的刷新页面,如果能在改变数据后,自动进行页面的刷新,当然再好不过了!


如果直接把 renderApp 写在 dispatch 里,显然是不太合适的,这样我们的 createStore 就失去了通用性。


我们可以在 createStore 中新增一个收集数组,把 dispatch 调用后需要执行的方法统一收集起来,然后再循环执行,这样,就保证了 createStore 的通用性:


# createStoreexport const createStore = (state, storeChange) => {  const listeners = [];  const store = state || {};  const subscribe = (listen) => listeners.push(listen);   const dispatch = (action) => {    storeChange(store, action);    listeners.forEach(item => {      item(store);    })  };  return { store, dispatch, subscribe }}
# index.js···const { store, dispatch, subscribe } = createStore(state, storeChange)··· ···// 添加 listenerssubscribe((store) => renderApp(store));renderApp(store);dispatch({ type: 'BODY_TEXT', text: '我是调用 dispatch 修改的 body' });
复制代码


这样,我们每次调用 dispatch 时,页面就会重新刷新。如果我们不想刷新页面,只想 alert 一句话,只需要更改添加的 listeners 就好了:


subscribe((store) => alert('页面刷新了'));renderApp(store);dispatch({ type: 'BODY_TEXT', text: '我是调用 dispatch 修改的 body' });
复制代码


这样我们就保证了 createStore 的通用性。

四、优化

到这里,我们似乎已经实现了之前想达到的效果:我们实现了一个全局公用的 store , 而且这个 store 的修改是经过严格把控的,并且每次通过 dispatch 修改 store 后,都可以完成页面的自动刷新。


可是,显然这样并不足够,以上的代码仍有些简陋,存在严重的性能问题,


虽然我们只是修改了 body 的文案,可是,在页面重新渲染时,head 也被再次渲染。那么,我们是不是可以在页面渲染的时候,来对比新旧两个 store 来感知哪些部分需要重新渲染,哪些部分不必再次渲染呢?


根据上面的想法,我们再次来修改我们的代码:


# storeChange.jsexport const storeChange = (store, action) => {  switch (action.type) {    case 'HEAD_COLOR':      return {         ...store,          head: {           ...store.head,           color: action.color         }      }    case 'BODY_TEXT':      return {         ...store,        body: {          ...store.body,          text: action.text        }      }    default:      return { ...store }  }}
# createStore.jsexport const createStore = (state, storeChange) => { const listeners = []; let store = state || {}; const subscribe = (listen) => listeners.push(listen); const dispatch = (action) => { const newStore = storeChange(store, action); listeners.forEach(item => { item(newStore, store); }) store = newStore; }; return { store, dispatch, subscribe }}
# index.jsimport { state } from './redux/state.js';import { storeChange } from './redux/storeChange.js';import { createStore } from './redux/createStore.js';const { store, dispatch, subscribe } = createStore(state, storeChange);
function renderHead (state){ console.log('render head'); const head = document.getElementById('head') head.innerText = state.text; head.style.color = state.color;}function renderBody (state){ console.log('render body'); const body = document.getElementById('body') body.innerText = state.text; body.style.color = state.color;}
function renderApp (store, oldStore={}){ if(store === oldStore) return; store.head !== oldStore.head && renderHead(store.head); store.body !== oldStore.body && renderBody(store.body); console.log('render app',store, oldStore);}// 首次渲染subscribe((store, oldStore) => renderApp(store, oldStore));renderApp(store);dispatch({ type: 'BODY_TEXT', text: '我是调用 dispatch 修改的 body' });
复制代码


以上,我们修改了 storeChange ,让他不再直接修改原来的 store,而是通过计算,返回一个新的 store 。我们又修改了 cearteStore 让他接收 storeChange 返回的新 store ,在 dispatch 修改数据并且页面刷新后,把新 store 赋值给之前的 store 。而在页面刷新时,我们来通过比较 newStore 和 oldStore ,感知需要重新渲染的部分,完成一些性能上的优化。

最后

我们通过简单的代码例子,简单了解下 redux,虽然代码仍有些简陋,可是我们已经实现了 redux 的几个核心理念:


  • 应用中的所有 state 都以一个 object tree 的形式存储在一个单一的 store 中。

  • 唯一能改 store 的方法是触发 action,action 是动作行为的抽象。


用户头像

还未添加个人签名 2022-07-31 加入

还未添加个人简介

评论

发布
暂无评论
自己手写一个redux_JavaScript_helloworld1024fd_InfoQ写作社区