state-management

State Management

2022年,我们还需要全局状态管理吗? - 知乎

各流派 React 状态管理对比和原理实现 - 知乎

各流派 React 状态管理对比和原理实现 https://zhuanlan.zhihu.com/p/394106764

简单的项目就用 useReducer 和 useContext 就好了

使用原生 JavaScript 构建状态管理系统

参考: https://juejin.im/post/5b763528e51d45559e3a5b64

pubsub-js: https://github.com/mroderick/PubSubJS/blob/master/src/pubsub.js

constructor()

  export default class PubSub {
    constructor() {
      this.eventList = {};
    }
  }

我们得到了一个全新的类,我们将 this.eventList 默认设置为空对象。this.eventList 对象将保存我们的具名事件。

subscribe()

给类的原型添加一个 subscribe() 方法

类似:添加事件监听

参数

  1. eventName:String ,事件名
  2. handler:Func ,处理该事件的回调函数

返回值 : 当前事件绑定的回调函数的个数

实现

  /**
   * @param  {String} event 
   * @param  {Function} handler 
   *
   * @api private
   */
  subscribe(event, handler) {
    // 如果该事件不存在,则创建该事件
    if(!this.eventList.hasOwnProperty(event)) {
      this.eventList[event] = [];
    }
    // 将回调函数添加到该事件维护的列表中
    return this.eventList[event].push(handler);
  }

作用 : 将回调函数推入事件队列

publish()

给类的原型添加一个 publish() 方法

类似:触发事件,需要我们 主动触发

参数

  1. eventName:String ,事件名
  2. ...rest

异常: 如果该事件没有订阅,报错

实现

  /**
   * @param  {String} event 
   * @api private
   */
  	publish(event,...rest) {
    if(!this.eventList.hasOwnProperty(event)) {
      // return error
      return new Error('error')
    }
    // 遍历执行该事件的回调函数列表
    this.eventList[event].forEach(callback => callback(...rest));
  }

作用: 遍历执行该事件的回调函数列表

unsubscribe()

给类的原型添加一个 unsubscribe() 方法

类似:解除事件绑定

参数

实现

  /**
   * @param  {String} event 
   * @param  {Function} handler 
   * @api private
   */
  	unsubscribe(event,handler){
    const handlers = this.eventList[event]
    if(handlers===undefined) return
  
    const handlerIndex = handlers.indexOf(handler)
    handlers.splice(handlerIndex,1)
  }

once()

subscribe(),包装一个函数,unsubscribe()

  once(event,handler){
    const self = this
    function func(...rest){
      handler.apply(,rest)
      self.unsubscribe(event,fuc)
    }
    this.subscribe(event,func)
  }

完整代码

  export default class PubSub {
    constructor() {
      this.eventList = {};
    }
    /**
     * @param  {String} event 
     * @param  {Function} handler 
     * @api private
     */
    subscribe(event, handler) {
      if(!this.eventList.hasOwnProperty(event)) {
        this.eventList[event] = [];
      }
      return this.eventList[event].push(handler);
    }
    /**
     * @param  {String} event 
     * @api private
     */
    publish(event,...rest) {
      if(!this.eventList.hasOwnProperty(event)){
        console.log('事件未定义');
        return
      }
  
      this.eventList[event].forEach(handler => handler(...rest));
    }
    /**
     * @param  {String} event 
     * @param  {Function} handler 
     * @api private
     */
    unsubscribe(event,handler){
      const handlers = this.eventList[event]
      if(handlers===undefined) return
  
      const handlerIndex = handlers.indexOf(handler)
      handlers.splice(handlerIndex,1)
      console.log('成功解除事件绑定');
    }
  }

测试

  import PubSub from './lib/pubsub/index.js'
  
  let pubSub = new PubSub()
  
  const testHandler = (...rest) => {
    rest.forEach((val) => {console.log(val)})
  }
  
  pubSub.subscribe('test',testHandler)
  
  pubSub.publish('test','hello','world')
  
  pubSub.unsubscribe('test',testHandler)
  
  pubSub.publish('test2','hello','world')
  
  pubSub.once('once',testHandler)
  pubSub.publish('once','once','once')
  pubSub.publish('once','two','two')

Store 对象(核心)

