一、React Hooks的简介
2018年底FaceBook的React小组推出Hooks以来,所有的React的开发者都对它大为赞赏。React Hooks就是用函数的形式代替原来的继承类的形式,并且使用预函数的形式管理state,有Hooks可以不再使用类的形式定义组件了。这时候你的认知也要发生变化了,原来把组件分为有状态组件和无状态组件,有状态组件用类的形式声明,无状态组件用函数的形式声明。那现在所有的组件都可以用函数来声明了。也就是说函数组件也可以有state、ref、生命周期等属性了.
二、为什么要出现hook的概念呢?
因为函数式组件是全局当中一个普通函数,在非严格模式下this指向window,但是react内部开启了严格模式,此时this指向undefined,无法像类式组件一样使用state、ref,函数式组件定义的变量都是局部的,当组件进行更新时会重新定义,也无法存储,所以在hook出现之前,函数式组件有很大的局限性,通常情况下都会使用类式组件来进行代码的编写。
三、原始React继承类组件写法和React Hook的写法有什么区别?
原始React继承类组件写法:
import React, { Component } from 'react';
class Example extends Component {
constructor(props) {
super(props);
this.state = { count:0 }
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={this.addCount.bind(this)}>Chlick me</button>
</div>
);
}
addCount(){
this.setState({count:this.state.count+1})
`}`
}
export default Example;
hooks写法:
import React, { useState } from 'react';
const Example=()=>{
const [ count , setCount ] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={()=>{setCount(count+1)}}>click me</button>
</div>
)
}
export default Example;
四、常用的hook
(1)useState()
useState使函数式组件也能保存状态的一个hook,这个hook的入参是状态的初始值,返回值是一个数组,数组里第一个参数为状态的值,第二个参数为修改状态的方法。
useState做了哪些事呢?
首次渲染:render()------得到Demo组件------调用Demo组件------得到虚拟div------创建真实的div
点击button后:调用setCount(count+1)------再次render()------得到Demo组件------调用Demo组件------得到虚拟div------使用DOM Diff对比------更新真实的div
(2)useEffect
useEffect是函数式组件用来模拟生命周期的hook,可以模拟组件挂载完成、更新完成、即将卸载三个阶段,即componentDidMount、componentDidUpdate、componentWillUnmount。
useEffect的一个参数为函数,表示组件挂载、更新时执行的内容,在函数里再返回一个函数,表示组件即将卸载时调用的函数。
第二个参数为可选项,可传入数组,数组里可以为空,表示不依赖任何状态的变化,即只在组件即将挂载时执行,后续任何状态发生了变化,都不调用此hook。数组里也可以定义一或多个状态,表示每次该状态变化时,都会执行此hook。
useEffect(()=>{
// 这样模拟的是 componentDidMount
}, [])
useEffect(()=>{
// 这样模拟的是componentDidMount 以及当count发生变化时执行componentDidUpdate
}, [count])
useEffect(()=>{
return ()=>{
// 这样模拟的是 componentWillUnmount
}
}, [])
useEffect两个注意点
- React首次渲染和之后的每次渲染都会调用一遍
useEffect函数,而之前我们要用两个生命周期函数分别表示首次渲染(componentDidMonut)和更新导致的重新渲染(componentDidUpdate)。 - useEffect中定义的函数的执行不会阻碍浏览器更新视图,也就是说这些函数是异步执行的,而
componentDidMonut和componentDidUpdate中的代码都是同步执行的。个人认为这个有好处也有坏处吧,比如我们要根据页面的大小,然后绘制当前弹出窗口的大小,如果这时候异步的就不好操作了。
(3)useContext
之前用 class 类创建组件的时候,是用 props 传值的。
那现在使用方法(Function)来声明组件,已经没有了constructor构造函数也就没有了props 的接收,那父子组件的传值就成了一个问题。
于是React Hooks为我们准备了useContext。
1.useContext可以帮助我们跨越组件层级直接传递变量,实现共享。
需要注意的是useContext和redux的作用是不同的!!!
useContext:解决的是组件之间值传递的问题
redux:是应用中统一管理状态的问题
但通过和useReducer的配合使用,可以实现类似Redux的作用。
import React, { useState, createContext, useContext } from 'react';
const CountContext = createContext(0);
//Counter--子组件
const Counter = () => {
const count:any = useContext(CountContext); // 一句话就可以得到count
return (<h2>接收父组件的count值:{count}</h2>);
};
const Example = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>
You clicked
{count}
times
</p>
<button type="button" onClick={() => { setCount(count + 1); }}>click me</button>
//父组件引用子组件
<CountContext.Provider value={count}>
<Counter />
</CountContext.Provider>
</div>
);
};
export default Example;
如果子组件不在同一个文件里定义,也可以import子组件,但是需要把CountContext传递给子组件
import Counter from './counter';
<CountContext.Provider value={count}>
<Counter CountContext={CountContext} />
</CountContext.Provider>`
//子组件从props中获取CountContext
const Counter = (props:any) => {
const { CountContext } = props;
const count:any = useContext(CountContext); // 一句话就可以得到count
return (<h2>接收父组件的count值:{count}</h2>);
};
(4)useRef
useRef和类式组件中createRef用法比较类似,返回一个ref对象,这个对象在函数的整个生命周期都不变
createRef 与 useRef 的区别?
useRef 在 react hook 中的作用, 正如官网说的, 它像一个变量, 类似于 this , 它就像一个盒子, 你可以存放任何东西.
在一个组件的正常的生命周期中可以大致分为3个阶段:
第一个阶段,useRef与createRef没有差别
第二个阶段,createRef每次都会返回个新的引用;而useRef不会随着组件的更新而重新创建
第三个阶段,两者都会销毁
为什么使用useRef?
如果只使用useState()不同渲染之间无法共享state状态值 useRef 是定义在实例基础上的,如果代码中有多个相同的组件,每个组件的 ref 只跟组件本身有关,跟其他组件的 ref 没有关系。
createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。
useRef常见用法:
1、 操作当前DOM
用于dom元素或者组件上,通过current属性可以获取到dom元素或者类式组件的实例对象。需要注意的是,无论是useRef还是createRef或者是回调形式、字符串形式的ref,都是不能直接给函数式组件定义的,因为函数式组件的this指向undefined,没有实例对象,只能通过forwardRef定义到函数式组件中的某个dom元素。
// 这样就将传递给函数式组件的ref绑定在了函数式组件内部的input标签上
import React, {
useRef,
} from 'react';
// 使用函数表达式的方式定义了一个函数式组件
const FocusInput = () => {
const inputRef:any = useRef(null);
const getFocus = () => {
inputRef.current.focus();
};
return (
<div>
<input type="text" ref={inputRef} />
<button type="button" onClick={getFocus}>获取焦点</button>
</div>
);
};
export default FocusInput;
2、获取表单的输入
import React, {
useRef,
} from 'react';
const FocusInput = () => {
const eleRef = useRef<any>(null);
const handleSubmit = (e:any) => {
console.log(eleRef.current.value);
// e.preventDefault();
alert(eleRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<span>
姓名:
<input type="text" ref={eleRef} />
</span>
<input type="submit" value="Submit" />
</form>
);
};
export default FocusInput;
3、获取子组件的属性或方法
import React, {
MutableRefObject,
useState,
useEffect,
useRef,
useCallback,
} from 'react';
interface IProps {
// prettier-ignore
label: string,
cRef: MutableRefObject<any>
}
// 子组件
const ChildInput: React.FC<IProps> = (props) => {
const { label, cRef } = props;
const [inputValue, setValue] = useState('');
const handleChange = (e: any) => {
const { value } = e.target;
setValue(value);
};
const getValue = useCallback(() => inputValue, [inputValue]);
useEffect(() => {
if (cRef && cRef.current) {
cRef.current.getValue = getValue;
}
}, [getValue]);
return (
<div>
<span>
{label}
:
</span>
<input type="text" value={inputValue} onChange={handleChange} />
</div>
);
};
// 父组件
const ParentCom: React.FC = (props: any) => {
console.log(props);
const childRef: MutableRefObject<any> = useRef({});
const handleFocus = () => {
const node = childRef.current;
alert(node.getValue());
};
return (
<div>
<ChildInput label="名称" cRef={childRef} />
<button type="button" onClick={handleFocus}>获取子组件input的值</button>
</div>
);
};
export default ParentCom;
4、通过useImperativeHandle,配合forwardRef,获取子组件的属性
forwardRef: 将父类的ref作为参数传入函数式组件中
React.forwardRef((props, ref) => {})
- 创建一个React组件,
- 这个组件将会接受到父级传递的ref属性,
- 可以将父组件创建的ref挂到子组件的某个dom元素上,
- 在父组件通过该ref就能获取到该dom元素
import React, {
MutableRefObject,
useState,
useRef,
useCallback,
forwardRef,
useImperativeHandle,
} from 'react';
// 子组件
const ChildInput = forwardRef((props:any, ref:any) => {
const { label } = props;
const [inputValue, setValue] = useState('');
const handleChange = (e: any) => {
const { value } = e.target;
setValue(value);
};
const getValue = useCallback(() => inputValue, [inputValue]);
// useImperativeHandle作用: 减少父组件获取的DOM元素属性,只暴露给父组件需要用到的DOM方法
// 参数1: 父组件传递的ref属性
// 参数2: 返回一个对象,父组件通过ref.current调用对象中方法
useImperativeHandle(ref, () => ({
getValue,
}));
return (
<div>
<span>
{label}
:
</span>
<input type="text" value={inputValue} onChange={handleChange} />
</div>
);
});
// 父组件
const ParentCom = (props: any) => {
console.log(props);
const childRef: MutableRefObject<any> = useRef({});
const handleFocus = () => {
const node = childRef.current;
alert(node.getValue());
};
return (
<div>
<ChildInput label="名称" ref={childRef} />
<button type="button" onClick={handleFocus}>获取子组件input的值</button>
</div>
);
};
export default ParentCom;
(5) useReducer
useReducer相当于是useState的升级版,作用与useState类似,都是用来保存状态,但它的不同点在于可以定义一个reducer的纯函数,来处理复杂数据。
reducer 其实是在下次 render 时才执行的,所以在 reducer 里,访问到的永远是新的 props 和 state
useReducer 返回的 dispatch 函数是自带了 memoize(闭包) 的,不会在多次渲染时改变。所以如果你想同时把 state 作为 context 传递下去,请分成两个 context 来声明。
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等.并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数
指定初始state:将初始值作为第二个参数传入
const [state, dispatch] = useReducer(reducer, initialArg, init)
// 定义一个处理数据的reducer纯函数
function reducer(prevState, action){
switch(action.type){
case 'increment':
return {...prevState, count: prevState.count + 1 }
case 'decrement':
return {...prevState, count: prevState.count - 1 }
default:
return prevState
}
}
// 初始化状态
const [ count, dispatch ] = useReducer(reducer, { count: 0 })
// 修改状态,此时的修改需要派发一个action,让传入的reducer函数进行处理
dispatch({ type: 'increment' })
import React, { useReducer } from 'react';
const init = (initialcount:any) => {
console.log('initialcount', initialcount);
return initialcount;
};
function reducer(state:any, action:any) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
// initialCount可以是传入的,这里测试就定义成全局了
const initialCount = { count: 0 };
const Counter = () => {
//*惰性初始化*:创建函数作为`useReducer`的第三个参数传入,初始的`state`就是函数的参数
//这么做可以将用于计算`state`的逻辑提取到` reducer` 外部,这也为将来对重置 `state` 的 `action `做处理提供了便利
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<div>
Count:
{' '}
{state.count}
<button
type="button"
// 重置按钮
onClick={() => dispatch({ type: 'reset', payload: initialCount })}
>
Reset
</button>
<button type="button" onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button type="button" onClick={() => dispatch({ type: 'increment' })}>+</button>
</div>
);
};
export default Counter;
如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用Object.is 比较算法 来比较 state。)
(6) useCallback
函数式组件中,每一次更新状态,自定义的函数都要进行重新的声明和定义,如果函数作为props传递给子组件,会造成子组件不必要的重新渲染,有时候子组件并没有使用到父组件发生变化的状态,此时可以使用useCallback来进行性能优化,它会为函数返回一个记忆的值,如果依赖的状态没有发生变化,那么则不会重新创建该函数,也就不会造成子组件不必要的重新渲染。
看个例子,没有加useCallback的情况下:
import React, { memo, useState } from 'react';
const ChildComp = memo(({ name, onClick }:any) => {
console.log('render child-comp ...');
return (
<>
<div>
child-comp ...
{name}
</div>
<button type="button" onClick={() => onClick('hello')}>改变 name 值</button>
</>
);
});
function ParentComp() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const [name, setName] = useState('hi~');
const changeName = (newName:any) => setName(newName); // 父组件渲染时会创建一个新的函数
return (
<div>
<button type="button" onClick={increment}>
点击次数:
{count}
</button>
<ChildComp name={name} onClick={changeName} />
</div>
);
}
export default ParentComp;
父组件在调用子组件时传递了 name 属性和 onClick 属性,此时点击父组件的按钮,可以看到控制台中打印出子组件被渲染的信息。
子组件包裹了memo,为什么子组件还是会重复渲染呢? 分析下原因:
- 点击父组件按钮,改变了父组件中 count 变量值(父组件的 state 值),进而导致父组件重新渲染;
- 父组件重新渲染时,会重新创建 changeName 函数,即传给子组件的 onClick 属性发生了变化,导致子组件渲染;
但是我们只是点击了父组件的按钮,并未对子组件做任何操作,压根就不希望子组件的 props 有变化,这个时候就可以用useCallback
解决方法:修改父组件的 changeName 方法,用 useCallback 钩子函数包裹一层。
import React, { useCallback } from 'react'
function ParentComp () {
// ...
const [ name, setName ] = useState('hi~')
// 每次父组件渲染,返回的是同一个函数引用
const changeName = useCallback((newName) => setName(newName), [])
return (
<div>
<button onClick={increment}>点击次数:{count}</button>
<ChildComp name={name} onClick={changeName}/>
</div>
);
}
此时点击父组件按钮,控制台不会打印子组件被渲染的信息了。
究其原因:useCallback() 起到了缓存的作用,即便父组件渲染了,useCallback() 包裹的函数也不会重新生成,会返回上一次的函数引用。
(7) React.memo
当数据变化时,代码会重新执行一遍,但是子组件数据没有变化也会执行,这个时候可以使用memo将子组件封装起来,让子组件的数据只在发生改变时才会执行
什么时候用React.memo?
React 中当组件的 props 或 state 变化时,会重新渲染视图,实际开发会遇到不必要的渲染场景。
//子组件
function ChildComp () {
console.log('render child-comp ...')
return <div>Child Comp ...</div>
}
//父组件
function ParentComp () {
const [ count, setCount ] = useState(0)
const increment = () => setCount(count + 1)
return (
<div>
<button onClick={increment}>点击次数:{count}</button>
<ChildComp />
</div>
);
}
上面的例子,子组件中有条 console 语句,每当子组件被渲染时,都会在控制台看到一条打印信息。 点击父组件中按钮,会修改 count 变量的值,进而导致父组件重新渲染,此时子组件压根没有任何变化(props、state),但在控制台中仍然看到子组件被渲染的打印信息。
我们期待的结果应该是:子组件的 props 和 state 没有变化时,即便父组件渲染,也不要渲染子组件
方法:修改子组件,用React.memo()包一层。这种写法是 React 的高阶组件写法,将组件作为函数(memo)的参数,函数的返回值(ChildComp)是一个新的组件。
import React, { memo } from 'react'
const ChildComp = memo(function () {
console.log('render child-comp ...')
return <div>Child Comp ...</div>
})
(8) useMemo
useMemo也是返回一个记忆的值,如果依赖的内容没有发生改变的话,这个值也不会发生变化,useMemo与useCallback的不同点在于useMemo需要在传入的函数里需要return 一个值,这个值可以是对象、函数
前面父组件调用子组件时传递的 name 属性是个字符串,如果换成传递对象会怎样?
下面例子中,父组件在调用子组件时传递 info 属性,info 的值是个对象字面量,点击父组件按钮时,发现控制台打印出子组件被渲染的信息。
import React, { useCallback } from 'react'
function ParentComp () {
// ...
const [ name, setName ] = useState('hi~')
const [ age, setAge ] = useState(20)
const changeName = useCallback((newName) => setName(newName), [])
const info = { name, age } // 复杂数据类型属性
return (
<div>
<button onClick={increment}>点击次数:{count}</button>
<ChildComp info={info} onClick={changeName}/>
</div>
);
}
分析原因跟调用函数是一样的:
- 点击父组件按钮,触发父组件重新渲染;
- 父组件渲染,
const info = { name, age }一行会重新生成一个新对象,导致传递给子组件的 info 属性值变化,进而导致子组件重新渲染。
解决
使用 useMemo 对对象属性包一层。
useMemo 有两个参数:
- 第一个参数是个函数,返回的对象指向同一个引用,不会创建新对象;
- 第二个参数是个数组,只有数组中的变量改变时,第一个参数的函数才会返回一个新的对象。
function ParentComp () {
// ....
const [ name, setName ] = useState('hi~')
const [ age, setAge ] = useState(20)
const changeName = useCallback((newName) => setName(newName), [])
const info = useMemo(() => ({ name, age }), [name, age]) // 包一层
return (
<div>
<button onClick={increment}>点击次数:{count}</button>
<ChildComp info={info} onClick={changeName}/>
</div>
);
}
React.memo和useMemo的区别??
-
React.memo
- 包裹
react组件,来自父组件的props没有发生改变的话,就不会渲染子组件 - 第二个参数,可以传入一个判断方
isEqual,可以拿到preProps和props做比较,返回布尔值,决定是否更新渲染组件
- 包裹
-
useMemo
useMemo可以用于处理颗粒度更细的情况,对于组件内的某一部分进行缓存,只有第二个参数更新,才会执行回调,得到最新的变量/组件,否则不变。useCallback的原理也是一样的,区别就是,它是为了避免函数重复定义,一种对函数的缓存
(8) useImperativeHandle
这个是与forwardRef配合来使用的,当我们对函数式组件使用forwardRef将ref指定了dom元素之后,那就父组件就可以任意的操作指定的dom元素,使用useImperativeHandle就是为了控制这样的一种行为,指定父元素可操作的子元素的方法。
可以看上上面useRef() 的例子
(9)useLayoutEffect
这个方法与useEffect类似,只是执行的顺序稍有不同,useEffect是在组件渲染绘制到屏幕上之后,useLayoutEffect是render和绘制到屏幕之间。
useEffect和useLayoutEffect区别??
useEffect
基本上90%的情况下,都应该用这个,这个是在render结束后,你的callback函数执行,但是不会block browser painting,算是某种异步的方式吧,但是class的componentDidMount 和componentDidUpdate是同步的,在render结束后就运行,useEffect在大部分场景下都比class的方式性能更好.
useLayoutEffect
这个是用在处理DOM的时候,当你的useEffect里面的操作需要处理DOM,并且会改变页面的样式,就需要用这个,否则可能会出现出现闪屏问题, useLayoutEffect里面的callback函数会在DOM更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制.
例子:
import React, { useEffect, useLayoutEffect, useRef } from "react";
import TweenMax from "gsap/TweenMax";
import './index.less';
const Animate = () => {
const REl = useRef(null);
useEffect(() => {
/*下面这段代码的意思是当组件加载完成后,在0秒的时间内,将方块的横坐标位置移到600px的位置*/
TweenMax.to(REl.current, 0, {x: 600})
}, []);
return (
<div className='animate'>
<div ref={REl} className="square">square</div>
</div>
);
};
export default Animate;
使用useLayoutEffect后:
参考文档:
blog.csdn.net/u011705725/… juejin.cn/post/702746… www.cnblogs.com/vigourice/p…