According to the principle of react hooks, a simpleuseState
Then, of course, we should also implement a simple useeffect
First review the usage of useeffect
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
console.log(`You clicked ${count} times`);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Summary: useeffect receives two parameters. The first parameter is a callback function and the second parameter is a dependency. The callback function is executed at different stages according to different dependencies
Also preset an execution environment
let onClick;
let onChange;
function render() {
// _ Resetting IDX to 0 is also in line with react. Each hook is updated from the hooks header node every time it is updated
_idx = 0;
const [count, setCount] = useState(0);
const [name, setName] = useState("77");
useEffect(()=>{
console.log("effect —— count", count);
console.log("effect —— name", name);
}, [name])
//Use onclick and onchange to simply simulate the update operation
onClick = () => { setCount(count + 1) };
onChange = (name) => { setName(name) };
}
render();
console.log("-------------");
onClick();
onClick();
console.log("-------------");
onChange("kiana")
onChange("kiana_k423")
The above code simulates one rendering, two clicks and two modifications. Meanwhile, the dependency of useeffect is name
Note: usestate is simulated in the previous article,Click to view
According to different dependencies, we can execute callback according to different situations
let _ memoizedState = []; // Multiple hooks are stored in this array
let _ idx = 0; // Current memoizedstate subscript
/**
*Simulated implementation of useeffect
*// different DEPs correspond to different situations,
// 1. When DEPs does not exist: every time the state is updated, a callback needs to be executed
// 2. When DEPs exists, but the array is empty, you only need to execute callback when mounting, that is, when rendering for the first time
// 3. If DEPs exists and has dependencies, the callback is executed only when the corresponding dependency is updated
*@ param {function} callback callback function
*@ param {array} DEPs dependency
*/
function useEffect(callback, deps) {
//If there are no dependencies, callback is executed every time
if(!deps) {
callback();
} else {
//First, get the original dependency stored in the current location in the global hooks list according to the current subscript
const memoizedDeps = _memoizedState[_idx];
if(deps.length === 0) {
//Through current_ Whether there are DEPs at the subscript position of memoizedstate to judge whether it is the first rendering
!memoizedDeps && callback();
//At the same time, you should also update the data of the dependencies of the current subscript of the global hooks list
_memoizedState[_idx] = deps;
} else {
//If it is the first rendering, call callback directly
//Otherwise, judge whether the dependency has been updated
memoizedDeps && deps.every((dep, idx) => dep === memoizedDeps[idx]) || callback();
//Update the data of the dependency of the current subscript
_memoizedState[_idx] = deps;
}
_idx++;
}
}
The results are as follows:

In addition, let’s change the dependencies and experiment with other results:


It seems that everything is going well here. However, the callback function of useeffect has another important feature, that is, it can return a function, which is executed in the willunmount phase.
In the improved version, the returned function of the useeffect callback is executed when the component is destroyed
The idea is also very simple, that is, during the initial rendering, the callback of each useeffect will be executed, and then if the callback execution result has a return value and the return value is a function, push it into a global effectdestroy array, and then execute the destroy function in turn when the component willunmount. The specific implementation is as follows:
const _ memoizedState = []; // Multiple hooks are stored in this array
let _ idx = 0; // Current memoizedstate subscript
const _ effectDestroy = []; // Store functions returned by multiple useeffect callback functions
/**
*Simulated implementation of useeffect
*// different DEPs correspond to different situations,
// 1. When DEPs does not exist: every time the state is updated, a callback needs to be executed
// 2. When DEPs exists, but the array is empty, you only need to execute callback when mounting, that is, when rendering for the first time
// 3. If DEPs exists and has dependencies, the callback is executed only when the corresponding dependency is updated
*@ param {function} callback callback function
*@ param {array} DEPs dependency
*/
function useEffect(callback, deps) {
//First, get the original dependency stored in the current location in the global hooks list according to the current subscript
const memoizedDeps = _memoizedState[_idx];
//If not currently, it will prove to be the first rendering, and a callback will be executed in any case
if(!memoizedDeps) {
const destroy = callback();
//Update dependencies at the same time
_memoizedState[_idx] = deps;
//If the return value of callback is a function, the function is stored in the global destory array first, and then executed successively in the willunmount stage
if(typeof destroy === "function") {
_effectDestroy.push(destroy);
}
//Otherwise, it is the stage of re rendering
} else {
//Execute callback directly without dependencies
if(!deps) {
callback();
} else {
//Only when the dependency is not an empty array and the dependency is updated can the callback be executed
deps.length !== 0 && !deps.every((dep, idx) => dep === memoizedDeps[idx]) && callback();
//Don't forget to update dependencies
_memoizedState[_idx] = deps;
}
}
_idx++;
}
The simulated react running environment is as follows:
let onClick;
let onChange;
const willUnMount = () => {
for(let destroy of _effectDestroy) {
destroy();
}
}
function render() {
// _ IDX indicates the location of the hooks currently executed
// _ Resetting IDX to 0 is also in line with react. Each hook is updated from the hooks header node every time it is updated
_idx = 0;
const [count, setCount] = useState(0);
const [name, setName] = useState("77");
useEffect(()=>{
console.log("effect —— count", count);
return () => {
console.log("count Effect Destroy");
}
},[count])
useEffect(()=>{
console.log("effect —— name", name);
},[name])
//Use onclick and onchange to simply simulate the update operation
onClick = () => { setCount(count + 1) };
onChange = (name) => { setName(name) };
}
console.log("-----render--------------");
render();
console.log("-----countChanged--------");
onClick();
onClick();
console.log("-----nameChanged---------");
onChange("kiana")
onChange("kiana_k423")
console.log("-----willUnMount---------");
willUnMount();
The operation results are as follows:

summary
This simple implementation is quite different from the source code of react, mainly because react has many situations to consider, such as asynchronous update, priority scheduling, custom hook and other scenarios. The react source code adopts the linked list structure, and then the data structure of each node in the linked list is defined as follows:
const effect: Effect = {
Tag, // used to identify whether the dependency has changed
Create, // the function body passed in by the user using useeffect
Destroy, // the function generated after the above function body is executed to remove side effects
DEPs, // dependency list
Next: (null: any), // point to the next effect
};
In order to simplify complex problems, this paper adopts array structure, and then only focuses on the core functions. However, the simple implementation of this article is also in line with the idea of react implementation. First, judge whether the current is the initial mount or update stage, and then save it if there is a function to remove side effects in callback. Different processing is carried out through different dependencies. Finally, before destruction, the previously saved functions to remove side effects are executed in turn. In addition, you can take a look at the previousAbout the simple usestate implementation
All codes are as follows:
const _ memoizedState = []; // Multiple hooks are stored in this array
let _ idx = 0; // Current memoizedstate subscript
const _ effectDestroy = []; // Store functions returned by multiple useeffect callback functions
/**
*Simulate the implementation of usestate
*@ param {any} defaultstate default
*@ returns state and setstate methods
*/
function useState(defaultState) {
//Check whether the current position has a value
_memoizedState[_idx] = _memoizedState[_idx] || defaultState;
//Once again, using closures, let setstate update the state of the corresponding location
const curIdx = _idx;
function setState(newState) {
//Update the state of the corresponding location
_memoizedState[curIdx] = newState;
//Trigger the rendering function after the update
render();
}
//Returns the current state in_ Location of memoizedstate
return [_memoizedState[_idx++], setState];
}
/**
*Simulated implementation of useeffect
*// different DEPs correspond to different situations,
// 1. When DEPs does not exist: every time the state is updated, a callback needs to be executed
// 2. When DEPs exists, but the array is empty, you only need to execute callback when mounting, that is, when rendering for the first time
// 3. If DEPs exists and has dependencies, the callback is executed only when the corresponding dependency is updated
*@ param {function} callback callback function
*@ param {array} DEPs dependency
*/
function useEffect(callback, deps) {
//First, get the original dependency stored in the current location in the global hooks list according to the current subscript
const memoizedDeps = _memoizedState[_idx];
//If not currently, it will prove to be the first rendering, and a callback will be executed in any case
if(!memoizedDeps) {
const destroy = callback();
//Update dependencies at the same time
_memoizedState[_idx] = deps;
//If the return value of callback is a function, the function is stored in the global destory array first, and then executed successively in the willunmount stage
if(typeof destroy === "function") {
_effectDestroy.push(destroy);
}
//Otherwise, it is the stage of re rendering
} else {
//Execute callback directly without dependencies
if(!deps) {
callback();
} else {
//Only when the dependency is not an empty array and the dependency is updated can the callback be executed
deps.length !== 0 && !deps.every((dep, idx) => dep === memoizedDeps[idx]) && callback();
//Don't forget to update dependencies
_memoizedState[_idx] = deps;
}
}
_idx++;
}
let onClick;
let onChange;
const willUnMount = () => {
for(let destroy of _effectDestroy) {
destroy();
}
}
function render() {
// _ IDX indicates the location of the hooks currently executed
// _ Resetting IDX to 0 is also in line with react. Each hook is updated from the hooks header node every time it is updated
_idx = 0;
const [count, setCount] = useState(0);
const [name, setName] = useState("77");
useEffect(()=>{
console.log("effect —— count", count);
return () => {
console.log("count Effect Destroy");
}
},[count])
useEffect(()=>{
console.log("effect —— name", name);
},[name])
//Use onclick and onchange to simply simulate the update operation
onClick = () => { setCount(count + 1) };
onChange = (name) => { setName(name) };
}
console.log("-----render--------------");
render();
console.log("-----countChanged--------");
onClick();
onClick();
console.log("-----nameChanged---------");
onChange("kiana")
onChange("kiana_k423")
console.log("-----willUnMount---------");
willUnMount();