我们现在已经有了 PubSub 模块,我们这个小应用程序的核心模块 Store 类有了它的唯一依赖。现在我们开始完善它:

 import PubSub from '../pubsub/index.js';

constructor()

  export default class Store {
    constructor(params) {
      this.actions = {};
      this.mutations = {};
      this.state = {};
      this.status = 'resting';
    }
  }

我们将为 stateactionsmutations 添加默认对象。我们还添加了一个 status 属性,我们将用它来确定对象在任意给定时间正在做什么。

之后,我们将创建一个新的 PubSub 实例,它将作为 storeevents 属性的值:

  this.events = new PubSub();

接下来,我们将搜索传入的 params 对象以查看是否传入了任何 actionsmutation。当实例化 Store 对象时,我们可以传入一个数据对象。其中包括 actionsmutation 的集合,它们控制着我们 store 中的数据流。在你添加的最后一行代码后面添加以下代码:

if(params.hasOwnProperty('actions')) {
    self.actions = params.actions;
}
  
if(params.hasOwnProperty('mutations')) {
    self.mutations = params.mutations;
}

这就是我们所有的默认设置和几乎所有潜在的参数设置。让我们来看看我们的 Store 对象如何跟踪所有的变化。我们将使用 Proxy(代理) 来完成此操作。

配置代理

Proxy(代理)所做的工作主要是代理 state 对象。如果我们添加一个 get 拦截方法,我们可以在每次询问对象数据时进行监控。与 set 拦截方法类似,我们可以密切关注对象所做的更改。这是我们今天感兴趣的主要部分。在你添加的最后一行代码之后添加以下内容,我们将讨论它正在做什么:

self.state = new Proxy((params.state || {}), {
	set: function(state, key, value) {
	
	  state[key] = value;
	
	  console.log(`stateChange: ${key}: ${value}`);
	
	  self.events.publish('stateChange', self.state);
	
	  if(self.status !== 'mutation') {
		console.warn(`You should use a mutation to set ${key}`);
	  }
	
	  self.status = 'resting';
	
	  return true;
	}
});

这部分代码说的是我们正在捕获状态对象 set 操作。这意味着当运行类似于 state.name ='Foo' 时,这个拦截器会在它被设置之前捕获它,并为我们提供了一个机会来处理更改甚至完全拒绝它。但在我们的上下文中,我们将会设置变更,然后将其记录到控制台。然后我们用 PubSub 模块发布一个 stateChange 事件。任何订阅了该事件的回调将被调用。最后,我们检查 Store 的状态。如果它当前不是一个 mutation,则可能意味着状态是手动更新的。我们在控制台中添加了一点警告,以便给开发人员一些提示。

这里做了很多事,但我希望你们开始看到这一切是如何结合在一起的,重要的是,我们如何能够集中维护状态,这要归功于 Proxy(代理)和 Pub/Sub。

Dispatch 和 Commit

现在我们已经添加了 Store 的核心部分,让我们添加两个方法。

一个是将调用我们 actionsdispatch

另一个是将调用我们 mutationcommit

让我们从 dispatch 开始,在 store.js 中的 constructor 之后添加这个方法:

dispatch(actionKey, payload) {
  let self = this;
  if(typeof self.actions[actionKey] !== 'function') {
    console.error(`Action "${actionKey} doesn't exist.`);
    return false;
  }
  console.groupCollapsed(`ACTION: ${actionKey}`);
  self.status = 'action';
  self.actions[actionKey](self, payload);
  console.groupEnd();
  return true;
}

此处的过程是:查找 action,如果存在,则设置状态并调用 action,同时创建日志记录组以使我们的所有日志保持良好和整洁。记录的任何内容(如 mutation 或 Proxy(代理)日志)都将保留在我们定义的组中。如果未设置任何 action,它将记录错误并返回 false。

commit 方法更加直截了当,在 dispatch 方法之后添加:

commit(mutationKey, payload) {
    let self = this;
    if(typeof self.mutations[mutationKey] !== 'function') {
      console.log(`Mutation "${mutationKey}" doesn't exist`);
      return false;
    }
    self.status = 'mutation';
    let newState = self.mutations[mutationKey](self.state, payload);
    self.state = Object.assign(self.state, newState);
    return true;
}

