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

36次阅读

共计 4066 个字符,预计需要花费 11 分钟才能阅读完成。

大家好,我是卡颂。

你是否很厌恶 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 链接

正文完
 0