"React series dry goods under" (must know and know)

"React series dry goods under" (must know and know)

This article is a continuation of "React Series Dry Goods Part 1". Because of the upper word limit, I opened it. The Nuggets are quite magical. Only less than 2w words are written on the word limit. Some previous articles wrote almost 30,000 words. The word limit is outrageous. . .

1. The diffing algorithm in React

  • What is virtual Dom: JSX is essentially the syntactic sugar of react.createElement(type,props,...children). The function of react.createElement returns virtual dom. Virtual dom is essentially a js object that describes what the real UI is State or describe what the real dom looks like , we can use libraries such as ReactDom to synchronize the virtual dom with the real dom, because the virtual dom can be said to be a state object used to describe the UI, so we can use the virtual dom across platforms, as long as A library similar to ReactDom is needed to transform it into a real UI.

  • The process from virtual Dom to real Dom:

    • 1. Insert the virtual Dom tree reacted by JSX into the html document through ReactDom.Render to construct a real Dom tree
    • 2. When re-rendering, reconstruct a new virtual Dom tree, and compare the new tree with the old tree to find the difference between the two trees
    • 3. Update all the differences to the real Dom at once to complete the view update.
  • Tree Edit Distance (tree edit distance) algorithm: a general solution for converting one tree into another tree with minimum operands, and its time complexity is O(n^3).

  • The diff algorithm in react: The minimal operation of converting virtual dom into real Dom tree in React is called coordination, and the specific implementation of coordination is the diff algorithm. . As for why the tree edit distance algorithm is not used, it is because the time complexity of the tree edit distance algorithm is O(n^3). If the tree edit distance algorithm is used in React to process a tree with 1000 elements, then its calculation amount will be In the range of 1 billion, the overhead is too large, and the time complexity of the diff algorithm implemented in React based on the following three strategies or assumptions is only O(n).

    • Strategy 1: There are very few cross-level operations of the dom node in the UI and can be ignored. That is, because there are very few cross-level operations of the d node in React, if a cross-level node operation occurs, a new node is directly created, and the node is deleted, and no comparison operation will be performed.
    • Strategy 2: Components of the same type will generate the same type of tree structure, and different types of tree components will generate different tree structures. That is, if two nodes in the new and old trees are of different types, the old node is directly deleted, a new node is created, and the comparison operation is performed only when the types are the same.
    • Strategy 3: For a group of child nodes at the same level, a unique id can be used to imply which child nodes will remain stable in the new and old trees. That is, by setting the key attribute for a group of child nodes, react can traverse the nodes with the same key as the new node in the old node during the diff process for node reuse. To a certain extent, the operation of node creation and deletion is reduced.

    Based on the above three strategies, React optimizes the tree diff, component diff, and element diff algorithms respectively, as follows: Note that the diff operation process is to find the difference between the new and the old virtual dom tree and record it, so all involved in the diff process below The actions (such as deleting old nodes, creating and inserting new nodes) only record the operations that need to be done when updating the real dom (the real dom update is not performed), and these operations will be aggregated into a patch package after the diff is completed , And then update the real dom at one time according to the patch package .

    • Tree diff optimization: Use depth-first traversal to compare trees hierarchically, only compare the nodes of the same level in the old and new trees (that is, all the child nodes under the same parent node), so that only one traversal is needed to find Find out all the differences in the two trees. For cross-level node operations, for example, there are child nodes B and C under parent node A. If node B moves to node C and becomes a child node of node C, then diff will only delete node B and node C. Create a new B node. Therefore, the cross-level node operation appears in the tree, and the node movement operation does not appear, but the two operations of deleting and creating nodes are completed.

    • Component diff optimization:

      • 1. If the two component types at the same level in the old and new trees are different, the old component will be judged as a dirty component, and there is no need to compare the two. The new component will directly replace all nodes under the old component .

      • 2. If the two components are of the same type, and shouldComponentUpdate returns false in the React lifecycle function, that is, the current component has not been updated this time, and there is no need to perform the diff operation, which saves the diff operation time of this component .

      • 3. If the two components are of the same type and shouldComponentUpdate returns true in the React lifecycle function, we need to compare the virtual dom nodes corresponding to the two components in the new and old trees .

    • Element diff optimization: For the operation of nodes in the same level, distinguish whether there is a key.

      • There is no key: compare the same level nodes of the new and old trees. If the types are the same, continue to compare (compare the difference between the two props records, and then continue to compare the child nodes in depth first), and directly delete the old node, create and insert a new node if the type is different . This method is very inefficient when the child node has not changed but the position has changed. The most efficient method is to directly move the child node to the corresponding position instead of deleting the created node. Therefore, the optimization strategy for this part of optimization is to allow developers to distinguish between adding keys to child nodes, which implies that the nodes with added keys will remain stable in the old and new dom trees.
      • Existing key:
        • 1. Perform a loop traversal of the child nodes in the new tree, and use the unique key to determine whether there is a node with the same key in the set of child nodes at this level in the old tree.
          • 1.1. If the key of the current new node does not exist in the old node set: then the new node is created, and the node position will be the position of the node in the new node set.

          • 1.2, if the key of the current new node exists in the old node set:

            • 1.2.1, if the new node is of the same type as the old node, the old node can be reused to compare the old and new nodes
              • 1.2.1.1, if the same, the node may be moved in the future
              • 1.2.1.2, if they are different, the information in this area has not been found. I personally think that it should compare the differences between the two, and the nodes may also be moved.
            • 1.2.2, if the new node and the old node are of different types, although the keys of the two are the same, delete the old node, create a new node and insert it into the position of the new node in the new collection
        • 2. Finally, we need to traverse the old tree, find out the nodes that exist in the old tree and not in the new tree, and delete these nodes.
      • How to move the child node with key:
        • When discussing this issue, let's first look at the meaning of old_index, last_index, and new_index in the diff of the old and new nodes in the figure below. The movement of nodes is carried out through these three values.

          • 1. As shown in the figure above, B's old_index is the position of B in the old node, counting from 0, so B's old_index is 1.

          • 2. Continue B's new_index, which is the position of B in the new node, as shown in the figure is 0. Obviously, old_index and new_index are both fixed constants.

          • 3. For last_index, it is a variable, the initial value is 0, it may change in the old and new nodes of the diff, so every time the new and old nodes are compared, last_index will be assigned the value of math.max (currentNode.old_index, last_index).

        • Specific key node movement mode: divided into the following three situations

          • 1. The key of the new node also exists in the old node: first judge the old_index and lastindex of the old node which is larger, when mountIndex<lastIndex, the node in the real dom needs to be moved and moved to the new_index position of the new node, if mountIndex>= lastIndex, then the node in the real dom does not move.

          • 2. The key in the new node does not exist in the old node: then create the real dom node of the new node and place it in the current last_index position

          • 3. If the node exists in the old node, but the node does not exist in the new node, then the node in the real dom is directly deleted.

      • Specific mobile analysis: 1. The new and old nodes have the same key node, but the location is different, as follows
        • The first step is to operate B in the new node first

          {new_index:0,old_index:1}
          At this time, last_index is 0, and it is found that B's old_index>last_index, so B does not move. At this time, the position of B is new_index, that is, position 0. At the same time, last_index is re-assigned to Math.max(old_index,last_index), that is, the current last_index:1.

        • The second step is to operate A in the new node

          {new_index:1,old_index:0}
          At this time, last_index is 1, and it is found that A's old_index<last_index, so A moves, and A moves to new_index, which is the position 1. At the same time, last_index is re-assigned to Math.max(old_index,last_index), that is, the current last_index:1.

        • The third step is to operate D in the new node

          {new_index:2,old_index:3}
          At this time, last_index is 1, and it is found that old_index>last_index of D, so D does not move. At this time, the position of D is new_index, that is, position 2. At the same time, last_index is re-assigned to Math.max(old_index,last_index), that is, the current last_index: 3.

        • The fourth step is to operate C in the new node

          {new_index:3,old_index:2}
          At this time, last_index is 3, and it is found that C's old_index<last_index, so C moves, and C moves to new_index, which is position 3. At the same time, last_index is re-assigned to Math.max(old_index,last_index), that is, the current last_index: 3.

        • In the fifth step, no node in the new node needs to diff with the old node. At this time, the old node set is traversed to determine whether there is a node where the old node set exists but the new node set does not exist. After the traversal is completed, in order to find the node that meets the condition, The diff is over.

      • Specific mobile analysis: 2. There is an E node in the new node that is not in the old node, and there is an E node in the old node and there is no D node in the new node.
        • The first step is to operate B in the new node first

          {new_index:0,old_index:1}
          At this time, last_index is 0, and it is found that B's old_index>last_index, so B does not move. At this time, the position of B is new_index, that is, position 0. At the same time, last_index is re-assigned to Math.max(old_index,last_index), that is, the current last_index:1.

        • The second step is to operate E in the new node

          {new_index:1,old_index:null}
          , The last_index is 0 at this time, and it is found that the E node does not exist in the old node, and the E node is created. At this time, the E position is new_index, that is, the 1 position, and the last_index is still 1.

        • The third step is to operate C in the new node

          {new_index:2,old_index:2}
          At this time, last_index is 1, and it is found that C s old_index===last_index, so C does not move. At this time, the position of C is new_index, which is position 2. At the same time, last_index is re-assigned to Math.max(old_index,last_index), which is the current last_index: 2.

        • The fourth step is to operate A in the new node

          {new_index:3,old_index:0}
          , At this time last_index is 2, find that A's old_index<last_index, C moves, and moves to new_index, which is position 3. At the same time, last_index is re-assigned to Math.max(old_index,last_index), that is, the current last_index: 3

        • In the fifth step, no node in the new node needs to diff with the old node. At this time, the old node set is traversed to determine whether there is a node in which the old node set exists but the new node set does not exist. If node D meets this condition, delete node D , The diff is over.

      • Regarding the movement operation of the key node, we also need to pay attention to the following figure, the D node moves from the last position to the head. According to our movement operation, all the nodes in front of D will move one position backward, so we In the development process, try to avoid operations like moving the last node to the first part, because this will affect the performance of React to a certain extent.
      • Why is it not recommended to use the array index as the key: It is not impossible to use the array index as the key, but the efficiency will be low in some cases, for example, we insert data in the array, delete data, which will cause all data behind the deleted data to be inserted The index of this part of the data changes. When the diff is performed on this part of the data, although the old node corresponding to the key can be found, the content of the new node corresponding to the key has changed and needs to be updated. But in fact, we just inserted and deleted the data, the data behind the data actually still exists, but the key has changed. Therefore, we do not recommend using an array index as the key, but a stable field as the key .
    • The patch method in react: When we complete the diff operation and collect all the differences between the old and new virtual dom trees, we need to update these differences to the real dom. Because we use in-depth first traversal in the process of traversing the old and new trees to collect differences, and when collecting differences, these differences are added in an orderly manner. There will be a field similar to index that represents the position of each difference in the tree. So we traverse the difference package and the real dom tree. Each real dom tree node matches the index position of the difference package according to its own position to observe whether there is a difference. If there is a difference, update the real dom node according to the corresponding difference, and skip the current node if there is no difference. , Continue to the next node comparison, so we can complete the update operation of the real dom by traversing the tree once.

