React 学习笔记
HOOK
useState
const [state, setState] = useState(initialState)
返回一个 state,以及更新 state 的函数。
在初始渲染期间,返回的状态 (state
) 与传入的第一个参数 (initialState
) 值相同。
setState
函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。
setState(newState)
在后续的重新渲染中,useState
返回的第一个值将始终是更新后最新的 state。
import React, { useState } from 'react'
import ReactDOM from 'react-dom'
function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(count - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
)
}
ReactDOM.render(
<Counter initialCount={1} />,
document.getElementById('root')
)
如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。
import React, { useState } from 'react'
import ReactDOM from 'react-dom'
function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
console.log('rerender')
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Not Rerender1</button>
<button onClick={() => setCount(prevCount => prevCount)}>Not Rerender2</button>
<button onClick={() => setCount(count)}>Not Rerender3</button>
<button onClick={() => setCount(count + 1)}>Rerender</button>
</>
)
}
ReactDOM.render(
<Counter initialCount={1} />,
document.getElementById('root')
)
上面代码中几个 Not Rerender 按钮按下时是不会重新渲染组件的,因为值没有发生变化。
与 class 组件中的 setState
方法不同,useState
不会自动合并更新对象。你可以用函数式的 setState
结合展开运算符来达到合并更新对象的效果。
setState(prevState => {
// 也可以使用 Object.assign
return {...prevState, ...updatedValues};
});
import React, { useState } from 'react'
import ReactDOM from 'react-dom'
function Counter() {
const [obj, setObj] = useState({ a: 10, b: 20, c: [1, 2, 3] });
console.log('rerender')
function updateObject1(updatedValues) {
setObj(prevState => {
// 也可以使用 Object.assign
return {...prevState, ...updatedValues}
});
}
function updateObject2() {
const newObj = obj;
newObj.a = 50;
newObj.b = 60;
setObj(newObj);
}
return (
<>
Object: {JSON.stringify(obj)}
<button onClick={() => updateObject1({ a: 30, b: 40, c: [4, 5, 6] })}>update</button>
<button onClick={() => updateObject2()}>not update</button>
</>
)
}
ReactDOM.render(
<Counter />,
document.getElementById('root')
)
updateObject2 将无法对状态进行修改,因为无法感知到 Object 的变化,所以如果状态是一个对象或者数组,那么都只能通过深、浅拷贝的方式而不能通过赋值的方式更新。
useReducer
是另一种可选方案,它更适合用于管理包含多个子值的 state 对象。
惰性初始 state
initialState
参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
跳过 state 更新
调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is
比较算法来比较 state。)
需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo
来进行优化。
同步更新状态:有时候我们会多次调用setState
对状态进行更新,但是结果经常会不尽如人意,我们可以看看下面这个例子:
import React, { useState } from 'react'
import ReactDOM from 'react-dom'
function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
console.log('rerender')
// 只会加1
function update1() {
setCount(count + 1)
setCount(count + 1)
}
// 会加2
function update2() {
setCount(prevCount => prevCount + 1)
setCount(prevCount => prevCount + 1)
}
return (
<>
Count: {count}
<button onClick={update1}>Update1</button>
<button onClick={update2}>Update2</button>
</>
)
}
ReactDOM.render(
<Counter initialCount={1} />,
document.getElementById('root')
)
直观的写法我们可以连续调用两次 setState 函数,不过更多的是为了说明异步更新带来的数据不可预测问题。上述代码 update1 的效果是每次点击之后计数值只会加1,实际上第二个 setState 并没有等待第一个 setState 执行完毕就开始执行了,因此其依赖的当前计数值完全是错的。我们可以使用 update2 的写法状态计算函数来保证同步性。
useEffect
useEffect(didUpdate)
该 Hook 接收一个包含命令式、且可能有副作用代码的函数。
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
使用 useEffect
完成副作用操作。赋值给 useEffect
的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它在只有某些值改变的时候才执行。
清除 effect
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect
函数需返回一个清除函数。以下就是一个创建订阅的例子:
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除订阅
subscription.unsubscribe();
};
});
import React, { useEffect, useState } from 'react'
import ReactDOM from 'react-dom'
function Child() {
useEffect(() => {
const timer = setInterval(() => {
console.log('set Interval1')
}, 1000)
return () => {
// 组件卸载时被调用
clearInterval(timer)
}
})
useEffect(() => {
setInterval(() => {
console.log('set Interval2')
}, 1000)
})
return (
<>
<p>child component</p>
</>
)
}
function Parent() {
const [show, setShow] = useState(true)
return (
<>
{show &&
<Child />
}
<button onClick={() => { setShow(false) }}>remove child</button>
</>
)
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
)
为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。在上述示例中,意味着组件的每一次更新都会创建新的订阅。若想避免每次更新都触发 effect 的执行,就需要知道 effect 执行的时机。
effect 执行的时机
与 componentDidMount
、componentDidUpdate
不同的是,在浏览器完成布局与绘制之后,传给 useEffect
的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。
然而,并非所有 effect 都可以被延迟执行。例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的useLayoutEffect
Hook 来处理这类 effect。它和 useEffect
的结构相同,区别只是调用时机不同。
虽然 useEffect
会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。
effect 的执行条件
默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。
然而,在某些场景下这么做可能会矫枉过正。比如,在上一章节的订阅示例中,我们不需要在每次组件更新时都创建新的订阅,而是仅需要在 source
prop 改变时重新创建。
要实现这一点,可以给 useEffect
传递第二个参数,它是 effect 所依赖的值数组。更新后的示例如下:
useEffect(
() => {
const subscription = props.source.subscribe()
return () => {
subscription.unsubscribe()
}
},
[props.source],
);
此时,只有当 props.source
改变后才会重新创建订阅。下面是另一个例子,带有第二参数 show 的 effect 只在点击 switch 按钮的时候才会之后,而不带参数的 other 则会在每一次渲染都执行,带 [] 参数的 once 则之后在第一次渲染的时候执行一次。
import React, { useEffect, useState } from 'react'
import ReactDOM from 'react-dom'
function Parent() {
const [show, setShow] = useState(true)
const [count, setCount] = useState(1)
useEffect(() => {
setTimeout(() => {
console.log('show')
}, 0)
}, [show])
useEffect(() => {
setTimeout(() => {
console.log('other')
}, 0)
})
useEffect(() => {
setTimeout(() => {
console.log('once')
}, 0)
}, [])
return (
<>
{count}
{show &&
<h1>hello world</h1>
}
<button onClick={() => { setShow(!show) }}>switch</button>
<button onClick={() => { setCount(count + 1) }}>+</button>
</>
)
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
)
useContext
const value = useContext(MyContext)
接收一个 context 对象(React.createContext
的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>
的 value
prop 决定。
当组件上层最近的 <MyContext.Provider>
更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext
时重新渲染。
别忘记 useContext
的参数必须是 context 对象本身:
- 正确: useContext(MyContext)
- 错误: useContext(MyContext.Consumer)
- 错误: useContext(MyContext.Provider)
调用了 useContext
的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化。
如果你在接触 Hook 前已经对 context API 比较熟悉,那应该可以理解,useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>。
useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。
import React, { useContext, createContext } from 'react'
import ReactDOM from 'react-dom'
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
}
const ThemeContext = createContext(themes.dark)
function App() {
// 上层爷爷辈组件
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
)
}
function Toolbar() {
// 父组件,没有传任何属性
return (
<div>
<ThemedButton />
</div>
)
}
function ThemedButton() {
// 子组件,可以使用上层传入的 context
const theme = useContext(ThemeContext)
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
)
}
ReactDOM.render(
<App />,
document.getElementById('root')
)
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
useState
的替代方案。它接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的 dispatch
方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)
在某些场景下,useReducer
会比 useState
更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer
还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数。
以下是用 reducer 重写 useState
一节的计数器示例:
import React, { useReducer } from 'react'
import ReactDOM from 'react-dom'
const initialState = { count: 0 }
function reducer(state, action) {
console.log(state, action)
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
default:
throw new Error()
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</>
);
}
ReactDOM.render(
<Counter />,
document.getElementById('root')
)
React 会确保 dispatch
函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffect
或 useCallback
的依赖列表中省略 dispatch
。
指定初始 state
有两种不同初始化 useReducer
state 的方式,你可以根据使用场景选择其中的一种。将初始 state 作为第二个参数传入 useReducer
是最简单的方法:
const [state, dispatch] = useReducer(
reducer,
{count: initialCount}
);
React 不使用 state = initialState
这一由 Redux 推广开来的参数约定。有时候初始值依赖于 props,因此需要在调用 Hook 时指定。如果你特别喜欢上述的参数约定,可以通过调用 useReducer(reducer, undefined, reducer)
来模拟 Redux 的行为,但我们不鼓励你这么做。
惰性初始化
你可以选择惰性地创建初始 state。为此,需要将 init
函数作为 useReducer
的第三个参数传入,这样初始 state 将被设置为 init(initialArg)
。
这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利:
import React, { useReducer } from 'react'
import ReactDOM from 'react-dom'
function init(initialCount) {
return { count: initialCount };
}
function reducer(state, action) {
console.log(state, action)
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();
}
}
function Counter({ initialCount }) {
const [state, dispatch] = useReducer(reducer, initialCount, init)
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({ type: 'reset', payload: initialCount })}>
Reset
</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</>
);
}
ReactDOM.render(
<Counter initialCount={0} />,
document.getElementById('root')
)
跳过 dispatch
如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用Object.is
比较算法来比较 state。)
需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo
来进行优化。
useRef
const refContainer = useRef(initialValue)
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。
一个常见的用例便是命令式地访问子组件:
import React, { useRef } from 'react'
import ReactDOM from 'react-dom'
function TextInputWithFocusButton() {
const inputEl = useRef(null)
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus()
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
)
}
ReactDOM.render(
<TextInputWithFocusButton />,
document.getElementById('root')
)
本质上,useRef
就像是可以在其 .current
属性中保存一个可变值的“盒子”。
如果你将 ref 对象以 <div ref={myRef} />
形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current
属性设置为相应的 DOM 节点。然而,useRef()
比 ref
属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。
这是因为它创建的是一个普通 Javascript 对象。而 useRef()
和自建一个 {current: ...}
对象的唯一区别是,useRef
会在每次渲染时返回同一个 ref 对象。我们把上面的 demo 改造一下,在文本框中输入字符,然后查看打印信息。
import React, { useRef, useEffect } from 'react'
import ReactDOM from 'react-dom'
function TextInputWithFocusButton() {
const [inputText, setInputText] = useState('')
const inputEl = useRef(null)
const testRef = useRef({ arr1: 'value1' })
const normalObj = { current: { arr1: 'value1' } }
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus()
};
useEffect(() => {
console.log(`上一次的值 Ref = ${testRef.current.arr1}`)
console.log(`上一次的值 NormalObj = ${normalObj.current.arr1}`)
testRef.current.arr1 = inputText
normalObj.current.arr1 = inputText
console.log(`设置后 Ref = ${testRef.current.arr1}`)
console.log(`设置后 NormalObj = ${normalObj.current.arr1}`)
})
return (
<>
<input ref={inputEl} value={inputText} type="text" onChange={v => setInputText(v.target.value)} />
<button onClick={onButtonClick}>Focus the input</button>
</>
)
}
ReactDOM.render(
<TextInputWithFocusButton />,
document.getElementById('root')
)
Ref 的打印信息每次输出上一次的值的时候都会输出正确的上一次的值,因为 ref 对象的特性就是在组件的整个生命周期内保持不变,而普通变量在每次渲染的时候都会被重新赋值,所以每次普通变量在输出上一次的值的时候输出的总是他的初始值。
下面是一个实际的应用场景,我们在 useEffect 之后创建了一个定时器,这时候我们想要在某一个时刻去取消这个定时器,那么用 ref 对象来保存这个定时器的 id 就是一种很有效的方法。如果用普通变量的话,你想去清除的那个定时器的 id 很可能被重置了,就已经找不到了。
function Timer() {
const intervalRef = useRef()
useEffect(() => {
const id = setInterval(() => {
// ...
})
intervalRef.current = id
});
function handleCancelClick() {
clearInterval(intervalRef.current)
}
// ...
}
当 ref 对象内容发生变化时,useRef
并不会通知你。变更 .current
属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref (useCallback)来实现。
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle
可以让你在使用 ref
时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref
这样的命令式代码。useImperativeHandle
应当与 forwardRef
一起使用:
import React, { useRef, useImperativeHandle, forwardRef } from 'react'
import ReactDOM from 'react-dom'
const FancyInput = forwardRef((props, ref) => { // props 就算没有使用也不能省略,否则会报错
const inputRef = useRef()
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
}
}))
return (
<div>
<input ref={inputRef} type='text' />
<button onClick={() => inputRef.current.focus()}>click me!</button>
</div>
)
})
ReactDOM.render(
<FancyInput />,
document.getElementById('root')
)
本例中只是在组件自身中使用了 ref 对象,实际项目中,还会经常用在父组件要获取子组件的值,或者直接操作子组件的一些动作等,我把上面的例子改造了一下:
import React, { useRef, useImperativeHandle, forwardRef, useState } from 'react'
import ReactDOM from 'react-dom'
const FancyInput = forwardRef((props, ref) => { // props 就算没有使用也不能省略,否则会报错
const inputRef = useRef()
const [value, setValue] = useState('')
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
},
getValue: () => {
return value
}
}))
return (
<div>
<input ref={inputRef} type='text' value={value} onChange={(val) => setValue(val.target.value)} />
</div>
)
})
function ParentComponents() {
const inputRef = useRef()
return (
<div>
<FancyInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>click me!</button>
<button onClick={() => alert(inputRef.current.getValue())}>get text value!</button>
</div>
)
}
ReactDOM.render(
<ParentComponents />,
document.getElementById('root')
)
下面我们再把例子改造一下,演示一下第三参数的用法:
import React, { useRef, useImperativeHandle, forwardRef, useState } from 'react'
import ReactDOM from 'react-dom'
const FancyInput = forwardRef((props, ref) => { // props 就算没有使用也不能省略,否则会报错
const inputRef = useRef()
const [value, setValue] = useState('')
const [num, setNum] = useState(100)
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
},
getValue: () => {
return value
},
updateValue: () => {
setNum(Math.random())
}
}), [num]) // 此处会有警告,不推荐这么用,仅为演示第三参数用
return (
<div>
<input ref={inputRef} type='text' value={value} onChange={(val) => setValue(val.target.value)} />
</div>
)
})
function ParentComponents() {
const inputRef = useRef()
return (
<div>
<FancyInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>click me!</button>
<button onClick={() => alert(inputRef.current.getValue())}>get text value!</button>
<button onClick={() => inputRef.current.updateValue()}>update value!</button>
</div>
)
}
ReactDOM.render(
<ParentComponents />,
document.getElementById('root')
)
输入一些内容之后我们可以点击 “get text value!” 按钮,你会发现 alert 的是空白。然后,点击 “update value!”,再点击 “get text value!” 按钮,这时就会弹出你输入的内容了,加了第三参数之后,如果此参数不发生变化,那么在执行的时候里面的方法拿到的值都是旧值,不会更新。
useCallback
const memoizedCallback = useCallback(() => { doSomething(a, b) }, [a, b])
返回一个 memoized 回调函数。
把内联回调函数及依赖项数组作为参数传入 useCallback
,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。
下面的例子中,handleChildClick2
将被缓存起来,如果它的依赖 count2 不变,那么该函数就不会更新,由于函数更新与否我们不能直接看到,所以引入了set
,如果该函数改变那么该函数就会作为一个不同的值加入到 set,如果是没有改变将不会重新添加到 set 中。如果你点击 change count1 按钮那么 set1 和 count1 会变化,但是 set2 是不会变的;如果点击 change count2 按钮那么 set1、set2 和 count2 都会变化。
import React, { useCallback, useState } from 'react'
import ReactDOM from 'react-dom'
const set1 = new Set()
const set2 = new Set()
function Parent() {
const [count1, setCount1] = useState(0)
const [count2, setCount2] = useState(100)
function handleChildClick1() {
setCount1(count1 + 1)
}
const handleChildClick2 = useCallback(() => {
setCount2(count2 + 1)
}, [count2])
set1.add(handleChildClick1)
set2.add(handleChildClick2)
return (
<div>
<Child1 handleClick={handleChildClick1} />
<Child2 handleClick={handleChildClick2} />
<p>count1</p>
<div>{count1}</div>
<p>count2</p>
<div>{count2}</div>
<p>set1</p>
<div>{set1.size}</div>
<p>set2</p>
<div>{set2.size}</div>
</div>
)
}
function Child1({ handleClick }) {
return (
<div>
<p>Child1</p>
<button onClick={handleClick}>change count1</button>
</div>
)
}
function Child2({ handleClick }) {
return (
<div>
<p>Child2</p>
<button onClick={handleClick}>change count2</button>
</div>
)
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
)
每当 ref 被附加到一个另一个节点,React 就会调用 callback。
import React, { useState, useCallback } from 'react'
import ReactDOM from 'react-dom'
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
console.log(node)
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, [])
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
)
}
ReactDOM.render(
<MeasureExample />,
document.getElementById('root')
)
在这个案例中,我们没有选择使用 useRef
,因为当 ref 是一个对象时它并不会把当前 ref 的值的变化通知到我们。使用 callback ref 可以确保 即便子组件延迟显示被测量的节点(比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。
注意到我们传递了 []
作为 useCallback
的依赖列表。这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。
在此示例中,当且仅当组件挂载和卸载时,callback ref 才会被调用,因为渲染的 <h1>
组件在整个重新渲染期间始终存在。如果你希望在每次组件调整大小时都收到通知,则可能需要使用 ResizeObserver
或基于其构建的第三方 Hook。
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
返回一个 memoized 值。
把“创建”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
记住,传入 useMemo
的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect
的适用范畴,而不是 useMemo
。
如果没有提供依赖项数组,useMemo
在每次渲染时都会计算新的值。
你可以把 useMemo
作为性能优化的手段,但不要把它当成语义上的保证。将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo
的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo
,以达到优化性能的目的。
useMemo
与useCallback
的主要区别是一个返回一个值,一个是返回一个函数,下面看一个例子:
import React, { useMemo, useState } from 'react'
import ReactDOM from 'react-dom'
function TestComponent() {
const [count1, setCount1] = useState(2)
const [count2, setCount2] = useState(2)
function getCalculateValue1() {
return Math.pow(count1, 2) + Math.random()
}
const getCalculateValue2 = useMemo(() => {
return Math.pow(count2, 2) + Math.random()
}, [count2])
return (
<div>
<p>count1</p>
<div>{getCalculateValue1()}</div>
<p>count2</p>
<div>{getCalculateValue2}</div>
<button onClick={() => setCount1(count1 + 1)}>change count1</button>
<button onClick={() => setCount2(count2 + 1)}>change count2</button>
</div>
)
}
ReactDOM.render(
<TestComponent />,
document.getElementById('root')
)
当你在改变 count1 的时候 getCalculateValue2 用到参数并没有改变,所以 getCalculateValue2 值就不会改变,而普通函数返回的值则会在每次都要重新计算,即使只改变了 count2 与它并没有任何关系,如果计算过程开销比较大的话,就对性能有较大的影响。
useLayoutEffect
其函数签名与 useEffect
相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect
内部的更新计划将被同步刷新。
尽可能使用标准的 useEffect
以避免阻塞视觉更新。
如果你正在将代码从 class 组件迁移到使用 Hook 的函数组件,则需要注意 useLayoutEffect
与 componentDidMount
、componentDidUpdate
的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect
,只有当它出问题的时候再尝试使用 useLayoutEffect
。
如果你使用服务端渲染,请记住,无论useLayoutEffect
还是 useEffect
都无法在 Javascript 代码加载完成之前执行。这就是为什么在服务端渲染组件中引入 useLayoutEffect
代码时会触发 React 告警。解决这个问题,需要将代码逻辑移至 useEffect
中(如果首次渲染不需要这段逻辑的情况下),或是将该组件延迟到客户端渲染完成后再显示(如果直到 useLayoutEffect
执行之前 HTML 都显示错乱的情况下)。
若要从服务端渲染的 HTML 中排除依赖布局 effect 的组件,可以通过使用 showChild && <Child />
进行条件渲染,并使用 useEffect(() => { setShowChild(true); }, [])
延迟展示组件。这样,在客户端渲染完成之前,UI 就不会像之前那样显示错乱了。
在处理DOM的时候,当你的useEffect
里面的操作需要处理DOM,并且会改变页面的样式,就需要用这个,否则可能会出现出现闪屏问题,useLayoutEffect
里面的 callback 函数会在 DOM 更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制。
下面来看一个例子:
import React, { useEffect, useLayoutEffect, useState } from 'react'
import ReactDOM from 'react-dom'
function TestComponent() {
const [count, setCount] = useState(0)
useLayoutEffect(() => {
// useEffect(() => {
const pre = new Date().getTime()
while (Date.now() - pre < 500) {
// 一段比较耗时的操作
if (count === 0) {
setCount(Math.random())
}
}
}, [count])
return (
<>
<div onClick={() => setCount(0)}>{count}</div>
</>
)
}
ReactDOM.render(
<TestComponent />,
document.getElementById('root')
)
如果我们使用 useEffect
是会看见先变成0再变成随机数的过程,如果是一个改变样式布局的操作的话,你就会看到屏闪;如果是用useLayoutEffect
就不会看见这个过程,不过useLayoutEffect
确实是会阻塞浏览器,让浏览器感觉比较卡。
useDebugValue
useDebugValue(value)
useDebugValue
可用于在 React 开发者工具(React Developer Tools)中显示自定义 hook 的标签。
我们不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。
延迟格式化 debug 值
在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook,否则没有必要这么做。
因此,useDebugValue
接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。
例如,一个返回 Date
值的自定义 Hook 可以通过格式化函数来避免不必要的 toDateString
函数调用:
useDebugValue(date, date => date.toDateString());
import React, { useDebugValue, useState } from 'react'
import ReactDOM from 'react-dom'
function useFriendStatus() {
const [isOnline, setIsOnline] = useState(null)
const switchFun = () => {
setIsOnline(!isOnline)
}
// 在开发者工具中的这个 Hook 旁边显示标签
// e.g. FriendStatus: "Online"
useDebugValue(isOnline ? 'Online' : 'Offline')
// 提供一个格式化函数,此时有两个 debug 的参数,将会以数组的方式显示出来
// e.g. FriendStatus: ["Online", "true"]
useDebugValue(isOnline, isOnline => {
if (isOnline) {
return 'true'
} else {
return 'false'
}
})
// 如果不写 useDebugValue 将会显示 FriendStatus:,state 也是 null
return [isOnline, switchFun]
}
function TestComponent() {
const [isOnline, setIsOnline] = useFriendStatus()
return (
<>
{isOnline}
<div onClick={() => setIsOnline()}>switch</div>
</>
)
}
ReactDOM.render(
<TestComponent />,
document.getElementById('root')
)
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!