react hooks入门

您所在的位置:网站首页 函数组件setstate第二个参数 react hooks入门

react hooks入门

2024-07-10 21:39| 来源: 网络整理| 查看: 265

函数组件和类组件 类组件:以class声明,拥有正常的生命周期函数,state,props函数组件: 本质是一个函数,接受一个props为参数,只相当于一个render方法,不存在生命周期函数,也不能维护自己到state 为什么要使用hooks 可以让传统的函数组件有内部状态state,并且可以通过一些hooks来模拟/替换class组件中的生命周期函数。在传统的react开发流程中,我们的自定义组件通常需要定义几个生命周期函数,在不同的生命周期处理各自的业务逻辑,有很多情况下他们是重复的。使用hooks可以简化这些重复的逻辑。this指向问题,class组件内部需要手动绑定this,而函数组件本身是一个函数,不需要绑定this useState 函数组件有状态了 const [state, setState] = useState(initialState); useState: 是一个方法,接收一个初始值initialState作为参数,返回一个数组,第一项为当前的state的值,第二项为更新state的方法initialState可以是一个方法, 也可以是基本数据类型或者一个对象。这里的setState方法与class组件中的setState有所不同,此setState 不会合并state中的值,而是整体的替换。hooks里需要通过setState({ ...state, changedState:changedValue})的方式手动merge。hooks中的setState是不支持第二个参数的和class组件中this.setState()一样,hooks中的setState也是异步的,连续调用两次setState,数据只改变一次。可以通过setState((preValue) => preValue + 1) 使用多个hooks,顺序很重要 import React, { useState } from 'react'; function Example(){ const [ age , setAge ] = useState(18) const [ sex , setSex ] = useState('男') const [ work , setWork ] = useState('前端程序员') return ( setAge(age + 1)}> 今年:{age}岁 性别:{sex} 工作是:{work} ) } export default Example; 所有的hooks保存在一个全局变量上,这个变量是一个链式结构函数组件重新render读取state的时候是根据这个链式结构来读取到当我们使用多个hooks时,不能在if...else.../ for循环语句里使用hooks,并且它只能使用在最顶级的作用域里。需要保证每次rerender的时候这些hooks都被执行到并且执行顺序不能改变 // hook的基本结构 { memoizedState: 当前值, queue: 更新队列, next: 指向下一个hook } const [ age , setAge ] = useState(18) const [ sex , setSex ] = useState('男') const [ work , setWork ] = useState('前端程序员') // 首次render memoizedState: { memoizedState: 18, queue: null, next: { memoizedState: '男', queue: null, next: { memoizedState: '前端程序员', queue: null, next : { ... next : { ... } } }, } } useEffect 替换生命周期函数,整合重复操作 为什么使用useEffect

