yliu

时来天地皆同力,运去英雄不自由

resso 源码解析


在之前用 React 写一些小项目的时候有时也需要用到状态管理,但是用 Redux 有一种“大材小用”感觉,就在寻找有没有很精巧的实现,最好是响应式的,最后搜索了一番在 Github 发现了两个满足我需求的库:

// valtio
import resso from 'resso';
import { proxy, useSnapshot } from 'valtio';

const state = proxy({ count: 0, text: 'hello' });

function Counter() {
  const snap = useSnapshot(state);
  return (
    <div>
      {snap.count}
      <button onClick={() => ++state.count}>+1</button>
    </div>
  );
}
// resso
const store = resso({ count: 0, text: 'hello' });

function App() {
  const { count } = store; // data used in UI → must destructure at top first 🥷
  return (
    <>
      {count}
      <button onClick={() => (store.count += 1)}>+</button>
    </>
  );
}

结合官方给出的文档示例还是最终在项目用了 resso,所以就有了这篇文章,同时作者在知乎也专门写了一篇文章介绍他的思路实现resso,世界上最简单的 React 状态管理器。有兴趣小伙伴可以专门去瞅瞅。

不过在阅读源码实现,需要先了解两个 API:

  • useSyncExternalStore
  • ReactDOM.unstable_batchedUpdates

useSyncExternalStore

这个 API 可能你完全陌生,事实上我也是,后面专门去官方文档查阅了一下 useSyncExternalStore,它的作用可以简单理解为订阅外部的 store,例如我们在使用 redux 之类的时候,它可能不是通过 useState 的形式而是在外部维护了一个 store,然后结合发布订阅模式来实现数据的更新。

下面用官方给出的切换离线和在线 hooks 例子快速看下这个 api

import { useSyncExternalStore } from 'react';

function getSnapshot() {
  return navigator.onLine;
}
function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function ChatIndicator() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  // ...
}

这里每次 online 或者 offline 发生变化执行回调函数,而每次执行 getSnapshot 如果返回值不同,就会让 React 重新渲染组件。

每次对比 getSnapshot 返回值使用的是 Object.is。

unstable_batchedUpdates

之所以介绍这个 API 其实和性能优化有关,在 React18 之前对于非同步代码和不是 react 事件处理函数 setState 不会进行批处理,例如下面代码:

import React, { useEffect, useState } from 'react';

const App: React.FC = () => {
  console.log('App组件渲染了!');
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  useEffect(() => {
    document.body.addEventListener('click', () => {
      setCount1((count) => count + 1);
      setCount2((count) => count + 1);
    });
    // 在原生js事件中不会进行批处理
  }, []);
  return (
    <>
      <div>count1: {count1}</div>
      <div>count2: {count2}</div>
    </>
  );
};

export default App;

这里每次点击组件都会渲染两次,共计输出六次 console.log 信息。

但是有什么办法可以改变呢?下面用 ReactDOM.unstable_batchedUpdates 重写上面这个例子

import React, { useEffect, useState } from 'react';

const App: React.FC = () => {
  console.log('App组件渲染了!');
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  useEffect(() => {
    document.body.addEventListener('click', () => {
      ReactDOM.unstable_batchedUpdates(() => {
        setCount1((count) => count + 1);
        setCount2((count) => count + 1);
      });
    });
  }, []);
  return (
    <>
      <div>count1: {count1}</div>
      <div>count2: {count2}</div>
    </>
  );
};

export default App;

这样就会每次更新状态组件只渲染一次了,最后提示一下 unstable_batchedUpdates 是同步代码

实现思想

在代码阅读之前有必要说下它的设计实现思想是啥子。

在使用 redux 的时候,我们更新可能是通过 dispatch 这样的一个函数来完成,但是有没有其他方式呢?ES6 引入了 proxy 可以对对象的值进行劫持,每次更新都可以获取到对应的 key 和 value。

事实上在 Vue2 中也可以通过监听属性的 seter 来实现监听,虽然效果不完整。

试着定义一个 store

export const store = { a: 123, b: 456 };

在组件中分别使用

// 组件 A
const {a} = store;

// 组件B
const {a,b} = store;

更新方式也很简单,直接更改 store 属性就行,例如:

// 组件 A 和 B 都得到更新
store.b = 'b';

那么要怎么实现上述我们定义的这样一个使用方式呢?

作者是通过 proxy 每次 get 的时候自动把对应的 key 值保存到 map 键名,而对应的 setState 作为 value,例如在组件 A 和 B 它们可能是这样储存的。

// 组件 A
const [a, setA] = useState(store.a);
// 组件 B
const [a, setA] = useState(store.a);
const [b, setB] = useState(store.b);
// map
const listenerMap = {
  a: [setA, setA],
  b: [setB],
};

之后监听修改的属性,如果有更新直接调用对应 key 下的 setState 就可以做到更新。

// 假设更新 A
listenerMap.a.forEach((setA) => setA(store.a));

源码分析

作者是通过 TypeScript 来实现的,但是这里关注具体的实现,对于类型直接删除了,如果有需要小伙伴可以自行去阅读。

因为代码量不是很大,所以这里直接通过贴代码+注释的形式来进行讲解。