这种方法非常相似,但无论如何我们都要自己了解这个过程。如果可以找到 mutation,我们运行它并从其返回值获得新状态。然后我们将新状态与现有状态合并,以创建我们最新版本的 state。

Store 实例

store 状态管理库,我们已经搭建好了,现在我们需要新建我们的 store 实例,以便在项目中使用它

import Store from '../../lib/flux/index'

import state from './state'
import mutations from './mutations'
import actions from './actions'

export default new Store({
	state,
	mutations,
	actions,
})

组件

基础组件

其他组件继承于该组件

lib/components/index.js

import Store from '../flux/index.js';

export default class Component {
  constructor(props = {}) {
    this.render = this.render || function () { };

    if (props.store instanceof Store) {
      props.store.events.subscribe('stateChange', () => this.render());
    }
    
    if (props.hasOwnProperty('element')) {
      this.element = props.element;
    }
  }
}

首先,我们要导入 Store 。这不是因为我们想要它的实例,而是更多用于检查 constructor 中的一个属性。

constructor 中我们要看看我们是否有一个 render 方法。如果这个 Component 类是另一个类的父类,那么子类可能会为 render 设置自己的方法。如果没有设置方法,我们创建一个空方法来防止事情出错。

在此之后,我们像上面提到的那样对 Store 类进行检查。我们这样做是为了确保 store 属性是一个 Store 类实例,这样我们就可以放心地使用它的方法和属性。说到这一点,我们订阅了全局 stateChange 事件,所以我们的对象可以做到 响应式。每次状态改变时都会调用 render 函数。@@@

这就是我们需要为该类所要写的全部内容。它将被用作其他组件类 extend 的父类。让我们一起来吧!

自定义组件

我们要完成三个组件,它们都通过 extend 关键字,继承了基类 Component。让我们从最大的一个组件开始开始:项目清单!

list 组件

src/components/list.js

import Component from '../lib/components/index.js';
import Store from '../lib/flux/index.js';
  
const store = new Store()
  
export default class List extends Component {
    constructor() {
      super({
          store,
          element: document.querySelector('.js-items')
      });
    }
    render() {
  
      if(store.state.items.length === 0) {
        this.element.innerHTML = `<p class="no-items">You've done nothing yet &#x1f622;</p>`;
          return;
      }
  
      this.element.innerHTML = `
        <ul class="app__items">
          ${store.state.items.map(item => {
            return `
            <li>${item}<button aria-label="Delete this item">×</button></li>
            `
          }).join('')}
        </ul>
      `;
  
      this.element.querySelectorAll('button').forEach((button, index) => {
        button.addEventListener('click', () => {
        	store.dispatch('clearItem', { index });
        });
      });
    }
}

我们先将 Store 实例传递给我们继承的 Component 父类。就是我们刚刚编写的 Component 类。

在那之后,我们声明了 render 方法,每次触发 Pub/Sub 的 stateChange 事件时都会调用的这个 render 方法。在这个 render 方法中,我们会生成一个项目列表,或者是没有项目时的通知。你还会注意到每个按钮都附有一个事件,并且它们会触发一个 action,然后由我们的 store 处理 action。这个 action 还不存在,但我们很快就会添加它。

接下来,再创建两个文件。虽然是两个新组件,但它们很小 —— 所以我们只是向其中粘贴一些代码即可,然后继续完成其他部分。

Count 组件

import Component from '../lib/components/index.js';
import Store from '../lib/flux/index.js';

const store = new Store()
  
export default class Count extends Component {
    constructor() {
      super({
        store,
        element: document.querySelector('.js-count')
      });
    }
  
    render() {
      let suffix = store.state.items.length !== 1 ? 's' : '';
      let emoji = store.state.items.length > 0 ? '&#x1f64c;' : '&#x1f622;';
  
      this.element.innerHTML = `
        <small>You've done</small>
        ${store.state.items.length}
        <small>thing${suffix} today ${emoji}</small>
      `;
    }
}

Status 组件

import Component from '../lib/components/index.js';
import Store from '../lib/flux/index.js';
  
const store = new Store()
  
export default class Status extends Component {
    constructor() {
      super({
          store,
          element: document.querySelector('.js-status')
      });
    }
  
    render() {
      let suffix = store.state.items.length !== 1 ? 's' : '';
      this.element.innerHTML = `${store.state.items.length} item${suffix}`;
    }
}

