Let’s start with the function
React is a very “functional” language among the three mainstream frameworks, including setState and render function design, function components and peripheral components Redux, all of which contain a certain functional style. So to understand React, we need to understand some functional programming.
As a form of declarative programming, functional programming, as opposed to imperative programming, originated from category theory and was first born to solve mathematical problems. Category theory holds that all members of the same category are “morphs” of different states, and one member can be morphed into another through morphism.
We can think of objects as sets, morphisms as functions, and define the relationship between the members of the category by functions, functions act as conduits, one value goes in, one new value comes out, without any other side effects, so called y = f(x).
Functional styles include a variety of features, such as first-class citizens of functions, pure functions, side effects, Currization, composition, etc. Here we will focus on the basic pure functions and side effects to further explain.
Pure functions
Pure functions – input and output data streams are all explicit.
Explicitly, this means that functions exchange data with the outside world through only one channel — parameters and return values; All input information that a function receives from outside the function is passed inside the function through arguments; All information that a function outputs outside the function is passed outside the function via the return value.
Same input, always the same output, with no side effects, independent of environment variables, can be called from anywhere.
//splice is an impure function
let arr = [1.2.3.4.5];
arr.splice(0.3); / / [1, 2, 3]
arr.splice(0.3); / / (4, 5)
Slice is a pure function
arr = [1.2.3.4.5];
arr.slice(0.3); / / [1, 2, 3]
arr.slice(0.3); / / [1, 2, 3]
Copy the code
Side effects
In computer science, a function side effect is an additional effect on the calling function that occurs when a function is called in addition to returning the value of the function. Such as modifying global variables (variables outside functions), modifying parameters, or changing external storage.
Typical side effects:
- Send an HTTP request
- new Date() / Math.random();
- console.log / IO
- DOM queries (external data)
However, only pure functions without side effects of the program, after the program runs, leaving no trace, just empty CPU, therefore, side effects are necessary. In functional languages, side effects are managed uniformly using functors to keep functions as pure as possible. Instead of going into the mechanics of functors, we need to keep in mind that functions are as pure as possible, and side effects are managed in a unified way.
Tracing the history – The birth background of Hook
The React components
The React component is written into function components and class components.
class ComponentA extends React.Component {
constructor(props) {
super(props);
this.state = { displayContent: 'Hello World'}}render() {
return <div>{this.state.displayContent}</div>}}Copy the code
function ComponentFunctionA = (props) = > <div>{props.displayContent}</div>
Copy the code
Function components are positioned as presentation components that are stateless and have no life cycle.
Class components are positioned as container components that manage their own state and lifecycle.
The problem
Function components want to manage state and life cycles
As requirements change, in the absence of a good design. It is common for functional components to be in a managed state due to factors such as functional cohesion.
Solution:
- Change to class component
- Lift state to upper container
However, there is a human cost to switching to class components; However, upgrading data to the upper container can not only increase the React component hierarchy, but also lose the cohesion of component functions, so it is not a good solution.
The class component lifecycle brings about logical separation
class DemoA extends React.PureComponent {
constructor(props) {
super(props);
this.listener = () = > {/* do something */};
}
componentDidMount() {
document.addEventListener('click'.this.listener);
}
componentWillUnmount() {
document.removeEventListener('click'.this.listener); }}Copy the code
This is just a simple example of the need to place the listen and cancel logic of an event in separate lifecycle-dependent situations. Once the logic is complex, it is easy to miss such clean-up of events and data, resulting in memory leaks and even logic errors.
Logical abstraction reuse
plan | advantages | disadvantages |
---|---|---|
Mixin | Easy to use and flexible | Poor maintainability is easily covered by mixins |
HOC | Class cannot use mixins | Have additional component-level attributes that vary in source and are easy to override |
Render Props | Identify the source and solve the problem of attribute coverage | The extra component hierarchy is not readable |
Hook | Eliminate additional component hierarchies for high readability and apply logical abstraction to data sources for explicit output | Dependency management closure issues |
Problems with the class itself, such as poor compression, can cause instability under thermal overload
Solve the problem – Hook design
In Hook mode, React introduces hooks such as useState, useEffect and useMemo that can manage side effects into function components to ensure that functions are as pure as possible and effectively solve the pain points mentioned in previous component development.
This article takes the data storage of useState and the side effect management of useEffect as examples.
Because function components are pure functions, they are not responsible for data storage and side effects. So the first problem we have to solve is data storage and side effects management.
useState
There are several main solutions to data storage in JavaScript.
- Class member variable
- Global state
- Dom
- LocalStorage solutions such as localStorage
- closure
The member variables of the class are the data storage scheme adopted by the class.
To avoid side effects as much as possible, we exclude global state, local storage, and DOM scenarios; Closures, by contrast, meet our data storage and reliability requirements.
Evolution of the DEMO
Using the react.usestate scenario, you should return a state data field and a dispatch to update the state.
function Demo () {
const [count, setCount] = useState(0)
return <div onClick={() => { setCount(count++); }}>{count}<div>
}
Copy the code
Based on the closure definition and the return value used, we can easily define the following methods:
var useState = (initState) = > {
let data = initState;
const dispatch = (newData) = > {
data = newData;
}
return [data, dispatch];
}
Copy the code
During the initialization phase, we can verify that the base useState above works. However, during each rendering process, the function is called again and reinitialized, which is not expected. Therefore, we need a data structure to store state for each execution, and we need to distinguish between different implementations of initialization and updating state.
type Hook {
memorizedState: any;
}
var useState = (initState) = > {
// Determine according to different life cycles
if (mounted) {
mountedState(initState);
}
if(updated) { updatedState(initState); }}var mountedState = (initState) = > {
const hook = createNewHook();
// Initialize render
hook.memoizedState = initalState;
return [hook.memorizedState, dispatchAction]
}
var createNewHook = () = > {
return {
memorizedState: null}}function dispatchAction(action){
// Use data structures to store all update actions so that the latest status values can be computed in the rerender process
storeUpdateActions(action);
// Perform fiber rendering
scheduleWork();
}
// The method actually called each time useState is executed after the first time
function updateState(initialState){
// Get the current working hook
const hook = updateWorkInProgressHook();
// Calculates the new status value from the update action stored in dispatchAction and returns it to the component
updateMemorizedState();
return [hook.memoizedState, dispatchAction];
}
Copy the code
So here we have two questions
- For the same state, how to share hook in different states of Mounted and updated
- How does storeUpdateActions in dispatchAction and updateMemorizedState in updateState work
For the first problem, a Hook exists relative to a component. Therefore, the ReactNode stored by the React component is a good fit for this scenario, and in the current version, we mounted it under the FiberNode node.
type FiberNode {
memorizedState: any;
}
Copy the code
For the second problem, we need to consider some complex scenarios.
In our actual scenario, re-render behavior with multiple calls in an update cycle is common
Here’s an example:
function Demo () {
const [count, setCount] = useState(0);
return <div onClick={() => {
setCount(count++);
setCount(count++)
setCount(count++)
}}>{count}<div>
}
Copy the code
The component does not actually render three times, but according to the final state. This means that when we call the dispatch update, we do not perform the update logic directly, but store it for the uniform scheduled update at the time of update. According to the order of execution, we use queues to store multiple calls of a hook.
type Queue {
last: Update,
dispatch: any,
lastRenderedState: any
}
type Update {
action: any,
next: Update
}
type Hook {
memorizedState: any,
queue: Queue;
}
function mountState(initState) {
const hook = mountWorkInProgressHook();
hook.memorizedState = initState;
const queue = (hook.queue = {
last: null.dispatch: null.lastRenderedState: null
});
// The closure is bound to queue for sharing
const dispatch = dispatchAction.bind(null, queue);
queue.dispatch = dispatch;
return [hook.memorizedState, dispatch]
}
function dispatchAction(queue, action) {
const update = {
action,
next: null};// Process queue updates
let last = queue.last;
if (last === null) {
update.next = update;
} else {
/ /... Update the circular linked list
}
// Perform fiber rendering
scheduleWork();
}
function updateState(initialState){
// Get the current working hook
const hook = updateWorkInProgressHook();
// Calculates the new status value from the update action stored in dispatchAction and returns it to the component
(function doReducerWork(){
let newState = null;
do{
// Loop the list to perform each update
}while(...). hook.memoizedState = newState; }) ();return [hook.memoizedState, hook.queue.dispatch];
}
Copy the code
In addition, in a real application scenario, we would split the state according to logic. You need to use a Hook multiple times within a component, so you need to keep track of all the hooks used. In this respect, the same as storing the same set of updates and the same Hook multiple calls before, can be stored in the form of a linked list.
type Hook = {
memoizedState: any, // Final status value since last full update
queue: UpdateQueue<any, any> | null.// Update the queue
next: any // Next hook
}
Copy the code
Here’s an example:
const Demo = () = > {
const [count, setCount] = useState(0);
const [time, setTime] = useState(Date.now());
return <div onClick={()= > {
setCount(count++);
setTime(Date.now());
}}>{count}-{time}</div>
}
Copy the code
The form of the node stored in ReactNode:
const fiber = {
/ /...
memoizedState: {
memoizedState: 0.queue: {
last: {
action: 1
},
dispatch: dispatch,
lastRenderedState: 0
},
next: {
memoizedState: 1603594106044.queue: {
// ...
},
next: null}},/ /...
}
Copy the code
The entire list is built on Mounted and executed in order on update. Therefore, it cannot be used in scenarios such as conditional loops.
useEffect
With useState design experience, useEffect can be used for reference. Create a hook object at mount time, create a new effectQueue, store each effect as a one-way linked list, bind the effectQueue to fiberNode, and execute the effect functions stored in the queue after rendering.
type EffectQueue{
lastEffect: Effect
}
type FiberNode{
memoizedState: any, // This is used to store all the Hook states in a component
updateQueue: EffectQueue
}
type Effect {
create: any;
destory: any;
deps: Array;
next: any;
}
Copy the code
Unlike useState, useEffect has a DEPS dependency array. When dependent array changes, a new side effect function is appended to the end of the chain.
function useEffect(fn, dependencies) {
if (mounted) {
mounteEffect(fn, dependencies)
}
if (updated) {
updateEffect(fn, dependencies)
}
}
function mountEffect(fn, deps) {
const hook = mountWorkInProgressHook();
hook.memorizedState = pushEffect(xxxTag, fn, deps)
}
function updateEffect(fn, deps) {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// Dependency changes trigger a destruction reset
if(currentHook! = =null) {const prevEffect = currentHook.memoizedState;
const destroy = prevEffect.destroy;
if(nextDeps! = =null) {if(areHookInputsEqual(deps, prevEffect.deps)){
pushEffect(xxxTag, create, destroy, deps);
return;
}
}
}
hook.memoizedState = pushEffect(xxxTag, create, deps);
}
function pushEffect(tag, create, destroy, deps) {
const effect = {
create,
destory,
deps,
next: null
};
// Build the effect queue
const updateQueue = fiberNode.updateQueue = fiberNode.updateQueue || newUpdateQueue();
if (updateQueue.lastEffect) {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
updateQueue.lastEffect = effect;
} else {
updateQueue.lastEffect = effect.next = effect;
}
return effect;
}
Copy the code
We can see that in the useEffect phase, no effect is actually executed, only an effect list is built to execute. The actual execution of create and destroy is in the COMMIT phase, which is outside the scope of this sharing. Interested partners can learn by themselves
Related links: ReactFiberCommitWork
conclusion
By locating the problem -> obtaining the demand -> realizing the design -> the steps of design evolution, we understand the original intention of Hook design step by step and improve the design step by step. In daily work, we should also learn from this thinking mode and improve our understanding of business pain points so as to truly take corresponding solutions.
❤️ Thank you
That is all the content of this sharing. I hope it will help you
Don’t forget to share, like and bookmark your favorite things.
Welcome to pay attention to the public number ELab team receiving factory good article ~