// 这里因为是状态库,需要考虑不同的 React 版本,React18 可以直接 import React 来获取 useSyncExternalStore
import { useSyncExternalStore } from 'use-sync-external-store/shim';

// 判断是否为开发环境
const __DEV__ = process.env.NODE_ENV !== 'production';

// 判断是否为对象, {} 这样的,通用的 map、set 之类的对象都统统不可以
const isObj = (val) => {
  return Object.prototype.toString.call(val) === '[object Object]';
};

// 这里是一个标识符后面讲解
let isGetStateInMethod = false;

let run = (fn) => {
  fn();
};

const resso = (obj) => {
  // 初始检查
  if (__DEV__ && !isObj(obj)) {
    throw new Error('object required');
  }

  /*
   * 它储存的形式为 {[key:string]: Set()}
   */
  const state = {};
  /*
   * 它储存的形式为 {key: Function}
   */
  const methods = {};

  Object.keys(obj).forEach((key) => {
    const initVal = obj[key];
    // 给 methods 添加属性
    if (initVal instanceof Function) {
      methods[key] = (...args) => {
        isGetStateInMethod = true;
        const res = initVal(...args);
        isGetStateInMethod = false;
        return res;
      };
      return;
    }
    // 给 state 添加属性
    const listeners = new Set();

    state[key] = {
      // 添加 getSnapshot 到 listeners
      subscribe: (listener) => {
        listeners.add(listener);
        return () => listeners.delete(listener);
      },
      // 数据快照
      getSnapshot: () => obj[key],
      // 更新方法
      setSnapshot: (val) => {
        if (val !== obj[key]) {
          obj[key] = val;
          run(() => listeners.forEach((listener) => listener()));
        }
      },
      useSnapshot: () => {
        return useSyncExternalStore(
          state[key].subscribe,
          state[key].getSnapshot,
          state[key].getSnapshot
        );
      },
    };
  });

  const setState = (key, val) => {
    // 只更新初始 obj 定义属性
    if (key in obj) {
      if (key in state) {
        // 判断是属性更新还是函数调用更新
        const newVal = val instanceof Function ? val(obj[key]) : val;
        // 调用上文的 setSnapshot
        state[key].setSnapshot(newVal);
      } else if (__DEV__) {
        throw new Error(`\`${key}\` is a method, can not update`);
      }
    } else if (__DEV__) {
      throw new Error(`\`${key}\` is not initialized in store`);
    }
  };

  return new Proxy(() => undefined, {
    get: (_target, key) => {
      if (key in methods) {
        return methods[key];
      }
      // 重点讲下这段代码,首先判断是否为 state,也就是上文中初始传递的对象非函数部分
      if (key in state) {
        // 这里判断是否 isGetStateInMethod 主要是因为可能存在在 methods 中来引用 store 中的属性,所以如果是这种情况直接返回
        if (isGetStateInMethod) {
          return obj[key];
        }
        // 这里从外部拿到对应的值返回,如果不是在最外层使用(React 限制,不可以在回调之类的函数内使用use之类方法,这里直接返回 obj 下的值)
        try {
          return state[key].useSnapshot();
        } catch (err) {
          return obj[key];
        }
      }
      if (__DEV__) {
        if (key !== 'prototype' && key !== 'name' && key !== 'displayName') {
          throw new Error(`\`${key}\` is not initialized in store`);
        }
      }
    },
    // 这里监听属性变动,例如这种形式调用更新 store.count = 60;
    set: (_target, key, val) => {
      setState(key, val);
      return true;
    },

    /* 
    * apple是指函数调用,官方文档是支持下面这种形式来更新的
    * store({ count: 60, text: 'world' });
    * 参数分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组
    * 这里之所以解构成 [firstArg, oneAction] 形式是因为,除了上面这种方式还可以
      store('count', (prev) => prev + 1); 
      store((prev) => ({
        count: prev.count + 1,
        text: prev.text === 'hello' ? 'world' : 'hello',
      }));
    */
    apply: (_target, _thisArg, [firstArg, oneAction]) => {
      // store('count', (prev) => prev + 1);
      if (typeof firstArg === 'string') {
        setState(firstArg, oneAction);
        return;
      }
      // store({ count: 60, text: 'world' });
      if (isObj(firstArg)) {
        const newObj = firstArg;
        Object.keys(newObj).forEach((key) => {
          setState(key, newObj[key]);
        });
        return;
      }
      //  store((prev) => ({
      //   count: prev.count + 1,
      //   text: prev.text === 'hello' ? 'world' : 'hello',
      // }));
      if (typeof firstArg === 'function') {
        const newObj = firstArg(obj);
        Object.keys(newObj).forEach((key) => {
          setState(key, newObj[key]);
        });
      }
    },
  });
};

// 这里是给用户暴露配置,例如上面说的,你想要在异步代码中执行批量更新 unstable_batchedupdates
resso.config = ({ batch }) => {
  run = batch;
};

export default resso;

最后

这是一篇很早之前就想写的文章,一直拖到现在才完成,最后如果有理解错误或者文笔错误欢迎指出,同时有可以内推的岗位欢迎滴滴。

文章参考: