preface

React Hooks basic usage, the official documentation is very detailed. This is the third article in a series looking at the hooks implementation mechanism.

  • Drill into React hooks — useState
  • Get into React hooks — useEffect
  • Personal Blog Address🍹 🍰 fe – code

useState

In this article, we have analyzed the implementation mechanism of useState and useEffect. If you want to learn more about hooks, you can implement the hooks themselves, or read the source code directly. Those of you who are not familiar with some of the basic features of Hooks can refer to the documentation, or to my first two articles.

UseState is the basis on which the hooks scheme can be implemented, which allows components to have their own state without relying on classes. It’s simple to use:

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

Two things to keep in mind: useState is a function; 2. Each update actually triggers its re-execution (as mentioned in the previous article, because the entire function component is re-executed).

Here’s how to make it simple:

function useState(initialState) {
      const state = initialState;
      const setState = newState= > state = newState;
      return [state, setState];
}
Copy the code

Now of course it’s incomplete because it doesn’t update the data at all. Even when we setState, we trigger the component update, and then the useState update, we just initialize the initialState again. Since the state of useState is a local variable each time, the update is a re-creation, and the previous value is never available. So, we need to save state.

The simplest thing is a global variable, but React isn’t implemented that way. For hooks, React uses linked lists. If you are interested, check them out for yourself.

const HOOKS = []; // Globally store hook variables
let currentIndex = 0; // Global dependency hook execution order subscript
function useState(initialState) {
   HOOKS[currentIndex] = HOOKS[currentIndex] || initialState; // Determine if initialization is required
   const memoryCurrentIndex = currentIndex; // currentIndex is globally mutable
   const setState = newState= > HOOKS[memoryCurrentIndex] = newState;
   return [HOOKS[currentIndex++], setState]; // To call hook multiple times, index needs to be +1 each time
}
Copy the code

We save all the hooks in a global array variable, just look for the corresponding subscript on each update. This is why it is important to ensure that the order in which hooks are executed is consistent throughout updates. Imagine that the first run executes four hooks, increasing the subscript to 3; When updating, the original third hook was not executed because of if judgment and other reasons, so is the value of the fourth hook wrong? .

To extend this, useState initialization can pass functions, as can setState, so you can use a little judgment.

function useState(initialState) {
   HOOKS[currentIndex] = HOOKS[currentIndex]
       || (typeof initialState === 'function' ? initialState() : initialState);
   const memoryCurrentIndex = currentIndex; // currentIndex is globally mutable
   const setState = p= > {
       let newState = p;
       // setCount(count => count + 1
       if (typeof p === 'function') newState = p(HOOKS[memoryCurrentIndex]);
       // If the values are the same, the update will not be done
       if (newState === HOOKS[memoryCurrentIndex]) return;
       HOOKS[memoryCurrentIndex] = newState;
   };
   return [HOOKS[currentIndex++], setState];
}
Copy the code

Tick

Of course, now that we have simply updated the corresponding state without notifying the Render update function, we can write a simple tool.

const HOOKS = []; // Globally store hook variables
let currentIndex = 0; // Global dependency hook execution order subscript
const Tick = {
 render: null.queue: [].push: function(task) {
     this.queue.push(task);
 },
 nextTick: function(update) {
     this.push(update);
     Promise.resolve(() = > {
         if (this.queue.length) { // After a loop, all are removed from the stack to ensure that a single event loop does not repeat the rendering
             this.queue.forEach(f= > f()); // Execute all tasks in the queue in sequence
             currentIndex = 0; // Reset the count
             this.queue = []; // Clear the queue
             this.render && this.render(); / / update the dom
         }
     }).then(f= >f()); }};Copy the code

In React, setState updates are synchronous, but we don’t know it, or at least it looks asynchronous. This is because React implements its own set of transaction management. We use Promise instead, which is similar to nextTick in Vue.

const setState = p= > {
   / /...
   Tick.nextTick(() = > {
       HOOKS[memoryCurrentIndex] = newState;
   });
 };
Copy the code

useEffect

UseEffect acts as a lifecycle role in the class, and we use it more to notify updates and clean up side effects. Knowing useState, the general principle is the same. If each item in the dependency array is the same as the last one, you don’t need to update it.

function useEffect(fn, deps) {
   const hook = HOOKS[currentIndex];
   const _deps = hook && hook._deps;
   // Check whether the dependency is passed, not default every update
   // Determine whether this dependency is the same as the last dependency
   consthasChange = _deps ? ! deps.every((v, i) = > _deps[i] === v) : true;
   const memoryCurrentIndex = currentIndex; // currentIndex is globally mutable
   if (hasChange) {
       const _effect = hook && hook._effect;
       setTimeout(() = > {
           // Check if any side effects need to be removed
           typeof _effect === 'function' && _effect();
           // Execute this time
           const ef = fn();
           / / update the effectsHOOKS[memoryCurrentIndex] = {... HOOKS[memoryCurrentIndex],_effect: ef}; })}// Update dependencies
   HOOKS[currentIndex++] = {_deps: deps, _effect: null};
}
Copy the code

You can see that useEffect stores two values in HOOKS, a dependency and a side effect, in the same order in which it is executed. Because useEffect needs to be executed after dom is mounted, setTimeout is used for simple emulation, which is not the case in React.

useReducer

UseReducer is easy to understand, which can be seen as a layer of packaging for useState, so that we can more intuitively manage the state. Remember how to use it:

const reducer = (state, action) = > {
    switch (action.type) {
        case 'increment':
            return {total: state.total + 1};
        case 'decrement':
            return {total: state.total - 1};
        default:
            throw new Error();
    }
}
const [state, dispatch] = useReducer(reducer, { count: 0});
// state.count ...
Copy the code

In other words, we only need to combine useState and Reducer.

function useReducer(reducer, initialState) {
    const [state, setState] = useState(initialState);
    const update = (state, action) = > {
      const result = reducer(state, action);
      setState(result);
    }
    const dispatch = update.bind(null, state);
    return [state, dispatch];
}
Copy the code

useMemo & useCallback

Both of these dependencies are used to optimize React performance for handling costly computations. UseMemo returns a value, and useCallback returns a function.

useMemo

function useMemo(fn, deps) {
    const hook = HOOKS[currentIndex];
    const _deps = hook && hook._deps;
    consthasChange = _deps ? ! deps.every((v, i) = > _deps[i] === v) : true;
    const memo = hasChange ? fn() : hook.memo;
    HOOKS[currentIndex++] = {_deps: deps, memo};
    return memo;
}
Copy the code

useCallback

function useCallback(fn, deps) {
    return useMemo(() = > fn, deps);
}
Copy the code

summary

This simple simulation implemented part of React Hook. If you are interested, you can improve it by yourself. If you find any problems, you can point them out in the comments and I will update them in time.

  • The full code is here in hook.js
  • The usage method is as follows:
function render() {
    const [count, setCount] = useState(0);
    useEffect(() = > {
        const time = setInterval(() = > {
            setCount(count= > count + 1);
        }, 1000)
        // Remove side effects
        return () = > {
            clearInterval(time);
        }
    }, [count]);
    document.querySelector('.add').onclick = () = > {
        setCount(count + 1);
    };
    document.querySelector('#count').innerHTML = count;
}
/ / bind render
Tick.render = render;
render();
Copy the code

Refer to the article

  • How do React hooks work
  • It’s an array, not magic
  • The React principle of hooks

Communication group

Wechat group: Scan code to reply to add group.

Afterword.

If you see here, and this article is a little help to you, I hope you can move a small hand to support the author, thank 🍻. If there is something wrong in the article, we are welcome to point out and encourage each other. Thanks for reading. See you next time!

  • The article warehouse 🍹 🍰 fe – code

Interested students can pay attention to my public number front-end engine, fun and material.