必威体育Betway必威体育官网
当前位置:首页 > IT技术

纯粹极简的react状态管理组件unstated

时间:2019-06-13 08:42:08来源:IT技术作者:seo实验室小编阅读:82次「手机版」
 

stated

简介

unstated是一个极简的状态管理组件

看它的简介:State so simple, it goes without saying

对比

对比redux:

  • 更加灵活(相对的缺点是缺少规则,需要使用者的自觉)

    redux的状态是存放在一棵树内,采用严格的单向流

    unstated的状态是用户自己定义,说白了就是object,可以放在一个组件的内,也可以放在多个组件内

  • 针对React,一致的API

    redux必须编写reduceraction,通过dispatch(action)改变状态,它不限框架

    unstated改变状态的API完全与React一致,使用this.setState,当然和ReactsetState不同,

    但是它的底层也是用到了setState去更新视图

  • 功能相对简单

    unstated没有中间件功能,每次状态改变(不管是否相等),都会重新渲染(V2.1.1)

    可以自定义listener,每次更新状态时都会执行。

对比React的自带state:

  • 天生将组件分割为Container(状态管理)component(视图管理)
  • 灵活配置共享状态或者私有状态
  • 支持promise
快速了解请直接跳到总结

初识

3大板块和几个关键变量


Provider: 注入状态实例,传递map,本质是context.Provider,可嵌套达成链式传递
container: 状态管理类,遵循React的API,发布订阅模式,通过new生成状态管理实例
Subscribe: 订阅状态组件,本质是Context.consumer,接收Provider提供的map,视图渲染组件
map: new Map(),通过类查找当前类创建的状态管理实例

深入

这里引入官方例子


// @flow
import React from 'react';
import { render } from 'react-dom';
import { Provider, Subscribe, Container } from 'unstated';

type CounterState = {
  count: number
};
// 定义一个状态管理类
class CounterContainer extends Container<CounterState> {
  state = {
    count: 0
  };

  increment() {
    this.setState({ count: this.state.count + 1 });
  }

  decrement() {
    this.setState({ count: this.state.count - 1 });
  }
}
// 渲染视图组件(Context.Consumer的模式)
function Counter() {
  return (
    <Subscribe to={[CounterContainer]}>
      {counter => (
        <p>
          <button onClick={() => counter.decrement()}>-</button>
          <span>{counter.state.count}</span>
          <button onClick={() => counter.increment()}>+</button>
        </p>
      )}
    </Subscribe>
  );
}

render(
  <Provider>
    <Counter />
  </Provider>,
  document.getelementbyid('root')
);

这里Counter是我们自定义的视图组件,首先使用<Provider>包裹,接着在Counter内部,调用<Subscribe>组件,

传递一个数组props.to,这个数组内存放了Counter组件需要使用的状态管理类(此处也可传递状态管理实例)。

Provider


export function Provider(props: ProviderProps) {
  return (
    &lt;StateContext.Consumer&gt;
      {parentMap =&gt; {
        let childMap = new Map(parentMap);
        // 外部注入的状态管理实例
        if (props.inject) {
          props.inject.foreach(instance =&gt; {
            childMap.set(instance.constructor, instance);
          });
        }

        // 负责将childMap传递,初始为null
        return (
          &lt;StateContext.Provider value={childMap}&gt;
            {props.children}
          &lt;/StateContext.Provider&gt;
        );
      }}
    &lt;/StateContext.Consumer&gt;
  );
}

这里的模式是


&lt;Consumer&gt;
  ()=&gt;{
    /* ... */
    return &lt;Provider&gt;{props.children}&lt;Provider /&gt;
  }
&lt;/Consumer&gt;  

有3个注意点:

  1. 外层嵌套<Consumer>可以嵌套调用。


&lt;Provider value={...}&gt;
 /* ... */
 &lt;Provider value={此处继承了上面的value}&gt;
 /* ... */ 
