- A+
React笔记-Hooks(九)
Hooks
概念
React Hooks 的意思是 组件尽量写成纯函数 如果需要外部功能和副作用 就用钩子把外部代码"钩"进来
函数组件和类组件区别
- 函数组件没有状态(state) 类组件有
- 函数组件没有生命周期 类组件有(挂载-更新-销毁)
- 函数组件没有this 类组件有
- 函数组件更适合做UI展示 类组件更适合做复杂的业务逻辑组件
为什么纯函数组件逐渐取代类组件
React团队希望 组件不要变成复杂的容器 最好只是数据流的管道 开发者根据需要 组合管道即可
Hooks用法
useState() 状态钩子
纯函数组件没有状态,useState()用于设置和使用组件的状态属性
// Hooks是无状态的 所以用这种方式替换类组件中的状态(state) const [state, setState] = useState(initialValue); // state:初始的状态属性,指向状态当前值,类似this.state // setState:修改状态属性值的函数,用来更新状态,小驼峰命名 // setState((currentState) => {]})可以传入一个函数 函数接收一个参数 用于存放当前state // initialValue:状态的初始值,该值会赋给state
import { useState } from 'react'; function LearnHooks () { let a = 1; const [num, setNum] = useState(0); const [name, setName] = useState([{name : 'bob', age : 18}, {name : 'sam', age : 20}, {name : 'kitty', age : 22}]); const addNum = () => { setNum(num + 1) // setNum是异步操作 所以console.log(num)输出的是更新前的值 console.log(num) a += 1; // 这里a永远输出为2 // 原因是hooks重新渲染是自调用 每次都会重新把a设为1 然后执行 a + 1 console.log(a) } console.log('LearnHooks渲染了') return ( <div> <h1>学习hooks的userState</h1> <div>当前num : {num}</div> <button onClick={() => addNum()}>num + 1</button> <div>{name.map((item) => { return <div key={item.name}>name : {item.name}, age : {item.age}</div> })} </div> <input type="text" onChange={ ({target : {value}}) => setName([{name : value}])}/> </div> ); } export default LearnHooks;
useEffect() 副作用钩子
可以实现特定的功能 如异步请求
useEffect(() => { // 回调函数,其中是要进行的异步操作代码 return () => {} // useEffect中的return语句可以用于清除effect产生的副作用。当组件卸载时,React会执行return语句中的函数,以清除effect产生的副作用。例如,如果在useEffect中订阅了一个事件,那么在return语句中取消订阅可以避免内存泄漏。 }, [array]) // [array]:useEffect执行的依赖,当该数组的值发生改变时,回调函数中的代码就会被执行 // 如果[array]省略,则表示不依赖,在每次渲染时回调函数都会执行 // 如果[array]是空数组,即useEffect第二项为[],表示只执行一次
import React, { useState, useEffect } from 'react'; function hook() { const [num, setNum] = useState(1) /** * 第一个参数是回调函数 * 第二个参数是依赖项 * 每次num变化时都会变化 * * 注意初始化的时候,也会调用一次 */ useEffect(() => { console.log("每次num,改变我才会触发") }, [num]) return ( <div> <button onClick={() => setNum(num + 1)}>+1</button> <div>你好,react hook{num}</div> </div> ); } export default hook;
useLayoutEffect()
useLayoutEffect是React提供的一个Hook,与useEffect功能类似,但在组件更新DOM前执行,而不是之后。
与useEffect不同的是,useLayoutEffect会阻塞浏览器渲染,并立即同步执行副作用函数。这可以确保使用组件的代码看到的是最新的DOM布局,因为它们在组件挂载或更新时都会在渲染通道中优先处理。
// useLayoutEffect接收两个参数:一个副作用函数和一个依赖项数组。副作用函数中可以进行DOM操作、计算布局等任务,并且可以通过返回一个清除函数来清理副作用产生的任何资源。 useLayoutEffect(() => { 副作用函数执行逻辑 }, [ 依赖项 ])
// 1. 编写副作用函数:useLayoutEffect需要传递一个函数作为参数,该函数称为副作用函数。在这个函数里,你可以访问到DOM、执行异步操作或计算等其它操作,并考虑它们的收尾工作。当组件的props或state发生变化时,都将重新运行该函数。 import { useLayoutEffect } from 'react'; function useMyLayoutEffect() { // 执行DOM相关操作 return () => { // 清理工作 }; }
// 2. 将useLayoutEffect挂载到组件:要在组件中使用useLayoutEffect这个函数,只需要调用它即可,并把上一步编写的副作用函数作为第一个参数。 function MyComponent() { useLayoutEffect(useMyLayoutEffect, []); return <div>Hello, world!</div>; }
// 给useLayoutEffect传递依赖项数组:和useEffect一样,useLayoutEffect的第二个参数是一个数组,其中包含在副作用函数中需要被“监视”的任何变量。当其中的变量发生更改时,useLayoutEffect将重新运行其副作用函数。 function MyComponent({ name }) { useLayoutEffect(() => { console.log(`MyComponent is mounted: ${name}`); }, [name]); return <div>Hello, {name}!</div>; }
useContext() 共享状态钩子
可以共享状态,作用是进行状态的分发,避免了使用Props进行数据的传递
// 第一步:创建全局的Context const AppContext = React.createContext([初始化参数]) // 第二步:通过全局的Context进行状态值的共享 <AppContext.Provider value={{ 属性名: 值 }}> <其他组件1 /> <其他组件2 /> </AppContext> // 第三步:使用context const context = useContext(AppContext); {context.name}
// 在 App.js 文件中 import React, { createContext, useState } from 'react'; import Child from './Child'; export const MyContext = createContext(); function App() { const [count, setCount] = useState(0); return ( <MyContext.Provider value={{ count, setCount }}> <div> <h1>Count: {count}</h1> <button onClick={() => setCount(count + 1)}>Increment</button> <Child /> </div> </MyContext.Provider> ); } // 在 Child.js 文件中 import React, { useContext } from 'react'; import { MyContext } from './App'; function Child() { const { count, setCount } = useContext(MyContext); return ( <div> <h2>Child Component</h2> <h3>Count: {count}</h3> <button onClick={() => setCount(count - 1)}>Decrement</button> </div> ); }
useMemo() 记忆钩子(值)
useMemo是React中的一个hook,用于优化组件的性能。它的作用是缓存函数的返回值,只有当依赖项发生变化时才重新计算。这样可以避免在每次渲染时都重新计算函数的返回值,从而提高组件的性能。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); // 第一个参数是一个回调函数,用于计算需要缓存的值 // 第二个参数是一个数组,用于指定依赖项。只有当依赖项发生变化时,才会重新计算memoizedValue的值。
import React, { useState, useMemo } from 'react'; function MyComponent() { const { a, setA } = useState(1); const result = useMemo(() => { // 只有当a发生变化时才会重新计算。这样可以避免在每次其他渲染时都重新计算结果,提高组件的性能 return a * 2; }, [a]); const add = () => { setA(a + 1) } return ( <div> <div>{result}</div> <button onClick={() => add()}>a+1</button> </div> ); }
useCallback() 记忆钩子(函数)
useCallback是React中的一个Hook函数,用于优化函数组件的性能。它的作用是返回一个记忆化的回调函数,当依赖项发生变化时才会重新生成新的回调函数。这样可以避免在每次渲染时都创建新的回调函数,从而提高组件的性能。
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], ); // 第一个参数是回调函数 // 第二个参数是依赖项数组。当依赖项数组中的任意一个值发生变化时,useCallback会重新生成新的回调函数。如果依赖项数组为空,则每次渲染都会返回同一个回调函数。 // 需要注意的是,useCallback返回的是一个记忆化的回调函数,而不是一个普通的函数。因此,如果需要在组件外部使用该回调函数,需要将其作为props传递给子组件。
// 举例:使用useCallback优化组件性能 import React, { useState, useCallback } from 'react'; function MyComponent() { const [count, setCount] = useState(0); // 普通的回调函数 const handleClick = () => { setCount(count + 1); }; // 使用useCallback优化的回调函数 const handleClickMemoized = useCallback(() => { setCount(count + 1); }, [count]); return ( <div> <p>Count: {count}</p> <button onClick={handleClick}>普通的回调函数</button> <button onClick={handleClickMemoized}>使用useCallback优化的回调函数</button> </div> ); } export default MyComponent;
useReducer() 行为钩子
useReducer是React中一个状态管理的Hooks,用于处理复杂的组件状态逻辑。它和useState类似,都是用于管理组件状态的,但是useReducer可以更好地处理复杂的状态逻辑,尤其是在多个状态相互影响的情况下会更加方便和清晰。
// 1. 定义初始状态(initial state)和reducer函数。reducer函数的作用是根据当前的状态和操作类型(action)来返回新的状态值。 const initialState = { count: 0, }; function reducer(state, action) { switch (action.type) { case 'ADD': return { count: state.count + 1 }; case 'SUB': return { count: state.count - 1 }; default: throw new Error(); } }
// 2. 在组件中使用useReducer Hook,传入reducer函数和initial state参数,获取当前的state值和dispatch函数。 import React, { useReducer } from 'react'; function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <div> <h2>Counter: {state.count}</h2> <button onClick={() => dispatch({ type: 'ADD' })}>+</button> <button onClick={() => dispatch({ type: 'SUB' })}>-</button> </div> ); }
useRef() 保存引用值
useRef是React中的一个hook,用于创建一个可变的引用,类似于在类组件中使用的this.refs。与useState不同,useRef返回一个可变的值,而不会触发重新渲染组件。
useRef可以用于保存任何可变值,例如DOM元素的引用、定时器的标识符、上一个渲染周期的状态等。它还提供了一个.current属性来访问保存的值。
使用场景
获取DOM元素的引用
保存上一个渲染周期的状态
在useEffect中访问最新的props和state值
保存定时器的标识符,以便在组件卸载时清除
// useRef创建的ref对象与组件生命周期不相关,因此它不会在调用setState或props更新时自动更新,ref改变取决于使用它的具体方式 const ref = useRef() // 因为hooks重新渲染其实是组件的自调用,所以我们不能在组件中直接定义一个值,let a = 1 或 const arr = []都是错误的,此时我们使用useRef来保存和访问持久性数据
useImperativeHandle()
useImperativeHandle是React Hook中的一个函数,用于在使用ref时,向父组件暴露子组件的方法或属性。它可以覆盖默认情况下通过ref自动公开该组件实例的方式,从而更加精准的控制哪些内容公开给父组件。
useImperativeHandle接受两个参数:ref对象和一个callback函数。callback函数应该返回包含想要挂载到ref上的任何公共方法或属性的对象。当父组件从ref调用该方法或访问该属性时,callback函数定义的逻辑将被执行。
// 1. 在子组件中使用forwardRef高阶组件转发ref,以在父组件中获得对子组件的引用。 import React, { forwardRef } from 'react'; const Child = forwardRef((props, ref) => { // 组件... });
// 2. 使用useImperativeHandle Hook,将可供父组件访问的方法或属性包装在一个callback函数中,并将该函数作为useImperativeHandle的第二个参数传递 import React, { forwardRef, useImperativeHandle } from 'react'; const Child = forwardRef((props, ref) => { const someMethod = () => { console.log('Hello from the child component'); }; useImperativeHandle(ref, () => ({ someMethod, })); return <div>...</div>; }); export default Child;
// 3. 在父组件中使用ref来调用从子组件暴露的方法 import React, { useRef } from 'react'; import Child from './Child'; function Parent() { const childRef = useRef(null); const handleClick = () => { // 调用子组件中公开的 someMethod 方法 childRef.current.someMethod(); }; return ( <div> <button onClick={handleClick}>调用子组件方法</button> <Child ref={childRef} /> </div> ); }
其他
Immutable Data(不可变数据)
解决的问题
在 js 中,对象都是引用类型,在按引用传递数据的场景中,会存在多个变量指向同一个内存地址的情况,如果有多个代码块同时更改这个引用,就会产生竞态
实现的原理
Persistent Data Structure(持久化数据结构):用一种数据结构来保存数据。当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费,也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变,同时为了避免 deepCopy把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享)
redux / flux
// redux / flux 要求采用返回新对象的形式,来触发数据更新、re-render,一般推荐的做法就是采用对象解构的方式。如果 state 对象巨大(注意:对象巨大),在结构、拷贝 state 的过程中,耗时会较长。 return { ...state, settings: { ...state.settings, profile:{ ...state.settings.profile, darkmode: true, } } }
immer
// 开源库实现思路:原始对象先做了一层 Proxy 代理,得到 draftState 传递给 function。function(带副作用) 直接更改 draftState,最后 produce 返回新的对象 // 安装 npm install immer //使用 import React, { useState } from "react"; import produce from "immer"; export default function App() { const [list, setList] = useState([1, 2, 3]); const addMutable = () => { list.push("新数据"); setList(list); }; const addImmutable = () => { /** * 第一个参数是要代理的数据 * 第二个参数是一个函数 */ const newVal = produce(list, draft => { /** * draft 相当于 list * 在这个方法里面,可以直接修改draft,注意draft也只能在这个方法里面修改 * 不需要返回值,immer内部已经帮我处理好了 */ draft.push('新数据') }) console.log(newVal) setList(newVal); }; return ( <div className="App"> <button onClick={addMutable}>已可变的方式添加</button> <button onClick={addImmutable}>已不可变的方式添加</button> {list.map((item, index) => (<li key={index}>{item}</li>))} </div> ); }
函数组件传值
hooks直接通过props传值
//父组件中在子组件标签上定义属性 import 子组件 <Son 属性={值}></Son> // 子组件接收父组件中传递属性 props.属性
父传子
// 父组件 import { useState } from 'react' import Son from './son' function Father() { const [data, setData] = useState(0) return ( <div> <Son {/*通过定义属性传值*/} name = 'bob' d = {data} ></Son> <button onClick={() => setData(data + 1)}>+1</button> </div> ) } export default Father
// 子组件 import { useState } from 'react' function Son (props) { return ( <div> <div>{props.name}</div> <div>{props.d}</div> </div> ) } export default Son
子传父
// 父组件 import { useState } from 'react' import Son from './son' function Father() { const [data, setData] = useState(0) const getSon = (msg) => { console.log(msg) } return ( <div> <Son {/*定义一个方法用于接收子组件传值*/} name = 'bob' d = {data} giveFather={(msg) => getSon(msg)} ></Son> <button onClick={() => setData(data + 1)}>+1</button> </div> ) } export default Father
// 子组件 import { useState } from 'react' function Son (props) { // 接收父组件方法 通过此方法传值给父组件 const set = () => { props.giveFather('儿子给父亲的') } return ( <div> <div>{props.name}</div> <div>{props.d}</div> <button onClick={set}>给父亲</button> </div> ) } export default Son
React.memo
一个高阶组件,用于优化React组件的性能。它可以帮助我们避免不必要的渲染,从而提高应用程序的性能。当组件的props没有改变时,React.memo会使用之前的渲染结果,而不会重新渲染组件。这对于那些渲染开销较大的组件特别有用。
import React from 'react'; const MyComponent = React.memo(props => { // 组件代码 }, (prevProps, currentProps) => { // prevProps 上次props // currentProps 当前props return Boolean; // false 渲染 // true 不渲染 }); // 在上面的代码中,我们将一个函数组件传递给React.memo(),并将其返回的新组件赋值给MyComponent。现在,MyComponent将只在其props发生更改时重新渲染。 // 需要注意的是,React.memo()仅检查props的浅层比较。如果props包含复杂的对象或函数,可能需要手动实现更深层次的比较。
import React, { useState } from "react"; // 子组件 const SonMemo = React.memo( // 第一个参数 接收一个hook (props) => { return ( <div>{props.data}</div> ) // 第二个参数 接收一个函数 }, (prevProps, currentProps) => { // 偶数不渲染 // 奇数渲染 return currentProps.data % 2 === 0 } ) function FatherMemo () { const [num, setNum] = useState(0) return ( <div> <h1>{num}</h1> {/*点击加一*/} <button onClick={() => setNum(num + 1)}>按钮</button> <SonMemo data={num}></SonMemo> </div> ) } export default FatherMemo;
React hooks中的过期闭包问题
过期闭包概念
过期闭包(stale closure)是指一个闭包在创建之后,所引用的外部作用域内的变量已经被修改,但闭包内仍然保存了旧值。这就导致闭包中的代码与外部作用域内的实际状态不一致,从而造成错误的结果。
useEffect中过期闭包体现和解决
// react hook中useEffect的过期闭包 import { useEffect, useState } from "react" function ExpiredClosure () { const [num, setNum] = useState(0) useEffect( () => { setInterval(() => { // 这里的num在初始化useEffect执行时取到0之后 因为闭包num值不会自动更新 console.log(num) }, 2000) }, [] ) return ( <div> <h1>{num}</h1> <button onClick={() => setNum(num + 1)}>+1</button> </div> ) } export default ExpiredClosure
// 解决react hook中useEffect过期闭包问题 import { useEffect, useState } from "react" function ExpiredClosure () { const [num, setNum] = useState(0) useEffect( () => { const timer = setInterval(() => { console.log(num) }, 2000) return () => { // 当组件卸载时 清除计时器 clearInterval(timer) } // 添加num为依赖项 }, [num] ) return ( <div> <h1>{num}</h1> <button onClick={() => setNum(num + 1)}>+1</button> </div> ) } export default ExpiredClosure
useState中过期闭包体现和解决
// react hook中useState的过期闭包 import { useState } from "react"; function ExpiredClosure2 () { const [num, setNum] = useState(0) // 点击正常+1 const add = () => { setNum(num + 1) } // 假设点击时num为3 两秒+2 = 5 在两秒之间不管点击多少次+1操作num变为678910...最后num都为5 const add2 = () => { setTimeout(() => { setNum(num + 2) }, 2000) } // 当我们点击+2时候会取得当前值 之后点击其他改变num值 +2中的num都不会随之改变 两秒后取得的num+2给setNum后 渲染页面 return ( <div> <h1>{num}</h1> <button onClick={add}>+1</button><br /> <button onClick={add2}>+2</button> </div> ) } export default ExpiredClosure2;
// 解决react hook中useState过期闭包问题 import { useState } from "react"; function ExpiredClosure2 () { const [num, setNum] = useState(0) const add = () => { setNum(num + 1) } const add2 = () => { setTimeout(() => { // setNum中可以传入一个函数 这个函数接收一个参数 用于获取当前num值 setNum((currentNum) => currentNum + 2) }, 2000) } return ( <div> <h1>{num}</h1> <button onClick={add}>+1</button><br /> <button onClick={add2}>+2</button> </div> ) } export default ExpiredClosure2;