解决的问题
- 在组件之间复用状态逻辑很难,可能要用到render props和高阶组件,React 需要为共享状态逻辑提供更好的原生途径,Hook 使你在无需修改组件结构的情况下复用状态逻辑
- 复杂组件变得难以理解,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
注意事项
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
- 只能在 React 的函数组件中调用 Hook。
useState
- useState 就是一个 Hook
- 通过在函数组件里调用它来给组件添加一些内部 state,React 会在重复渲染时保留这个 state
- useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并
- useState 唯一的参数就是初始 state
1
const [state, setState] = useState(initialState);
1】 alert会“捕获”点击按钮时候的状态
1 | import React, {useState} from 'react'; |
点击 alertNumber
按钮一次, 再点击 +
按钮两次,number变为 2, 3秒钟后alert 0
2】 函数式更新
1 | import React, {useState} from 'react'; |
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setNumber。该函数将接收先前的 state,并返回一个更新后的值。
3】惰性初始state
initialState初始状态参数只会在组件初始渲染的时候调用,后续渲染会被忽略
与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果
4】性能优化
Object.is
调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较 state。)
1 | import React, {useState} from 'react'; |
点击 +
按钮会输出 Counter render 组件刷新
点击 old state +
不更改数据,会刷新一次,之后都不会刷新组件
减少渲染次数
useCallback
减少渲染次数
1 | // 情况一 |
添加了useCallback之后
1 | import React, {useState, useCallback} from 'react'; |
第一次是false,点击按钮可以看到后面都是 true
将line27注释打开,这里的number就是依赖的变量,所以这里当number改变的时候,都会重新生成 addClick
示例
1 | // 情况一 |
input框中改变input 的值触发onChange,App
和 Child
都会渲染
点击Child的button,改变number的值,App
和 Child
都会渲染
子组件Child是一个纯函数,希望只有当props改变的时候才会渲染Child组件
1】 memo
1 | import React, {useState, memo} from 'react'; |
2】 useCallback
这里每次App组件渲染的时候都会重新生成addClick,改变如下:
1 | + import React, {useState, useCallback, memo} from 'react'; |
第一次进来的时候 addClick: false,之后改变name的值时 都是true,只有改变 addClick函数缓存依赖的变量number时,才会重新生成,输出true
这里 const data = {number}
每次App函数渲染的时候也都重新生成了
3】useMemo
1 | + import React, {useState, useCallback, useMemo} from 'react'; |
5】注意事项
只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
因为react内部hook是以链表的形式一个一个按顺序存储的,如下情况,一次if判断,存储 ① ② ③,第二次if判断只存储 ① ② ,它是通过前后存储的hook按顺序判断的,少了一个就会出错。
1 | import React, {useState, useEffect} from 'react'; |
useReducer
hooks的作者就是redux的作者
- useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法
- 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等
1 | import React, {useReducer} from 'react'; |
如上,点+
加一,点-
减一
useState是基于useReducer实现的,基于上面的useReducer自己实现useState
原生useState
1 | import React, {useState} from 'react'; |
自定义useState
1 | import React, {useCallback, useReducer} from 'react'; |
useContext
- 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
- 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
- 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
- useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
- useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context
不使用useContext时的用法
1 | import React, {useState, useContext} from 'react'; |
使用useContext
1 | import React, {useState, useContext} from 'react'; |
effect
- 在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性
- 使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道
- useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API
- 该 Hook 接收一个包含命令式、且可能有副作用代码的函数
1 | import React, {useState, useEffect} from 'react'; |
点击+
页面数字变化,title也跟着变化
1】调过effect进行性能优化
- 如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可
- 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行
1 | import React, {useState, useEffect} from 'react'; |
如果没有第二个参数
1 | import React, {useState, useEffect} from 'react'; |
有第二个参数,它是每隔1s加一,没有,就会各种叠加,因为没渲染一次,就会生成一个新的定时器。
2】 清除副作用
- 副作用函数还可以通过返回一个函数来指定如何清除副作用
- 为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除
1 | import React, {useState, useEffect} from 'react'; |
点击hide
按钮后,组件App不显示,页面报错
1 | Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. |
不能在一个已经卸载的组件上执行状态更新。会出现内存泄露。为了修复,需要在useEffect清理函数中取消所有的订阅和异步任务。
1 | import React, {useState, useEffect} from 'react'; |
3】useRef
- useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)
- 返回的 ref 对象在组件的整个生命周期内保持不变
1 | const refContainer = useRef(initialValue); |
1 | import React, {useState, useRef} from 'react'; |
用 React.createRef
时, 每次渲染Child组件都会重新生成 refObj
用 useRef
时,返回的ref对象在组件的整个生命周期内保持不变。
实现useRef
1 | let currentRefObject; |
4】forwardRef
现在想把获取焦点按钮放到App组件中,如下
1 | import React, {useRef} from 'react'; |
页面会报错
1 | Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()? |
使用React.forwardRef
1 | +import React, {useRef} from 'react'; |
直接这样传递的话,Child组件的输入框对象直接暴露给了父组件,也破坏了封装的原则,很危险
useImperativeHandle
- useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值
- 在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用
1 | import React, {useRef, useImperativeHandle} from 'react'; |
如果不用useImperativeHandle
,子组件直接用父组件传递过来的<input ref={ref} />
,当父组件App执行到line22时,就会直接取修改子组件input中的value,现在父组件中getFocus
方法只有line21是生效的,因为它调用的focus方法是子组件有暴露给外部的。
useLayoutEffect
- 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect
- 可以使用它来读取 DOM 布局并同步触发重渲染
- 在浏览器执行绘制之前useLayoutEffect内部的更新计划将被同步刷新
- 尽可能使用标准的 useEffect 以避免阻塞视图更新
如图是浏览器呈现一张页面的过程。
在红色分割线左边界面并没有绘制,绿色圈中是布局,蓝色圈中是绘制,页面上能看到效果。
如图可以看到 useLayoutEffect 和 useEffect 的执行顺序。
1 | import React, {useState, useEffect, useLayoutEffect} from 'react'; |
点击按钮颜色切换时,先alert useLayoutEffect
,界面变色,再console.log出 useEffect
自定义hooks
- 有时候我们会想要在组件之间重用一些状态逻辑
- 自定义 Hook 可以让你在不增加组件的情况下达到同样的目的
- Hook 是一种复用状态逻辑的方式,它不复用 state 本身
- 事实上 Hook 的每次调用都有一个完全独立的 state
- 自定义 Hook 更像是一种约定,而不是一种功能。如果函数的名字以 use 开头,并且调用了其他的 Hook,则就称其为一个自定义 Hook
示例
当有2个逻辑相同的组件时,如下:
1 | import React, {useState, useEffect} from 'react'; |
页面上同时展示 每隔1秒 number + 1
使用自定义hooks(函数的名字以 use 开头,并且调用了其他的 Hook
),如下:
1 | import React, {useState, useEffect} from 'react'; |
这里line8,将number生成了随机数来展示说明,Hook是一种复用状态逻辑的方式,它不复用state本身,事实上Hook的每次调用都有一个完全独立的state
中间件
logger
1 | import React, {useReducer, useEffect} from 'react'; |
thunk
thunk里面派发的是函数
1 | import React, {useReducer, useEffect} from 'react'; |
promise
1 | import React, {useReducer, useEffect} from 'react'; |
ajax
新建api.js
1 | let express = require('express'); |
npm install express
node ./api.js
运行后台
1 | import React, { useState, useEffect, useLayoutEffect } from 'react'; |
小结
useMemo
、useCallback
、useRef
本质上都是为了缓存,这些东西在没有hooks之前,在以前我们都用类组件,一但创建就有类的实例,上面的属性也可以存在。但是现在我们用hooks,hooks只能用在函数组件里,函数组件没有this,就没有实例,就没有办法在实例上挂属性和状态。现在就要靠 useMemo
、useCallback
、useRef
实现缓存。
官方文档
https://zh-hans.reactjs.org/docs/hooks-intro.html
1、React 需要为共享状态逻辑提供更好的原生途径。
2、你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。
相关文章
https://zh-hans.reactjs.org/docs/hooks-intro.html
http://www.zhufengpeixun.cn/2020/html/62.5.react-hooks.html