Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 知识梳理(二):实现自己的 redux #2

Open
wumouren opened this issue Mar 9, 2018 · 1 comment
Open

React 知识梳理(二):实现自己的 redux #2

wumouren opened this issue Mar 9, 2018 · 1 comment
Labels

Comments

@wumouren
Copy link
Owner

wumouren commented Mar 9, 2018

No description provided.

@wumouren wumouren added the notes label Mar 9, 2018
@wumouren
Copy link
Owner Author

wumouren commented May 24, 2018

写在前面的话(示例代码在这里

推荐大家读一下胡子大哈老师的 《React.js 小书》

提起 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 -- 只负责计算,修改 store
export 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 -- 创建全局 store
export 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 的通用性:

# createStore
export 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)
··· 
···
// 添加 listeners
subscribe((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 后,都可以完成页面的自动刷新。
可是,显然这样并不足够,以上的代码仍有些简陋,存在严重的性能问题,我们在 render 函数中打印日志可以看到:

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

# storeChange.js
export 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.js
export 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.js
import { 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 ,感知需要重新渲染的部分,完成一些性能上的优化。
重新打开控制台,我们可以看到,在我们修改 body 时,head 并没有重新渲染:

最后

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

  • 应用中的所有state都以一个object tree的形式存储在一个单一的store中。
  • 唯一能改store的方法是触发action,action是动作行为的抽象。

以上,是自己对《React.js 小书》的读后总结,限于篇幅,在下篇中,我们再来结合 react ,实现自己的 react-redux。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant