资讯专栏INFORMATION COLUMN

[源码阅读]纯粹极简的react状态管理组件unstated

FrancisSoung / 1059人阅读

摘要:此处继承了上面的可以注入现成的状态管理实例,添加到之中。返回值写成的意义简单一句话概括,这么写可以避免改变导致子组件的重复渲染。就是创建状态管理组件时默认传递的监听函数,用的是的更新一个空对象。返回值写成的意义。

简介

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 {
  state = {
    count: 0
  };

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

  decrement() {
    this.setState({ count: this.state.count - 1 });
  }
}
// 渲染视图组件(Context.Consumer的模式)
function Counter() {
  return (
    
      {counter => (
        
{counter.state.count}
)}
); } render( , document.getElementById("root") );

这里Counter是我们自定义的视图组件,首先使用包裹,接着在Counter内部,调用组件,
传递一个数组给props.to,这个数组内存放了Counter组件需要使用的状态管理类(此处也可传递状态管理实例)。

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

        // 负责将childMap传递,初始为null
        return (
          
            {props.children}
          
        );
      }}
    
  );
}

这里的模式是


  ()=>{
    /* ... */
    return {props.children}
  }
  

有3个注意点:

外层嵌套可以嵌套调用。


 /* ... */
 
 /* ... */ 

props.inject可以注入现成的状态管理实例,添加到map之中。

返回值写成props.children

返回值写成props.children的意义

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

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

Container
export class Container {
  // 保存状态 默认为{}
  state: State;
  // 保存监听函数,默认为[]
  _listeners: Array = [];

  setState(
    updater: $Shape | ((prevState: $Shape) => $Shape),
    callback?: () => void
  ): Promise {
    return Promise.resolve().then(() => {
      let nextState;

      /* 利用Object.assign改变state */

      // 执行listener(promise)
      let promises = this._listeners.map(listener => listener());

      // 所有Promise执行完毕
      return Promise.all(promises).then(() => {
        // 全部listener执行完毕,执行回调
        if (callback) {
          return callback();
        }
      });
    });
  }

  // 增加订阅(这里默认的订阅就是React的setState空值(为了重新渲染),也可以添加自定义监听函数)
  subscribe(fn: Listener) {
    this._listeners.push(fn);
  }

  // 取消订阅
  unsubscribe(fn: Listener) {
    this._listeners = this._listeners.filter(f => f !== fn);
  }
}

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

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

每个状态管理实例存在一个默认监听函数onUpdate
这个默认的监听函数的作用就是调用React的setState强制视图重新渲染

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

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

例如,在官方例子中:

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

2个注意点:

setStateReact API一致,第一个参数传入object或者function,第二个传入回调

这里通过Promise.resolve().then模拟this.setState的异步执行

关于Promise.resolve和setTimeout的区别

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

setTimeout(()=>{},0)会放入下一个新的任务队列

Promise.resolve().then({})会放入微任务,在调用栈为空时立刻补充调用栈并执行(简单理解为当前任务队列尾部)

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

Subscribe
export class Subscribe extends React.Component<
  SubscribeProps,
  SubscribeState
> {
  state = {};
  // 存放传入的状态组件
  instances: Array = [];
  unmounted = false;

  componentWillUnmount() {
    this.unmounted = true;
    this._unsubscribe();
  }

  _unsubscribe() {
    this.instances.forEach(container => {
      // container为当前组件的每一个状态管理实例
      // 删除listeners中的this.onUpdate
      container.unsubscribe(this.onUpdate);
    });
  }

  onUpdate: Listener = () => {
    return new Promise(resolve => {
      // 组件未被卸载
      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 extends React.Component<
  SubscribeProps,
  SubscribeState
> {
  /* 上面已讲 */

  _createInstances(
    map: ContainerMapType | null,
    containers: ContainersType
  ): Array {
    // 首先全部instances解除订阅
    this._unsubscribe();

    // 必须存在map 必须被Provider包裹才会有map
    if (map === null) {
      throw new Error(
        "You must wrap your  components with a "
      );
    }

    let safeMap = map;
    // 重新定义当前组件的状态管理组件(根据to传入的数组)
    let instances = containers.map(ContainerItem => {
      let instance;

      // 传入的是Container组件,则使用
      if (
        typeof ContainerItem === "object" &&
        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 extends React.Component<
  SubscribeProps,
  SubscribeState
> {
  
  /* 上面已讲 */
  
  render() {
    return (
      
      /* Provider传递的map */
      {map =>
          // children是函数
          this.props.children.apply(
            null,
            // 传给子函数的参数(传进当前组件的状态管理实例)
            this._createInstances(map, this.props.to)
          )
        }
      
    );
  }
}

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

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

总结

简单易用,与React一致的API,一致的书写模式,让使用者很快上手。

并没有规定如何管理这些状态管理类,非常灵活。

我们可以学redux将所有状态放到一个共享状态管理实例内部,
例如通过Providerinject属性注入,

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

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

仅仅是管理了状态,每次更新都是一个全新的instance集合,并没有做任何对比,需要我们在视图层自己实现。

返回值写成props.children的意义。

关于Promise.resolve().then({})setTimeout(()=>{},0)的区别。

导图

源码阅读专栏对一些中小型热门项目进行源码阅读和分析,对其整体做出导图,以便快速了解内部关系及执行顺序。
当前源码(带注释),以及更多源码阅读内容:https://github.com/stonehank/sourcecode-analysis,欢迎fork,求

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/99808.html

相关文章

  • 理解 React 轻量状态管理Unstated

    摘要:返回,用来包裹顶层组件,向应用中注入状态管理实例,可做数据的初始化。方法返回创建的状态管理实例,作为参数传递给调用的函数,函数拿到实例,操作或显示数据。用来实现一个状态管理类。为中的状态管理实例数据。 个人网站: https://www.neroht.com 在React写应用的时候,难免遇到跨组件通信的问题。现在已经有很多的解决方案。 React本身的Context Redux结合...

    Profeel 评论0 收藏0
  • React组件设计实践总结05 - 状态管理

    摘要:要求通过要求数据变更函数使用装饰或放在函数中,目的就是让状态的变更根据可预测性单向数据流。同一份数据需要响应到多个视图,且被多个视图进行变更需要维护全局状态,并在他们变动时响应到视图数据流变得复杂,组件本身已经无法驾驭。今天是 520,这是本系列最后一篇文章,主要涵盖 React 状态管理的相关方案。 前几篇文章在掘金首发基本石沉大海, 没什么阅读量. 可能是文章篇幅太长了?掘金值太低了? ...

    ideaa 评论0 收藏0
  • 读zent源码库之Dialog组件实现

    摘要:但是,最后一步,事件怎么绑定呢这块没有深入研究了,不过我想,应该这样去实现也是没有问题的。的具体做法是,把方法放到了一个叫做的组件上去实现这个功能,然后再把内容放进这个组件。其他的逻辑比如显示隐藏之类,全部都放到组件自身上去实现。 1、Dialog组件提供什么功能,解决什么问题? zent的Dialog组件,使用姿势是这样的(代码摘自zent官方文档:https://www.youza...

    陈江龙 评论0 收藏0
  • 前端每周清单第 50 期: AngularJS and Long Term Support, Web

    摘要:在该版本发布之后,开发团队并不会继续发布新的特性,而会着眼于进行重大的错误修复。发布每六个星期,团队就会创建新的分支作为发布通道,本文即是对新近发布的版本进行简要介绍。 showImg(https://segmentfault.com/img/remote/1460000013229009); 前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点;分为新闻热...

    DobbyKim 评论0 收藏0
  • React 新 Context API 在前端状态管理的实践

    摘要:本文转载至今日头条技术博客众所周知,的单向数据流模式导致状态只能一级一级的由父组件传递到子组件,在大中型应用中较为繁琐不好管理,通常我们需要使用来帮助我们进行管理,然而随着的发布,新成为了新的选择。 本文转载至:今日头条技术博客showImg(https://segmentfault.com/img/bVbiNJO?w=900&h=383);众所周知,React的单向数据流模式导致状态...

    wing324 评论0 收藏0

发表评论

0条评论

FrancisSoung

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<