在类组件中,我们经常在一些生命周期函数里处理一些额外的操作(数据请求、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

useLayoutEffect

它和 useEffect 的结构相同,区别只是调用时机不同。它的effect函数执行是同步执行的,所以一般操作DOM或修改样式都使用这个hook

useContext: 全局共享数据 Context API

Context 是React中用来共享那些对于一个组件树而言是“全局”的数据(主题/语言/用户信息)。它解决的是多级组件之间传参的问题

// 祖先组件 创建一个context对象 const MyContext = React.createContext(defaultValue); // 生成的context对象具有两个组件类对象 { Provider: React.ComponentType, Consumer: React.ComponentType React.ReactNode}> } // 祖先组件 MyContext.Provider // 子孙组件 MyContext.Consumer {value => /* 基于 context 值进行渲染, 当前的 value 值由上层组件中距离当前组件最近的 的 value prop 决定。*/} useContext 让父子组件传值更简单

useContext是基于Context API实现的,它可以帮助我们跨越组件层级直接传递变量,实现共享。

使用useContext就表示当前组件被包裹,并且它的返回值就是上的value属性

const context = useContext(MyContext) // context相当于 上接受的value属性

useContext使用Demo

需要注意的是useContext和redux的作用是不同的,一个解决的是组件之间值传递的问题,一个是应用中统一管理状态的问题,但通过和useReducer的配合使用,可以实现类似Redux的作用。

useReducer 什么是reducer

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

useReducer配合useContext来实现简易的redux

我们知道实现redux需要满足两点条件

一个全局的状态,并且做统一管理更新这些状态,实现业务逻辑

useContext:可访问全局状态,避免一层层的传递状态。

useReducer:通过action的传递,更新复杂逻辑的状态,可以实现类似Redux中的Reducer部分

需要注意的是,useReducer的dispatch操作必须是同步的,如果需要执行异步操作,需要模拟类似react-redux的实现方式。

具体实现

useCallback

useCallback主要用来解决使用React hooks产生的无用渲染的性能问题。

在class组件中,我们渲染时的性能优化一般可以通过shouldCompnentUpdate函数来进行, 但是在函数组件里,由于它不具备生命周期函数,也就是说我们没有办法通过组件更新前条件来决定组件是否更新。函数组件的每一次调用都会执行内部的所有逻辑,就带来了非常大的性能损耗。useMemo和useCallback都是解决上述性能问题的。

组件多次复用的性能问题 // Counter.tsx 多次复用Count, Button, Title function Counter() { const [age, setAge] = useState(18); const [salary, setSalary] = useState(5000); const incrementAge = () => { setAge(age + 1) } const incrementSalary = () => { setSalary(salary + 1000) } return ( 修改年龄 修改工资 ); } // Count.tsx function Count(props: { text: string, count: number }) { console.log(`Rendering ${props.text}`) return ( {props.text} - {props.count} ) } // Title.tsx function Title() { console.log('Rendering Title') return ( useCallback ) } // Button.tsx function Button(props: { handleClick: () => void children: string }) { console.log('Rendering button', props.children) return ( {props.children} ) }

具体实现

当我们每次点击按钮时,看到以下日志:

Rendering Title Rendering age Rendering button 修改年龄 Rendering salary Rendering button 修改工资

每次状态改变都触发了所有组件的rerender,然而我们期望是当我们修改年龄时,只有依赖age的那个组件rerender。

使用 Reacmo 优化

不同class组件中可以使用shoudComponentUpdate, PureComponent来做性能优化, React为函数式组件提供了叫Reacmo一个高阶组件.

我们可以通过将组件包装在Reacmo 中调用,通过这种记忆组件渲染结果的方式来提高组件的性能。这意味着当props没有变化时, React将跳过渲染组件的操作并直接复用最近一次渲染的结果。

const MyComponent = Reacmo(function MyComponent(props) { /* 使用 props 渲染 */ });

Reacmo 仅检查 props 变更。如果函数组件被 Reacmo 包裹,且其实现中拥有 useState 或 useContext 的 Hook,当 context 发生变化时,它仍会重新渲染。

使用了 Reacmo 后,我们看到点击增加年龄的按钮时,日志变为了

// Rendering age // Rendering button 修改年龄 // Rendering button 修改工资

依然有不相关的 rerender Rendering button 修改工资出现。说明修改工资这个组件的props发生里变化。

简单分析一下:

点击增加年龄按钮触发setAge()方法age改变导致组件重新渲染,重新执行Counter()方法Counter()内部的方法重新被创建修改工资 Button 传入的 props 发生了变化Button()重新render

因此这个 Button 传入的 props 发生了变化,这时候Reacmo没有阻止 rerender。而我们的useCallback这个`hook就是为了解决这个问题。

在js中,当函数执行时,会创建一个被称为执行环境的对象,这个对象在每次函数执行时都是不同的,当多次执行该函数时会创建多个执行环境。这个执行环境会在函数执行完毕后销毁。所以每次rerender时都会创建新的执行环境,并为其内部的方法重新分配空间

什么是 useCallback` const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );

返回一个memoized回调函数。 把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。

在上述例子中

