Zustand 源码阅读计划(1)- JS 篇 - Vallina(框架无关)逻辑

9 天前(已编辑)
/ ,
20
摘要
在本文中,我们逐步理解并实现状态管理的核心。首先,定义了记录当前状态(state)、获取(getState)、修改(setState)的三大功能。然后添加了发布订阅模式,当状态变化时通知所有订阅者。在今天就讨论了一组包含 enhance、createContext、createSlice 的 JavaScript 变体代码。后续章节将涵盖适配 React 框架的代码。文中还提到了性能优化,如避免重复更新状态。这些概念共同构成了 Zustand 库的基石。

阅读此文章之前,你可能需要首先阅读以下的文章才能更好的理解上下文。

Zustand 源码阅读计划(1)- JS 篇 - Vallina(框架无关)逻辑

源码内部是 TS 的,我们暂时先只关注最核心的 JS 功能实现 你问我怎么只看 JS ?当然是看编译后的产物啦

完整代码

'use strict';

const createStoreImpl = (createState) => {
  let state;
  const listeners = /* @__PURE__ */ new Set();
  const setState = (partial, replace) => {
    const nextState = typeof partial === "function" ? partial(state) : partial;
    if (!Object.is(nextState, state)) {
      const previousState = state;
      state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
      listeners.forEach((listener) => listener(state, previousState));
    }
  };
  const getState = () => state;
  const getInitialState = () => initialState;
  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };
  const api = { setState, getState, getInitialState, subscribe };
  const initialState = state = createState(setState, getState, api);
  return api;
};
const createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;

exports.createStore = createStore;

设计思路

首先,我们不要为了看源码而看源码,知其然不知其所以然,源码是看懂了,但全然不知道为什么这么设计的,这也不行。

Zustand 本质是什么?是一个状态管理库,不管它再怎么样,实现的都是管理状态的。那么要管理状态,需要哪些功能?最需要的当然是记录当前状态,这是最重要的功能。有了记录了,我们还需要去获取,所以还需要获取当前状态的功能。状态从哪来呢?那就还需要修改当前状态的功能。

那么我们可以得出结论,要实现一个状态管理库,那么必须要有三个功能

  1. 记录存储当前的状态
  2. 获取当前最新的状态
  3. 修改当前存储的状态

初步源码解析

知道核心功能了,我们对照着源码来看看。

'use strict';

const createStoreImpl = (createState) => {
  // 1️⃣
  let state;
  // 2️⃣
  const setState = (partial) => {
      state = Object.assign({}, state, partial)
  };
  // 3️⃣
  const getState = () => state;
  // 4️⃣
  const api = { setState, getState };
  state = createState(setState, getState, api);
  return api;
};
const createStore = (createState) => createStoreImpl(createState);

exports.createStore = createStore;

我们简化了一下源码,去掉了一些工程上便利的东西,只保留了最核心的逻辑。

可以看到

1️⃣ 定义了一个 state 变量,用以记录存储当前的状态

2️⃣ 定义了一个 setState 方法,用以修改当前存储的状态

3️⃣ 定义了一个 getState 方法,用以获取当前最新的状态

4️⃣ 将 setStategetState 聚合到 api 中,创建 store 的时候就会将这两个方法返回出去,这样我们就可以随处调用这两个方法来获取状态或者修改状态了

这样,一个最简单的状态管理库就实现啦! 是不是很简单

我们来使用一下

最小实现示例

最小实现示例

当然,还未完待续,我们继续深入看看刚才被简化掉的内容,作用是什么。

发布订阅

除了我们主动获取最新数据以外,很多时候我们还需要被动通知。当状态发生变化的时候,需要通知每一处需要同步变化的地方。即,发布订阅模式。

我们提取一下发布订阅的核心逻辑

const createStoreImpl = (createState) => {
  let state;
  // 1️⃣ 
  const listeners = /* @__PURE__ */ new Set();
  const setState = (partial, replace) => {
    const nextState = partial;
    const previousState = state;
    state = Object.assign({}, state, nextState);
    // 2️⃣
    listeners.forEach((listener) => listener(state, previousState));
  };
  const getState = () => state;
  // 3️⃣
  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };
  const api = { setState, getState, subscribe };
  state = createState(setState, getState, api);
  return api;
};
const createStore = createStoreImpl(createState);

exports.createStore = createStore;

在上面的最小实现的基础上,添加了发布订阅的相关逻辑。