2. Basic handwriting implementation and use of react-hook

This article focuses on the handwritten implementation of hooks, including useState, useEffect, useLayoutEffect, useCallback, useMemo, useContext, and useRef. Basically every line in the specific implementation will add comments. If you don t understand, you can read the comments.

  • What is react-hook: Hook allows us to use state and other features of react that were not supported by other functional components before.

  • Advantages of using hook function components (compared to class components):

    • Class component reuse logic generally uses renderProps or high-level components, but such behavior will make components more complicated, such as component nesting in high-level components, and hooks can reuse logic without modifying the components .
    • Many unrelated side-effect logics of class components can be concentrated in the same life cycle, resulting in bloated components. In the hook function component, we can separate these logics to form a single-function logic, the code is clearer, and at the same time It is also very convenient for logical abstraction.
    • Class components need to use class and determine this, and there is no such trouble for using hook function components
  • React-hooks implementation:

    • useState (supports single state processing) : useState is used to process data management in function components, similar to the state of class components. useState() returns the latest state and update state method setState, similar to this.state and this.setState in class components. Note: React will ensure that the identity of the setState function is stable and will not change during re-rendering, that is, every time the function component is re-rendered, the function component is re-executed, and the useState is re-executed, but the returned setState function is the same as the setState before the re-rendering. The function is the same, and the state may change (if you update the state). This is why setState is used in useEffect and useCallback, but we can safely omit setState from the dependency list.
      Import React from 'REACT' ; Import ReactDOM from 'DOM-REACT' ; //1. Declare that stateInfo is used to save state and setState let stateInfo = { state : undefined , //state save location stateUsed : false , //used to identify whether the state is used for the first time, the first use of state is passed The default value, subsequent re-rendering and execution of useState will use the used state value setState : null , //setState function saves the location } //2. Implement useState function useState ( initState ) { //2.1, initialize state, if state has been used, then take the previously used state, otherwise use the incoming initial state //use the following judgment instead of stateInfo.state = stateInfo.state||initState is because we must ensure that when the subsequent stateInfo.state is set to a false value, we still use stateInfo.state stateInfo.state = stateInfo.stateUsed? stateInfo.state: (stateInfo.stateUsed = true , initState) //2.2, initialize setState, if setState has been initialized before, use the previously initialized setSstate directly, otherwise initialize setState //The reason for this is that we must ensure that the setState function returned by useState is the same if ( !stateInfo.setState) { stateInfo.setState = function ( newState ) { //2.2.1, if the setState parameter is a function, pass in the current state and get its return value const newStateTemp = typeof newState === 'function' ? newState(stateInfo.state): newState //2.2.2, if the new and old state are the same, then ignore this update if ( Object .is(newStateTemp, stateInfo.state)) return //2.2.3, if the new and old state are different, update the state stateInfo.state = newStateTemp //2.2.4, after setState we need to re-render the current component ReactDOM.render( < Index/> , document .getElementById( 'root' )); } } //2.3, return state and setState return [stateInfo.state, stateInfo.setState] } //3. Use our useState function Index () { const [count, setCount] = useState(- 1 ) return < button onClick = {() => setCount(count => count + 1)}>{count} </button > } ReactDOM.render ( < Index/> , Document .getElementById ( 'the root' )); duplicated code
    • useState (supports multiple state processing) : The above useState can only process one state. Now we have to process multiple states. We will store multiple states in an array and map different states with array subscripts (actually using arrays). Subscript mapping useState uses the order to indirectly realize the array subscript mapping different states) to ensure that multiple state states will not be confused. This is why we emphasize that useState cannot be placed in conditional statements, because the order of use of useState is related to its state storage location. Once the order is out of order, the state position will also be out of order.
      Import React from 'REACT' ; Import ReactDOM from 'DOM-REACT' ; //1. Declare stateInfoList to save multiple useState consumer states, including the state and setState of the current useState consumer let stateInfoList = [] //2. Declare the index index, each time useState is used, the index will be +1, so many The useState consumer can use index to distinguish who the current useState consumer is let index = 0 //3. Implement useState function useState ( initState ) { //3.1, if there is no state corresponding to the current index, indicating that the current useState consumer is using useState for the first time, initialize the current index corresponding state if (!stateInfoList[index]) stateInfoList[ index] = { state : initState} //3.2, if there is no current index corresponding to the setState function, it means that the current useState consumer is using useState for the first time, then initialize the index corresponding to setState if (!stateInfoList[index].setState) { //3.2 .1, use the closure to cache the current index of the current useState consumer, next time you re-render the component, you can get the stateInfoList according to the cache index to get the state saved by the useState consumer const currentIndex = index //3.2.2, initialize the current useState consumer setState function stateInfoList[currentIndex].setState = function ( newState ){ //3.2.2.1, if newState is a function, pass in the current state to get the return value const newStateTemp = typeof newState === 'function' ? NewState(stateInfoList[currentIndex].state): newState //3.2.2.2, if If newStateTemp is the same as the last state, cancel the update operation if ( Object .is(stateInfoList[currentIndex].state, newStateTemp)) return //3.2.2.3, if newStateTemp is different from the last state, update state stateInfoList[currentIndex].state = newStateTemp //3.2.2.4, the index needs to be restored to the initial value before the component is re-rendered every time the state is updated, so that when the function component is updated and re-executes the code, it can correspond to the last useState consumer index, which can be found in the stateInfoList The last state and setState function index = 0 //3.2.2.5, re-render the current function component ReactDOM.render( < Index/> , document .getElementById( 'root' )); } } //3.3, return the current index corresponding to state, setState, and index needs to be +1, so that the next useState consumer can get a unique index return [stateInfoList[index].state, stateInfoList[index++].setState] } //4. Use our useState function Index () { const [count, setCount] = useState(- 1 ) const [string, setString] = useState( '*' ) return < div > < button onClick = {() => setCount(count => count + 1)} >{count} </button > < button onClick = {() => setString(count => count +'*')}>{string} </button > </div > } ReactDOM.render ( < Index/> , Document .getElementById ( 'the root' )); duplicated code
    • useReducer : an alternative to useState, it accepts a form like
      (state,action)=>newState
      The reducer function returns the current state and the dispatch function that updates the state. In some scenarios, useReducer is more suitable than useState. For example, the state is complex and contains multiple sub-values, or the next state depends on the previous state. Of course, similar to setState, react will also ensure the stability of the dispatch function and will not change when the component is re-rendered. The implementation principle of useReducer is similar to useState, but the biggest difference is the use of reducer to update state.
      Import React from 'REACT' ; Import ReactDOM from 'DOM-REACT' ; //1. Create a reducerStateList to save the state of multiple useReducer consumers let reducerStateList = [] //2. Use different index indexes to identify different useReducer consumers let index = 0 //3. Create our own useReducer function useReducer ( reducer, initialState ) { if (!reducerStateList[index]) reducerStateList[index] = { state : initialState} if (!reducerStateList[index].disptach) { const currentIndex = index reducerStateList[currentIndex].disptach = function ( action ) { const newState = reducer(reducerStateList[currentIndex].state, action) if ( Object .is(newState, reducerStateList[currentIndex].state)) return reducerStateList[currentIndex].state = newState index = 0 ReactDOM.render( < Index/> , document .getElementById( 'root' )); } } return [reducerStateList[index].state, reducerStateList[index++].disptach] } //4, the use of our useReducer function reducerAdd ( State, Action ) { Switch (action.type) { Case 'the Add' : return { addCount : state.addCount + . 1 } default : the throw new new Error ( 'not match the current transmission Type of entry' ) } } function reducerMinus ( State, Action ) { Switch (action.type) { Case 'minus' : return { minusCount : state.minusCount - . 1 } default : the throw new new Error ( 'not match the current incoming type' ) } } function Index () { const [{ addCount }, addDisptach] = useReducer(reducerAdd, { addCount : 7 }) const [{ minusCount }, minusDisptach] = useReducer(reducerMinus, { minusCount : 1 }) return < div > < button onClick = {( ) => addDisptach({ type:'add' })}>{addCount} </button > < button onClick = {() => minusDisptach({ type:'minus' })}>{minusCount} </button > > </div } ReactDOM.render ( < Index/> , Document .getElementById ( 'the root' )); duplicated code
    • useCallBack :
      • useCallBack: useCallback passes in the dependency of a function and the current function, and returns a memorized version of the function. The returned memorized function is not functionally different from the original function.

      • The principle of useCallback: For the setState function returned by useState, the same setState function is returned every time the function component is re-rendered. When the component is executed, the useCallBack returns are all the same memory function, but if the dependency changes, the memory function returned is not the last memory function (the address has changed)

      • UseCallback function: If you create a function in the parent component and pass the function to the child component (props pass data), and the child component is optimized for performance (react.memo), if we don t use useCallback for the function, then every time The parent component is re-rendered, and the function passed by props is a new function (because the function will be re-created every time the parent component is re-rendered), causing props to change every time, and performance optimization methods such as react.memo fail, so We can use useCallback to handle the function, as long as its dependency does not change, then we pass to the child components are all the same function, so as to avoid unnecessary child component rendering.

      • UseCallback implementation (single useCallback consumption scenario): This implementation is only for useCallback consumer scenarios. Multiple useCallBack consumer scenarios need to be used in conjunction with the index, and the index is restored to the initial value before the component is re-rendered. Here we are not good to monitor the component renewal Render timing, so the implemented useCallback only supports one consumer.

        Import React, useState {} from 'REACT' ; Import ReactDOM from 'DOM-REACT' ; //1. Declare callbackState to save the callback and its dependencies let callbackState //2. Realize our useCallback function useCallback ( callback, dep ) { //2.1, if no dependencies are passed in, then every component renderer will return the latest callback if ( !dep) return callback //2.2, if it is the first time render uses useCallback, save the callback and dependencies in callbackState, and return the current callback if (!callbackState) { callbackState = {callback, dep} return callbackState.callback } //2.3, if useCallback is used for non-first rendering, then callbackState must have stored the callback and dependencies after the last render, then we need to compare whether the dependencies after this render and the dependencies after the last render have changed const notChanged = callbackState. dep.every( ( e, i ) => Object .is(e, dep[i])) //2.4, if the dependency of the two renders changes, then we update the latest dependency to callbackState and return Latest callback if (!notChanged) { callbackState = { callback : callback, dep : dep} return callbackState.callback } //2.5, if the two render dependencies have not changed, return to the last callback return callbackState.callback } //3. Use our useCallback: you can find that for the sub-components after using react.memo: //3.1, when our count is updated, the sub-component will be re-rendered because log1 depends on count, and the change in count leads to incoming The new log1, //3.2, when our string is updated, the child component will not be re-rendered, because log1 does not depend on string, so the old log1 is passed in. function Child ( props ) { console .log( 'child render' , props); return < button onClick = {() => props.log1()}>Child </button > } const ChildMemo = React.memo(Child) function Index () { const [count, setCount] = useState( 7 ), [string, setString] = useState( '*' ), log1 = useCallback( () => console .log(count), [count]); return < div > < button onClick = {() => setCount(count => count + 1)}>{count} </button > < button onClick = {() => setString(string => string +'*')}>{string} </button > < ChildMemo log1 = {log1}/> </div > } ReactDOM.render ( < Index/> , Document .getElementById ( 'the root' )); duplicated code
    • useMemo :
      • useMemo: useMemo accepts the callback function and dependencies, and returns the execution result of the callback function, similar to useCallback, as long as the dependency does not change, the callback function will not be re-executed.
      • UseMemo purpose: If a function containing a large number of calculations is executed every time a function component is rendered, then we can use useMemo to process the function and recalculate it only when its dependent data changes, otherwise do not calculate to achieve performance optimization. Note: useMemo will be executed during rendering. Please do not execute code that has nothing to do with rendering in this function. For operations such as side effects, please put them in useEffect for processing.
      • UseMemo implementation (single useMemo consumption scenario): Similar to useCallback implementation, the biggest difference is that the cache function becomes the result of the cache function.
      Import React, useState {} from 'REACT' ; Import ReactDOM from 'DOM-REACT' ; //. 1, the callback function declaration memoState save useMemo results with consumer reliance the let memoState //2, to achieve our useMemo function useMemo ( the callback , dep ) { if (!dep) return callback() if (!memoState) { memoState = { result : callback(), dep : dep} return memoState.result } const notChanged = memoState.dep.every( ( e, i ) => Object .is(e, dep[i])) if (!notChanged) { memoState = { result : callback(), dep : dep} return memoState.result } return memoState.result } //3. Use our useMemo: we can find that when we click count, the callback in useMemo will be re-executed, and when we click string, the callback in useMemo will not re-execute function Index () { const [count, setCount] = useState( 7 ), [string, setString] = useState( '*' ); const result = useMemo( () => { return ( console .log( 'memo used' ), count + 1 ) }, [count]) return < div > < div > Current ++count:{result} </div > < button onClick = {() => setCount(count => count + 1)}>{count } </button > < button onClick = {() => setString(string => string +'*')}>{string} </button > </div > } ReactDOM.render ( < Index/> , Document .getElementById ( 'the root' )); duplicated code
    • useContext (single useContext consumption scenario) : use useContext
      const context = useContext(MyContext)
      And in the class component
      static contextType = MyContext
      The behavior is the same, and the current context data comes from the value attribute of MyContext.Provider in the most recent parent component. At the same time, when MyContext is updated, the components that consume MyContext will be re-rendered, even if react.memo or shouldComponentUpdate is used.
      Import React from 'REACT' ; Import ReactDOM from 'DOM-REACT' ; const of ColorContext = React.createContext ( 'Red' ) //1. UseContext implementation: get the value in Context directly and return it function useContext ( context ) { return context._currentValue } //2, use our useContext function Child () { const color = useContext(ColorContext) return < div > {color} </div > } function Index () { return < ColorContext.Provider value = { ' blue '}> < Child/> </ColorContext.Provider > } ReactDOM.render ( < Index/> , Document .getElementById ( 'the root' )); duplicated code
    • useEffect :
      • useEffect: useEffect accepts functions that may have side effects, and dependencies. The side-effect function may return a function. The side-effect function will be executed after the function component is rendered, and the side-effect function return function will be executed before the next side-effect function is executed.
      • UseEffect usage scenarios: useEffect is equivalent to componentDidMount and componentDidUpdate in class components. We generally handle side-effect functions in useEffect, such as subscriptions, data requests, and side-effect return functions to cancel subscriptions. Side-effect return functions are not only executed before the next side-effect function , It will also be executed before the component is uninstalled.
      • UseEffect implementation (single useEffect consumption scenario): Because the side effect function in useEffect is executed after the page is rendered, we wrap the side effect function in a timer to form a macro task, because the macro task will be executed after the page is rendered.
        Import React, useState {} from 'REACT' ; Import ReactDOM from 'DOM-REACT' ; //1. Declare that effectState saves useEffect depends on let effectState //2. Realize our useEffect: Because the execution timing of useEffect is equivalent to the componentDidmount and componentDidUpdate of the class component, which is executed at the end of the browser rendering, //every time we execute the callback , we need to put it in the macro task setTimeout to ensure that we The callback is executed at the end of the browser rendering. //At the same time, if the callback returns a function resFn, then the function will be executed before the next callback, so before the next callback, you need to execute the previous callback's return function function useEffect ( callback, dep ) { //2.1. If the function component is rendered for the first time, then save our dependencies and execute the callback at the same time. If there is a return function, save it for executing the function before the next callback if (!effectState) { effectState = {dep} return setTimeout ( () => { const resFn = callback() typeof resFn === 'function' && (effectState.resFn = resFn) }, 0 ); } //2.2, if there is no dependency and not the first function component rendering, if the last callback has a return function, the return function is executed first, and then the callback is executed, if there is no return function, the callback is executed directly if (!dep) { return setTimeout ( ( ) => { typeof effectState.resFn === 'function' && effectState.resFn() const resFn = callback() if ( typeof resFn === 'function' ) effectState.resFn = resFn }, 0 ); } //2.3, if there is a dependency and the function component is not rendered for the first time, then we need to compare whether the current render and the last render dependency have changed const notChanged = effectState.dep.every( ( e, i ) => Object .is(e, dep[i])) //2.4, if there is a change in the dependency, save the changed dependency, and then judge whether there is a return function in the last callback, execute if there is, then execute the current callback, if not, execute the current callback directly if (!notChanged) { effectState.dep = dep return setTimeout ( () => { typeof effectState.resFn === 'function' && effectState.resFn() const resFn = callback() if ( typeof resFn === 'function' ) effectState.resFn = resFn }, 0 ); } } //3. Use our useEffect function Index () { const [count, setCount] = useState( 7 ) useEffect( () => { Promise .resolve( 2 ).then( e => console .log(e)); return () => console .log( '1' ) }, [count]) return < button onClick = {() => {console.log('click'); setCount(count => count + 1) }}> {count} </button > } ReactDOM.render ( < Index/> , Document .getElementById ( 'the root' )); duplicated code
    • useLayoutEffect :
      • useLayoutEffect:
        • useLayoutEffect: The usage of useLayoutEffect is the same as useEffect. It accepts functions and dependencies that may have side effects, and the accepted function may return a function.
        • The difference between useLayoutEffect and useEffect: they both occur after render, and useEffect is executed after page rendering (that is, after collecting virtual dom differences to complete the real dom update, and componentWillDidMount an opportunity), so if the page update is triggered in useEffect, then the update Also in the next page rendering. The useLayoutEffect execution timing is executed before all virtual doms are updated and the real doms are updated, and if the page update is triggered in useLayoutEffect, it will be completed in the next page rendering, and will not wait until the next page rendering like useEffect. I think this is the reason why it is called useLayoutEffect. If there is a page update operation in useLayoutEffect, the page will not be rendered first, but the dom will be updated first. The dom update is completed, that is, the dom is completely laid out, and the page is rendered again, that is to say in useLayoutEffect. The page update will block this page rendering. So we use useEffect as much as possible to avoid blocking page rendering.
        • UseLayoutEffect implementation: similar to useEffect, but different from useEffect is its execution timing, which is executed before all pages are rendered, so we can use microtasks to wrap them and execute them to ensure that they are executed before page rendering. Note: Generally speaking, the browser rendering timing (react update real dom timing) is executed after the resynchronization task microtask is executed, and before the macro task is executed, so this is why I use micro-implementation before the page is rendered. After rendering, I use macro tasks to achieve it.
          Import React, useState {} from 'REACT' ; Import ReactDOM from 'DOM-REACT' ; let layoutEffectState function useLayoutEffect ( callback, dep ) { if (!layoutEffectState) { layoutEffectState = {dep} return Promise .resolve().then( () => { const resFn = callback() typeof resFn === 'function' && (layoutEffectState.resFn = resFn) }, 0 ); } if (!dep) { return Promise .resolve().then( () => { typeof layoutEffectState.resFn === 'function' && layoutEffectState.resFn() const resFn = callback() if ( typeof resFn === ' function' ) layoutEffectState.resFn = resFn }, 0 ); } const notChanged = layoutEffectState.dep.every( ( e, i ) => Object .is(e, dep[i])) if (!notChanged) { layoutEffectState.dep = dep return Promise .resolve().then( () => { typeof layoutEffectState.resFn === 'function' && layoutEffectState.resFn() const resFn = callback() if ( typeof resFn === 'function' ) layoutEffectState.resFn = resFn }, 0 ); } } //3. Use our useLayoutEffect function Index () { const [count, setCount] = useState( 7 ) useLayoutEffect( () => { Promise .resolve( 2 ).then( e => console .log(e)); return () => console .log( '1' ) }, [count]) return < button onClick = {() => {console.log('click'); setCount(count => count + 1) }}> {count} </button > } ReactDOM.render( < Index/> , document .getElementById( 'root' )); Copy code
    • useRef: useRef returns a variable ref object. The current property of the object is initialized to the default value passed in, and the returned ref will not change during the entire life cycle of the component. From the code level, this sentence is: useRef returns the same object every time the component is rendered. Specifically, the address of the returned object is the same. You can modify the internal properties of the object or re-assign the object. , But no matter what kind of operation, the next time you re-render, useRef returns the object, and the address will not change.
      • useRef purpose:
        • 1. Access dom: equivalent to use in class components
          this.ref = React.createRef()
          , And then hang ref to the component that actually needs to be accessed.
          function Index () { let divRef = useRef() useEffect( () => { //Through .current, you can access the dom node of the bound ref console .log(divRef.current.innerHTML); }) return < div ref = {divRef} > ref </div > } Copy code
        • 2. Save data: Similar to the class component, we will save some data on this that will not be used when the page is rendered, such as saving toast prompt information
          this.message ='Interface failed'
          Wait.
          function Index () { let message = useRef( 'Interface failed' ) useEffect( () => { //A fixed message pops up when the interface call fails request().catch( err => alrt(message)) }, []) return < div > ref </div > } Copy code
      • UseRef note: changes in useRef content will not trigger re-rendering.
      • UseRef implementation: The implementation is to save the data version, and the access dom operation is not in this implementation. And this time it is implemented as a single useRef, because it is not easy to get the render timing, the index is not easy to reset.
        Import React, useState {} from 'REACT' ; Import ReactDOM from 'DOM-REACT' ; //1. Declare refData to save useRef data let refData //2. Create our useRef function useRef ( initData ) { //2.1, if refData does not exist, that is, use for the first time, create refData, save the initial value if (!refData) refData = { current : initData} //2.2, not the first use, each time it returns the refData created for the first time return refData } //3. Use our useRef function Index () { let info = useRef( 'Use ref' ) const [count, setCount] = useState( 7 ) return < div > < button onClick = (() => setCount(count => count + 1)}>add:{ count} </button > < div > {info.current} </div > </div > } ReactDOM.render ( < Index/> , Document .getElementById ( 'the root' )); duplicated code

3. Redux core handwriting implementation and how to use it with React (Redux+React)

This article focuses on the redux handwritten implementation, including createStore, combineReducer, applyMiddleWares, compose. At the same time, some important details in the source code are retained in the handwritten implementation, and some judgments in the source code are deleted. In the specific implementation, there will be some for every line of code. The notes explain the specific functions, you can refer to the notes for understanding.

  • What is redux: redux is a predictable state management container. It stores the data state inside and exposes the data access interface. Of course, we not only need to access the data, but also need to change the data state. In redux, if Expect to change the data, you must provide the corresponding reducer, and then make changes through the dispatch method exposed by redux, which brings out the predictability of redux.

  • Realize basic redux (redux.createStore): redux.createStore will implement three methods here, namely getState, dipatch and subscribe, focusing on the implementation of subscribe, because the functions in the subscription queue will be executed every time the action is dispatched in redux , Adding and canceling the subscription during the execution of the subscription function will not affect the execution of this subscription queue.

    • getState: return the state in redux
    • Dispatch: dispatch action, update state, subscribe function queue execution, note:: every time dispatch finishes updating state, the execution of the monitor queue is the monitor queue at the moment of current dispatch, if the monitor queue is operated during the execution of the monitor queue function , Can not affect the monitoring queue at this time of disptach. There are mainly the following two situations that need to be implemented:
      • 1. The execution of the monitor queue adds a new monitor to the monitor queue. The monitor will only be executed in the next dispatch and will not affect this dispatch.
      • 2. The execution of the monitor queue cancels the original monitor from the monitor queue, and does not affect the current disptach monitor queue, that is, the original monitor will still execute this disptach, and the next disptach will not execute.
    • subscribe: Add a subscription function. All subscription functions will be executed every dispatch. Note: The returned cancel listener function guarantees that only one cancel listener can be executed.
    function createStore ( reducer, preloadedState, enhancer ) { //0, if the enhancer is passed in, it will directly return the result returned by createStore processed by the enhancer if ( typeof enhancer === 'function' ) return enhancer(createStore)(reducer , preloadedState) //1, initialize currentState let currentState = preloadedState //2, currentListeners: used to save the subscription function, responsible for executing the subscription function let currentListeners = [] //3, nextListeners: used to save the subscription function, responsible for processing the subscription And unsubscribe //currentListeners and nextListeners separate the addition and cancellation of the subscription function from the execution of the subscription function, avoiding bugs in the execution of the subscription function, adding and canceling the subscription, see the subscribe and dispatch implementation let for details nextListeners = currentListeners //4, when isDispatching controls the execution of the reducer in dispatch, it is forbidden to perform getState, subscribe, unsubscribe and continue dispatch operations during the execution of the reducer let isDispatching = false //5, getState: Returns the data state in the current redux function getState () { if (isDispatching) throw new Error () return currentState } //6, dispatch: Dispatch action to update the data status in redux. After the update is completed, the subscription function queue needs to be executed. The function return value is the accepted value, that is, action function dispatch ( action ) { if (isDispatching) throw new Error () //6.1, Execute the reducer to update the state state, and prohibit getState, subscribe, unsubscribe, and dispatch operations during the execution of the reducer. try { isDispatching = true currentState = reducer(currentState, action) } finally { isDispatching = false } //6.2, Get a snapshot of the latest listening queue, that is, get the listening queue at the current dispatch time let listeners = (currentListeners = nextListeners) //6.3, Execute the function in the listening queue for ( let i = 0 ; i <listeners.length; i++ ) { //6.3.1, first assign a value to listeners[i] and then execute it in order to prevent the listener function from changing the listener queue through this, because when direct listeners[i]() is executed, this points to the current listener queue let listener = listeners[ i] listener() } //6.4, return action return action } //7, getShallowCopyFromCurrentListeners: Get a shallow copy of currentListeners, separate nextListeners and currentListeners from two objects with different addresses. This function will be used when subscribing and unsubscribing. function getShallowCopyFromCurrentListeners () { if (currentListeners === nextListeners) nextListeners = currentListeners.slice() } //8, subscribe: Add a subscription function, this function returns the function to cancel the current subscription function subscribe ( listener ) { if (isDispatching) throw new Error () //8.1, isSubscribed is mainly used to return the cancel subscription function, Prevent continuous unsubscription of the same function, that is, only cancel once, and then cancel the old ignore let isSubscribed = true //8.2, when subscribing to the function, first separate nextListeners from currentListeners, and add the subscription function to nextListeners. //When dispatching, synchronize nextListeners to currentListeners, so that if you add a subscription function in the nested subscription function, //it will only be added to the next, not to the current, and only the next dispatch will execute the last one The subscription function added in the subscription function //To achieve that each dispatch only processes the listener under the queue snapshot when the current dispatch is processed getShallowCopyFromCurrentListeners() nextListeners.push(listener) //8.3, return to cancel the subscription function return () => { //8.3.1, ensure that the cancel listen function is executed only once if (!isSubscribed) return if (isDispatching) throw new Error () isSubscribed = false //8.3.2, the same as when adding a subscription, canceling a subscription also only operates on nextListeners, and will not affect the currentListeners used by the current dispatch. //Ensure that when dispatch is executed, the executed listener queue is the current dispatch action. Snapshot of the listening queue at time getShallowCopyFromCurrentListeners() //8.3.3, get the current listener function and then listen to the position in the queue const index = nextListeners.indexOf(listener) //8.3.4, delete the listener function nextListeners.splice(index, 1 ) //8.3. 5. Set currentListeners to null, which should prevent memory leaks. The time from canceling the monitoring until the next dispatch is executed, currentListeners is not used currentListeners = null } } //9, initialize the reducer and fill the default state to the current state, (generally reducers have default) dispatch({ type : `@@redux/INIT ${ Math .random().toString( 36 ).substring( 7 ).split ( '' ).join( '.' )) ` }) //10, return getState, dispatch, subscribe return {getState, dispatch, subscribe} } Copy code
  • Realization of combineReducers: used to merge multiple reducers, the usage is described in the specific implementation below.

    function combineReducers ( reducerCompose ) { //1, //reducerKeys: pass in the keys corresponding to the reducer//goodReducer: filtered reducer key-value pairs const reducerComposeKeys = Object .keys(reducerCompose), goodReducer = {} //2. Filter out reducer that is not a function type for ( let i = 0 ; i <reducerComposeKeys.length; i++) { let key = reducerComposeKeys[i] typeof reducerCompose[key] === 'function' && (goodReducer[key ] = reducerCompose[key]) } //3, return to the merged reducer return function ( state = {}, action ) { //3.1, //goodReducerKeys: the keys corresponding to the filtered reducer, used later for state traversal //nextState: the reducer is updated and the state returns The final result is nextState const goodReducerKeys = Object .keys(goodReducer), nextState = (); //3.2, Identifies whether there is a change between nextState and the original state, and returns nextState if there is a change, otherwise it returns to the original state let hasChanged = false ; //3.3, traverse the goodReducerKeys, update the value of each sub-state in the state, and save the final result in nextState. At the same time, compare the updated sub-state with each sub-state in the previous state for ( let i = 0 ; i < goodReducerKeys.length; i++) { let key = goodReducerKeys[i] let reducer = goodReducer[key] nextState[key] = reducer(state[key], action) hasChanged = hasChanged || nextState[key] !== state[key] } //3.4, compare whether the key collection of goodReducer is equal to the key collection in state. If you don t want to wait, definitely nextState doesn t want to wait with the original state. hasChanged = hasChanged || goodReducerKeys.length !== Object .keys(state).length //3.5, if the updated state has changed compared to the original state, return the updated state, otherwise return to the original state return hasChanged? NextState: state } } //Basic use ////1. Custom reducer //function reducer0(state, action) { //switch (action.type) { // case'add0 ': return {...state, count: state. count + 1} //case'minus0': return {...state, count: state.count-1} //default: return {...state} //} //} ////2, from Define reducer //function reducer1(state, action) { //switch (action.type) { // case'add1 ': return {...state, count: state.count + 1} //case'minus1': return {...state, count: state.count-1} //default: return {...state} //} //} ////3, initial state //const state = {reducer0: {count: 0 }, reducer1: {count: 0}} ////4, use createStore with combineReducer //const {dispatch, subscribe, getState} = createStore(combineReducers({ reducer0, reducer1}), state) copy the code
  • What is middleware: The essence is the extension and enhancement of the dispatch function. The specific extension position is after dispatch and before the reducer is updated. Ke function middleware uses physics and chemistry, it is recommended to first understand before research middleware function Ke physics and chemistry .

  • Why is the middleware three-level function nesting? : The following is the standard format of the middleware, three-layer functions (see fn1, fn2, fn3 in the notes below).

    function middleWare ( store ) { //fn1 return function ( next ) { //fn2 return function ( action ) { //fn3 //do sth next(action) } } } //Or like this const middleWare = store => next => action => { //do sth next(action) } Copy code
    • 1. Before studying why there are three layers of fn1, fn2, and fn3, let us first think about it. The nature of the behavior of middleware is the extension and enhancement of the dispacth function. So in the form of a function to express the nature of this behavior of the middleware, is it equivalent to having an enhancement function (a function specifically used to enhance the disptach function), and then passing the disptach to the enhancement function, and the enhancement function internally enhances the dipatch , And then return to an enhanced disptach. This process is actually the nesting of fn2 and fn3, fn2 is the enhanced function, the parameter next accepted by fn2 is the disptach function, and the returned fn3 is the enhanced diptach.

    • 2. Understand fn2 and fn3, then let's look at fn1 again. Let's think about it, while middleware expands dispatch, we may use the store created by createStore to handle certain needs (such as using store. getState gets the current store state), and for encapsulating a general tool (middleware), the action of getting the store is so general that it will definitely not be implemented by ourselves (usually it will be implemented in a certain place, we can use it directly) , So give up the idea of manually obtaining store in fn2 or fn3, then where can we get it, can we pass parameters in fn2 or fn1? Not suitable, because fn2 is essentially a function used to enhance dispatch. We expect it to be more pure, that is, accept dispatch and return to enhanced dispatch. The essence of fn1 is the enhanced dispatch function. We must not modify the parameter transfer mode of fn1 (that is, keep It is consistent with the original dispatch signature), then where is the most appropriate action to get the store? Yes, it is to nest a layer of function fn1 outside fn2, fn1 accepts the parameter as store, yes, this is the function in the curation Cache parameter (cache our store), so that we can use it directly if we need to use store in the process of expanding disptach.

    • 3. Reorganize fn1, fn2, fn3, fn1 cache store parameters, fn2 is equivalent to dispatch enhancer, accept dispatch as a parameter, return an enhanced version of dispatch, fn3 is that enhanced version of dispatch. So fn1=>fn2=>fn3 will eventually return an enhanced dispatch. Of course, we will use multiple middleware to enhance dispatch many times, so the enhanced disptach returned by the fn1, fn2, fn3 process may have to become the next fn1, fn2, fn3 process in the fn2 parameter to continue to enhance, we I definitely don't want to handle this process manually one by one. What we expect to do is definitely to provide middleware in the format of fn1, fn2, fn3, so in order to automate this process , applyMiddleWares comes on the scene. It can help us automatically complete the middleware iterative enhancement dispatch, and finally return to a dispatch process that includes all the middleware enhancement capabilities.

  • Implement applyMiddleWares: We know the basic format of middleware, and the essence of middleware is to extend and enhance dispatch, so for the automatic realization of the iterative process of multiple middleware to enhance dispatch capabilities, applyMiddleWares is needed to complete, and the basic applyMiddleWares will be implemented below. , Not very complicated, some judgment behaviors have been deleted from the source code. applyMiddleWares will involve compose function, it is recommended to look compose function .

    • Before implementing middleware, let s look at how to use middleware in createStore. The following code, enhancer is the applyMiddleWares(...middleWares) that we passed in, and enhancer continues to accept parameters in the form of physics and chemistry, namely createStore and reducer, preloadedState, so we applyMiddleWares (...middleWares) must return a function, first accept createStore and then return a new function to accept reducer and preloadedState. Code like this
      const applyMiddleWares = (...middleWares)=>createStore=>(reducer,preloadedState)=>{}
      function createStore ( reducer, preloadedState, enhancer ) { //0, if the enhancer is passed in, it will directly return the result returned by createStore processed by the enhancer if ( typeof enhancer === 'function' ) return enhancer(createStore)(reducer , preloadedState) //other code } Copy code
    • The basic implementation of applyMiddleWares: almost every line of code has a comment indicating what to do, if you still don t understand, please leave a message.
      //Implement applyMiddleWares function applyMiddleWares ( ...middleWares ) { return createStore => ( reducer, preloadedState ) => { //1. Use the parameters createStore and reducer, preloadedState of Ke Lihua cache, execute createStore to get the original store const store = createStore (reducer, preloadedState) //2. Dispatch is not allowed during the execution of the middleware combination, so the dispatch that is initially passed to the store in the first layer of all middleware is initially an error-throwing function, //to prevent in the middle of the combination It is used in the process of software, only after the combination is completed, that is, only in the third layer of the middleware, can you use the original dispatch to do the enhanced processing you want, if you need to operate the original dispatch. let dispatch = () => { throw new Error ( 'dispatch is not allowed to be used during middleware execution')} //3. Save a copy of getState in the store and the dispatch we defined above, and leave it to the first layer functions of all middleware to cache const storeAPI = { getState : store.getState, //3.1, make a copy dispatch, here should be the same as in the reducer, to prevent the middleware enhanced dispatch function from getting the store's this dispatch : ( action, ...args ) => dispatch(action, ...args) } //4. Execute all middleware first layer functions to cache the first parameter store, and then the returned middleware function will have two layers of functions left, that is, the middleware mentioned above accepts dispatch and returns the enhanced dispatch function That enhancer function. const chain = middleWares.map( middleWares => middleWares(storeAPI)) //5. Through compose, execute all middleware enhancer functions (ie middleware second layer functions), //because js is called by value, so The entire compose process is: //Accept the original dispatch (store.dispatch) as a parameter from the last middleware enhancer function (the last one in the chain array) (this parameter is the next parameter accepted by the second layer of the middleware), execute, //Return the enhanced dispatch to the previous middleware enhancer function in the chain array as a parameter, execute it, return the second enhanced dispatch function and then give it to the previous middleware enhancer function, //until the chain array is the first After the execution of the middleware enhancer function is completed, the final disptach function enhanced by all middleware is returned. //After this code is executed, store.dispatch is the original dispatch, and storeAPI.disptach is the enhanced dispatch dispatch = compose(...chain)(store.dispatch) //5. Return to our storeAPI, where dispatch is replaced with the ultimate dispatch after all middleware enhancements return {...store, dispatch} } } //The current compose function will combine fn1, fn2, fn3 into (...args) => fn1(fn2(fn3(...args))) function compose ( ...fns ) { //1, if not Pass in the middleware enhancement function that needs to be combined (that is, the second level function in the middleware three-level function nesting), and the default returns what is accepted and what is returned (do not do any enhancement of disptach, apply to the disptach is to accept dispatch Return dispatch if (fns.length === 0 ) return args => args //2. If there is only one middleware enhancement function, return the middleware enhancement function if (fns.length === 1 ) return fns[ 0 ] //3. If multiple middleware enhance functions, they are combined, fn1, fn2, fn3 are combined into a function (...args) => fn1(fn2(fn3(...args))) returns, where args The original dispatch will be passed in. return fns.reduce( ( preFn, nextFn) => ( ...args ) => preFn(nextFn(...args))) } Copy code
  • redux-thunk: redux-thunk is an asynchronous solution for redux. The original dispatch only supports actions that accept pure objects (including the type attribute), while redux-thunk allows us to accept actions with side effects, and this action can be a function ( Because our side-effect code needs to be executed in the function after all), update the state when the side-effect is executed. So the specific redux-thunk process is: When redux-thunk receives the side effect function action, it will not enter the dispatch process, but will first execute the side effect function and inject dispatch at the same time, so that we will manually call the injection after the side effect is executed. The dispatch completes the redux status update.

    • Redux-thunk handwritten implementation: There is not much code, but you need to understand how redux-thunk completes the side-effect function processing and returns to the process of dispatching pure objects. Note: The use of redux-thunk middleware requires redux-thunk to be placed in the first place of applyMiddleWare. which is

      applyMiddleWare(reduxThunk,...otherMiddleWares)
      , Otherwise it is likely to cause multiple dispatches. Because redux will call dispatch again after completing the side-effect function (this time it is a pure object action). Putting it in the first part is expected to process the side-effect function first. After the completion, all middleware will be executed in turn. If not in the first part, put At the end, all middleware will be executed first. When the execution reaches redux-thunk, redux-thunk processes the side-effect function first, and executes dispatch after processing, causing the middleware to execute again in turn.

      function createReduxThunk ( ...args ) { //1. The following part is the redux-thunk middleware source code, so the last exported part is also this part. //The outer function createReduxThunk is used to inject data into the side-effect function of redux-thunk If you have the opportunity to use it if you need it, //do not need to directly introduce reduxThunk, combine middleware applyMiddleware(reduxThunk,...otherMiddlewares) to return ( {dispatch,getState} )=> next => action => { //1.1 , If the action is a function, execute the function and pass in the enhanced dispatch, getState, and possibly injected parameters. Note that this is the enhanced dispatch, which is obtained from storeAPI, not store.dispacth //at this stage In fact, it is out of the dispatch process. 1. focus on the execution of the side-effect function. After the execution is completed, //use the injected enhanced dispatch in the side-effect callback to dispatch the original object action, and re-enter the dispatch process to update the redux through the reducer state. //So this function will generally go twice, the first time the action comes in is a side-effect function (execute side-effect function), the second time comes in is the pure object action of dispacth after the side-effect function is executed (execute all middleware to complete state update through reducer ) If ( typeof action === 'function' ){ return action(dispatch,getState,...args) } //1.2, if it is not a function, simply dispatch the action and hand it over to the next middleware enhanced dispatch (ie next here) to process return next(action) } } //2. Get the reduxThunk middleware const reduxThunk = createReduxThunk() //3. The middleware hangs a function for creating redux-thunk that injects parameters. If you need to inject parameters, use it to recreate reduxThunk and inject parameters. reduxThunk.withExtraArguments = createReduxThunk //4, export redux-thunk middleware Export default reduxThunk copy the code
  • How to use redux in react: The following is a simple small example, just follow the order of comments

    Import React from 'REACT' Import ReactDOM from 'DOM-REACT' Import {createstore} from 'Redux' function the reducer ( State = 10 , Action ) { Switch (action.type) { Case 'the INCREMENT' : return State + . 1 Case 'DECREMENT push-' : return State - . 1 default : return State } } //1. Pass in the reducer to create our store (dispatch, getState, subscribe out of the structure) const {dispatch, getState, subscribe} = createStore(reducer) class Counter extends React . Component { constructor ( props ) { super (props) this .unSubscribe = null //2. Associate store data with state this .state = { num : getState()} } componentDidMount () { //3. Monitor the data changes in the store. If there is a change, update the changed data to the current state and re-render the page this .unSubscribe = subscribe( () => { this .setState({ num : getState( ) }) }) } componentWillUnmount () { //5. When the component is uninstalled, cancel the store data change subscription this .unSubscribe && this .unSubscribe() } //4. Use dispatch to update store data add = () => dispatch({ type : 'INCREMENT' }) minus = () => dispatch({ type : 'DECREMENT' }) render () { return < div > < div > count now: {this.state.num} </div > < button onClick = {this.add} > add </button > < button onClick = {this.minus} > minus </button > </div > } } ReactDOM.render ( < Counter/> , Document .getElementById ( 'the root' )) copying the code

4. The use of react-redux and the principle and implementation of connect

  • Basic use of react-redux:

    • 1. Create redux.store, and react.Context
    • 2. Wrap the entire react application with Context.Provider, and set the value to store, so that store can be used in the entire react application (of course, you have to do the corresponding Context configuration to access it)
    • 3. Bind our component with connect, and then we can get the action dispatch and the data in the store from props inside the component
  • Principle of connect:

    • 1. Connect is essentially an accepting (mapStateToProps and mapDispatchToProps) function, which returns high-level components. The high-level components accept components that currently need to use the redux state for enhancement. The enhancement part includes the action dispatch function and enhanced components such as disptach passed in through proprs The required state is given to the enhanced component, and finally the enhanced component is returned.
  • The use of react-redux and the principle and realization of connect: They are all in the following code, and the comments are very detailed.

    Import React from 'REACT' Import ReactDOM from 'DOM-REACT' Import {createstore} from 'Redux' //1. Get Context (deconstruct Provider, Consumer) const {Provider, Consumer} = React.createContext() //2. Implement a reducer (nothing to say) function reducer ( state, {type} ) { if ( type === 'INCREMENT' ) return {...state, num : state.num + 1 } if (type === 'DECREMENT' ) return {...state, num : state.num- 1 } return state } //3. Create redux store (nothing to say) const store = createStore(reducer, { num : 10 }) //4. The core connect function in react-redux is implemented. The essence of connect is a function that returns higher-order components. The essence of higher-order components is a function that returns enhanced components. The execution of connect ultimately needs to return a component function connect ( mapStateToProps, mapDispatchToProps ) { //4.1, connect accepts mapStateToProps, mapDispatchToProps, and returns a higher-order component function return function HOC ( Component ) { class Wrapper extends React . Component { constructor ( props ) { super (props) //4.1.1, put in the store Use mapStateToProps to process the data, get the data of the enhanced component (Component), and put it into the higher-order component to wrap the component (Wrapper) state in this.state = mapStateToProps( this .props.store.getState()) } componentDidMount () { //4.1.2, subscribe to data changes in the store, once the data changes, update the state of the package component, trigger the package component and its subcomponents to re-render this .unSubscribe = store.subscribe( () => this .setState(mapStateToProps( this .props.store.getState()))) } componentWillUnmount () { //4.1.3, when the component is uninstalled, cancel the store data subscription this .unSubscribe && this .unSubscribe() } render () { //4.1.4. Currently only mapDispatchToProps is processed as a function, that is, if mapDispatchToProps is a function, it will be processed as an action, and these actions will be handed over to the sub-component (Component) in the form of props, so that the sub-component can be directly this.props.action operation dispatches the let actions = { dispatch : the this .props.store.dispatch} IF ( typeof mapDispatchToProps === 'function' ) { actions = mapDispatchToProps( this .props.store.dispatch) } //4.1.5, transfer the data in action and store (stored in the package component state) to the enhanced child component through props passing parameters return < Component { ...this.state }{ ...actions }/> } } //4.1.6, returns a function component, the render content of the function component is the high-level component processed by the Consumer package, and the Consumer is responsible for handing over the store in the Context to the current high-level component. return () => < Consumer > {value => < Wrapper store = {value}/> } </Consumer > } } //5. The Counter component will be applied to our own implementation of connect class Counter extends React . Component { //5.1, because we did not set mapDispatchToProps, so we directly use the original dispatch to dispatch the action. add = () => this .props.dispatch({ type : 'INCREMENT' }) minus = () => this .props.dispatch({ type : 'DECREMENT' }) render () { return < div > < div > count now: {this.props.num} </div > < button onClick = {this.add} > add </button > < button onClick = {this.minus} > minus </button > </div > } } //6, react-redux maps the state of higher-order components to the props of the currently bound component (basic operation in react-redux) const mapStateToProps = state => { return { num : state.num}} //7, Use connect to handle our Counter component const ConnectCounter = connect(mapStateToProps)(Counter) //8. The outermost layer of the entire react application uses Context.Provider to wrap the value and transfer it to the store (basic operation in react-redux) ReactDOM.render( < Provider value = {store} > < ConnectCounter/> </Provider > , document .getElementById( 'root' ) ) Copy code

Thanks for reference

  • Chen Yi: In-depth REACT technology stack
  • React's diff algorithm
  • Deep understanding of React: diff algorithm
  • redux[createStore] source code
  • Redux Chinese Document-Middleware
  • Thunks in Redux: The Basics