前言 react学习笔记第四篇,这篇介绍一下hook
为什么要有Hook 在React Hook出现之前的版本中,组件主要分为两种:函数式组件和类组件。其中,函数式组件通常只考虑负责UI的渲染,没有自身的状态也没有业务逻辑代码,是一个纯函数。而类组件则不同,类组件有自己的内部状态,界面的显示结果通常由props 和 state 决定,因此它也不再那么纯净。相比于函数式组件,类组件有如下一些缺点:
状态逻辑难以复用。在类组件中,为了重用某些状态逻辑,社区提出了render props 或者 hoc 等方案,但是这些方案对组件的侵入性太强,并且组件嵌套还容易造成嵌套地狱的问题。
滥用组件状态。大多数开发者在编写组件时,不管这个组件有没有内部状态,会不会执行生命周期函数,都会将组件编写成类组件,这造成不必要的性能开销。
额外的任务处理。使用类组件开发应用时,需要开发者额外去关注 this 、事件监听器的添加和移除等等问题。
在函数式组件大行其道的当前,类组件正在逐渐被淘汰。不过,函数式组件也并非毫无缺点,在之前的写法中,想要管理函数式组件状态共享就是比较麻烦的问题。例如,下面这个函数组件就是一个纯函数,它的输出只由参数props决定,不受其他任何因素影响。
1 2 3 4 5 6 7 8 function App (props ) { const {name, age } = props.info return ( <div style={{ height : '100%' }}> <h1>Hello,i am ({name}),and i am ({age}) old</h1> </div> ) }
在上面的函数式组件中,一旦我们需要给组件加状态,那就只能将组件重写为类组件,因为函数组件没有实例,没有生命周期
为了解决函数式组件状态的问题,React 在16.8版本新增了Hook特性,可以让开发者在不编写 类(class) 的情况下使用 state 以及其他的 React 特性。HOOK本质上就是一个函数
useState
Hook规则:https://zh-hans.reactjs.org/docs/hooks-rules.html#explanation
useState用于在函数组件中使用状态
函数有一个参数,这个参数的值表示状态的默认值
函数的返回值是一个数组,该数组一定包含两项
使用也非常简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React, {useState} from "react" ;export default function App ( ) { const [n, setN] = useState(0 ); return ( <div> <button onClick={() => { setN(n - 1 ) }}>- </button> <span>{n}</span> <button onClick={() => { setN(n + 1 ) }}>+ </button> </div> ); }
有关它的原理,可以看这几篇文章
从源码剖析useState的执行过程
React Hooks 源码解析:useState
其实概括一下就是,每个react组件对应的虚拟DOM中会存储一个链表,这个链表的节点叫Hook节点,Hook节点中存储了现在的state和更新setState的函数,还有一个queue用于合并多次提交,知道了大致的结构,你就可以理解下面的事项了
useState严禁出现在判断或者循环中,因为源数据是用链表维护的,如果运行的次数不一样,React数据的对应关系就会错误
useState返回的更新函数,引用不变,这样可以节约内存
使用函数改变数据后,若数据和之前的数据完全相等(使用Object.is比较),不会导致重新渲染,以此优化效率
使用函数改变数据,传入的值不会和原来的数据进行合并,而是直接替换。
如果要实现强制刷新组件
类组件:使用forceUpdate函数
函数组件:使用一个空对象的useState
和类组件的状态一样,函数组件中改变状态可能是异步的(在DOM事件中),多个状态变化会合并以提高效率,此时,不能信任之前的状态,而应该使用回调函数的方式改变状态。如果状态变化要使用到之前的状态,尽量传递函数。
比如下面的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import React, { useState } from 'react' export default function App ( ) { console .log("App render" ) const [n, setN] = useState(0 ); return ( <div> <button onClick={() => { setN(prevN => prevN - 1 ); setN(prevN => prevN - 1 ); }}>-</button> <span>{n}</span> <button onClick={() => { setN(prevN => prevN + 1 ); setN(prevN => prevN + 1 ); }}>+</button> </div> ) }
useEffect useEffect用于在函数组件中处理副作用
接收一个函数作为参数,接收的函数就是需要进行副作用操作的函数
副作用函数的运行时间点,是在组件重新渲染,页面完成重绘之后。因此它的执行是异步的,不会阻塞浏览器。
类组件中的componentDidMount和componentDidUpdate,已经使用了JS修改了DOM,但是浏览器还没有进行回流和重绘,然后就同步运行了这两个生命周期
useEffect中的副作用函数,可以有返回值,返回值必须是一个函数,该函数叫做清理函数
该函数运行时间点,在每次运行副作用函数之前
首次渲染组件不会运行
组件被销毁时一定会运行
useEffect函数,可以传递第二个参数
第二个参数是一个数组,数组中记录该副作用的依赖数据
当组件重新渲染后,只有依赖数据与上一次不一样的时,才会执行副作用(使用Object.is比较所有的数组项)
所以,当传递了依赖数据之后,如果数据没有发生变化
副作用函数仅在第一次渲染后运行
清理函数仅在卸载组件后运行
我们可以用这个来模拟生命周期
注意:副作用函数中,如果使用了函数上下文中的变量,则由于闭包的影响,会导致副作用函数中变量不会实时变化。
下面的例子演示了怎么使用useEffect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import React, { useState, useEffect } from 'react' export default function App ( ) { const [n, setN] = useState(0 ); const [val, setVal] = useState("" ); useEffect(() => { console .log("改变页面标题的副作用操作, 只在n更新后触发" ) document .title = `计数器:${n} ` ; }, [n]); useEffect(() => { console .log("只在val更新后触发的副作用函数" ) }, [val]) useEffect(() => { console .log("副作用函数, 组件更新就会触发" ); }) useEffect(() => { console .log("组件挂载了" ); return function ( ) { console .log("组件卸载了" ); } }, []) return ( <div> <p>{n}</p> <div> <button onClick={() => { setN(n + 1 ); }}>+</button> </div> <div> <input type ="text" defaultValue={"" } onChange={(event ) => { setVal(event.target.value) }}/> </div> </div> ) }
useContext 这个Hook用于让函数式组件更方便的获取上下文数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import React, { useContext } from 'react' const ctx = React.createContext();function Comp ( ) { const context = useContext(ctx); return <h1 > {context.name}</h1 > } export default function App ( ) { return ( <div> <ctx.Provider value={{name : "sena" }}> <Comp /> </ctx.Provider> </div> ) }
useCallback 用于得到一个固定引用值的函数,通常用它进行性能优化
该函数有两个参数
回调函数,useCallback会固定该函数的引用,只要依赖项没有发生变化,则始终返回之前函数的地址
一个数组,用于记录依赖项,只有依赖项变化了才会重新生成函数(和useEffect一样是使用Object.is比较数组的每一项)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import React, { useState, useCallback } from 'react' class Child extends React .PureComponent { render ( ) { console .log("Child Render" ) return <div> <h1>{this .props.text}</h1> <button onClick={this .props.onClick}>改变文本</button> </div> } } function Parent ( ) { console .log("Parent Render" ) const [txt, setTxt] = useState(1 ) const [n, setN] = useState(0 ) const handleClick = useCallback(() => { setTxt(txt + 1 ) }, [txt]) return ( <div> <Child text={txt} onClick={handleClick} /> <input type="number" value={n} onChange={e => { setN(parseInt (e.target.value)) }} /> </div> ) } export default function App ( ) { return ( <div> <Parent /> </div> ) }
useMemo useMemo也是用来固定数据的
该函数有两个参数
一个函数,useMemo会固定该函数的返回值的引用,只要依赖项没有发生变化,则始终返回之前的地址
一个数组,用于记录依赖项,只有依赖项变化了才会重新执行传入的函数生成返回值
和useCallback的区别在于,useMemo可以固定的东西多了,所以一般用于存储一些需要大量计算的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import React, { useState, useMemo } from 'react' function Item (props ) { return <li > {props.value}</li > } export default function App ( ) { const [range,] = useState({ min : 1 , max : 10000 }) const [n, setN] = useState(0 ); const list = useMemo(() => { const list = []; for (let i = range.min; i <= range.max; i++) { list.push(<Item key ={i} value ={i} > </Item > ) } return list; }, [range.min, range.max]); return ( <div> <ul> {list} </ul> <input type="number" value={n} onChange={e => { setN(parseInt (e.target.value)) }} /> </div> ) }
useRef useRef也是用来固定数据的,不过它可以固定ref
useRef有一个参数,代表数据的默认值
它返回一个固定的对象,{current: val}
示例如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import React, { useState, useRef } from 'react' export default function App ( ) { const inpRef = useRef(); const [n, setN] = useState(0 ) return ( <div> <input ref={inpRef} type="text" /> <button onClick={() => { console .log(inpRef.current.value) }}>得到input的值</button> <input type="number" value={n} onChange={e => { setN(e.target.value) }} /> </div> ) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import React, { useState, useRef, useEffect } from 'react' export default function App ( ) { const [n, setN] = useState(10 ); const timerRef = useRef() useEffect(() => { if (n === 0 ) { return ; } timerRef.current = setTimeout (() => { console .log(n) setN(n - 1 ) }, 1000 ) return () => { clearTimeout (timerRef.current); } }, [n]) return ( <div> <h1>{n}</h1> </div> ) }
useImperativeHandleHook 这个Hook用于在Hook转发时更方便的设置Ref的值,比如如果你想把Ref设置成一个对象啥的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import React, { useRef, useImperativeHandle } from 'react' function Test (props, ref ) { useImperativeHandle(ref, () => { return { method ( ) { console .log("Test Component Called" ) } } }, []) return <h1 > Test Component</h1 > } const TestWrapper = React.forwardRef(Test)export default function App ( ) { const testRef = useRef(); return ( <div> <TestWrapper ref={testRef} /> <button onClick={() => { testRef.current.method(); }}>点击调用Test组件的method方法</button> </div> ) }
useLayoutEffectHook useLayoutEffectHook和useEffect都是React 提供给用户处理副作用的Hook,它们的函数签名是完全一致的
1 2 3 4 5 6 7 useLayoutEffectHook(() => { return () => { } }, [])
那么他们的区别是什么呢
首先我们知道,浏览器中 JS 线程和渲染线程(注意是线程)是互斥的,对于 React 的函数组件来说,它的更新过程大致分为以下步骤:
state改变
React 内部更新 state 变量
React重新调用render函数生成虚拟dom,进行dom diff并用JS操作DOM
将更新过后的 DOM 数据绘制到浏览器中
用户看到新的页面
前三步都是 React 在处理,也就是 JS 线程执行我们所写的代码,都是在内存中进行一系列操作,第四步才是真正将更新后数据交给渲染线程进行处理。
而useEffect只会在第五步后才会调用,也就是在浏览器绘制完后,在帧还有空闲的时候才调用,而且 useEffect 还是异步执行的,所谓的异步就是被 React 使用 requestIdleCallback 封装的,只在浏览器空闲时候才会执行,这就保证了不会阻塞浏览器的渲染过程。
而 useLayoutEffect 就不一样,它会在第三第四步之间执行,而且是同步阻塞后面的流程。和类组件的componentDidMount,componentDidUpdate 调用时机是一致的,且都是被 React 同步调用,都会阻塞浏览器渲染。
在某些场景下,useLayoutEffectHook有一些用处
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React, { useState, useLayoutEffect, useRef } from 'react' export default function App ( ) { const [n, setN] = useState(0 ) const h1Ref = useRef(); useLayoutEffect(() => { h1Ref.current.innerText = Math .random().toFixed(2 ); }) return ( <div> <h1 ref={h1Ref}>{n}</h1> <button onClick={() => { setN(n + 1 ) }}>+</button> </div> ) }
比如上面的代码,使用useLayoutEffectHook就不会闪屏,而使用useEffect就会闪屏,因为是在重绘后再修改了DOM,要重新绘制,而useLayoutEffect会一起提交所有的修改,就不会闪屏
自定义Hook 自定义Hook是指将一些常用的、跨越多个组件的Hook功能,抽离出去形成一个函数,该函数就是自定义Hook。
举一个简单的例子,如果很多个组件都要请求一个数据,我们就可以把请求数据的操作做成一个Hook
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import React from 'react' import { useEffect, useState } from "react" async function getData ( ) { return await new Promise ((resolve, reject ) => { setTimeout (() => { resolve({ name : "sena" }) }, 1000 ) }); } function useGetData ( ) { const [data, setData] = useState([]) useEffect(() => { (async ()=>{ const val = await getData(); setData(val); })(); }, []) return data; } function Comp ( ) { const data = useGetData(); return (<p > {JSON.stringify(data)}</p > ) } export default function App ( ) { return ( <div> <Comp/> </div> ) }