1️⃣ 定义了事件收集器,在这里用了 set 来做去重(记得通常是要先定义函数再传入,而非直接传入箭头函数。直接传入箭头函数无法去重,且 set 取消订阅时删除的效率高)。/* @__PURE__ */ 的标记是给打包工具看的,它的作用是告诉这些工具:“这个函数调用是‘纯粹的’(pure),没有副作用(side-effects)。”

如何理解这句话?这会影响到打包过程中的 tree-shaking 流程,当打包工具认为代码可能存在副作用(在这里是 set 定义与否会影响到其他地方使用,如果贸然删除会导致报错)。而声明了 /* @__PURE__ */,就等于告明打包工具,如果它判断该变量没有使用到,那么完全可以删除掉这个定义。

例如当用户完全没有使用到 subscribe 的时候,最终代码里也没必要保留相关的逻辑。

2️⃣ 当前状态发生变更时,通知所有的订阅者,将最新的状态和变化前的状态都传给订阅回调函数。在这里任何变化都会通知,但其实实际上我们订阅的时候只关注某些内容,这个留待后续讲解。

3️⃣ 订阅通知,传入一个回调函数,添加到事件中心,并返回一个取消监听的方法。

复杂判断

回看完整代码,显然还是比我们的实现复杂很多,那么复杂在哪里了呢。

1. setState

const nextState = typeof partial === "function" ? partial(state) : partial;

setStatepartial 部分是可以传值或者传一个函数进去的。而我们需要的是一个值,所以需要对传入的内容进行处理。

首先就是判断是否为函数,如果是函数,则调用,传入完整的 state 进去,由该函数返回需要变化的对象值。如果不是函数,那么就是变化的对象值,直接记录。

2. 创建 store

const createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;

createStore 这里你需要对 zustand 熟悉一点,这里的两种方法是为了对中间件做更好的类型支持,现在我们只讲 js 的部分,所以可以忽略。这个地方只支持传入回调函数都是可以的。即 const createStore = (createState) => createStoreImpl(createState)

3. 计算更新后的状态

state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);

这里又出现了新的东西,也就是 replace 参数,这个参数是用来表达是否需要完全替换当前状态,这个功能适用于同步后端最新数据,强制刷新,恢复某个状态等情况。

我们来分解一下这个复杂表达式。首先是大框架 (replace != null ? replace : T) ? nextState : Object.assign({}, state, nextState) 这个意思就是如果 replace 传递了值,那就看 replacetrue or false

  1. 如果是 true,那么就开启替换模式,将变化的状态直接赋值给 state
  2. 如果是 false,那么就是合并模式,将变化的状态覆盖原本的 state,创建到一个新对象中。

如果 replace 就是没传递值呢?那就取决于这块内容了 typeof nextState !== "object" || nextState === null 判定 nextState 如果不是对象,或者 nextState 是 null,那就没法和 state 合并,所以直接使用 nextState 替换当前状态。这个时候你可能有点疑问,什么时候会这个样子?

那答案当然是,setState(null)

性能优化

setState 中的 !Object.is(nextState, state) 如果完全没变化,那就没必要更新状态并通知所有监听者

SSR(服务端渲染) 支持

服务端渲染的时候,需要有个直接就能获取到的初始的值,以便预渲染页面内容。

'use strict';

const createStoreImpl = (createState) => {
  let state;
  const listeners = /* @__PURE__ */ new Set();
  const setState = (partial, replace) => {
    const nextState = typeof partial === "function" ? partial(state) : partial;
    if (!Object.is(nextState, state)) {
      const previousState = state;
      state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
      listeners.forEach((listener) => listener(state, previousState));
    }
  };
  const getState = () => state;
  // 1️⃣
  const getInitialState = () => initialState;
  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };
  // 2️⃣
  const api = { setState, getState, getInitialState, subscribe };
  // 3️⃣
  const initialState = state = createState(setState, getState, api);
  return api;
};
const createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;

exports.createStore = createStore;

1️⃣ 此处定义了一个函数,返回初始的状态

2️⃣ 将 getInitialState 方法暴露出去以供调用

3️⃣ 预执行传入的createState函数,返回一个初始的状态值。将这个初始的状态值赋值给 stateinitialState

结语

框架无关的部分就到此结束了,在本篇文章里,我们从最小实现开始逐步理解并实现状态管理的本质、核心、易用的逐渐外扩。

在下一章节中,我们将学习 JS 版的 react 框架适配代码。

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...