乐趣区

关于javascript:突破Hooks所有限制只要50行代码

大家好,我是卡颂。

你是否很厌恶 Hooks 调用程序的限度(Hooks不能写在条件语句里)?

你是否遇到过在 useEffect 中应用了某个 state,又遗记将其退出 依赖项 ,导致useEffect 回调执行机会出问题?

怪本人大意?怪本人不好难看文档?

许可我,不要怪本人。

根本原因在于 React 没有将 Hooks 实现为响应式更新。

是实现难度很高么?本文会用 50 行代码实现有限制版 Hooks,其中波及的常识也是VueMobx 等基于响应式更新的库的底层原理。

本文的正确食用形式是珍藏后用电脑看,跟着我一起敲代码(残缺在线 Demo 链接见文章结尾)。

手机党要是看了懵逼的话不要自责,是你食用形式不对。

注:本文代码来自 Ryan Carniato 的文章 Building a Reactive Library from Scratch
,老哥是 SolidJS 作者

万丈高楼平地起

首先来实现useState

function useState(value) {const getter = () => value;
  const setter = (newValue) => value = newValue;
  
  return [getter, setter];
}

返回值数组第一项负责取值,第二项负责赋值。相比 React,咱们有个小改变:返回值的第一个参数是个函数而不是state 自身。

应用形式如下:

const [count, setCount] = useState(0);

console.log(count()); // 0
setCount(1);
console.log(count()); // 1

没有黑魔法

接下来实现useEffect,包含几个要点:

  • 依赖的 state 扭转,useEffect回调执行
  • 不须要显式的指定依赖项(即 ReactuseEffect的第二个参数)

举个例子:

const [count, setCount] = useState(0);

useEffect(() => {window.title = count();
})
useEffect(() => {console.log('没我啥事儿')
})

count变动后第一个 useEffect 会执行回调(因为他外部依赖 count),然而第二个useEffect 不会执行。

前端没有黑魔法,这里是如何实现的呢?

答案是:订阅公布。

持续用下面的例子来解释订阅公布关系建设的机会:

const [count, setCount] = useState(0);

useEffect(() => {window.title = count();
})

useEffect 定义后他的回调会立即执行一次,在其外部会执行:

window.title = count();

count执行时会建设 effectstate之间订阅公布的关系。

当下次执行 setCount(setter)时会告诉订阅了count 变动的useEffect,执行其回调函数。

数据结构之间的关系如图:

每个 useState 外部有个汇合 subs,用来保留 订阅该 state 变动 effect

effect是每个 useEffect 对应的数据结构:

const effect = {
  execute,
  deps: new Set()}

其中:

  • execute:该 useEffect 的回调函数
  • deps:该 useEffect 依赖的 state 对应 subs 的汇合

我晓得你有点晕。看看下面的结构图,缓缓,咱再持续。

实现 useEffect

首先须要一个栈来保留以后正在执行的 effect。这样当调用getterstate才晓得应该与哪个 effect 建立联系。

举个例子:

// effect1
useEffect(() => {window.title = count();
})
// effect2
useEffect(() => {console.log('没我啥事儿')
})

count执行时须要晓得本人处在 effect1 的上下文中(而不是 effect2),这样能力与effect1 建立联系。

// 以后正在执行 effect 的栈
const effectStack = [];

接下来实现useEffect,包含如下性能点:

  • 每次 useEffect 回调执行前重置依赖(回调外部 stategetter会重建依赖关系)
  • 回调执行时确保以后 effect 处在 effectStack 栈顶
  • 回调执行后将以后 effect 从栈顶弹出

代码如下:

  function useEffect(callback) {const execute = () => {
      // 重置依赖
      cleanup(effect);
      // 推入栈顶
      effectStack.push(effect);

      try {callback();
      } finally {
        // 出栈
        effectStack.pop();}
    }
    const effect = {
      execute,
      deps: new Set()}
    // 立即执行一次,建设依赖关系
    execute();}

