11
头图

Hi everyone, this is Kasong.

Do you hate the Hooks calling sequence of Hooks (060ed0fd26591a cannot be written in conditional statements)?

Have you ever encountered a useEffect used in state , and forgot to add it to the dependency, causing problems with the timing of the useEffect

Blame yourself for being careless? Blame yourself for not reading the document?

Promise me, don't blame yourself.

The root cause is that React did not Hooks as a responsive update.

Is it difficult to implement? This article will use 50 lines of code to implement the unlimited version of Hooks , and the knowledge involved is also Vue , Mobx and other libraries based on responsive updates.

The correct way to eat this article is to read it with a computer after collecting it, and follow me to type the code (see the end of the article for the Demo

Don’t blame yourself if the mobile party is dumbfounded, it’s because you eat it in the wrong way.

Note: The code in this article comes from Ryan Carniato's article Building a Reactive Library from Scratch
SolidJS brother is the author of 060ed0fd265aa4

Rise from the ground

First, implement useState :

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

The first item of the return value array is responsible for obtaining values, and the second item is responsible for assigning values. Compared to React , we have a small change: the first parameter of the return value is a function instead of state itself.

The usage is as follows:

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

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

No black magic

Next, implement useEffect , including several key points:

  • Dependent state change, useEffect callback execution
  • No need to explicitly specify dependencies (that is, the second parameter of React useEffect

for example:

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

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

count changes, the first useEffect will execute the callback (because it relies on count internally), but the second useEffect will not be executed.

There is no black magic on the front end, how is it achieved here?

The answer is: subscribe to publish.

Continue to use the above example to explain the timing of the establishment of a subscription-publishing relationship:

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

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

When useEffect defined, its callback will be executed immediately, and it will be executed internally:

window.title = count();

count will establish the subscription and publication relationship between effect and state

The next execution setCount when (setter) will notify subscribed count change useEffect , execute its callback function.

The relationship between the data structure is shown in the figure:

Each useState has a set subs , which is used to save subscribed to the state change effect .

effect is the data structure corresponding to useEffect

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

among them:

  • execute : the callback function of useEffect
  • deps : The useEffect state that the 060ed0fd265db4 depends on corresponds to subs

I know you are a little dizzy. Look at the structure diagram above, slowly, let's continue.

Implement useEffect

First, a stack is needed to save the currently executing effect . In this way, when getter is state effect should be connected with.

for example:

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

count need to know what is in the execution effect1 context of (but not effect2 ), in order to effect1 establish contact.

// 当前正在执行effect的栈
const effectStack = [];

Next, implement useEffect , including the following function points:

  • Reset the dependencies before each useEffect callback is executed (the state of the getter inside the callback will rebuild the dependencies)
  • When the callback is executed, make sure that the current effect is at the top effectStack
  • effect from the top of the stack after the callback is executed

code show as below:

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

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

cleanup used to remove the effect with all his dependent state link between, including:

  • Subscribe relationship: the effect all subscribed state changes removed
  • Dependency: Remove state effect depends on
function cleanup(effect) {
  // 将该effect订阅的所有state变化移除
  for (const dep of effect.deps) {
    dep.delete(effect);
  }
  // 将该effect依赖的所有state移除
  effect.deps.clear();
}

After removal, execute the useEffect callback to rebuild the relationship one by one.

Transform useState

Next, transform useState to complete the logic of establishing a subscription-publishing relationship. The main points are as follows:

  • Call getter get the current context when effect , build relationships
  • When calling setter effect callbacks that subscribe to the state change to execute
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 also includes the establishment of two relationships:

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

Let's try it out:

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

Implement useMemo

Next, based on the existing two hook achieve useMemo :

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

Automatic dependency tracking

This set of 50 lines of Hooks also has a powerful hidden feature: automatic dependency tracking.

Let's extend the above example:

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()))

Now we have 3 state : name1 , name2 , showAll .

whoIsHere as memo , depends on the above three state .

Finally, when whoIsHere changes, the useEffect callback will be triggered.

When the above code runs, based on the initial three state , whoIsHere calculated, and then the useEffect callback will be triggered and printed:

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

Next call:

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

The following things are interesting when called:

setName2('XiaoHong');

And there is no log printed.

This is because when triggerShowAll(false) causes showAll state to false , whoIsHere enters the following logic:

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

name2 not executed, name2 and whoIsHere no longer have a subscription publishing relationship!

Only after triggerShowAll(true) , whoIsHere enters the following logic:

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

At this time, whoIsHere will rely on name1 and name2 .

Automatic dependency tracking, isn’t it cool~

to sum up

At this point, based on subscription publication , we can achieve the automatic dependency tracking unlimited Hooks .

Is this concept only used in recent years?

As early as the beginning of 2010, KnockoutJS used this fine-grained way to implement responsive updates.

I don't know at that time, Steve Sanderson ( KnockoutJS ) did you foresee that fine-grained updates will be widely used in various libraries and frameworks today, 10 years later.

Here is: complete online Demo link


卡颂
3.1k 声望16.7k 粉丝