Jsx的本质是什么
Jsx是语法糖,无法直接被浏览器解析,需要转换为js,通过babel 创建tagNode,createElement
// 自定义组件jsx代码
return (
<div>
<Input addTitle={this.addTitle.bind(this)} />
<List data={this.state.list} />
</div>
);
// 解析结果
return React.creatElement('div', null,
React.createElement(Input, { addTitle: this.addTitle.bind(this) }),
React.createElement(List, { data: this.state.list })
)
可以看到React.createElement传的第一个参数不是字符串形式的了,而是一个函数类型,其实就是构造函数。
解析得出:
-
div 直接渲染即可,vdom可以做到
-
Input和List,是自定义组件(class),vdom默认不认识
-
因此Input和List定义的时候必须声明 render函数
-
根据props初始化实例,然后执行实例的render函数
-
render函数返回的还是vnode对象 最后替换node,使用了React.createElement()方法进行的jsx转换
-
初次渲染 - ReactDOM.render(, container) :会触发patch(container, vnode)
-
re-render - setState:会触发patch(vnode, newVnode)
-
为何需要vdom: JSX需要渲染成html, 数据驱动视图
-
React.createElement和h,都生成vnode
-
何时patch: ReactDOM.render(…)和setState
-
自定义组件的解析:初始化实例,然后执行render
范式化
多数情况下我们的应用是要配合 Redux
或者 MobX
使用的。拿 Redux
举例,Redux store
的组织是一门大学问,Redux
官方推荐将 store
组织得扁平化和范式化,所谓扁平化,就是整个 store
的嵌套关系不要太深,实体之下不再挂载实体,扁平化带来的好处是:
当某些数据需要在不同的地方出现时,就会存在必然重复。例如,可能存在很多 state
部分都要存储同一份“用户评论列表”,这样需要花费很多心思去保障多处“用户评论列表”数据状态一致,否则就会造成页面数据不同步的 Bug;
嵌套深层的数据结构,会直接造成你 reducers
编写复杂。比如,你想更新一个很深层次的数据片段,很容易代码就变得丑陋。
造成负面的性能影响。即便你使用了类似 immutable.js
这样的不可变数据类库,最大限度的想保障深层数据带来的性能压力,那你是否知道 immutable.js
采用的 'Persistent data structure'
思路,更新节点会造成同一条链儿上的祖先节点的更新。更恐怖的是,也许这些都会关联到众多 React
组件的 re-render
;
范式化是指尽量去除数据的冗余,因为这样会给维护数据的一致性带来困难,就像官方推荐 state
记录尽可能少的数据,不应该存放计算得到的数据和 props
的副本,而是将他们直接在 render
中使用,这也是避免了维护数据一致性的困难,并且避免了相同数据满天飞不知道源头数据是哪个的尴尬。
state VS store
首先要明确的是,不要将所有的状态全部放在store中,其实再延伸一下可以延伸出render() {}中的变量,也就是store VS state VS render,store中应该存放异步获取的数据或者多个组件需要访问的数据等等,redux官方文档中也有写什么数据应该放入store中。
- 应用中的其他部分需要用到这部分数据吗?
- 是否需要根据这部分原始数据创建衍生数据?
- 这部分相同的数据是否用于驱动多个组件?
- 是否需要能够将数据恢复到某个特定的时间点?
- 是否需要缓存数据?
而 store
中不应该保存 UI 的状态(除非符合上面的某一点,比如回退时页面的滚动位置)。UI 的状态应该被限定在 UI 的 state
中,随着组件的卸载而销毁。而 state
也应该用最少的数据表示尽可能多的信息。在 render
函数中,根据 state
去衍生其他的信息而不是将这样冗余的信息都存在 state
中。store
和 state
都应该尽可能的做到熵最小,具体的可以看 redux store
取代react state
合理吗?。而 render
中的变量应该尽可以去承担一个衍生数据的责任,这个过程是无副作用的,可以减少在 state
中产生冗余数据的情况。
React常见优化
1. 重新渲染 / 多组件优化:
存在维度的划分:层级,class组件和函数组件,节点多寡
- 节点少:class : scu–>purecomponent
- 节点少:函数:memo
- 节点多:immutable+scu/memo
对于react的性能优化还是有必要的,不能因为父组件渲染而子组件没有变化也得跟着渲染从而产生不必要的花销。解决的原理就是通过props和state的浅对比来判断子组件是否渲染,具体操作是在class组件下通过生命周期的scu来判断,也可以通过继承purecomponent来减少每次重复写法;在函数组件下通过memo来判断;在大量节点的情况下就考虑用immutable配合了。
浅对比:对象的对比他们的内存地址,只要内存地址一致,就不重新渲染,反之,对象的内存地址不一致,就渲染
immutable:每次操作都会产生一个新的对象出来,由于它会复用之前数据的数据结构,所以产生新的数据也很快,ImmutableJS提供了不可变的数据,即要让数据改变只能通过创建新数据的方式,而不能直接修改,这很大程度的降低了前后两个数据比较时的复杂度。
2. 传参优化
切记将props/state
以展开形式传递给子组件,除非子组件需要用到父组件的所有props/state
。
3. Key
对于数组形式的数据,遍历时React
会要求你为每一个数据值添加Key
,而Key
必须时独一无二的,在选取Key
值时尽量不要用索引号,因为如果当数据的添加方式不是顺序添加,而是以其他方式(逆序,随机等),会导致每一次添加数据,每一个数据值的索引号都不一样,这就导致了Key
的变化,而当Key
变化时,React
就会认为这与之前的数据值不相同,会多次执行渲染,会造成大量的性能浪费。所以只在万不得已时,才将数据的索引号当做Key
。
4. 按需加载
使用React.Lazy
和React.Suspense
做分片打包,实现组件的按需加载,大大提高页面速度
5. 调整CSS而不是强制组件加载和卸载
尽量的减少组件的创建和销毁,这样对于性能还是有一定的损耗的,我们可以将组件隐藏掉,比如加hidden
属性,控制css
的display
、opacity
、visibility
等等隐式隐藏的方法。
diff算法的key值是如何比较的
* MOVE_EXISTING -- 存在相同的节点则复用以前的 DOM 节点,做移动操作。
* INSERT_MARKUP -- 新的节点不在旧集合里则插入新的节点。
* REMOVE_NODE -- 新集合里在旧集合中对应的 node 不同,不能直接复用和更新,需要执行删除操作,或者旧集合中的节点不在新集合里的。
* 遍历 newChildrens,基于 key 判断 newChild 是否在 oldChildrens 存在相同的节点。 如果存在相同节点(prevChild === nextChild) ,先判断原先节点的变化顺序(不考虑头部新插入的节点),节点的挂载顺序变大(从前往后),移动节点 。节点的挂载顺序变小(从后往前或不变),不做操作 。节点的 _mountIndex 变为新集合中的 index 。如果不存在相同节点,之前存在相同,在上一个新集合中的节点后插入新节点
* 遍历 oldChildrens,移除在新集合中不存在的节点
React的渲染机制
React
在内部维护了一套虚拟 DOM(VDOM)
,在内部维护着一颗 VDOM
树,这颗 VDOM
树映射到浏览器真实的 DOM
树,React
通过更新 VDOM
树来对真实 DOM
更新,VDOM
是 plain object
所以很明显操作 VDOM
的开销要比操作真实 DOM
快得多,再加上 React
内部的 reconciler
(调节器,这个模块用于发起顶层组件或者子组件的挂载渲染重绘),React
会在 reconsilation
(重新编译)之后最小化的进行 VDOM
的更新,再 patch
(修复)到真实 DOM
上最终完成用户看得到的更新。
React的错误处理机制
错误边界介绍
部分 UI 中的 JavaScript 错误不应该破坏整个应用程序。 为了解决 React 用户的这个问题,React 16引入了一个 “错误边界(Error Boundaries)” 的新概念。
错误边界是 React 组件,它可以在子组件树的任何位置捕获 JavaScript 错误,记录这些错误,并显示一个备用 UI ,而不是使整个组件树崩溃。 错误边界(Error Boundaries) 在渲染,生命周期方法以及整个组件树下的构造函数中捕获错误。
使用方法
如果一个类组件定义了生命周期方法中的任何一个(或两个)static getDerivedStateFromError() 或 componentDidCatch(),那么它就成了一个错误边界。 使用static getDerivedStateFromError()在抛出错误后渲染回退UI。 使用 componentDidCatch() 来记录错误信息。
捕获范围
组件内异常主要包括:
- 渲染过程中异常;
- 生命周期方法中的异常;
- 子组件树中各组件的constructor构造函数中的异常;
- 事件处理器中的异常;(使用try/catch进行捕获)
- 异步任务的异常,如setTimeout,ajax请求异常等;(使用全局事件window.addEventListener捕获)
- 服务端渲染异常;
- 异常边界组件自身内的异常;(将边界组件和业务组件分离,各司其职,不能在边界组件中处理逻辑代码,也不能在业务组件中使用didcatch
错误边界尽可以捕获其子组件的错误,无法捕获其自身的错误;如果一个错误边界无法渲染错误信息,则错误会向上冒泡至最接近的错误边界。这也类似于 JavaScript 中 catch {} 的工作机制)
如何放置错误边界
错误边界的粒度完全取决于你的应用。你可以将其包装在最顶层的路由组件并为用户展示一个 “发生异常(Something went wrong)“的错误信息,就像服务端框架通常处理崩溃一样。你也可以将单独的插件包装在错误边界内部以保护应用不受该组件崩溃的影响。
借鉴Facebook的message项目,他们应用错误边界的方式是将大的模块应用错误边界包裹,这样当一个主要模块因为意外的错误崩溃后,其它组件仍然能够正常交互
错误边界示例
// 首先我定义了一个高阶组件
import React from 'react'
const ErrorBoundary = errorInfo => WrapComponent => {
return class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false };
}
// 这个静态方法和componentDidCatch方法定义一个即可
static getDerivedStateFromError(error) {
// 当发生错误时,设置hasError为true,然后展示自己的错误提示组件
return {
hasError: true };
}
componentDidCatch(error, info) {
// 这里可以将报错信息上报给自己的服务
// logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
return {
errorInfo }
;
}
return;
}
}
} export default ErrorBoundary
// 接下来可以使用边界组件包裹业务组件,这里列举我认为react项目中可以处理的错误方式,例如事件处理器的错误,异步错误,promise错误,渲染错误等
import React from 'react'
import ErrorBoundary from '../../utils/ErrorBoundary'
@ErrorBoundary('i am not ok')
export default class Error extends React.Component {
constructor() {
super()
}
componentWillMount() {
window.addEventListener('error', event => {
console.log(event)
}, true)
window.addEventListener('unhandledrejection', event => {
console.log(event)
})
}
// 这个异步错误 ErrorBoundary组件不会捕获到 但是在入口写的全局window.onerror事件捕获到了
componentDidMount() {
setTimeout(() => {
// console.log(b)
}, 100)
}
// 事件处理器中的错误 onerror也可以捕获到
// 这里如果想要hold住错误 需要使用try catch
handleEventError = () => {
console.log(error)
}
// promise 如果reject 但是没有写catch语句的话 会报错
// 但是onerror和try-catch和ErrorBoundary组件都无法捕获
// 需要写一个全局unhandledrejection 事件捕获
handlePromiseError = () => {
const promise = new Promise((resolve, reject) => {
reject()
})
promise.then()
}
render() {
return
} }
受控组件和非受控组件
受控以及非受控组件的边界划分取决于当前组件对于子组件值的变更是否拥有控制权。如若有则该子组件是当前组件的受控组件; 如若没有则该子组件是当前组件的非受控组件。
受控组件: 在HTML的表单元素中,它们通常自己维护一套state
,并随着用户的输入自己进行UI上的更新,这种行为是不被我们程序所管控的。而如果将React
里的state
属性和表单元素的值建立依赖关系,再通过onChange
事件与setState()
结合更新state
属性,就能达到控制用户输入过程中表单发生的操作。被React
以这种方式控制取值的表单输入元素就叫做受控组件。
非受控组件:要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以使用ref
来从DOM
节点中获取表单数据。
React Patch
React
构建虚拟标签,执行组件的生命周期,更新state
,计算diff
等,这一系列的操作都是在virtualDOM
中执行的,此时浏览器并未显示出更新的数据。React Patch
实现了最后这关键的一步,将tree diff
算法计算出来的差异队列更新到真实的DOM
节点上,最终让浏览器能够渲染出更新的数据。
Patch
主要是通过遍历差异队列实现的,遍历差异队列时,通过更新类型进行相应的插入、移动和移除等操作。
React
并不是计算出一个差异就执行一次patch
,而是计算出全部的差异并放入差异队列后,再一次性的去执行Patch
方法完成真实的DOM
更新。
React Reconciliation
当你使用 React
,在任何一个单点时刻你可以认为 render()
函数的作用是创建 React
元素树。在下一个 state
或props
更新时,render()
函数将会返回一个不同的 React
元素树。接下来 React
将会找出如何高效地更新 UI
来匹配最近时刻的 React
元素树。
目前存在大量通用的方法能够以最少的操作步骤将一个树转化成另外一棵树。然而,这个算法是复杂度为O(n3)
,其中n
为树中元素的个数。
如果你在 React
中展示 1000 个元素,那么每次更新都需要10亿次的比较,这样的代价过于昂贵。然而,React
基于以下两个假设实现了时间复杂度为 O(n) 的算法:
1. 不同类型的两个元素将会产生不同的树。
2. 开发人员可以使用一个 key prop 来指示在不同的渲染中那个那些元素可以保持稳定。
处理过程:
当执行 setState()
或首次 render()
时,进入工作循环,循环体中处理的单元为 Fiber Node
, 即是拆分任务的最小单位,从根节点开始,自顶向下逐节点构造 workInProgress tree
(构建中的新 Fiber Tree
)
Fiber 之前架构卡顿的原因
React
中调用 render()
和 setState()
方法进行渲染和更新时,主要包含两个阶段:
- 调度阶段(Reconciler):
Fiber
之前的reconciler
(被称为Stack reconciler
)是自顶向下的递归算法,遍历新数据生成新的Virtual DOM
,通过Diff
算法,找出需要更新的元素,放到更新队列中去。 - 渲染阶段(Renderer): 根据所在的渲染环境,遍历更新队列,调用渲染宿主环境的
API
, 将对应元素更新渲染。在浏览器中,就是更新对应的DOM
元素,除浏览器外,渲染环境还可以是Native
、WebGL
等等。
Fiber
之前的调度策略Stack Reconciler
,这个策略像函数调用栈一样,递归遍历所有的Virtual DOM
节点,进行Diff
,一旦开始无法中断,要等整棵Virtual DOM
树计算完成之后,才将任务出栈释放主线程。而浏览器中的渲染引擎是单线程的,除了网络操作,几乎所有的操作都在这个单线程中执行,此时如果主线程上用户交互、动画等周期性任务无法立即得到处理,影响体验。
React事件绑定原理
react
中的事件都是合成事件,不是把每一个dom
的事件绑定在dom
上,而是把事件统一绑定到document
中,触发时通过事件冒泡到document
进行触发合成事件,因为是合成事件,所以我们无法去使用e.stopPropagation
去阻止,而是使用e.preventDefault
去阻止。
- 事件注册:组件更新或者装载时,在给
dom
增加合成事件时,需要将增加的target
传入到document
进行判断,给document
注册原生事件回调为dispatchEvent
(统一的事件分发机制)。 - 事件存储:
EventPluginHub
负责管理React
合成事件的callback
,它将callback
存储到listennerBank
中,另外还存储了负责合成事件的Plugin
,Event
存储到listennerbank
中,每一个元素在listennerBank
中会有唯一的key
。 - 事件触发执行:点击时冒泡到
docunment
中,触发注册原生事件的回调dispatchEvent
,获取到触发这个事件的最深层元素,事件执行利用react
的批处理机制。
<div onClick={
this.parentClick} ref={
ref => this.parent = ref}>
<div onClick={
this.childClick} ref={
ref => this.child = ref}>
button
</div>
</div>
点击button后
- 首先获取到
this.child
- 遍历此元素的所有父元素,依次对每一级元素进行处理
- 构成合成事件
- 将每一级的合成事件存储在
eventQueen
事件队列中 - 遍历,是否组织冒泡,是则停止,否则继续
- 释放已经完成的事件
- 合成事件:循环所有类型的
eventPlugin
,对应每个事件类型,生成不同的事件池,如果是空,则生成新的,有则用之前的,根据唯一key
获取到指定的回调函数,再返回带有参数的回调函数。 - 流程:组件装载/更新 – 新增/删除事件 –
eventplugin
添加到ListennerBank
中监听事件 – 触发事件 – 生成合成事件 – 通过唯一key
获取到指定函数 – 执行指定回调函数 – 执行完毕后释放
React Route懒加载
React
利用React.lazy
与import()
实现了渲染时的动态加载 ,并利用Suspense
来处理异步加载资源时页面显示的问题
React.lazy 原理
对于最初 React.lazy()
所返回的 LazyComponent
对象,其 _status
默认是 -1,所以 首次渲染 时,会进入 readLazyComponentType
函数中的 default
的逻辑,这里才会真正异步执行 import(url)
操作,由于并未等待,随后会检查模块是否 Resolved
,如果已经Resolved
了(已经加载完毕)则直接返回 moduleObject.default
(动态加载的模块的默认导出),否则将通过 throw
将 thenable
抛出到上层。
import()原理
function import(url) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
const tempGlobal = "__tempModuleLoadingVariable" + Math.random().toString(32).substring(2);
script.type = "module";
script.textContent = `import * as m from "${
url}"; window.${
tempGlobal} = m;`;
script.onload = () => {
resolve(window[tempGlobal]);
delete window[tempGlobal];
script.remove();
};
script.onerror = () => {
reject(new Error("Failed to load module script with URL " + url));
delete window[tempGlobal];
script.remove();
};
document.documentElement.appendChild(script);
}); }
React.Suspense原理
React
捕获到异常之后,会判断异常是不是一个 thenable
,如果是则会找到 SuspenseComponent
,如果 thenable
处于 pending
状态,则会将其 children
都渲染成 fallback
的值,一旦 thenable
被 resolve
则 SuspenseComponent
的子组件会重新渲染一次。
Hooks
useCallback和useDemo
useMemo 和 useCallback 接收的参数都是一样,第一个参数为回调 第二个参数为要依赖的数据
两者区别:
useMemo
计算结果是return
回来的值, 主要用于缓存计算结果的值 ,应用场景大多是需要计算的状态useCallback
计算结果是函数, 主要用于缓存函数,应用场景如:需要缓存的函数,因为函数式组件每次任何一个state
的变化整个组件都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。
注意: 不要滥用,会造成性能浪费。react
中减少render
就能提高性能,所以这个仅仅只针对缓存能减少重复渲染时使用和缓存计算结果。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
// 根据官网文档的介绍我们可理解:在a和b的变量值不变的情况下,memoizedCallback的引用不变。即:useCallback的第一个入参函数会被缓存,从而达到渲染性能优化的目的。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// 根据官方文档的介绍我们可理解:在a和b的变量值不变的情况下,memoizedValue的值不变。即:useMemo函数的第一个入参函数不会被执行,从而达到节省计算量的目的。
hooks带来了什么
- 用于在函数组件中引入状态管理和生命周期方法
- 取代高阶组件和
render props
来实现抽象和可重用性
hooks的实现原理
核心是将useState
,useEffect
按照调用的顺序放入memoizedState
中,每次更新时,按照顺序进行取值和判断逻辑,我们根据调用hook
顺序,将hook
依次存入数组memoizedState
中,每次存入时都是将当前的currentcursor
作为数组的下标,将其传入的值作为数组的值,然后在累加currentcursor
,所以hook
的状态值都被存入数组中memoizedState
。
先将旧数组memoizedState
中对应的值取出来重新复值,从而生成新数组memoizedState
。对于是否执行useEffect
通过判断其第二个参数是否发生变化而决定的。
这里我们就知道了为啥不要在循环,条件或嵌套函数中调用 Hook
, 确保总是在你的 React
函数的最顶层调用他们。因为我们是根据调用hook的顺序依次将值存入数组中,如果在判断逻辑循环嵌套中,就有可能导致更新时不能获取到对应的值,从而导致取值混乱。同时useEffect
第二个参数是数组,也是因为它就是以数组的形式存入的。
hooks闭包
useEffect
、useMemo
、useCallback
都是自带闭包的。每一次组件的渲染,它们都会捕获当前组件函数上下文中的状态(state, props)
,所以每一次这三种hooks
的执行,反映的也都是当前的状态,你无法使用它们来捕获上一次的状态。
hooks体验
在日常业务开发中,hooks
带来的收益是明显大于使用成本的。
hooks
对TypeScript
支持更加友好。例如useState
对应的状态类型可以被自动推断出来,useEffect
不需要写类型声明(class
写法下几个生命周期函数的参数签名写起来是相当繁琐的),hooks
能帮我们省去大量手动声明类型的操作。同时hooks
让重构更加简单,使用hooks
的代码中不会出现this
,组件的props
或是state
在组件代码中都是普通的变量,减少了重构的成本,例如原先 重命名this.state
中某个字段 ,在hooks
下就成了重命名某个变量。hooks
不再要求状态必须在this.state
中声明,生命周期写在componentDidMount
/componentDidUpdate
中。useState
/useEffect
的写法更加自由,我们可以按照页面功能/特性来组织hooks
的书写位置。- 自定义
hooks
提供了更加灵活的逻辑复用机制。自定义hooks
带来了更多的可能性,当组件/页面变得更为复杂时,hooks
会比class
写法带来更大的心智负担的,stale closure
、过多的re-render
、useEffect
依赖膨胀、缺少getDerivedStateFromProps
…… ,这个时候就不能像原来那样自由发挥hooks
了,还是老老实实用回class
写法了。 hook
的useEffect
本质上就是为了模糊生命周期以及渲染周期的概念的,只要render
函数中不存在副作用、消耗较小(适当的memo
),多渲染几次其实没什么问题的。这里唯一值得注意的是,render
中千万要小心副作用,如果不用useEffect
基本上都会产生问题。
useEffect和useLayoutEffect的区别
useEffect
里面的操作需要处理DOM
,并且会改变页面的样式,就需要用这个,否则可能会出现出现闪屏问题, useLayoutEffect
里面的callback
函数会在DOM
更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制。
执行时机:
useLayoutEffect
和平常写的ClassComponent
的componentDidMount
和componentDidUpdate
同时执行。useEffect
会在本次更新完成后,也就是第1点的方法执行完成后,在开启一次任务调度,在下次任务调度中执行useEffect
。
hooks模拟全部生命周期
// constructor
function Example() {
const [count, setCount] = useState(0);
return null;
}
//getDerivedStateFromProps
// 这里注意到其实这个state并不是真实的state,而是一个跟props相关的对象
const useGetDeriveStateFromProps = (state, props, handle) => {
const cacheState = useRef(state);
const newState = handle(cacheState.current, props);
if (newState) {
cacheState.current = newState;
}
return cacheState.current;
};
// 使用
const Component = props => {
const state = useGetDeriveStateFromProps({
x: 1 }, props, (state, props) => {
console.log('new getDerivedStateFromProps')
if (props.add) {
state.x += 1;
return state;
}
return null;
});
return <div>{
state.x}</div>;
};
// componentDidMount
function Example() {
useEffect(() => console.log('mounted'), [])
}
// shouldComponentUpdate
// React.memo 包裹一个组件来对它的 props 进行浅比较,
// 但这不是一个 hooks,因为它的写法和 hooks 不同,
// 其实React.memo 等效于 PureComponent,但它只比较 props。
const MyComponent = React.memo(_MyComponent, (prevProps, nextProps) => nextProps.count !== prevProps.count)
// getSnapshotBeforeUpdate
// 在最近一次渲染输出(提交到 DOM 节点)之前调用。 它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。 此生命周期的任何返回值将作为参数传递给 componentDidUpdate
// 这里有点难解决,可以考虑讲整个生命周期抽象出来
// componentDidUpdate
useEffect(() => console.log('mounted or updated'));
// 值得注意的是,这里的回调函数会在每次渲染后调用,因此不仅可以访问 componentDidUpdate,还可以访问componentDidMount,如果只想模拟 componentDidUpdate,我们可以这样来实现。
const mounted = useRef();
useEffect(() => {
if (!mounted.current) {
mounted.current = true;
} else {
console.log('I am didUpdate')
}
});
// useRef 在组件中创建“实例变量”。它作为一个标志来指示组件是否处于挂载或更新阶段。当组件更新完成后在会执行 else 里面的内容,以此来单独模拟 componentDidUpdate。
// componentWillUnmount
useEffect(() => {
return () => {
console.log('will unmount');
}
}, []);
useReducer
const [state, dispatch] = useReducer(reducer, initState);
// useReducer接收两个参数:
// 第一个参数:reducer函数。
// 第二个参数:初始化的state。返回值为最新的state和dispatch函数(用来触发reducer函数,计算对应的state)。按照官方的说法:对于复杂的state操作逻辑,嵌套的state的对象,推荐使用useReducer。听起来比较抽象,我们先看一个简单的例子:
// 官方 useReducer Demo
// 第一个参数:应用的初始化
const initialState = {
count: 0 };
// 第二个参数:state的reducer处理函数
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {
count: state.count + 1 };
case 'decrement':
return {
count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
// 返回值:最新的state和dispatch函数
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
{
/* useReducer会根据dispatch的action,返回最终的state,并触发rerender */}
Count: {
state.count}
{
/* dispatch 用来接收一个 action参数「reducer中的action」,用来触发reducer函数,更新最新的状态 */}
<button onClick={
() => dispatch({
type: 'increment' })}>+</button>
<button onClick={
() => dispatch({
type: 'decrement' })}>-</button>
</>
); }
使用reducer的场景
- 如果你的
state
是一个数组或者对象 - 如果你的
state
变化很复杂,经常一个操作需要修改很多state
- 如果你希望构建自动化测试用例来保证程序的稳定性
- 如果你需要在深层子组件里面去修改一些状态
- 如果你用应用程序比较大,希望
UI
和业务能够分开维护
useRef
1. 原理
const useRef = (initialValue) => {
const [ref] = useState({
current: initialValue });
return ref
}
2. 使用
- 每次渲染
useRef
返回值都不变; ref.current
发生变化并不会造成re-render
;ref.current
发生变化应该作为Side Effect
(因为它会影响下次渲染),所以不应该在render
阶段更新current
属性;- 不可以在
render
里更新ref.current
值,在异步渲染里render
阶段可能会多次执行。 - 可以在
render
里更新ref.current
值,只要保证每次render
不会造成意外效果,都可以在render
阶段更新ref.current
。但最好别这样,容易造成问题,useRef
懒初始化毕竟是个特殊的例外; ref.current
不可以作为其他hooks
(useMemo
,useCallback
,useEffect
)依赖项;ref
作为其他hooks
(useMemo
,useCallback
,useEffect
)依赖项;
3. 动机
- 函数组件访问DOM元素;
- 函数组件访问之前渲染变量。
函数组件每次渲染都会被执行,函数内部的局部变量一般会重新创建,利用useRef可以访问上次渲染的变量,类似类组件的实例变量效果。