cleanup用来移除该 effect 与所有他依赖的 state 之间的分割,包含:

  • 订阅关系:将该 effect 订阅的所有 state 变动移除
  • 依赖关系:将该 effect 依赖的所有 state 移除
function cleanup(effect) {
  // 将该 effect 订阅的所有 state 变动移除
  for (const dep of effect.deps) {dep.delete(effect);
  }
  // 将该 effect 依赖的所有 state 移除
  effect.deps.clear();}

移除后,执行 useEffect 回调会再逐个重建关系。

革新 useState

接下来革新useState,实现建设订阅公布关系的逻辑,要点如下:

  • 调用 getter 时获取以后上下文的effect,建设关系
  • 调用 setter 时告诉所有订阅该 state 变动的 effect 回调执行
function useState(value) {
  // 订阅列表
  const subs = new Set();

  const getter = () => {
    // 获取以后上下文的 effect
    const effect = effectStack[effectStack.length - 1];
    if (effect) {
      // 建立联系
      subscribe(effect, subs);
    }
    return value;
  }
  const setter = (nextValue) => {
    value = nextValue;
    // 告诉所有订阅该 state 变动的 effect 回调执行
    for (const sub of [...subs]) {sub.execute();
    }
  }
  return [getter, setter];
}

subscribe的实现,同样包含 2 个关系的建设:

function subscribe(effect, subs) {
  // 订阅关系建设
  subs.add(effect);
  // 依赖关系建设
  effect.deps.add(subs);
}

让咱们来试验下:

const [name1, setName1] = useState('KaSong');
useEffect(() => console.log('谁在那儿!', name1())) 
// 打印:谁在那儿!KaSong
setName1('KaKaSong');
// 打印:谁在那儿!KaKaSong

实现 useMemo

接下来基于已有的 2 个 hook 实现useMemo

function useMemo(callback) {const [s, set] = useState();
  useEffect(() => set(callback()));
  return s;
}

主动依赖跟踪

这套 50 行的 Hooks 还有个弱小的暗藏个性:主动依赖跟踪。

咱们拓展下下面的例子:

const [name1, setName1] = useState('KaSong');
const [name2, setName2] = useState('XiaoMing');
const [showAll, triggerShowAll] = useState(true);

const whoIsHere = useMemo(() => {if (!showAll()) {return name1();
  }
  return `${name1()} 和 ${name2()}`;
})

useEffect(() => console.log('谁在那儿!', whoIsHere()))

当初咱们有 3 个statename1name2showAll

whoIsHere作为memo,依赖以上三个state

最初,当 whoIsHere 变动时,会触发 useEffect 回调。

当以上代码运行后,基于初始的 3 个 state,会计算出whoIsHere,进而触发useEffect 回调,打印:

// 打印:谁在那儿!KaSong 和 XiaoMing

接下来调用:

setName1('KaKaSong');
// 打印:谁在那儿!KaKaSong 和 XiaoMing
triggerShowAll(false);
// 打印:谁在那儿!KaKaSong

上面的事件就乏味了,当调用:

setName2('XiaoHong');

并没有 log 打印。

这是因为当 triggerShowAll(false) 导致 showAll statefalse后,whoIsHere进入如下逻辑:

if (!showAll()) {return name1();
}

因为没有执行 name2,所以name2whoIsHere曾经没有订阅公布关系了!

只有当 triggerShowAll(true) 后,whoIsHere进入如下逻辑:

return `${name1()} 和 ${name2()}`;

此时 whoIsHere 才会从新依赖 name1name2

主动的依赖跟踪,是不是很酷~

总结

至此,基于 订阅公布 ,咱们实现了能够 主动依赖跟踪 的无限度Hooks

这套理念是最近几年才有人应用么?

早在 2010 年初 KnockoutJS 就用这种细粒度的形式实现响应式更新了。

不晓得那时候,Steve SandersonKnockoutJS作者)有没有预见到 10 年后的明天,细粒度更新会在各种库和框架中被宽泛应用。

这里是:残缺在线 Demo 链接

退出移动版