&lt;/Provider&gt;
  • props.inject可以注入现成的状态管理实例,添加到map之中。
  • 返回值写成props.children
  • 返回值写成props.children的意义

    简单一句话概括,这么写可以避免React.Context改变导致子组件的重复渲染。

    具体看这里:避免React Context导致的重复渲染

    Container

    
    export class Container&lt;State: {}&gt; {
      // 保存状态 默认为{}
      state: State;
      // 保存监听函数,默认为[]
      _listeners: Array&lt;Listener&gt; = [];
    
      setState(
        updater: $Shape&lt;State&gt; | ((prevState: $Shape&lt;State&gt;) =&gt; $Shape&lt;State&gt;),
        callback?: () =&gt; void
      ): Promise&lt;void&gt; {
        return Promise.resolve().then(() =&gt; {
          let nextState;
    
          /* 利用Object.assign改变state */
    
          // 执行listener(promise)
          let promises = this._listeners.map(listener =&gt; listener());
    
          // 所有Promise执行完毕
          return Promise.all(promises).then(() =&gt; {
            // 全部listener执行完毕,执行回调
            if (callback) {
              return callback();
            }
          });
        });
      }
    
      // 增加订阅(这里默认的订阅就是React的setState空值(为了重新渲染),也可以添加自定义监听函数)
      subscribe(fn: Listener) {
        this._listeners.push(fn);
      }
    
      // 取消订阅
      unsubscribe(fn: Listener) {
        this._listeners = this._listeners.filter(f =&gt; f !== fn);
      }
    }
    

    Container内部逻辑很简单,改变state,执行监听函数。

    其中有一个_listeners,是用于存放监听函数的。

    每个状态管理实例存在一个默认监听函数onUpdate

    这个默认的监听函数的作用就是调用React的setState强制视图重新渲染

    这里的监听函数内部返回Promise,最后通过Promise.all确保执行完毕,然后执行回调参数

    因此setState在外面使用也可以使用then

    例如,在官方例子中:

    
    increment() {
        this.setState({ count: this.state.count + 1 },()=&gt;console.log('2'))
        .then(()=&gt;console.log('3') )
        console.log('1') 
      }
      // 执行顺序是 1 -&gt; 2 -&gt;3
    

    2个注意点:

    1. setStateReact API一致,第一个参数传入object或者function,第二个传入回调
    2. 这里通过Promise.resolve().then模拟this.setState的异步执行

    关于Promise.resolve和settimeout的区别

    简单的说两者都是异步调用,Promise更快执行。

    • setTimeout(()=>{},0)会放入下一个新的任务队列
    • Promise.resolve().then({})会放入微任务,在调用栈为空时立刻补充调用栈并执行(简单理解为当前任务队列尾部)

    更多详细可以看这里提供的2个视频https://stackoverflow.com/a/38752743

    Subscribe

    
    export class Subscribe&lt;Containers: ContainersType&gt; extends React.Component&lt;
      SubscribeProps&lt;Containers&gt;,
      SubscribeState
    &gt; {
      state = {};
      // 存放传入的状态组件
      instances: Array&lt;ContainerType&gt; = [];
      unmounted = false;
    
      componentWillUnmount() {
        this.unmounted = true;
        this._unsubscribe();
      }
    
      _unsubscribe() {
        this.instances.forEach(container =&gt; {
          // container为当前组件的每一个状态管理实例
          // 删除listeners中的this.onUpdate
          container.unsubscribe(this.onUpdate);
        });
      }
    
      onUpdate: Listener = () =&gt; {
        return new Promise(resolve =&gt; {
          // 组件未被卸载
          if (!this.unmounted) {
            // 纯粹是为了让React更新组件
            this.setState(dummy_STATE, resolve);
          } else {
            // 已经被卸载则直接返回
            resolve();
          }
        });
      };
      
      /* ... */
    }
    

    这里的关键就是instances,用于存放当前组件的状态管理实例

    当组件unmount的时候,会unsubscribe当前状态管理实例的默认监听函数,那么如果当前的状态管理实例是共享的,会不会有影响呢?

    不会的。往后看可以知道,当state每次更新,都会重新创建新的状态管理实例(因为props.to的值可能会发生变化,例如取消某一个状态管理实例),

    而每次创建时,都会先unsubscribesubscribe,确保不会重复添加监听函数。

    onUpdate就是创建状态管理组件时默认传递的监听函数,用的是ReactsetState更新一个DUMMY_STATE(空对象{})。

    
    export class Subscribe&lt;Containers: ContainersType&gt; extends React.Component&lt;
      SubscribeProps&lt;Containers&gt;,
      SubscribeState
    &gt; {
      /* 上面已讲 */
    
      _createInstances(
        map: ContainerMapType | null,
        containers: ContainersType
      ): Array&lt;ContainerType&gt; {
        // 首先全部instances解除订阅
        this._unsubscribe();
    
        // 必须存在map 必须被Provider包裹才会有map
        if (map === null) {
          throw new ERROR(
            'You must wrap your &lt;Subscribe&gt; components with a &lt;Provider&gt;'
          );
        }
    
        let safeMap = map;
        // 重新定义当前组件的状态管理组件(根据to传入的数组)
        let instances = containers.map(ContainerItem =&gt; {
          let instance;
    
          // 传入的是Container组件,则使用
          if (
            typeof ContainerItem === 'object' &amp;&amp;
            ContainerItem instanceof Container
          ) {
            instance = ContainerItem;
          } else {
            // 传入的不是Container,可能是其他自定义组件等等(需要用new执行),尝试获取
            instance = safeMap.get(ContainerItem);
    
            // 不存在则以它为key,value是新的Container组件
            if (!instance) {
              instance = new ContainerItem();
              safeMap.set(ContainerItem, instance);
            }
          }
    
          // 先解绑再绑定,避免重复订阅
          instance.unsubscribe(this.onUpdate);
          instance.subscribe(this.onUpdate);
    
          return instance;
        });
    
        this.instances = instances;
        return instances;
      }
      
      /* ... */
    }
    

    _createInstances内部,如果检查到传入的props.to的值已经是状态管理实例(私有状态组件),那么直接使用即可,

    如果传入的是类class(共享状态组件),会尝试通过查询map,不存在的则通过new创建。

    
    export class Subscribe&lt;Containers: ContainersType&gt; extends React.Component&lt;
      SubscribeProps&lt;Containers&gt;,
      SubscribeState
    &gt; {
      
      /* 上面已讲 */
      
      render() {
        return (
          &lt;StateContext.Consumer&gt;
          /* Provider传递的map */
          {map =&gt;
              // children是函数
              this.props.children.APPly(
                null,
                // 传给子函数的参数(传进当前组件的状态管理实例)
                this._createInstances(map, this.props.to)
              )
            }
          &lt;/StateContext.Consumer&gt;
        );
      }
    }
    

    每一次render都会创建新的状态管理实例

    到此,3大板块已经阅读完毕。

    总结

    1. 简单易用,与React一致的API,一致的书写模式,让使用者很快上手。
    2. 并没有规定如何管理这些状态管理类,非常灵活。

      我们可以学redux将所有状态放到一个共享状态管理实例内部,

      例如通过Providerinject属性注入,

      或者针对每一个组件创建单独的状态管理实例(可共享可独立)(unstated作者推荐),

      一切可以按照自己的想法,但同时也要求使用者自己定义一些规则去约束写法。

    3. 仅仅是管理了状态,每次更新都是一个全新的instance集合,并没有做任何对比,需要我们在视图层自己实现。
    4. 返回值写成props.children的意义。
    5. 关于Promise.resolve().then({})setTimeout(()=>{},0)的区别。

    导图


    源码阅读专栏对一些中小型热门项目进行源码阅读和分析,对其整体做出导图,以便快速了解内部关系及执行顺序。

    当前源码(带注释),以及更多源码阅读内容:https://github.com/stonehank/sourcecode-analysis,欢迎fork,求

    来源:https://segmentfault.com/a/1190000017305209

    相关阅读

    关于layuiAdmin 后台管理模板购买授权的问题

    购买授权之前,建议认真阅读下述 “解惑”,以免造成不必要的困惑。另外也可以阅读 《layui 付费产品服务条款》注意:layuiAdmin 受国

    第七章 软件配置管理

    本章内容提要软件配置管理的作用软件配置管理的相关概念建立软件配置管理环境版本控制系统集成分支管理变更管理配置审计和配置状

    python之Django的入门08------事务管理、悲观锁、乐观

    上一篇文章链接Django07我们接着上一篇文章的基础上,来继续了解进一步的Django框架一.事务管理在实际项目里,事务管理是一个很重要

    使用Issue管理软件项目(入门)

    只要开始进行软件开发,数人配合,就必须进行项目管理,Issue是最通用的管理工具之一。 一、什么是Issue Issue指的的是一项待完成的

    电脑开机黑屏并弹出Windows 资源管理器已停止工作该怎

    电脑开机黑屏并弹出Windows 资源管理器已停止工作该怎么办?出现了一个问题,导致程序停止正常工作。如果有可用的解决方案,Windows将

    分享到:

    栏目导航

    推荐阅读

    热门阅读