一、为什么要有Hook
下面有这么个场景, 父组件想要获取子组件的input标签,因为ref不属于props的属性,所以子组件通过this.props.ref会报错import React, { Component, createRef } from 'react'
class Sub extends Component {
render() {
return <input ref={this.props.ref} />
}
}
export default class Parent extends Component {
// 创建ref对象
input = createRef()
// 获取焦点
focus = () => {
this.input.current.focus()
}
render() {
return <Sub {...this.props} ref={this.input} />
}
}
我们可以使用React.forwardRef进行转发。
先来看个高阶组件ref转发的用法
import React, { createRef, forwardRef, Component } from 'react'
// 增强组件的函数
const insertLog = WrappedComponent => {
class Log extends Component {
render() {
// 把forwardRef取出来 传递给被增强的组件
const { forwardedRef, ...props } = this.props
return <WrappedComponnet {...props} ref={forwardedRef} />
}
}
// 通过Log把转发ref递给被包裹组件
return forwardRef((props, ref) => <Log {...props} forwardedRef={ref} />)
}
class Sub extends Component {
input = createRef()
focus = () => {
// focus ⽅法执⾏时会让 input 元素聚焦。
this.input.current.focus()
};
render() {
return <input {...this.props} ref={this.input} />
}
}
export default class Parent extends Component {
state = {
value: ''
}
input = createRef() // 引用子组件实例, 便于调用实例上方法
onFocus = () => {
this.input.current.focus() // 调用子组件实例上的方法
}
onChange = (e) => {
this.setState({value: e.target.value })
}
Wrap = insertLog(Sub)
render() {
const wrap = this.Wrap
return (
<>
<Wrap onChange={this.onChange} value={this.state.value} ref={this.input} />
<button onClick={this.onFocus}>点击聚焦</button>
</>
)
}
}
graph
Parernt --> Wrap --> Log --> WrappedComponnet(Sub)
再来看一下hooks的写法
import React, { createRef, forwardRef } from 'react'
// 子组件
const Sub = forwardRef((props, ref) => {
return <input {...props} ref={ref} />
})
// 父组件
export default class Parent extends React.Component {
input = createRef()
// 通过input.current就可以获取到子组件的input标签了
onFocus = () => {
this.input.current.focus()
}
render() {
return <>
<Sub ref={this.input} />
<button onClick={this.onFocus}>点击聚焦</button>
</>
}
}
graph TD
Parent --> Sub
这样嵌套的问题就解决了 代码也简洁了很多
二、常见的hook
1.useState
const [state, setState] = useState(initialState)
useState返回一个state和它的更新函数 可以通过这个更新函数setState 去改变state的值
方式有两种
-
直接赋值 直接为之前的state重新赋值为newVal setState(newVal)
-
通过函数赋值 接收一个先前的state, 并返回更新后的值 setState(prevState => prevState + 1)
export default function UseState() {
// 返回一个count当作数据
// setCount是一个函数用于改变数据
// useState(0) 相当于将count设置初始值为0
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count+1)}>点击更新count(直接更新) --- {count}</button>
<button onClick={() => setCount(prevState => prevState + 1)}>点击更新count(函数式更新)--- {count}</button>
)
}
2. useEffect
①useEffect在组件初次渲染和更新的时候执行第一个回调函数,
②有时候我们不希望它每次更新执行,可以通过第二个依赖项来控制, 只有依赖项变化第一个回调函数才会执行
③有时候我们希望执行完回调函数后清除副作用, 那么可以返回一个函数用来做清除 操作
useEffect(() => {
return () => {
}
}, [])
上面提到了副作用, 那么什么是副作用呢,就是在调用函数之后,还做了其他的事情, 下面举一个例子
useEffect(() => {
let [count, setCount] = useState(0)
useEffect(() => {
// 每隔一秒钟让count + 1
let timer = setInterval(() => {
setCount(count + 1)
}, 1000);
})
return <span>{count}</span>
})
刚开始我们发现count正常的变化,等过了一段时间后, 显示开始出现鬼畜的效果,这是因为useEffect中的回调函数每次更新都会生成一个新的定时器,这样就导致了定时器越来越多,就出现了显示异常的后果,所以我们要返回一个函数用来清除这个副作用
useEffect(() => {
let [count, setCount] = useState(0)
useEffect(() => {
// 每隔一秒钟让count + 1
let timer = setInterval(() => {
setCount(count + 1)
}, 1000);
// 这里用来清除这一次定时器
return () => {
clearInterval(timer)
}
})
return <span>{count}</span>
})
function UseEffect() {
const [count, setCount] = useState(0)
// 相当于componentDidMount和componentDidUpdate 只要本组件有更新,里面的回调函数就会调用
useEffect(() => {
console.log(`mount update: ${count}`)
})
useEffect(() => {
console.log(`mount: ${count}`)
// 第二个参数是依赖项, 只有当依赖项发生改变,才会继续执行, 这里是空数组 所以只执行第一次 相当于componentDidMount
}, [])
useEffect(() => {
console.log(`mount + update count: ${count}`) // 只要count发生变化,才会继续执行
}, [count])
return (<button onClick={() => setCount(c => c + 1)}>父组件+1 {count}</button>)
}
3. useRef
const refContainer = useRef(initVal)
- 返回一个对象, 改对象只有一个current属性,初始值为initVal
- 当更新current值,组件是不会重新渲染的, 这点和state不同
- 返回的对象可以绑定在dom节点上,在挂载完毕后可以拿到他
下面是一个获取dom元素的例子
import React, { useRef, useEffect } from 'react'
export default function UseRef() {
console.log('render')
let refObj = useRef(null)
console.log('在渲染之前获取', refObj)
useEffect(() => {
console.log('在dom元素挂载之后获取', refObj)
})
const handleClick = () => {
console.log(count.current)
count.current = count.current + 1
}
return (<>
<button onClick={() => refObj.current.focus()}>点击input获取焦点</button>
<input ref={refObj} />
<div>
<button onClick={handleClick}>点击增加count --- {count.current}</button>
</div>
</>)
}
在我们点击增加count的按钮后, 触发了handleClick函数 发现count.current 在变化,但是render没有重新打印, 说明UseRef函数没有被重新执行,而useState则是每次改变state,函数就会重新调用 并且渲染最新的状态
所以我们可以用它来模拟componentDidMount和componentDidUpdate
export default function UseRef() {
const container = useRef(false)
let [count, setCount] = useState(0)
useEffect(() => {
if(container.current) {
// 模拟componentDidUpdate
console.log('DidUpdate')
}else {
// 模拟componentDidMount
console.log('DidMount')
container.current = true
}
})
return (<div>模拟生命周期
<button onClick={() => setCount(count + 1)}>+1</button>
<span>count: {count}</span>
</div>)
}
4. forwardRef
forwarRef((props, ref) => {})
- forwardRef会创建一个组件
- 组件可以接收到父组件传递的ref
- 子组件可以把ref挂在自己的dom元素上
- 父组件通过ref就能获取到该dom元素
export default function Parent() {
let sonRef = useRef(null)
return (<>
<Son ref={sonRef}></Son>
<button onClick={() => sonRef.current.focus()}>点击获取子组件input焦点</button>
</>
)
}
const Son = forwardRef((props, ref) => {
return <>
<input ref={ref} />
</>
})
5. useImperativeHandle
它和forwardRef经常配套使用,有时候我们不希望父组件操作子组件ref的过多dom属性, 就需要用useImperativeHandle, 它用来限制子组件暴露的信息, 只有他定义的第二个参数的属性和方法,父组件才能拿取到
useImperativeHandle(ref, createHandle, [deps])
- ref: 定义暴露哪个ref
- createHandle: 定义暴露的信息
- deps: 当前依赖的列表,当其中的依赖发生变化时, 才会重新将子组件的实例属性输出到父组件
export default function Parent() {
let sonRef = useRef(null)
useEffect(() => {
console.log('parent')
})
return (<>
<Son ref={sonRef}></Son>
<button onClick={() => sonRef.current.focus()}>点击获取子组件input焦点</button>
<button onClick={() => console.log(sonRef.current)}>点击查看子组件绑定的ref</button>
</>
)
}
const Son = forwardRef((props, ref) => {
useEffect(() => {
console.log('son')
})
useImperativeHandle(ref, () => ({
focus: () => {
ref.current.focus()
}
}), [])
return <>
<input ref={ref} />
</>
})
当点击第二个按钮 可以看到只能访问定义的focus函数
6. useMemo和useCallback
先看一个例子
const UseCallbackSub = ({value, onChange}) => {
console.log('子元素发生了渲染value', value)
return <input onChange={onChange} value={value} type="number" />
}
export default function UseCallback() {
const [count, setCount] = useState(0)
const [value, setVal] = useState('')
const onChange = (e) => {
setVal(e.target.value)
}
return (
<>
<button onClick={() => setCount(count + 1)}>+1</button>
<UseCallbackSub value={value} onChange={onChange}></UseCallbackSub>
<div>value: {value}</div>
</>
)
}
父组件更新了count,导致重新渲染 也会引起子组件的渲染,但是我们子组件的props没有发生变化,这样的话这次渲染就浪费了性能,那么我们怎么根据props的变化控制子组件的重新渲染呢?
const UseCallbackSub = memo(({value, onChange}) => {
console.log('子元素发生了渲染value', value)
return <input onChange={onChange} value={value} type="number" />
})
我们可以通过memo的方式来进行控制,memo会对函数式的所有props进行对比, 现在我们传递的props是onChange和value 当onChange和value发生变化后, 我们才更新子组件, 不变的话,始终返回上一次渲染过的组件,这里利用了缓存.
但是这时候发现子元素还是重新渲染了, 问题就在onChange, 每次父组件重新渲染导致了每次的onChange函数都是新的引用值,这样每次给子组件传递的onChange就都不一样, 所以我们需要用useCallback给onChange函数包裹一下 ,
const onChange = useCallback((e) => {
setVal(e.target.value)
}, [])
- 第一个参数缓存的函数
- 第二个参数依赖项, 当依赖项发生改变时,生成新的函数, 否则用之前缓存过的
如果我们想直接在memo里控制默认不比较onChange的话 也可以这么写
const UseCallbackSub = memo(({value, onChange}) => {
console.log('子元素发生了渲染value', value)
return <input onChange={onChange} value={value} type="number" />
// 如果之前的value和这次的value相同的话, 子组件不会重新渲染,走缓存
}, (prev, cur) => prev.value === cur.value)
7.useContext
import React, { useState, createContext, useContext } from "react";
const Context = createContext()
首先通过createContext生成Context对象, Context对象可以跨层级传递变量,实现共享
使用Context的时候 将组件用 Context.Provider包裹, 并传入value value就是你时你需要传递的变量
<Context.Provider value={store}>
组件
</Context.Provider>
export default function Parent() {
const [count, setCount] = useState(0)
const store = {
count, setCount
}
return (
<Context.Provider value={store}>
<button onClick={() => setCount(count + 1)}>+1 {count}</button>
<Sub1 />
</Context.Provider>
)
}
当组件需要使用Context时,通过useContext(Context)可以得到传递过来的value
function Sub1() {
const ctx = useContext(Context)
return (<>
<button onClick={() => ctx.setCount(c => c + 1)}>
Sub1能通过 Context访问数据源 {ctx.count}
</button>
<Sub2 />
</>)
}
function Sub2() {
const ctx = useContext(Context)
return (<>
<button onClick={() => ctx.setCount(c => c + 1)}>
Sub2能通过 Context访问数据源 {ctx.count}
</button>
</>)
}
无论嵌套多少层组件,都可以通过Context拿到传递的值
三、useState和useEffect实现
1.useState
- 本质都用到了闭包
- 通过全局的一个数组储存每次的状态值,用index作为游标 当组件重新渲染后,index归为0 取出之前缓存的状态
import ReactDOM from "react-dom";
// 定义一个缓存所有状态的数组
let state = [];
// 定义其实索引
let index = 0;
function myuseState(initVal) {
// 记录这个state的索引 ,因为index会一直变化
let currentIndex = index
// 第一次state[index]为undefined 取 初始值, 之后渲染会拿第一次缓存的值
state[currentIndex] = state[currentIndex] || initVal;
function dispatch(newVal) {
// 这里用到了闭包,改变当前state的值为newVal
state[currentIndex] = newVal;
// 重新渲染
render()
}
// 把当前的state 和 对应的更新函数 return出去, 并把索引值+1
return [state[index++], dispatch];
}
function render() {
// 这里为了让下次渲染起始index为0 这样就可以拿到上次缓存的值
index = 0;
// 组件重新渲染
ReactDOM.render(<App />, document.getElementById('root'))
}
export default function App() {
let [count, setCount] = myuseState(0);
let [name, setName] = myuseState("xiaoming");
return (<div>
<div>count: {count}</div>
<div>name: {name}</div>
<div>
<button onClick={() => setCount(count + 1)}>count + 1</button>
<button onClick={() => setName('xiaohong')}>changeName</button>
</div>
</div>)
}
2. useEffect
和useState的实现大致相同,也需要在render里面清空index
import ReactDOM from "react-dom";
//定义一个缓存所有状态的数组
let state = [];
//定义其实索引
let index = 0;
function myuseState(initVal) {
// 记录这个state的索引 ,因为index会一直变化
let currentIndex = index
// 第一次state[index]为undefined 取 初始值, 之后渲染会拿第一次缓存的值
state[currentIndex] = state[currentIndex] || initVal;
function dispatch(newVal) {
// 这里用到了闭包,改变当前state的值为newVal
state[currentIndex] = newVal;
render()
}
// 把当前的state 和 对应的更新函数 return出去
return [state[index++], dispatch];
}
// 定义effect数组
let effects = []
// 定义effect索引,作为游标,来缓存多次的useEffect的依赖项
let effectIndex = 0
function myUseEffect(callback, deps) {
// 说明deps没传, 每次callback执行
if(!deps) {
callback()
}else {
if(effects[effectIndex]) { // 说明不是第一次
// 拿到上一次的依赖数组, 目的是和这一次的进行比较
let lastDeps = effects[effectIndex]
let same = deps.every((item, index) => item === lastDeps[index])
if(same) {
// 如果相同了, 代表依赖没有变化, 则让索引+1, 找到下一个useEffect
effectIndex++
}else {
// 如果变化了, 则把最新的依赖值赋给effects, 并让索引+1, 找到下一个useEffect
effects[effectIndex++] = deps
callback()
}
}else {// 说明是第一次渲染, 并让索引+1, 找到下一个useEffect
effects[effectIndex++] = deps
callback()
}
}
}
function render() {
// 这里为了让下次渲染起始index为0 这样就可以拿到上次缓存的值
index = 0;
// 同理, 让effectIndex为0 获取先前的deps依赖值
effectIndex = 0
// 组件重新渲染
ReactDOM.render(<App />, document.getElementById('root'))
}
export default function App() {
let [count, setCount] = myuseState(0);
let [name, setName] = myuseState("xiaoming");
myUseEffect(() => {
console.log('只触发一次', count)
}, [])
myUseEffect(() => {
console.log('只有count变化才会触发', count)
}, [count])
return (<div>
<div>count: {count}</div>
<div>name: {name}</div>
<div>
<button onClick={() => setCount(count + 1)}>count + 1</button>
<button onClick={() => setName('xiaohong'+Math.random())}>changeName</button>
</div>
</div>)
}
⚠记得把严格模式关闭,否则运行会出问题