在类组件中,我们经常在一些生命周期函数里处理一些额外的操作(数据请求、js事件绑定/解绑、DOM操作、样式的修改),我们把这些操作叫做副作用,很多时候这些操作都是重复的。
useEffect 就是用来替换常用的生命周期函数(componentDidMount, ComponentDidUpdate, componentWillUnmount),并把这些重复的操作整合到一起。
useEffect(() => { // DOM更新之后要执行某些操作。 return () => { // 清除副作用 } },deps)useEffect接受两个参数:
effect
是一个匿名函数,这个函数会在DOM 更新之后被执行可以返回一个匿名函数,这个函数叫清除函数,它会在组件卸载前执行(替换componentWillUnmount)。deps
deps是一个可选参数,它是一个数组,数组里项可以是state、props、function,表示这个effect依赖的对象默认情况下: 会在dom每次更新(包括第一次渲染)后调用effect(替换componentDidMount, ComponentDidUpdate)。第二个参数是个空数组: 表示只会在第一次render结束后调用一次effect(替换ComponentDidMount)第二个是非空数组: 表示数组里依赖的某属性变化后就会执行effect,注意这里的变化进行的是引用地址的比较。关于清除函数的执行时机
默认情况下或deps不为空时,如果非首次渲染,它的执行次序是 // setState -> rerender -> dom更新、ui渲染 -> 执行上一次的清除函数 -> 执行effect函数 deps的数组为空:则清除函数会在组件销毁前执行useEffect注意点:
需要保证在effect里使用的state、props都必须存在与deps里。一般不在useEffect的effect函数中执行操作DOM/样式的相关操作:useEffect中定义的函数的执行不会阻碍浏览器更新视图,在浏览器完成布局与绘制之后,会延迟调用effect。 而componentDidMonut和componentDidUpdate中的代码都是同步执行的。useEffect使用Demo
它和 useEffect 的结构相同,区别只是调用时机不同。它的effect函数执行是同步执行的,所以一般操作DOM或修改样式都使用这个hook
Context 是React中用来共享那些对于一个组件树而言是“全局”的数据(主题/语言/用户信息)。它解决的是多级组件之间传参的问题
// 祖先组件 创建一个context对象 const MyContext = React.createContext(defaultValue); // 生成的context对象具有两个组件类对象 { Provider: React.ComponentType<{value: T}>, Consumer: React.ComponentType<{children: (value: T)=> React.ReactNode}> } // 祖先组件 MyContext.Provider <MyContext.Provider value={/* 某个值,可以在我的圈子内共享 */}> <ComponentA /> <ComponentB /> </MyContext.Provider> // 子孙组件 MyContext.Consumer <MyContext.Consumer> {value => /* 基于 context 值进行渲染, 当前的 value 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。*/} </MyContext.Consumer>useContext是基于Context API实现的,它可以帮助我们跨越组件层级直接传递变量,实现共享。
使用useContext就表示当前组件被<MyContext.Consumer>包裹,并且它的返回值就是<MyContext.Provider>上的value属性
const context = useContext(MyContext) // context相当于 <MyContext.Provider>上接受的value属性useContext使用Demo
需要注意的是useContext和redux的作用是不同的,一个解决的是组件之间值传递的问题,一个是应用中统一管理状态的问题,但通过和useReducer的配合使用,可以实现类似Redux的作用。
reducer其实就是一个函数,这个函数接收两个参数,一个是状态state,一个用来控制业务逻辑的判断参数action。
function countReducer(state, action) { switch(action.type) { case 'add': return state + 1; case 'sub': return state - 1; default: return state; } } useReducer的使用useState的替代方案,一般用在state逻辑较复杂且包含多个子值,或者下一个 state依赖于之前的state等场景下。useReducer可以将更新和操作解耦
两种使用方式
// 指定初始值的使用方式 const [state, dispatch] = useReducer(reducer, initState); /** * reducer:reducer函数 * initState:初始值 * **/ // 惰性初始化,初始值需要经过比较复杂的计算时使用 const [state, dispatch] = useReducer(reducer, initialArg, init); /** * reducer:reducer函数 * initialArg:传给init的参数 * init:指定的初始化函数 * **/ /** * state: 返回的状态值 * dispatch: 触发reducer的方法 **/useReducer使用Demo
我们知道实现redux需要满足两点条件
一个全局的状态,并且做统一管理更新这些状态,实现业务逻辑useContext:可访问全局状态,避免一层层的传递状态。
useReducer:通过action的传递,更新复杂逻辑的状态,可以实现类似Redux中的Reducer部分
需要注意的是,useReducer的dispatch操作必须是同步的,如果需要执行异步操作,需要模拟类似react-redux的实现方式。
具体实现
useCallback主要用来解决使用React hooks产生的无用渲染的性能问题。
在class组件中,我们渲染时的性能优化一般可以通过shouldCompnentUpdate函数来进行, 但是在函数组件里,由于它不具备生命周期函数,也就是说我们没有办法通过组件更新前条件来决定组件是否更新。函数组件的每一次调用都会执行内部的所有逻辑,就带来了非常大的性能损耗。useMemo和useCallback都是解决上述性能问题的。
具体实现
当我们每次点击按钮时,看到以下日志:
Rendering Title Rendering age Rendering button 修改年龄 Rendering salary Rendering button 修改工资每次状态改变都触发了所有组件的rerender,然而我们期望是当我们修改年龄时,只有依赖age的那个组件rerender。
不同class组件中可以使用shoudComponentUpdate, PureComponent来做性能优化, React为函数式组件提供了叫React.memo一个高阶组件.
我们可以通过将组件包装在React.memo 中调用,通过这种记忆组件渲染结果的方式来提高组件的性能。这意味着当props没有变化时, React将跳过渲染组件的操作并直接复用最近一次渲染的结果。
const MyComponent = React.memo(function MyComponent(props) { /* 使用 props 渲染 */ });React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState 或 useContext 的 Hook,当 context 发生变化时,它仍会重新渲染。
使用了 React.memo 后,我们看到点击增加年龄的按钮时,日志变为了
// Rendering age // Rendering button 修改年龄 // Rendering button 修改工资依然有不相关的 rerender Rendering button 修改工资出现。说明修改工资这个组件的props发生里变化。
简单分析一下:
点击增加年龄按钮触发setAge()方法age改变导致组件重新渲染,重新执行Counter()方法Counter()内部的方法重新被创建修改工资 Button 传入的 props 发生了变化Button()重新render因此这个 Button 传入的 props 发生了变化,这时候React.memo没有阻止 rerender。而我们的useCallback这个`hook就是为了解决这个问题。
在js中,当函数执行时,会创建一个被称为执行环境的对象,这个对象在每次函数执行时都是不同的,当多次执行该函数时会创建多个执行环境。这个执行环境会在函数执行完毕后销毁。所以每次rerender时都会创建新的执行环境,并为其内部的方法重新分配空间
返回一个memoized回调函数。 把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
在上述例子中
const incrementAge = useCallback( () => { setAge(age + 1) }, [age], ) // Rendering salary // Rendering button 修改工资useMemo和useCallback类似,都是用来做性能优化的。
useMemo:缓存的是值useCallback: 缓存的是函数先来看一个例子
import React, { useState } from 'react' function Counter() { const [counterOne, setCounterOne] = useState(0) const [counterTwo, setCounterTwo] = useState(0) const incrementOne = () => { setCounterOne(counterOne + 1) } const incrementTwo = () => { setCounterTwo(counterTwo + 1) } const isEven = () => { let i = 0 while (i < 1000000000) i += 1 return counterOne % 2 === 0 } return ( <div> <button onClick={incrementOne} >Count One = {counterOne}</button> <span> { isEven() ? 'even' : 'odd' } </span> <br /> <button onClick={incrementTwo} >Count Two = {counterTwo}</button> </div> ) } export default Counter具体实现
点击第一个按钮有较长的延迟,因为我们的判断偶数的逻辑中包含了大量的计算逻辑。但是,我们点击第二个按钮,也有较长的延迟!
这是因为,每次 state 更新时,组件会 rerender,isEven 会被执行,这就是我们点击第二个按钮时,也会卡的原因。我们需要优化,告诉 React 不要有不必要的计算,特别是这种计算量复杂的。
这时就需要 useMemo hook 登场了。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);返回一个 memoized 值。 把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
传入 useMemo 的函数会在渲染期间执行。不要在这个函数内部执行与渲染无关的操作。
如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传递的参数(initialValue)。返回的对象将存留在整个组件的生命周期中。
useRef一般有两种用途
获取DOM节点,这一点和class组件中的ref类似。用来保存变量自定义hook
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
通过自定义 Hook,可以将组件中重复的逻辑提取到可重用的函数中。
function Example () { const [count, setCount] = useState(0); const prevCount = usePrevious(count) console.log(prevCount, count, '之前的状态和现在的状态') return ( <div> <div>{count}</div> <button onClick={() => {setCount(count+1)}}>+</button> </div> ) } function usePrevious (value) { const ref = useRef() useEffect(() => { ref.current = value }) return ref.current }react会在每次rerender时捕获自己独立的state、props、effects、事件处理函数
函数每次执行时会形成新的执行环境,这个对象上存在一个[[Scope]]的属性,它指向到是它所在环境的作用域链.之后会生成一个活动对象(AO),这个AO上保存这当前函数到变量、参数、方法,并且会将这个AO对象放在[[Scope]]的最顶端。一般来说,这个AO对象会在函数执行完成时随执行环境清除而清除。
但是,当我们在函数内部返回一个函数并在其外部被一个变量接收时,这个变量(返回的函数)的作用域链指向的是它所处环境的的作用域链,只要这个函数存在则它的作用域链就会一直存在,这样它的作用域链上的变量得不到释放,即能在函数外部访问作用域内部的变量,这样就形成里闭包
形成闭包最简单的方式就是在函数内部返回另一个函数。
function a() { var b = 2; function c() { var d = 4; console.log(b) } return c } var d = a() // a的[[scope]]指向全局环境,并生成自己的AO,放在[[scope]]的最顶端 d() // 2 c的[[scope]]指向a的[[scope]],并生成自己的AO,放在[[scope]]的最顶端而在函数组件内部,正是因为js闭包机制,所以才有了Capture Value属性
在线案例
let _state = null; function useState(initialValue) { const state = _state | initialValue; function setState(newState) { _state = newState; // 会重新执行组件函数 // render(); } return [state, setState]; } function Component() { const [count, setCount] = useState(0); const handleAlertClick = () => { setTimeout(() => { console.log("You clicked on: " + count); }, 3000); }; // 暴露页面上可以执行的函数 return [handleAlertClick, setCount]; } // 首次 render count = 0 const [handleAlertClick, setCount] = Component(); // 点击按钮,形成闭包,此时闭包[[Scope]]上的count=0,执行setCount,_count 变为1, // 此时会重新执行Component(),生成新的执行环境,并返回新的 handleClick,setCount, setCount(count + 1); setCount(count + 1); // count = 1 _count=2 setCount(count + 1); // count = 2 _count=3 // 模拟点击showAlert, handleAlertClick(); // count = 3 3s后alert的是3 setCount(count + 1); // count = 3 _count=4 setCount(count + 1); // count = 4 _count=5每一次渲染都有它自己的 Props、State and Effects,每一个组件内的函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获某次渲染中定义的props和state,并且在这次渲染中它的state是固定不变的。也就是说他们都有Capture Value属性,这是函数组件区别与class组件到的特性之一。
由于Capture Value的存在,我们在class组件中有些比较合理的想法,在函数组件中使用似乎就会有点问题
考虑这么一个需求: 定义一个count,让这个count每秒加一,并且显示在页面上。
按照我们class组件的想法,在componentDidMount里,定义一个计时器setInterval, 并且在componentWillUnmount里清除计时器
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, []); // 传空数组只执行一次 return <h1>{count}</h1>; }这看起来似乎没什么问题,但是由于 useEffect 符合 Capture Value 的特性,拿到的 count 值永远是初始化的 0。相当于 setInterval 永远在 首次render的Scope 中执行,你后续的 setCount 操作并不会产生任何作用。
这显然和我们的需求不符,于是我们在deps里添加一个属性count,告诉react当count变化后再执行
useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, [count]);这种方式满足了了我们的需求,但是我们的诚实也带来了一定的代价
计时器不准了,因为每次 count 变化时都会销毁并重新计时。频繁 生成/销毁 定时器带来了一定性能负担。setState有一种回调函数式的调用方式setState((preState) => preState + 1)
useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); }, 1000); return () => clearInterval(id); }, []);某一天,我们改变了需求,希望显示在页面上的值,依赖两个数据的变化
useEffect(() => { const id = setInterval(() => { setCount(c => c + step); }, 1000); return () => clearInterval(id); }, [step]);我们会发现不得不依赖step这个变量,那有没有什么办法能将更新和动作解耦呢?
更新变成了dispatch({ type: "tick" }) 所以不管更新时需要依赖多少变量,在调用更新的动作里都不需要依赖任何变量。 具体更新操作在 reducer 函数里写就可以了。
React Redux 从 v7.1.0 开始支持hook的api
React Router 从 v5.1 开始支持 hook
Mobx + Hooks
umi Hooks
react-use
useHooks
