react 知识梳理(二):手写一个本身的 redux

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

推荐你们读一下胡子大哈老师的 《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。html

好了,如今咱们言归正传,来实现咱们本身的 redux。react

1、最初

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

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

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

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’。 redux

若是咱们把 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 的值,那么,全部用到这个值的组件都将受到影响,并且这个改变是不可预期的,显然给咱们的代码调试增长了难度系数,这样的结果是咱们不肯意看到的!

2、dispatch

如今看来,在咱们面前出现了一个矛盾:咱们须要数据共享,但共享数据被任意的修改又会形成不可预期的问题!
为了解决这个矛盾,咱们须要一个管家,专门来管理共享数据的状态,任何对共享数据的操做都要经过他来完成,这样,就避免了随意修改共享数据带来的不可预期的危害! 咱们从新定义一个函数,用这个函数充当咱们的管家,来对咱们的共享数据进行管理:segmentfault

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 单独抽离出来:bash

  • 用一个文件单独保存 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);
复制代码

经过以上的文件拆分,咱们看到,不只使单个文件更加精简,文件的职能也更加明确:app

  • 在 state 中,咱们只保存咱们的共享数据
  • 在 storeChange 中,咱们来维护改变 store 的对应逻辑,计算出新的 store
  • 在 createStore 中,咱们建立 store
  • 在 index.js 中,咱们只须要关心相应的业务逻辑

3、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 的通用性。

4、优化

到这里,咱们彷佛已经实现了以前想达到的效果:咱们实现了一个全局公用的 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。

相关文章
相关标签/搜索