前端数据存储库hox源码分析

1,404 阅读7分钟

hox做数据存储的方案是通过闭包实现的:

hox api只有一个 createModel(hook);   文档连接:github.com/umijs/hox

我模仿hox的源码写了一个简化版的数据存储,主要是为了分析原理,去掉了传入hook的逻辑处理(container)// 这里的数据是写死的,为了方便梳理分析过程,等等继续深入分析源码传入hook的处理。:

CreateModel 部分


export function myCreateModel() {
  const element = document.createElement('div');
  let state = {
    count: 1,
    decrement: () => {},
    increment: () => {},
    notify: val => {},
  };
  ReactDOM.render(
    <Executor
      onUpdate={val => {
        state = Object.assign(state, val);
        state.notify(val);
      }}
    />,
    element
  );

  function useModel() {
    let [data, setData] = useState(state);

    useEffect(() => {
      state.notify = val => setData(val) as any;
    }, [state]);

    return data;
  }
  return useModel;
}

export const useMyFn = myCreateModel();

代码说到useModel之前:

这里我先用一个临时变量state保存数据。

然后实例化一个函数组件Executor挂载到空div上,且Executor接受一个更新state的函数取名onUpdate。

再来看看Executor做了什么:

对hooks不熟悉的可以看这里:zh-hans.reactjs.org/docs/hooks-…

Executor 部分

import { ReactElement, useEffect, useMemo, useRef, useState } from 'react';

export function Executor<T>(props) {
  const [count, setCount] = useState(0);
  const decrement = () => setCount(count - 1);
  const increment = () => setCount(count + 1);
  const data = {
    count,
    decrement,
    increment,
  };
  props.onUpdate(data);
  return null as ReactElement;
}

Executor使用了useState来保存count,并且有两个更新count的fn,除了notify方法,data与上方的state属性一致。

这里其实挂载的是一个null,但是把Executor的逻辑保存了起来,利用了react的hooks特性,每次Executor的count发生变化时,都会触发内部逻辑。即执行传入的onUpdate方法。

而onUpdate方法会将Executor的data通过Object.assign方法赋值给createModel的临时变量state,从而更新了createModel中的state。

继续看createModel后续做了什么:

声明了useModel方法,useModel是一个自定义hook,用useState保存了临时变量 state作为data的初始化。且将state的notify属性改为更新自身的data。函数最后返回data。

createModel最后将useModel返回出去。

也就是说在你用createModel包了一层之后,其实后续使用的是useModel这个hook。

createModel利用闭包,将state数据以及Executor触发更新的执行器保存下来之后返回一个useModel。然后useModel会返回自身的data,这个data是与state数据保持一致的。就做到了数据存储。

这里有一篇作者写的简要的原理分析:zhuanlan.zhihu.com/p/89518937 会对整个hox原理有个大致的认知。

看一下使用例子:

示例

import React from 'react';
import { useMyFn } from './myHox';


export default function App(props) {
  const data = useMyFn();
  return (
    <div>
      <button onClick={data.decrement}>Decrement</button>
      <p>{data.count}</p>
      <br />
    </div>
  );
}

这个组件使用了useMyFn也就是myCreateModel()    ---→ 其实就是useModel。

这里的data即是useModel返回的data。但点击按钮的时候,触发了data.decrement函数,如果你对Executor还有印象的话,其实Executor的decrement函数,会将count减1,此时Executor的useState的数据发生了变更,触发react的renderWithHooks函数,会走一遍Executor的逻辑,触发useEffect,进而更新了外部闭包的state以及useModel的data。

而useModel的data会呈现到视图中,渲染视图更新。

上面就是createModel核心的数据存储变更的工作流程。

此时我们已经实现了静态数据存储,也就是createModel写死的state。当我们需要保存动态的数据时,只需要将state改成动态传入的参数(必须为hook,只有hook才能触发react的renderWithHooks函数),再对onUpdate做些更改即可。

此时的createModel还无法动态传入自定义hook。接下来看看源码是什么样的:

源码多出来的部分是干嘛用的:

CreateModel 源码

import { ModelHook, UseModel } from "./types";
import { Container } from "./container";
import ReactDOM from "react-dom";
import { Executor } from "./executor";
import React, { useEffect, useRef, useState } from "react";

export function createModel<T>(hook: ModelHook<T>) {
  const element = document.createElement("div");
  const container = new Container(hook);
  ReactDOM.render(
    <Executor
      onUpdate={val => {
        container.data = val;
        container.notify();
      }}
      hook={hook}
    />,
    element
  );
  const useModel: UseModel<T> = depsFn => {
    const [state, setState] = useState<T | undefined>(() =>
      container ? (container.data as T) : undefined
    );
    const depsFnRef = useRef(depsFn);
    depsFnRef.current = depsFn;
    const depsRef = useRef<unknown[]>([]);
    useEffect(() => {
      if (!container) return;
      function subscriber(val: T) {
        if (!depsFnRef.current) {
          setState(val);
        } else {
          const oldDeps = depsRef.current;
          const newDeps = depsFnRef.current(val);
          if (compare(oldDeps, newDeps)) {
            setState(val);
          }
          depsRef.current = newDeps;
        }
      }
      container.subscribers.add(subscriber);
      return () => {
        container.subscribers.delete(subscriber);
      };
    }, [container]);
    return state!;
  };
  Object.defineProperty(useModel, "data", {
    get: function() {
      return container.data;
    }
  });
  return useModel;
}