目录结构

 │  index.html
 │  index.js
 │
 ├─lib
 │  ├─components
 │  │      index.js
 │  │
 │  ├─flux
 │  │      index.js
 │  │
 │  └─pubsub
 │          index.js
 │
 ├─src
 │  ├─components
 │  │      count.js
 │  │      list.js
 │  │      status.js
 │  │
 │  └─store
 │          actions.js
 │          index.js
 │          mutaions.js
 │          state.js

Store 实例

我们已经让 store 系统和组件通过数据来渲染和交互。现在让我们把应用程序的两个独立部分联系起来,让整个项目一起协同工作。我们需要添加一个初始状态,一些 actions 和一些 mutations。在 store 目录中,添加一个名为 state.js 的新文件。

state.js

export default {
    items: [
    'I made this',
    'Another thing'
    ]
};

actions.js

//src/store/actions.js
export default {
    addItem(context, payload) {
    context.commit('addItem', payload);
    },
    clearItem(context, payload) {
    context.commit('clearItem', payload);
    }
};

这个应用程序中的 actions 非常少。本质上,每个 action 都会将 payload(关联数据)传递给 mutation,而 mutation 又将数据提交到 store。正如我们之前所了解的那样,contextStore 类的实例,payload 是触发 action 时传入的。

在 redux 和 vuex 中,都只有一个 store 实例,所以不需要传递 context

mutaions.js

// src/store/mutaions.js
export default {
      addItem(state, payload) {
      state.items.push(payload);
  
      return state;
      },
      clearItem(state, payload) {
      state.items.splice(payload.index, 1);
  
      return state;
      }
};

与 actions 一样,这些 mutations 很少。在我看来,你的 mutations 应该保持简单,因为他们有一个工作:改变 store 的 state。因此,这些例子就像它们最初一样简单。任何适当的逻辑都应该发生在你的 actions 中。正如你在这个系统中看到的那样,我们返回新版本的 state,以便 Store<code>commit 方法可以发挥其魔力并更新所有内容。

有了这个,store 系统的主要模块就位。

连接 index.html

我们需要做的最后一件事是添加 index.html 中包含的 main.js 文件。

src/main.js

绑定监听

 import store from './store/index.js'; 
 
 import Count from './components/count.js';
 import List from './components/list.js';
 import Status from './components/status.js';
 
 const formElement = document.querySelector('.js-form');
 const inputElement = document.querySelector('#new-item-field');
 
 formElement.addEventListener('submit', evt => {
   evt.preventDefault();
 
   let value = inputElement.value.trim();
 
   if(value.length) {
     store.dispatch('addItem', value);
     inputElement.value = '';
     inputElement.focus();
   }
 });

我们在这里做的是向表单添加一个事件监听器并阻止它提交。

然后我们获取文本框的值并修剪它两端的空格。我们这样做是因为我们想检查下一步是否会有任何内容传递给 store。

最后,如果有内容,我们将使用该内容作为 payload(关联数据)触发我们的 addItem action,并且让我们闪亮的新 store 为我们处理它。

组合组件

让我们在 main.js 中再添加一些代码。在事件监听器下,添加以下内容:

const countInstance = new Count();
const listInstance = new List();
const statusInstance = new Status();

countInstance.render();
listInstance.render();
statusInstance.render();

我们在这里所做的就是创建组件的新实例并调用它们的每个 render 方法,以便我们在页面上获得初始状态。

Zustand

谈谈复杂应用的状态管理(上):为什么是 Zustand - 知乎
react的状态库最佳实践非zustand莫属 - 掘金

FAQ

#faq/framework

拥抱响应式

只有 react 和 redux 是非响应式的

新出的状态管理工具也都是精确更新的, 可以认为是响应式的理念

如何实现状态共享

为啥 redux 和 context api 需要 provider

为啥 zustand 直接用 useStore 就完事了? useStore 默认单实例. zustand 也提供了对应的 Provider 的书写方式,这种方式在组件库中比较常用。ProEditor 也是用的这种方式做到了多实例。

此外,zustand 的 store 状态既可以在 react 世界中消费,也可以在 react 世界外消费。

状态变更

为啥 redux 要一堆模板代码, 而 zustand 可以直接写, 不区分同步异步?

状态派生