React一直提倡的是函数式组件,可是一直依赖函数式组件没有办法使用如类、生命周期等等一系列特性,hook的出现让开发这可以在函数里面也能使用这些特性
Hook的优势劣势是什么
类组件的三大问题
- 状态逻辑难以复用:在类组件当中如果要复用状态逻辑,则需要用到高阶组件或者使用render prop,这两种方法都需要新增一层父组件用于保存状态,导致层级的增加,而且难以理解。
this的指向问题:父组件给子组件传入函数的时候,需要绑定this,其中this的绑定最新的方法如下,这种方法相当于语法糖
this.handler = this.handler.bind(this)class Comp extends React.Component {constructor() {}handler = () => {}}
随着复杂度增加,类会变得难以维护。
- 在组件中不断的需要在生命周期中添加与卸载事件,逻辑分散,容易出bug
- 类到处都是状态的访问跟处理,难以拆分成更小的组件
Hook对比类组件有以下优势
- 无需更改组件结构就能复用状态逻辑
- 相互关联的部分能定义成更小的函数,如鼠标位置的获取,事件的绑定与解绑
- 副作用的关注点分离:与渲染无关的一些操作(如定时器设定、添加删除订阅等等)都能收进useEffect函数当中,在渲染以后统一处理
useState
const [state, setState] = useState(initialState)
initialState
initialState可以分为两类:
- 非函数:此时initialState将作为初始值,组件初次渲染时的state值即为initialState
- 函数:React将会通过执行此函数来得到初始值,适用于需要经过复杂逻辑得到初始值的情况
initialState是惰性的,这意味着它只会在初次渲染的时候执行一次,这就意味着如果后续initialState发生变化也不会对state值产生任何影响
setState
状态的更新通过useState返回的setState进行,setState函数的参数同样可以有两类:
- 非函数:此时setState会直接将对应状态更新为参数值,然后触发更新。注意,与component组件的setState方法不同,这个函数并不会对状态进行合并,而是直接将状态进行替换。
- 函数:此函数的参数为最新的state值,如果需要依赖上一次的state进行更新,使用函数作为setState的参数是不二之选
上面的例子中,我们的初衷是连续对a进行+1操作,也就是+2,但是结果却是每次点击都还是加1,这是因为第二次调用setA的时候,对应的a还没进行更新。 setA应该改用函数进行更新,以随时随地获得最新值:import React, {useState} from "react";import ReactDOM from "react-dom";function App() {const [a, setA] = useState(0);const update2times = () => {setA(a + 1);setA(a + 1);};return <div onClick={update2times}>{a} hello</div>;}ReactDOM.render(<App />, document.getElementById("root"));
上面的例子就没有问题了,因为每次setA都能从state得到最新的值。const update2times = () => {setA(state => state + 1);setA(state => state + 1);};
每次的渲染互相独立
上面的例子中,每次点击App组件都会触发重新渲染,执行一次App函数,而每次执行App函数都会产生一个独立的内部变量import React, {useReducer, useState, useRef, useEffect} from "react";import ReactDOM from "react-dom";function App() {const [a, setA] = useState(0);useEffect(() => {setInterval(() => {console.log(a);}, 1000);}, []);return <div onClick={() => setA(a + 1)}>{a} hello</div>;}ReactDOM.render(<App />, document.getElementById("root"));
a 和 setA,与其他的渲染无关。所以setInterval回调函数引用的a始终是第一次渲染时的App上下文中的a,与后面的渲染无关。
换言之,函数组件的每一次渲染都等同于对应函数的一次执行,所以每次执行都会产生一个独立的执行上下文。
useEffect
关键点:
- 副作用(effect)的含义
- 副作用的分类
- 函数组件与副作用的关系,怎么清除
- 执行时机
- class组件的相关缺陷以及useEffect是如何改进的
副作用与useEffect
副作用在函数式编程中意义为与外界系统发生交互的一系列行为,在react中可理解为与组件渲染无关的一些行为,譬如Ajax请求,设置定时器,本地化存储等等。以前的React函数组件是不允许有任何副作用的,也就是说不能在函数组件进行Ajax请求等等操作,需要保证函数组件的纯洁性。useEffect的出现相当于赋予函数组件副作用的能力
副作用的分类与清除
React当中的副作用可大概分为两种,需要清除的和不需要清除的。需要清除的如设置定时器等等,不需要清除的如使用Ajax请求数据。
在useEffect当中,如果需要在组件卸载时清除副作用,可以在传入的函数中返回一个清除副作用的函数,这样组件在卸载的时候就会执行此函数。以定时器为例:
useEffect(() => {const id = setInterval(() => {console.log('hello')}, 1000)return () => {clearInterval(id)}},[])
控制副作用的执行
useEffect第二个参数为数组,这个数组元素为一些变量,这些变量可以是state,props等等,在每一次的渲染后,只要这些变量发生变化,那么对应的副作用就会执行。一般来说这个数组可以有三种用法:
- 空数组:这种情况下副作用只会在组件初次渲染的时候执行,后续组件的更新不会执行
- 不存在:这种情况下组件的每次渲染都会执行此副作用
- 其他:这种情况下每次渲染后数组中的任一变量如果发生变化都会触发副作用的执行
类组件的问题及useEffect的优势
在class组件中我们经常遇到的问题就是需要将同一类的操作分散到不同的生命周期钩子当中,还是以定时器为例,以下的例子需要在组件挂载后定时打印’hello’,卸载后就把定时器清除
上面与定时器相关的代码分散在constructor,componentDidMount,componentWillUnmount三个地方,一旦组件复杂度提升,将对维护造成很大的困难。useEffect的优势就在于将相关代码都聚合到一个函数当中,可维护性大大提升class Hello extends React.Component {constructor() {super()this.timer = null}componentDidMount() {this.timer = setInterval(() => {console.log('hello')}, 1000)}componentWillUnmount() {clearInterval(this.timer)}rener() {return <div>hello</div>}}
useEffect(() => {const id = setInterval(() => {console.log('hello')}, 1000)return () => {clearInterval(id)}}, [])
useMemo
接受一个函数以及依赖数组,当依赖发生改变时才会执行函数重新计算新的值,否则将沿用旧值适用范围及误区
useMemo适用于需要大量计算才能得到的值,并且这个值与组件的渲染相关。
常见的误区有两个:
- 对能简单计算出来的值也使用useMemo:useMemo本身也是有一定的开销,对于简单计算来说可能起不到性能优化的效果,甚至可能加重性能负担
对与渲染无关的值也使用useMemo:useMemo会在渲染期间执行,如果该值与页面渲染无关而且计算量大,则会阻塞渲染管道,起到负优化的效果,这类的计算值应该放到useEffect上面。
useCallback
常见用法
常见的场景是父组件需要向子组件传入回调函数供子组件调用,由于父组件每次渲染都会生成一个新的函数,所以每次传入到子组件的函数的内存地址都不一致,导致子组件反复进行不必要的更新,例子如下:
const Child = React.memo((props) => {console.log('child render')return <div onClick={props.handleClick}>child</div>})const Parent = () => {const handleClick = () => {console.log('click')}const [count, setCount] = useState(0)return (<div >parent<button onClick={() => setCount(count+1)}>add</button><Child handleClick={handleClick} /></div>)}
在上面的例子中,即便我们已经使用了
React.memo对Child组件包裹,使其仅在props变化的时候更新,但由于每次Parent渲染的时候生成的handleClick地址各不相同,导致Child每次接收到的handleClick都不一致,进而导致更新。要改善这种状况就需要使用useCallback:const handleClick = useCallback(() => {console.log('click')}, [])
上面的代码使得Parent每次渲染时得到的handleClick指向的内存地址是一致的,所以Child每次接收到的handleClick参数不变,也就不会发生更新。
useRef
用法
接受一个值initialValue,返回一个对象,其对应current属性的初始值即为initialValue
用户可以通过ref.current对ref的值进行修改,但是这并不会触发组件的重新渲染
React.forwardRef
ref的一个常见用法是用来引用dom元素,从而获取dom元素信息,例子:
const App = () => {const refDom = useRef(null)useEffect(() => {console.log(refDom.current.nodeName)}, [])return <h1 ref={refDom}>hello</h1>}
但如果ref引用的是一个函数组件,一般情况下函数组件是无法像上面的dom元素那样接受ref参数的,此时需要使用
React.forwardRef转换函数组件:const Child = forwardRef((props, ref) => {return (<div>child<h1 ref={ref}>child header</h1></div>)})const App = () => {const refDom = useRef(null)useEffect(() => {console.log(refDom.current.nodeName)}, [])return <Child ref={refDom} />}
上面的代码简单解释就是:
Child的函数组件经forwardRef转换,使得ref引用能够传入
- refDom引用由父组件App产生,通过ref参数传给子组件Child
- Child将其和自身的h1元素绑定,这就使得父组件App能操作子组件当中的h1元素。
useImperativeHandle
有时候组件想自定义自身被引用的时候的返回值,譬如说上面App引用Child组件的时候,Child组件仅仅想提供一些dom的操作方法,而非自身的dom元素,此时可以使用useImperativeHandle:
上面的例子中,Child组件只想暴露一个改变h1内容的函数,就可以使用const Child = forwardRef((props, ref) => {const refH1 = useRef(null)useImperativeHandle(ref, () => {return {changeText(text) {refH1.current.innerText = text}}})return (<div>child<h1 ref={refH1}>child header</h1></div>)})const App = () => {const refDom = useRef(null)return (<div><Child ref={refDom} /><button onClick={() => {refDom.current.changeText(Math.random())}}>change</button></div>)}
useImperativeHanlde来定义返回的内容useContext
context的意思是上下文,通俗一点来说就是环境。在React的语境中,上下文意味着一组在一定范围内的组件能共享的一组数据,可以理解为组件被包裹在这个数据的环境当中。
React中上下文的使用React.createContext来进行创建,对应上下文的Provider**属性为一个组件,Provider组件内的所有子组件都能共享上下文的数据。
子组件中使用context的方法
Provider组件树内的子组件要想使用对应的context,可以有三种方法:
.contextType
这种方法是针对类组件,通过将context赋值给类组件的 contextType 属性,可以在自身方法中使用 this.context 获取到上下文的对应值了
class A extends Component {handleClick = () => {this.context.setCount(this.context.count+1)}render() {return <div onClick={this.handleClick}>child, {this.context.count}</div>}}A.contextType = Contextconst App = () => {const [count, setCount] = useState(0)return (<Context.Provider value={{count ,setCount}}><div >parent {count}<A /></div></Context.Provider>)}
Context.Consumer
Consumer组件内接受一个函数组件,此函数参数即为context值,使用方法如下
const Context = createContext(null)class A extends Component {render() {return <div>child,<Context.Consumer>{value => value.count}</Context.Consumer></div>}}const App = () => {const [count, setCount] = useState(0)return (<Context.Provider value={{count ,setCount}}><div >parent {count}<button onClick={() => setCount(count+1)}>add</button><A /></div></Context.Provider>)}
useContext
这种方法针对的是函数组件,在函数组件内订阅对应context,只需要使用useContext即可订阅更新,使用方法如下:
const Context = createContext(null)function A() {const {count} = useContext(Context)return <div>child, {count}</div>}const App = () => {const [count, setCount] = useState(0)return (<Context.Provider value={{count ,setCount}}><div >parent {count}<button onClick={() => setCount(count+1)}>add</button><A /></div></Context.Provider>)}
注意点
当context变化时,订阅它的所有组件都会触发重新渲染,即便其父组件没有进行任何重新渲染的操作,例子如下
const Context = createContext(null)const A = memo(() => {console.log('A render')return <div>A<B /></div>})function B() {console.log('B render')const {count} = useContext(Context)return <div>B, {count}</div>}const App = () => {const [count, setCount] = useState(0)return (<Context.Provider value={{count ,setCount}}><div >parent {count}<button onClick={() => setCount(count+1)}>add</button><A /></div></Context.Provider>)}
此外,类组件来说,shouldComponentUpdate不能控制context变化引发的更新。这意味着即便shouldComponentUpdate返回值为false,context变化的情况下组件仍然会更新
useReducer
接受一个reducer:
(state, action) => newState,reducer中会根据不同的action返回不同的state,简而言之就是对一个state的所有操作都集中到这个reducer上面来
useReducer接受三个参数:
- reducer:负责根据不同的action返回不同的state
- initArg:作为传入init函数的初始参数
- init:在初次调用useReducer时,会调用此函数并传入initArg得到state的初始值,如果init不存在,则会直接返回initArg
```jsx
const initialState = 0;
function reducer(state, action) {
switch (action.type) {
case ‘increment’:
return {number: state.number + 1};
case ‘decrement’:
return {number: state.number - 1};
default:
throw new Error();
}
}
function init(initialState){
return {number:initialState};
}
function Counter(){
const [state, dispatch] = useReducer(reducer, initialState,init);
return (
) }<>Count: {state.number}<button onClick={() => dispatch({type: 'increment'})}>+</button><button onClick={() => dispatch({type: 'decrement'})}>-</button></>
``` 比起useState,useReducer更适合处理复杂的state,因为它能汇集有关state的各种复杂操作,用户只需要dispatch即可,而state则需要返回所有操作state先关的回调给用户。