function compare(oldDeps: unknown[], newDeps: unknown[]) {
  if (oldDeps.length !== newDeps.length) {
    return true;
  }
  for (const index in newDeps) {
    if (oldDeps[index] !== newDeps[index]) {
      return true;
    }
  }
  return false;
}

这里多出了一个container,看一下container的源码:

Container 源码

export class Container<T = unknown> {
  constructor(public hook: ModelHook<T>) {}
  subscribers = new Set<Subscriber<T>>();
  data!: T;

  notify() {
    for (const subscriber of this.subscribers) {
      subscriber(this.data);
    }
  }
}

container的源码很简单,是一个类,构造函数是传入的hook。有个data属性,有个notify方法,看起来很像订阅发布的功能。

container分析完了,接着看Executor发生了上面变化:

Executor 源码

export function Executor<T>(props: {
  hook: ModelHook<T>;
  onUpdate: (data: T) => void;
}) {
  const data = props.hook();
  const initialLoad = useRef(false);

  useMemo(() => {
    // notify the initial value
    props.onUpdate(data);
    initialLoad.current = false;
  }, [])

  useEffect(()=>{
    if (initialLoad.current) {
      // notify the following value changes
      props.onUpdate(data);
    } else {
      initialLoad.current = true;
    }
  })

  return null as ReactElement;
}

可以看到,Executor只有data不一样,此时的data是传入的自定义hook逻辑执行完之后的返回值。

以及传入的onUpdate发生了变化:将新的val值更新到container的data属性,并且调用container的notify方法。

到目前可以知道,createModel先是创建了一个container的实例,用于保存传入自定义hook的返回值(container的构造函数就是hook)

接着创建了一个Executor组件实例,同时组件实例自身也拥有一个hook(即传入的hook)保存在Executor的data(其实和之前写死的hook逻辑一个道理),当data发生改变时会触发Executor的逻辑执行。

接着看看useModel做了什么:

useModel 部分

const useModel: UseModel<T> = depsFn => {
    const [state, setState] = useState<T | undefined>(() =>
      container ? (container.data as T) : undefined
    );
    const depsFnRef = useRef(depsFn);
    depsFnRef.current = depsFn;
    const depsRef = useRef<unknown[]>([]);
    useEffect(() => {
      if (!container) return;
      container.subscribers.add(subscriber);
      return () => {
        container.subscribers.delete(subscriber);
      };
    }, [container]);
    return state!;
  };

这里为了方便看清楚逻辑去掉了subscriber这个函数的定义。

useModel先是用useState保存container的data,然后定义了一个变量depsFnRef,用来保存使用useModel传入的函数,传入的函数作为判断state是否变更的依赖。

然后useEffect里面每当container发生变化时,就往container的subscribers增加一个subscriber,组件销毁的时候就会delete掉这个subscriber。

最后useModel返回state,也就是container.data的副本。

来看看subscribe这个函数定义:

subscribe

  function subscriber(val: T) {
    if (!depsFnRef.current) {
      setState(val);
    } else {
      const oldDeps = depsRef.current;
      const newDeps = depsFnRef.current(val);
      if (compare(oldDeps, newDeps)) {
        setState(val);
      }
      depsRef.current = newDeps;
    }
  }

判断是否有传入的变更依赖,没有直接设置当前的state为val,有的话就判断变更依赖是否改变,如改变则setState为val。

其实subscribe函数就是用来更新useModel的state。

subscribe会在container调用notify的时候触发,也就是在Executor触发onUpdate的时候触发,也就是在Executor的data发生改变的时候触发。Executor的data会通过onUpdate传入到container的data,进而传入到useModel的state。

就是通过Executor和container的hook逻辑一致,通过闭包保存了container的实例,来保存数据;通过Executor的实例组件来触发更新,最终反应到useModel上。

当我们用createModel包裹了我们的自定义hook之后,所使用的其实是createModel返回的useModel。

createModel通过闭包存储了数据,useModel则会同步这个数据,从而实现数据存储。

最后

最终的使用示例:官方demo

创建一个自定义hook,并且使用createModel API

import { createModel } from "hox";
import { useState } from 'react';

export function useCounter() {
  const [count, setCount] = useState(0);
  const decrement = () => setCount(count - 1);
  const increment = () => setCount(count + 1);
  return {
    count,
    decrement,
    increment
  };
}

export default createModel(useCounter);

在组件中引用这个向外导出的hook

import useCounterModel from './state';


function Myref() {
  const counter = useCounterModel();

  const onButtonClick = () => {
    counter.increment();
  };
  return (
    <>
      <p>{counter.count}</p>
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}


demo仓库:github.com/mango-lzp/h…

是一个create-react-app项目  加了hox,可以clone下来自己体验一下。

更新

///

更新:最新的代码改了Executor和createModel中ReactDom.render的部分。

Executor去掉了没有作用的判断,以及render使用了react-reconciler库代替原先的ReactDom.render。 hox核心逻辑(闭包暂存+订阅派发)没有变动。