本文续 《React (上)》,因为上的字数限制,所以开了下,掘金挺神奇的,上只写了不到2w字就字数限制,之前有的文章写了快三万字也没提字数限制,离谱。。。
1,React中diffing算法
-
什么是虚拟Dom:JSX本质是react.createElement(type,props,...children)的语法糖,react.createElement函数返回的就是虚拟dom,虚拟dom本质是一个js对象,用来描述真实UI是什么状态或者说描述真实dom什么模样,我们可以使用ReactDom等库将虚拟dom与真实dom同步,因为虚拟dom本质可以说是一个用来描述UI的状态对象,所以我们可以将虚拟dom跨平台使用,只要需要有类似ReactDom的库将其转化成真实的UI。
-
React主要工作流程(react16之前)
- 协调阶段:自顶向下递归生成虚拟dom,通过diff算法找到需要变更元素,放到更新队列中(patch queue)
- 渲染阶段:将更新队列中的元素更新到真实dom中(注意,仅事dom更新,不包括页面绘制)
-
Tree Edit Distance(树编辑距离)算法:一棵树转换成另一棵树的最小操作数的一种通用解决方案,它的时间复杂度是O(n^3)。
-
react中的diff算法:React中将虚拟dom转换成真实Dom树的最小操作的过程称为协调,而协调的具体实现便是diff算法。。至于为什么不实用树编辑距离算法,是因为树编辑距离算法时间复杂度是O(n^3),如果在React中使用树编辑距离算法处理一棵1000个元素的树,那么它的计算量将在10亿范围,开销太大,而react中通过基于以下三个策略或者说是假设实现的diff算法时间复杂度只有O(n)。
- 策略一:UI中的dom节点跨层级操作非常少,可以忽略。即因为react中d节点的跨层级操作很少,如果发生跨层级的节点操作,直接创建新节点,删除节点,不会去进行对比操作。
- 策略二:相同类型的组件将生成相同类型的树结构,不同类型的树组件生成不同的树结构。即如果新旧树中如果两个节点类型不同,则直接删除老节点,创建新节点,类型相同才会进行比较操作。
- 策略三:对于同一层级的一组子节点,可以通过唯一id暗示哪些子节点将在新老树中保持稳定。即我们对一组子节点通过设置key属性,react在diff过程中可以遍历老节点中与新节点相同key的节点进行节点的复用。一定程度减少了节点创建删除的操作。
基于以上三个策略,React分别对tree diff,component diff,element diff进行了算法优化,如下:注意 diff操作过程是找出新旧两棵虚拟dom树的差异记录下来,所以下面diff过程中的所有涉及到的动作(比如删除旧节点,创建插入新节点这种描述)只是记录更新真实dom时需要做的操作(并没有进行真正的更新dom操作),这些操作将在diff完毕之后汇聚成一个补丁包,然后根据补丁包一次性将真实dom完成更新。
-
tree diff 优化:采用深度优先遍历的方式,对树进行分层比较,仅比较新旧两棵树中同一层级的节点(即同一个父节点下的所有子节点),这样只需要一次遍历即可找出两棵树中的所有差异。 对于出现跨层级的节点操作,比如父节点A下面有子节点B与C,如果节点B移动到C节点下,变成了C节点的子节点,那么diff只会将B节点删除,C节点下面创建新的B节点。所以树中出现跨层级节点操作,不会出现节点的移动操作,而是以删除创建节点的两个操作完成。
-
component diff 优化:
-
1,新旧树中相同层级位置上的两个组件类型如果不同,则将旧组件判定为脏组件,不需要对二者进行比较操作,新组件将直接替换旧组件下所有节点。
-
2,如果两个组件类型相同,同时React生命周期函数中shouldComponentUpdate返回false,即当前组件没有本次更新没有发生变化,不需要进行diff操作,即节省本组件的diff运算时间。
-
3,如果两个组件类型相同,同时React生命周期函数中shouldComponentUpdate返回true,我们需要正常对新老树中两个组件对应的虚拟dom节点进行比较。
-
-
element diff 优化:对于同一层级内节点的操作,区分有无key情况下。
- 不存在key:则对新旧树同一层级节点进行对比,如果类型相同继续比较(比较二者props记录差异,之后继续深度优先继续比较孩子节点),类型不同直接删除老节点,创建并插入新节点。这种方式在子节点没有发生变化仅是位置发生变化时效率很低,效率最高的方式是直接将子节点进行移动到对应位置即可,而不是删除创建节点。所以对于这一部分优化的优化策略就是允许开发者对子节点们添加key进行区分,暗示添加了key的节点将在新旧dom树中保持稳定。
- 存在key:
- 1,对于新树中子节点进行循环遍历,通过唯一key去判断旧树中该层级子节点集合中有没有相同key的节点。
-
1.1,如果在旧节点集合中不存在当前新节点的key:那么创建该新节点,同时节点位置将为该节点在新节点集合中的位置。
-
1.2,如果在旧节点集合中存在当前新节点的key:
- 1.2.1,如果新节点与旧节点类型相同,则可以复用旧节点,对新旧节点进行比较
- 1.2.1.1,如果相同,则将来可能会对节点进行移动
- 1.2.1.2,如果不同,这方面的资料没查到,我个人觉得应该是比较二者差异,同时也可能会对节点进行移动。
- 1.2.2,如果新节点与旧节点类型不同,虽然二者key相同,那么也要删除旧节点,创建新节点插入到新节点在新集合中的位置
- 1.2.1,如果新节点与旧节点类型相同,则可以复用旧节点,对新旧节点进行比较
-
- 2,最后还要对旧树进行遍历,找出旧树中存在且在新树中不存在的节点,对这些节点进行删除。
- 1,对于新树中子节点进行循环遍历,通过唯一key去判断旧树中该层级子节点集合中有没有相同key的节点。
- 如何对带key的子节点进行移动:
-
讨论这个问题时,我们先看old_index,last_index,new_index在下图新老节点diff中的含义。节点的移动就是通过这三个值来进行。
-
1,如上图B的old_index即B在老节点中的位置,从0开始数,所以B的old_index为1,
-
2,继续B的new_index即B在新节点中的位置,如图是0,对于old_index与new_index很明显都是固定的常量,
-
3,而对于last_index,它是个变量,初始值为0,它在diff新旧节点都可能会发生变化,所以每次对比新旧节点时,last_index 会被赋值为math.max(currentNode.old_index,last_index)。
-
-
具体带key节点移动方式:分下面三种情况
-
1,新节点的key在旧节点中也存在:先判断旧节点的old_index与lastindex谁大,当mountIndex<lastIndex,真实dom中的该节点需要移动,移动到新节点的new_index位置,如果mountIndex>=lastIndex,那么真实dom中该节点不移动。
-
2,新节点中的key在旧节点中不存在:那么创建新节点的真实dom节点,放在当前last_index位置
-
3,如果老节点中存在该节点,但新节点中不存在该节点,那么直接删除真实dom中的该节点。
-
-
- 具体移动解析:1,新旧节点存在相同key节点,但位置不同,如下
-
第一步,先操作新节点中的B
{new_index:0,old_index:1}
,此时last_index为0,发现B的old_index>last_index,所以B不移动,此时B位置为new_index,即0位置,同时last_index重新赋值为Math.max(old_index,last_index),即当前last_index:1。 -
第二步,操作新节点中的A
{new_index:1,old_index:0}
,此时last_index为1,发现A的old_index<last_index,所以A移动,A移动到new_index,即1位置,同时last_index重新赋值为Math.max(old_index,last_index),即当前last_index:1。 -
第三步,操作新节点中的D
{new_index:2,old_index:3}
,此时last_index为1,发现D的old_index>last_index,所以D不移动,此时D位置为new_index,即2位置,同时last_index重新赋值为Math.max(old_index,last_index),即当前last_index:3。 -
第四步,操作新节点中的C
{new_index:3,old_index:2}
,此时last_index为3,发现C的old_index<last_index,所以C移动,C移动到new_index,即3位置,同时last_index重新赋值为Math.max(old_index,last_index),即当前last_index:3。 -
第五步,新节点中无节点需要与旧节点diff,此时对旧节点集合进行遍历,判断是否存在旧节点集合存在而新节点集合不存在的节点,遍历完毕,为发现符合该条件节点,diff完毕。
-
- 具体移动解析:2,新节点存在旧节点中没有的E节点,旧节点中存在新节点中没有D节点
-
第一步,先操作新节点中的B
{new_index:0,old_index:1}
,此时last_index为0,发现B的old_index>last_index,所以B不移动,此时B位置为new_index,即0位置,同时last_index重新赋值为Math.max(old_index,last_index),即当前last_index:1。 -
第二步,操作新节点中的E
{new_index:1,old_index:null}
,此时last_index为0,发现E节点不存在旧的节点中,创建E节点,此时E位置为new_index,即1位置,同时last_index仍然为1。 -
第三步,操作新节点中的C
{new_index:2,old_index:2}
,此时last_index为1,发现C的old_index===last_index,所以C不移动,此时C位置为new_index,即2位置,同时last_index重新赋值为Math.max(old_index,last_index),即当前last_index:2。 -
第四步,操作新节点中的A
{new_index:3,old_index:0}
,此时last_index为2,发现A的old_index<last_index,C移动,移动到new_index,即3位置,同时last_index重新赋值为Math.max(old_index,last_index),即当前last_index:3 -
第五步,新节点中无节点需要与旧节点diff,此时对旧节点集合进行遍历,判断是否存在旧节点集合存在而新节点集合不存在的节点,发现D节点符合该条件,删除节点D,diff完毕。
-
- 关于带key节点的移动操作,我们也有需要注意的地方,如下图,D节点从最后一个位置移动到首部,按照我们的移动操作,将会导致D前面所有节点都会向后移动一个位置,所以我们在开发过程中尽量避免类似将最后一个节点移动到首部操作,因为这将一定程度上影响React的性能。
- 为什么不建议使用数组索引作为key: 使用数组索引当key不是不可以,只是在某些情况下效率会低,比如我们在数组中插入数据,删除数据,这都会导致该插入删除的数据后面所有数据的索引发生变化,这一部分数据当进行diff时,虽然能找到对应key的旧节点,但是对应key的新节点内容都发生了变化,需要比较更新。但实际上我们只是插入删除了数据,该数据后面的数据其实还存在,只不过是key发生了变化。所以我们不建议使用数组索引作key,而是以一个稳定的字段作为key。
-
react中的patch方法:当我们完成diff操作,收集了新旧两棵虚拟dom树之间的所有差异,此时我们需要将这些差异更新到真实dom中。因为我们在遍历新旧树去收集差异的过程中采用深入优先遍历,而且在收集差异的时候对这些差异是有序的添加,会有类似于index的字段代表者每一个差异在树中的位置,所以我们对差异包与真实dom树进行遍历,每个真实dom树节点根据自身位置匹配差异包对应index位置观察是否有差异,有差异则根据对应差异更新真实dom节点,无差异则跳过当前节点,继续下一个节点对比,所以我们可以通过一次对树遍历完成真实dom的更新操作。
2,react-hook基本手写实现及使用
本文重点在hooks的手写实现上,包括useState,useEffect,useLayoutEffect,useCallback,useMemo,useContext,useRef,具体实现中基本每行都会添加注释,不明白的话可以看注释。
-
什么是react-hook:hook可以让我们在函数组件中能够使用state等之前其他函数组件不支持的react特性。
-
使用hook的函数组件优点(对比类组件):
- 类组件复用逻辑一般使用renderProps或高阶组件的方式,但这样的行为会让组件变得复杂,比如高阶组件中的组件嵌套,而hook可以在不修改组件的情况下复用逻辑。
- 类组件许多不相关的副作用逻辑肯能会集中在同一生命周期内,导致组件臃肿,而在hook的函数组件中我们可以将这些逻辑分离出来,形成一个个功能单一的逻辑,代码更加清晰,同时对于逻辑抽象也很方便。
- 类组件需要使用class以及确定this,对于使用hook函数组件则没有这些麻烦
-
react-hooks实现:
- useState(支持单个state处理) :useState 用于处理函数组件内的数据管理,类似类组件的state。useState()返回最新state与更新state方法setState,类似于类组件中的this.state与this.setState作用。注意:react会保证setState函数的标识是稳定的,不会在重新渲染时发生变化,即每次函数组件重新渲染,函数组件重新执行,useState重新执行,但是返回的setState函数与重新渲染之前的setState函数是同一个,而state是有可能发生变化的(如果你对state进行更新的话)。这就是为什么在useEffect与useCallback中使用了setState,但是我们可以安全的在依赖列表中省略setState。
import React from 'react'; import ReactDOM from 'react-dom'; // 1,声明stateInfo用于保存state与setState let stateInfo = { state: undefined, // state保存位置 stateUsed: false, // 用于标识state是否是第一次使用,第一次使用state使用传入默认值,后续重新渲染执行useState都使用已经使用过的state值 setState: null, // setState函数保存位置 } // 2,实现useState function useState(initState) { // 2.1,初始化state,如果state被使用过,那么取之前用过的state,否则使用传入初始state // 用下面这么判断而不是 stateInfo.state = stateInfo.state||initState 的原因是 // 我们必须保证当后续stateInfo.state被设置成假值的情况下,依然使用stateInfo.state stateInfo.state = stateInfo.stateUsed ? stateInfo.state : (stateInfo.stateUsed = true, initState) // 2.2,初始化setState,如果之前已经初始化了setState,则直接使用之前初始化的setSstate,否则初始化setState // 这么做的原因是,我们必须保证每次调用useState返回的setState函数都是同一个 if (!stateInfo.setState) { stateInfo.setState = function (newState) { // 2.2.1,setState参数如果是函数,则传入当前state并获取其返回值 const newStateTemp = typeof newState === 'function' ? newState(stateInfo.state) : newState // 2.2.2,如果新旧state相同,那么忽略本次更新 if (Object.is(newStateTemp, stateInfo.state)) return // 2.2.3,如果新旧state不同,更新state stateInfo.state = newStateTemp // 2.2.4,setState之后我们需要重新渲染当前组件 ReactDOM.render(<Index />, document.getElementById('root')); } } // 2.3,返回state与setState return [stateInfo.state, stateInfo.setState] } // 3,使用我们的useState function Index() { const [count, setCount] = useState(-1) return <button onClick={() => setCount(count => count + 1)}>{count}</button> } ReactDOM.render(<Index />, document.getElementById('root'));
- useState(支持多个state处理) :上面的useState只能处理一个state,现在我们要处理多个state,我们会将多个state保存在数组里,用数组下标映射不同state(实际上是用数组下标映射useState使用顺序从而间接实现数组下标映射不同state),保证多个state状态不会混乱。这也就是为什么我们强调useState不能放在条件语句中,因为useState的使用顺序关系到其state保存位置。顺序一旦乱了,那么state位置也就乱了。
import React from 'react'; import ReactDOM from 'react-dom'; // 1,声明stateInfoList保存多个useState消费者状态,包括当前useState消费者的state与setState let stateInfoList = [] // 2,声明index索引,每次使用useState后,index都会+1,这样多个useState消费者就可以用index来区别当前useState消费者是谁 let index = 0 // 3,实现useState function useState(initState) { // 3.1,如果没有当前索引对应的state,说明当前useState消费者是首次使用useState,则初始化当前索引对应state if (!stateInfoList[index]) stateInfoList[index] = { state: initState } // 3.2,如果没有当前索引对应setState函数,说明当前useState消费者是首次使用useState,则初始化该索引对应setState if (!stateInfoList[index].setState) { // 3.2.1,使用闭包缓存当前useState消费者的索引currentIndex,在下次重新渲染组件,可以根据缓存索引获取stateInfoList该useState消费者保存的状态 const currentIndex = index // 3.2.2,初始化当前useState消费者的setState函数 stateInfoList[currentIndex].setState = function (newState) { // 3.2.2.1,newState如果是函数,则传入当前state获取返回值 const newStateTemp = typeof newState === 'function' ? newState(stateInfoList[currentIndex].state) : newState // 3.2.2.2,如果newStateTemp 与 上一次的state相同,则取消更新操作 if (Object.is(stateInfoList[currentIndex].state, newStateTemp)) return // 3.2.2.3,如果newStateTemp与上一次的state不同,则更新state stateInfoList[currentIndex].state = newStateTemp // 3.2.2.4,每次更新state导致组件重新渲染之前都需要将索引恢复初始值,这样在函数组件更新重新执行代码时,才能对应上上一次每个useState消费者索引,从而在stateInfoList中找到上一次的state与setState函数 index = 0 // 3.2.2.5,重新渲染当前函数组件 ReactDOM.render(<Index />, document.getElementById('root')); } } // 3.3,返回当前索引对应state,setState,同时index需要+1,这样下一个useState消费者就能获取到到独一无二的索引 return [stateInfoList[index].state, stateInfoList[index++].setState] } // 4,使用我们的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('root'));
- useReducer :useState的替代方案,它接受一个形如
(state,action)=>newState
的reducer函数,返回当前state以及更新state的dispatch函数。某些场景下useReducer会比useState更适用,比如state复杂且包含多个子值,或者下一个state依赖上一个state等。当然类似于setState,react也会保证dispatch函数的稳定,不会在组件重新渲染时改变。 useReducer实现原理类似useState,最大区别就是使用了reducer去更新state。import React from 'react'; import ReactDOM from 'react-dom'; // 1,创建reducerStateList保存多个useReducer消费者的状态 let reducerStateList = [] // 2,使用不同index索引标识不同useReducer消费者 let index = 0 // 3,创建我们自己的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,使用我们的useReducer function reducerAdd(state, action) { switch (action.type) { case 'add': return { addCount: state.addCount + 1 } default: throw new Error('无法匹配到当前传入类型') } } function reducerMinus(state, action) { switch (action.type) { case 'minus': return { minusCount: state.minusCount - 1 } default: throw new Error('无法匹配到当前传入类型') } } 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('root'));
- useCallBack :
-
useCallBack:useCallback传入一个函数与当前函数的依赖,返回一个记忆版本的该函数,返回的记忆函数与原函数功能上无任何区别。
-
useCallback原理:对于useState返回的setState函数,每次重新渲染函数组件时执行useState返回的都是同一个setState函数(setState地址不变),而对于useCallback,只要依赖不发生变化,那么每次重新渲染函数组件时执行useCallBack返回也都是同一个记忆函数,但是如果依赖发生变化,返回的就不是上次的记忆函数(地址变了)
-
useCallback作用:如果在父组件内创建函数并向子组件传入该函数时(props传递数据),同时子组件做了性能优化(react.memo),如果我们不对该函数使用useCallback处理,那么每次父组件重新渲染,props传递的该函数都是一个新函数(因为该函数每次父组件重新渲染时都会被重新创建),导致props每次都会发生变化,react.memo等性能优化手段失效,所以我们可以使用useCallback处理该函数,只要其依赖没有发生变化,那么我们传递给子组件都是同一个函数,从而避免不必要的子组件渲染。
-
useCallback实现(单个useCallback消费场景):本次实现只针对于useCallback消费者场景,多个useCallBack消费者场景,需要索引配合使用,且在组件重新render之前索引恢复初始值,这里我们不好监听组件重新render时机,所以实现的useCallback只支持一个消费者。
import React, { useState } from 'react'; import ReactDOM from 'react-dom'; // 1,声明callbackState保存callback及其依赖 let callbackState // 2,实现我们useCallback function useCallback(callback, dep) { // 2.1,如果没有传入依赖,那么每次组件rerender都返回最新的callback if (!dep) return callback // 2.2,如果是首次render使用useCallback,将callback与依赖保存到callbackState中,并返回当前callback if (!callbackState) { callbackState = { callback, dep } return callbackState.callback } // 2.3,如果非首次渲染使用useCallback,那么callbackState一定已经存储上一次render后的callback与依赖,那么我们需要对比本次render后的依赖与上一次render后的依赖是否有变化 const notChanged = callbackState.dep.every((e, i) => Object.is(e, dep[i])) // 2.4,如果两次render的依赖发生了变化,那么我们将最新的依赖更新至callbackState中,并返回最新callback if (!notChanged) { callbackState = { callback: callback, dep: dep } return callbackState.callback } // 2.5,如果两次render依赖没有发生变化,则返回上一次的callback return callbackState.callback } // 3,使用我们的useCallback:可以发现对于使用了react.memo后的子组件: // 3.1,当我们count发生更新,子组件会发生重新渲染,因为log1依赖为count,count变化导致传入了新的log1, // 3.2,当我们string发生更新,子组件不会发生重新渲染,因为log1不依赖string,所以传入了旧的log1。 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('root'));
-
- useMemo :
- useMemo:useMemo接受回调函数与依赖,返回回调函数执行结果,类似于useCallback,只要依赖不发生变化,回调函数就不会重新执行。
- useMemo用途:如果每次函数组件渲染时都会执行一个包含大量计算的函数,那么我们可以使用useMemo对该函数进行处理,仅在其依赖数据发生变化才重新计算,否则不要计算从而达成性能优化。注意:useMemo会在渲染时执行,请不要在该函数内执行与渲染无关的代码,诸如副作用的这些操作请把它们放在useEffect中处理。
- useMemo实现(单个useMemo消费场景):类似于useCallback实现,最大区别就是将缓存函数变成缓存函数结果。
import React, { useState } from 'react'; import ReactDOM from 'react-dom'; // 1,声明memoState保存useMemo消费者的回调函数结果与依赖 let memoState // 2,实现我们的useMemo function useMemo(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,使用我们的useMemo:可以发现当我们点击count时,useMemo中callback都会重新执行,而点击string时useMemo中的callback不会重新执行 function Index() { const [count, setCount] = useState(7), [string, setString] = useState('*'); const result = useMemo(() => { return (console.log('memo used'), count + 1) }, [count]) return <div> <div>当前++count:{result}</div> <button onClick={() => setCount(count => count + 1)}>{count}</button> <button onClick={() => setString(string => string + '*')}>{string}</button> </div> } ReactDOM.render(<Index />, document.getElementById('root'));
- useContext(单个useContext消费场景) :使用useContext
const context = useContext(MyContext)
和类组件中的static contextType = MyContext
行为是一样的,且当前context数据来自于最近父组件中使用MyContext.Provider的value属性,同时当MyContext更新时,消费MyContext的组件都将会发生重新渲染,即使使用了react.memo或者shouldComponentUpdate。import React from 'react'; import ReactDOM from 'react-dom'; const ColorContext = React.createContext('red') // 1,useContext实现:直接获取Context中的value返回即可 function useContext(context) { return context._currentValue } // 2,使用我们的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('root'));
- useEffect:
- useEffect:useEffect接受可能存在副作用的函数,与依赖,副作用函数可能会返回一个函数,副作用函数将在函数组件渲染完毕执行,且副作用函数返回函数会在下一次副作用函数执行之前执行。
- useEffect使用场景:useEffect位于屏幕完成布局及绘制之后(这也就是为什么我们后面使用定时器(宏任务)实现useEffect) 我们一般再useEffect中处理副作用函数,比如订阅,数据请求,副作用返回函数可以进行订阅的取消,副作用返回函数不仅再下一次副作用函数之前执行,也会在组件卸载之前执行。
- useEffect实现(单个useEffect消费场景):因为useEffect中的副作用函数是在页面渲染完毕(这里的页面渲染完毕指的是完成dom更新或挂载之后,浏览器完成绘制之后)执行,所以我们会将副作用函数包裹在定时器中,形成宏任务,因为宏任务将在页面渲染完毕之后执行。
import React, { useState } from 'react'; import ReactDOM from 'react-dom'; // 1,声明effectState保存useEffect依赖 let effectState // 2,实现我们的useEffect:因为useEffect执行时机相当于类组件的componentDidmount与componentDidUpdate,即在浏览器渲染结束执行, // 所以我们每次执行callback需要将其放在宏任务setTimeout中,保证我们的callback在浏览器渲染结束执行 // 同时callback如果返回有函数resFn,那么该函数会在执行下一个callback之前执行,所以在下一个callback之前需要先执行上一个callback的返回函数 function useEffect(callback, dep) { // 2.1,如果首次渲染的函数组件,那么保存我们的依赖,同时执行callback,如果有返回函数则将其保存,用于在下一次执行callback之前执行该函数 if (!effectState) { effectState = { dep } return setTimeout(() => { const resFn = callback() typeof resFn === 'function' && (effectState.resFn = resFn) }, 0); } // 2.2,如果没有依赖且非首次函数组件渲染,如果上一次callback有返回函数,则先执行返回函数,再执行callback,如果没有返回函数,直接执行callback if (!dep) { return setTimeout(() => { typeof effectState.resFn === 'function' && effectState.resFn() const resFn = callback() if (typeof resFn === 'function') effectState.resFn = resFn }, 0); } // 2.3,如果存在依赖且非首次函数组件渲染,那么我们需要对比当前render与上一次render依赖有没有变化 const notChanged = effectState.dep.every((e, i) => Object.is(e, dep[i])) // 2.4,如果依赖有变化,保存变化的依赖,然后判断上一次callback是否有返回函数,有则执行,再执行当前callback,无则直接执行当前callback 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,使用我们的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('root'));
- useLayoutEffect :
- useLayoutEffect:
- useLayoutEffect:useLayoutEffect用法与useEffect相同,接受可能有副作用的函数与依赖,接受的函数可能返回一个函数。
- useLayoutEffect:
- 与useEffect的区别:最大区别就是二者的执行时机,useLayoutEffect执行时机为componentDidMount与componentDidUpdate时机相同,既dom的更新与挂载执行完毕之后,浏览器绘制之前,useEffect执行时机为屏幕更新之后,既浏览器绘制完成之后
- 阻塞渲染:所以在useLayoutEffect中如果出现dom更新,它将阻塞浏览器绘制,等待dom更新完毕,才继续浏览器的绘制,因此useLayoutEffect会阻塞页面更新(当然对于componentDidMount也是如此)
- 服务端渲染不会执行useLayoutEffect:因此服务端渲染使用该函数会有警告,因为服务端渲染不执行该方法,所以如果该方法内有任何操作,比如dom更新行为(setState()),那么这个行为不会执行,从而导致页面呈现和预期不一致,解决方法可以通过判断当前环境是浏览器环境还是node环境,如果是浏览器环境继续使用useLayoutEffect,node环境则使用useEffect执行原来useLayoutEffect的内容
- useLayoutEffect实现:类似于useEffect,但是区别于useEffect是它执行时机,它在所有页面即将渲染之前执行,所以我们可以使用requestAnimationFrame将其包裹执行。
import React, { useState } from 'react'; import ReactDOM from 'react-dom'; let layoutEffectState function useLayoutEffect(callback, dep) { if (!layoutEffectState) { layoutEffectState = { dep } return requestAnimationFrame(() => { const resFn = callback() typeof resFn === 'function' && (layoutEffectState.resFn = resFn) }); } if (!dep) { return requestAnimationFrame(() => { typeof layoutEffectState.resFn === 'function' && layoutEffectState.resFn() const resFn = callback() if (typeof resFn === 'function') layoutEffectState.resFn = resFn }); } const notChanged = layoutEffectState.dep.every((e, i) => Object.is(e, dep[i])) if (!notChanged) { layoutEffectState.dep = dep return requestAnimationFrame(() => { typeof layoutEffectState.resFn === 'function' && layoutEffectState.resFn() const resFn = callback() if (typeof resFn === 'function') layoutEffectState.resFn = resFn }); } } // 3,使用我们的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'));
- useLayoutEffect:
- useRef:useRef返回一个可变的ref对象,该对象current属性被初始化传入的默认值,且返回的ref在组件的整个生命周期中不会发生变化。从代码层面上看这句话就是:useRef在每次组件渲染返回的都是同一个对象,再具体的说返回对象的地址都是相同的,你可以去修改对象内部属性或者对该对象重新赋值,但不管那种操作,下一次重新渲染,useRef返回的还是那个对象,地址也不会发生变化。
- useRef用途:
- 1,访问dom:相当于类组件中使用
this.ref = React.createRef()
,然后将ref挂到实际需要访问的组件上。function Index() { let divRef = useRef() useEffect(() => { // 通过.current可以访问到绑定ref的dom节点 console.log(divRef.current.innerHTML); }) return <div ref={divRef} > ref </div> }
- 2,保存数据:类似于类组件中我们会在this上面保存一些不会再页面渲染时用到的数据,比如保存toast提示信息
this.message = '接口失败'
等。function Index() { let message = useRef('接口失败') useEffect(() => { // 接口调用失败弹出固定信息 request().catch(err => alert(message)) }, []) return <div > ref </div> }
- 1,访问dom:相当于类组件中使用
- useRef注意点:useRef内容发生变化不会引发重新渲染。
- useRef实现:实现的为保存数据版本,访问dom操作不在本实现中。且本次实现为单个useRef,因为不好获取render时机,索引就不好重置。
import React, { useState } from 'react'; import ReactDOM from 'react-dom'; // 1,声明refData保存useRef数据 let refData // 2,创建我们的useRef function useRef(initData) { // 2.1,如果不存在refData,即首次使用,创建refData,保存初始值 if (!refData) refData = { current: initData } // 2.2,非首次使用,每次都返回首次创建的refData return refData } // 3,使用我们的useRef function Index() { let info = useRef('使用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('root'));
- useRef用途:
- useState(支持单个state处理) :useState 用于处理函数组件内的数据管理,类似类组件的state。useState()返回最新state与更新state方法setState,类似于类组件中的this.state与this.setState作用。注意:react会保证setState函数的标识是稳定的,不会在重新渲染时发生变化,即每次函数组件重新渲染,函数组件重新执行,useState重新执行,但是返回的setState函数与重新渲染之前的setState函数是同一个,而state是有可能发生变化的(如果你对state进行更新的话)。这就是为什么在useEffect与useCallback中使用了setState,但是我们可以安全的在依赖列表中省略setState。
3,Redux核心手写实现及如何配合React使用(Redux+React)
本文重点在redux手写实现上,包括createStore,combineReducer,applyMiddleWares,compose,同时在手写实现上保留了源码中的一些重要细节,删除了一些源码中的判断,在具体实现中,对于每一行代码都会有注释说明具体作用,可以参考注释理解。
-
redux是什么:redux是一个可预测的状态管理容器,它内部保存了数据状态,同时暴露出了数据的访问接口,当然我们不仅需要访问数据,也需要对数据状态进行更改,而在redux中如果期望对数据更改,必须提供对应的reducer,然后通过redux暴露出来的dispatch方法进行更改,这就提现了redux可预测性。
-
实现基本redux(redux.createStore):redux.createStore这里将实现三个方法,分别是getState,dipatch与subscribe,着重看subscribe实现,因为redux中每次派发动作时刻中订阅队列中的函数将会全部执行,订阅函数执行过程添加取消订阅都不会影响本次订阅队列执行。
- getState:返回redux中的状态
- dispatch:派发动作,更新state,订阅函数队列执行,注意::dispatch每次更新完state,监听队列的执行都是当前dispatch那一时刻的监听队列,如果在监听队列函数执行中对监听队列进行操作,是不可以影响到本次disptach时刻的监听队列。主要有以下两种情况需要注意实现:
- 1,监听队列执行向监听队列加入新的监听,该监听只会在下次dispatch执行,不对本次disptach造成影响
- 2,监听队列执行向监听队列取消原有监听,不影响本次disptach监听队列,即原有监听本次disptach还会执行,而下次disptach将不会执行。
- subscribe:添加订阅函数,每一次dispatch时将执行所有订阅函数,注意:返回的取消监听函数保证只能执行一次取消监听。
function createStore(reducer, preloadedState, enhancer) { // 0,如果传入增强器,则直接返回增强器处理后的createStore返回的结果 if (typeof enhancer === 'function') return enhancer(createStore)(reducer, preloadedState) // 1,初始化currentState let currentState = preloadedState // 2,currentListeners:用来保存订阅函数,负责执行订阅函数 let currentListeners = [] // 3,nextListeners:用来保存订阅函数,负责处理加入订阅与取消订阅 // currentListeners 与 nextListeners 将订阅函数添加取消与订阅函数的执行分离开来,避免在订阅函数执行添加取消订阅出现bug,具体看subscribe与dispatch 实现 let nextListeners = currentListeners // 4,isDispatching 控制dispatch中执行reducer时,禁止在reducer执行过程中出现getState,订阅,取消订阅以及继续dispatch的操作 let isDispatching = false // 5,getState:返回当前redux中的数据状态 function getState() { if (isDispatching) throw new Error() return currentState } // 6,dispatch:派发动作更新redux中的数据状态,更新完毕需要执行订阅函数队列,该函数返回值为接受值,即action function dispatch(action) { if (isDispatching) throw new Error() // 6.1,执行reducer更新state状态,禁止在reducer执行过程中出现getState,订阅,取消订阅,dispatch操作 try { isDispatching = true currentState = reducer(currentState, action) } finally { isDispatching = false } // 6.2,获取最新监听队列快照,即获取当前dispatch时刻的监听队列 let listeners = (currentListeners = nextListeners) // 6.3,执行监听队列中的函数 for (let i = 0; i < listeners.length; i++) { // 6.3.1,对listeners[i]先赋值再执行是为了避免监听函数通过this改变监听队列,因为直接listners[i]()执行时this指向的是当前监听队列 let listener = listeners[i] listener() } // 6.4,返回action return action } // 7,getShallowCopyFromCurrentListeners:获取currentListeners的浅拷贝,将nextListeners与currentListeners分离城两个地址不同的对象,将在订阅以及取消订阅的时候用到该函数 function getShallowCopyFromCurrentListeners() { if (currentListeners === nextListeners) nextListeners = currentListeners.slice() } // 8,subscribe:添加订阅函数,该函数返回的是取消当前订阅的函数 function subscribe(listener) { if (isDispatching) throw new Error() // 8.1,isSubscribed 主要在用于返回的取消订阅函数,防止对同一函数连续取消订阅,即只取消一次就可以了,后面再取消旧忽略 let isSubscribed = true // 8.2,订阅函数时先将nextListeners与currentListeners分离开,订阅函数添加到nextListeners之中, // dispatch的时候再将nextListeners同步到currentListeners中,这样如果在订阅函数嵌套添加订阅函数, // 也只会添加到next中,不会添加到current中,只有再下一次dispatch时才会执行上一次订阅函数中添加的订阅函数, // 达到每次dispatch只处理当前dispatch时监听队列快照下的监听 getShallowCopyFromCurrentListeners() nextListeners.push(listener) // 8.3,返回取消订阅函数 return () => { // 8.3.1,保证取消监听函数只完全执行一次 if (!isSubscribed) return if (isDispatching) throw new Error() isSubscribed = false // 8.3.2,与添加订阅时相同,取消订阅也只对nextListeners操作,不会影响到当前dispatch使用的currentListeners, // 保证disptach执行时,执行的监听队列都是当 前dispatch动作发生时 的监听队列的快照 getShallowCopyFromCurrentListeners() // 8.3.3,获取当前监听函数再监听队列中的位置 const index = nextListeners.indexOf(listener) // 8.3.4,根据该位置删除该监听函数 nextListeners.splice(index, 1) // 8.3.5,currentListeners置null,应该是防止内存泄露,取消监听到下一次dispatch执行这段时间,currentListeners都用不到 currentListeners = null } } // 9,初始化reducer默认state填充至当前currentState,(一般reducer都有default) dispatch({ type: `@@redux/INIT${Math.random().toString(36).substring(7).split('').join('.')}` }) // 10,返回 getState, dispatch, subscribe return { getState, dispatch, subscribe } }
-
实现combineReducers:用来合并多个reducer,使用方式在具体实现下面有。
function combineReducers(reducerCompose) { // 1, // reducerKeys:传入对应reduer的keys // goodReducer:过滤后的reducer键值对 const reducerComposeKeys = Object.keys(reducerCompose), goodReducer = {} // 2,过滤掉不是函数类型的reuducer for (let i = 0; i < reducerComposeKeys.length; i++) { let key = reducerComposeKeys[i] typeof reducerCompose[key] === 'function' && (goodReducer[key] = reducerCompose[key]) } // 3,返回合并后的reducer return function (state = {}, action) { // 3.1, // goodReducerKeys:过滤后的reducer对应的keys,后面与state遍历用 // nextState:reducer更新完毕state返回的最后结果即nextState const goodReducerKeys = Object.keys(goodReducer), nextState = {}; // 3.2,标识nextState与原state有无变化,有变化才返回nextState,否则返回原state let hasChanged = false; // 3.3,对goodReducerKeys遍历,更新state中每个子state值,最终结果保存在nextState中,同时对比更新后的每个子state与之前state中每个子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,比较goodReducer的键集合与state中键集合长度是否相等,如果不想等,肯定nextState与原state不想等 hasChanged = hasChanged || goodReducerKeys.length !== Object.keys(state).length // 3.5,如果更新后的state与原state相比有变化,返回更新后state,否则返回原state return hasChanged ? nextState : state } } // 基本使用 // // 1,自定义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,自定义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,初始state // const state = { reducer0: { count: 0 }, reducer1: { count: 0 } } // // 4,使用createStore 配合combineReducer // const { dispatch, subscribe, getState } = createStore(combineReducers({ reducer0, reducer1 }), state)
-
什么是中间件:本质就是对dispatch功能的扩展增强,具体扩展位置为dispatch之后,reducer更新之前。 中间件用到了函数柯理化,建议研究中间件之前先了解 函数柯理化。
-
中间件的为什么是三层函数嵌套?:下面是中间件的标准格式,三层函数(见下图注释的fn1,fn2,fn3)。
function middleWare(store) { // fn1 return function (next) { // fn2 return function (action) { // fn3 // do sth next(action) } } } // 或者这样 const middleWare = store=>next=>action=>{ // do sth next(action) }
-
1,研究为什么是fn1,fn2,fn3三层之前我们先想一下,中间件的行为本质就是对dispacth功能的拓展增强。 那么以函数的形式来表现中间件的这种行为本质,是不是相当于我们有一个增强函数(专门用于增强disptach功能的函数),然后向增强函数传入disptach,增强函数内部对dipatch进行增强,然后返回一个增强后的disptach。 这个过程其实就是fn2,fn3的嵌套,fn2就是增强函数,fn2接受的参数next就是disptach函数,返回出去的fn3就是增强后的diptach。
-
2,理解了fn2与fn3,那么我们再来看fn1,我们想一下,中间件对dispatch进行扩展的同时,我们是可能会用到createStore创建的store用于处理某些需求的(比如用到store.getState获取当前store的状态),而对于封装一个通用的工具(中间件),这个获取store的动作如此通用肯定不会由我们自己实现(一般会在某个地方实现好,我们直接用即可),所以放弃在fn2或者fn3中手动获取store的念头, 那么我们还可以在哪获取呢,fn2或者fn1中传参吗?不合适,因为fn2本质就是一个用于增强disptach的函数,我们期望它更纯粹,即接受disptach,返回增强dispatch,fn1本质就是增强后的dispatch函数,我们肯定不能修改fn1的传参模式(即保持与原始dispatch签名一致),那么这个获取store的动作在哪里最合适呢,没错,就是在fn2外面再嵌套一层函数fn1,fn1接受参数为store,没错,这就是函数柯理化中的缓存参数(缓存我们的store),这样我们可以在扩展disptach过程中如若需要用到store,即可以直接使用。
-
3,重新梳理一下fn1,fn2,fn3,fn1缓存store参数,fn2相当于dispatch增强器,接受dispatch作为参数,返回一个增强版的dispatch,fn3就是那个增强版dispatch。所以fn1=>fn2=>fn3最终会返回出来的一个增强后的dispatch。 当然我们会用多个中间件对dispatch进行多次增强,所以经过fn1,fn2,fn3过程的返回的增强disptach可能又要变成下一个fn1,fn2,fn3流程中fn2的参数来继续增强,我们肯定不希望手动挨个中间件来处理这个过程,我们期望做的肯定只有提供fn1,fn2,fn3这种格式的中间件,所以为了实现这个过程自动化,applyMiddleWares登场。它可以帮助我们自动完成中间件迭代增强disptach,最终返回一个包含了所有中间件增强能力的dispatch的过程。
-
-
实现applyMiddleWares:我们知道中间件的基本格式,以及中间件本质就是对dispatch进行扩展增强,那么对于多个中间件对dispatch能力增强的迭代过程自动实现,就需要用到applyMiddleWares完成,下面将实现基本applyMiddleWares,不是很复杂,相对于源码删除了一些判断行为。applyMiddleWares中会涉及compose函数,所以 十分建议先了解一下compose函数。
- 实现中间件之前现看createStore中如何使用中间件,如下代码,enhancer就是我们传入的applyMiddleWares(...middleWares),enhancer以柯理化形式继续接受参数,分别是createStore与reducer, preloadedState,所以我们applyMiddleWares(...middleWares)返回的肯定是个函数,先接受createStore然后返回新的函数接受reducer与preloadedState。类似于这样的代码
const applyMiddleWares = (...middleWares)=>createStore=>(reducer,preloadedState)=>{}
function createStore(reducer, preloadedState, enhancer) { // 0,如果传入增强器,则直接返回增强器处理后的createStore返回的结果 if (typeof enhancer === 'function') return enhancer(createStore)(reducer, preloadedState) // other code }
- applyMiddleWares基本实现:几乎每一行代码都有注释标明做什么,如果还不明白欢迎留言。
// 实现applyMiddleWares function applyMiddleWares(...middleWares) { return createStore => (reducer, preloadedState) => { // 1,使用柯理化缓存的参数createStore与reducer, preloadedState,执行createStore获取原始store const store = createStore(reducer, preloadedState) // 2,中间件组合执行过程中不允许使用dispatch派发动作,所以初始传给所有中间件第一层中的store里面的dispatch初始是个抛错函数, // 防止在组合中间件过程中被使用,只有组合完毕之后,即只有在中间件第三层中,才可以使用原始的dispatch做你想要的增强处理,如果你需要操作原始dispatch的话。 let dispatch = () => { throw new Error('中间件执行过程中不允许使用dispatch派发动作') } // 3,保存一份store中的getState,以及我们上面定义的dispatch,留给所有中间件的第一层函数来缓存 const storeAPI = { getState: store.getState, // 3.1,核心操作:闭包保持storeAPI.dispatch可以catch住步骤2声明的dispatch,在步骤5将强化后的dispatch赋值给步骤2声明的dispatch,所以步骤4中chain中每一个中间件拿到的都是强化后的dispatch(这里面又有curry的操作,使用科里化为中间件缓存参数dispatch),这就是为什么不直接 dispath: dispatch 这么写,而通过函数套一层(闭包妙用) dispatch: (action, ...args) => dispatch(action, ...args) } // 4,执行所有中间件第一层函数缓存第一个参数 store,之后返回的中间件函数将剩下两层函数,即上面中间件中所说的 接受dispacth,返回增强后的dispatch函数的那个增强器函数。 const chain = middleWares.map(middleWares => middleWares(storeAPI)) // 5,通过compose,执行所有中间件的增强器函数(即中间件第二层函数), // 因为js是传值调用,所以整个compose过程就是: // 从最后一个中间件增强器函数(chain数组最后一个)接受原始disptach(store.dispatch)作为参数(这个参数就是中间件第二层接受的那个next参数),执行, // 返回增强后的dispatch交给chain数组中前一位中间件增强器函数作为参数,执行,返回二次增强的dispatch函数再交给前一位中间件增强器函数, // 直到chain数组第一个中间件增强器函数执行完毕,返回被所有中间件加强后的终极disptach函数。 // 这段代码执行完毕之后,store.dispatch是原始的dispatch,而storeAPI.disptach是增强后的dispatch dispatch = compose(...chain)(store.dispatch) // 5,返回我们的storeAPI,其中dispatch被替换成经过所有中间件增强后的终极dispatch return { ...store, dispatch } } } // 当前 compose函数会将 fn1, fn2, fn3 组合成(...args) => fn1(fn2(fn3(...args))) function compose(...fns) { // 1,如果没有传入需要组合的中间件增强函数(即中间件三层函数嵌套中的第二层函数),默认返回一个 接受什么返回什么(不做任何disptach的增强的函数 ,应用到disptach中就是接受dispatch 返回dispatch if (fns.length === 0) return args => args // 2,如果只有一个中间件增强函数,返回该中间件增强函数 if (fns.length === 1) return fns[0] // 3,如果多个中间件增强函数,则进行组合,fn1, fn2, fn3 组合成函数(...args) => fn1(fn2(fn3(...args)))返回,这里的args将会传入原始的dispatch。 return fns.reduce((preFn, nextFn) => (...args) => preFn(nextFn(...args))) }
- 实现中间件之前现看createStore中如何使用中间件,如下代码,enhancer就是我们传入的applyMiddleWares(...middleWares),enhancer以柯理化形式继续接受参数,分别是createStore与reducer, preloadedState,所以我们applyMiddleWares(...middleWares)返回的肯定是个函数,先接受createStore然后返回新的函数接受reducer与preloadedState。类似于这样的代码
-
redux-thunk:redux-thunk是redux的异步解决方案,原始dispatch仅支持接受纯对象(包含type属性)的action,而redux-thunk可以让我们接受带有副作用的action,同时这个action可以是个函数(因为我们的副作用代码终究需要放在函数内执行),当副作用执行完毕再去更新state。所以具体的redux-thunk流程即:redux-thunk在接收到副作用函数action时,不会进入dispatch流程,而是会先将副作用函数执行完毕,同时注入dispatch,这样在副作用执行完毕我们再手动调用注入的dispatch完成redux状态的更新。
-
redux-thunk手写实现:代码不多,不过需要好好理解redux-thunk如何做到副作用函数处理完毕回到dispatch纯对象的过程,注意:使用redux-thunk中间件需要将redux-thunk放在applyMiddleWare首位,即
applyMiddleWare(reduxThunk,...otherMiddleWares)
,否则很有可能导致dispatch多次。因为redux在完成副作用函数会重新调用dispatch(这一次派发的是个纯对象action),放在首部是期望上来先处理副作用函数,完毕之后从依次执行所有中间件,而如果不放在首部,放在尾部,那么会先执行所有中间件,执行到redux-thunk时,redux-thunk先处理副作用函数,处理完毕执行dispatch,导致中间件又重头依次执行一遍。function createReduxThunk(...args){ // 1,下面这一部分是redux-thunk中间件源码,所以最后导出的也是这一部分, // 外层函数createReduxThunk作用是提供向redux-thunk中副作用函数注入数据的机会,有需要就用, // 不需要直接引入reduxThunk,组合中间件applyMiddleware(reduxThunk,...otherMiddlewares)即可 return ({dispatch,getState})=>next=>action=>{ // 1.1,如果action是函数,则将函数执行,并传入增强后的dispatch,getState,以及可能注入的参数,注意这是增强后的dispatch,是从storeAPI中获取,而不是store.dispacth // 这个阶段其实是脱离了dispatch的过程,先将注意力放在副作用函数执行上,执行完毕后, // 在副作用回调中使用注入的增强dispatch来派发原始对象action,重新进入dispatch流程去通过reducer更新redux中state 。 // 所以这个函数一般会走两次,第一次进来action是副作用函数(执行副作用函数),第二次进来是副作用函数执行完毕之后dispacth的纯对象action(执行所有中间件通过reducer完成state更新) if(typeof action === 'function'){ return action(dispatch,getState,...args) } // 1.2,如果不是函数,就单纯的派发action,交给下个中间件增强后的dispatch(即这里的next)处理 return next(action) } } // 2,获取reduxThunk中间件 const reduxThunk = createReduxThunk() // 3,中间件挂上注入参数的创建redux-thunk的函数,如果需要注入参数,就用其重新创建reduxThunk并注入参数。 reduxThunk.withExtraArguments = createReduxThunk // 4,导出redux-thunk中间件 export default reduxThunk
-
-
react中如何使用redux:下面是个简单的小例子,跟着注释顺序看即可
import React from 'react' import ReactDOM from 'react-dom' import { createStore } from 'redux' function reducer(state = 10, action) { switch (action.type) { case 'INCREMENT': return state + 1 case 'DECREMENT': return state - 1 default: return state } } // 1,传入reducer创建我们的store(结构出来dispatch, getState, subscribe ) const { dispatch, getState, subscribe } = createStore(reducer) class Counter extends React.Component { constructor(props) { super(props) this.unSubscribe = null // 2,将store数据与state关联起来 this.state = { num: getState() } } componentDidMount() { // 3,监听store中数据变化,如果发生变化,将变化的数据更新到当前state中,使页面重新渲染 this.unSubscribe = subscribe(() => { this.setState({ num: getState() }) }) } componentWillUnmount() { // 5,组件卸载时,取消对store数据变化订阅 this.unSubscribe && this.unSubscribe() } // 4,使用dispatch更新store数据 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('root'))
4,react-redux使用及connect原理与实现
-
react-redux基本使用:
- 1,创建redux.store,与react.Context
- 2,将Context.Provider包裹整个react应用,并将value设置成store,这样整个react应用内就可以使用store(当然得做相应Context配置才能访问到)
- 3,将我们的组件使用connect绑定,之后即可组件内部从props中获取动作派发与store中的数据
-
connect原理:
- 1,connect本质是一个接受(mapStateToProps与mapDispatchToProps)函数,返回高阶组件,高阶组件接受当前需要使用redux状态的组件进行增强,增强部分包括,通过proprs 传入disptach等动作派发函数与被增强组件需要的状态给被增强组件,最后返回增强后的组件。
-
react-redux使用及connect原理与实现:都在下面代码中,注释很详细。
import React from 'react' import ReactDOM from 'react-dom' import { createStore } from 'redux' // 1,获取Context(解构出Provider, Consumer) const { Provider, Consumer } = React.createContext() // 2,实现一个reducer(没啥说的) 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,创建redux的store(也没啥说的) const store = createStore(reducer, { num: 10 }) // 4,react-redux中的核心connect函数实现,connect本质是一个返回高阶组件的函数,高阶组件本质是一个返回增强组件的函数,connect执行最终需要返回一个组件 function connect(mapStateToProps, mapDispatchToProps) { // 4.1,connect接受mapStateToProps, mapDispatchToProps,返回一个高阶组件函数 return function HOC(Component) { class Wrapper extends React.Component { constructor(props) { super(props) // 4.1.1,将store中的数据使用mapStateToProps处理,获取被增强组件(Component)的数据,放入高阶组件中包裹组件(Wrapper)state中 this.state = mapStateToProps(this.props.store.getState()) } componentDidMount() { // 4.1.2,订阅store中的数据变化,一旦数据变化,更新包裹组件state状态,触发包裹组件及其子组件(Component)重新渲染 this.unSubscribe = store.subscribe(() => this.setState(mapStateToProps(this.props.store.getState()))) } componentWillUnmount() { // 4.1.3,组件卸载时,取消对store数据订阅 this.unSubscribe && this.unSubscribe() } render() { // 4.1.4,目前只处理为函数的mapDispatchToProps,即如果mapDispatchToProps是函数,则将处理成一个个action,通过props形式将这些action交给子组件(Component),这样子组件就可以直接this.props.action进行派发动作 let actions = { dispatch: this.props.store.dispatch } if (typeof mapDispatchToProps === 'function') { actions = mapDispatchToProps(this.props.store.dispatch) } // 4.1.5,将action,store中的数据(保存在包裹组件state中)通过props传参方式交给被增强子组件 return <Component {...this.state}{...actions} /> } } // 4.1.6,返回一个函数组件,函数组件render内容为 Consumer包裹上面处理后的高阶组件,Consumer在这里负责将Context中store交给当前高阶组件。 return () => <Consumer>{value => <Wrapper store={value} />}</Consumer> } } // 5,Counter组件将被应用我们自己实现的connect class Counter extends React.Component { // 5.1,因为我们没有设置mapDispatchToProps,所以我们就直接使用原始dispatch派发动作吧。 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 将高阶组件的state映射到当前绑定组件的props中(react-redux中基本操作) const mapStateToProps = state => { return { num: state.num } } // 7,使用connect处理我们的Counter组件 const ConnectCounter = connect(mapStateToProps)(Counter) // 8,整个react应用最外层使用Context.Provider包裹value传入store(react-redux中基本操作) ReactDOM.render( <Provider value={store}><ConnectCounter /></Provider>, document.getElementById('root') )