const incrementAge = useCallback( () => { setAge(age + 1) }, [age], ) // Rendering salary // Rendering button 修改工资 useMemo

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 computeExpensiveValue(a, b), [a, b]);

返回一个 memoized 值。 把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

传入 useMemo 的函数会在渲染期间执行。不要在这个函数内部执行与渲染无关的操作。

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

useRef const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传递的参数(initialValue)。返回的对象将存留在整个组件的生命周期中。

useRef一般有两种用途

获取DOM节点,这一点和class组件中的ref类似。用来保存变量 获取上一轮的 props 或 state function Example () { const [count, setCount] = useState(0); const prevCountRef = useRef(); useEffect(() => { prevCountRef.current = count }); const prevCount = prevCountRef.current console.log(prevCount, count, '之前的状态和现在的状态') return ( {count} {setCount(count+1)}}>+ ) }

自定义hook

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

通过自定义 Hook,可以将组件中重复的逻辑提取到可重用的函数中。

function Example () { const [count, setCount] = useState(0); const prevCount = usePrevious(count) console.log(prevCount, count, '之前的状态和现在的状态') return ( {count} {setCount(count+1)}}>+ ) } function usePrevious (value) { const ref = useRef() useEffect(() => { ref.current = value }) return ref.current } 总结 useState: 用来声明状态state,修改值需要手动合并useEffect: 用来替换类组件中的生命周期函数,简化重复的操作useContext: 全局共享状态,解决祖先/子孙组件之间的传参问题useReducer: useState的替换方案,将操作和更新解绑,配合useContxet可以实现简易reduxuseCallback: 对函数进行缓存,优化性能useMemo: 对值进行缓存,优化性能useRef:获取DOM节点或组件实例, 保存变量 Capture Value 捕获属性

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属性

每次 Render 都有自己的 Props、State export default class ClassCounter extends React.Component{ constructor(props){ super(props); this.state = { count: 0 } } handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + this.state.count); }, 3000); } return ( You clicked {this.state.count} times in class Component this.setState(this.state.count + 1)}> Click me Show alert ); } function Counter() { const [count, setCount] = useState(0); function handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + count); }, 3000); } return ( You clicked {count} times setCount(count + 1)}> Click me Show alert ); } // 连续点击3次`Click me` // 再点击一次`Show alert`,并在3s内点击两次`Click me` // alert时count的值是多少?

在线案例

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 function Counter() { const [count, setCount] = useState(0); const latestCount = useRef(count) function handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + latestCount.current); }, 3000); } return ( You clicked {count} times { setCount(count + 1); latestCount.current = count + 1; }}> Click me Show alert ); }

由于Capture Value的存在,我们在class组件中有些比较合理的想法,在函数组件中使用似乎就会有点问题

不要对 Dependencies 撒谎

考虑这么一个需求: 定义一个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 {count}; }

这看起来似乎没什么问题,但是由于 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这个变量,那有没有什么办法能将更新和动作解耦呢?

金手指模式 const [state, dispatch] = useReducer(reducer, initialState); const { count, step } = state; useEffect(() => { const id = setInterval(() => { dispatch({ type: "tick" }); // Instead of setCount(c => c + step); }, 1000); return () => clearInterval(id); }, []);

更新变成了dispatch({ type: "tick" }) 所以不管更新时需要依赖多少变量,在调用更新的动作里都不需要依赖任何变量。 具体更新操作在 reducer 函数里写就可以了。

关于组件内部的函数 如果某些函数仅在某个effect中调用,可以把它们的定义移到effect中如果某些函数不依赖于组件中的任何数据,可以把它们的定义移到组件外部如果这个函数需要通过props传给子组件,一般最好使用useCallback做一下缓存如果这个函数的作用是做大批量的计算,且返回会值需要显示在页面上,最好使用useMemo做一下缓存 一些已经支持hook的库

React Redux 从 v7.1.0 开始支持hook的api

React Router 从 v5.1 开始支持 hook

Mobx + Hooks

umi Hooks

react-use

useHooks



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3