背景
hooks对我们习惯了生命周期的前端同学来说,有一些很难理解的地方,e.g.
1 什么是side effect?
2 hooks如何调用组件生命周期的钩子?
3 为什么需要手动优化性能,怎么手动优化性能?
...
本文主要是围绕hooks的使用场景,react性能优化和react引入的一些概念一一说明,适合有了解过react hooks的同学阅读。
side effect
中文翻译为"副作用", 导致一开始在使用react的时候非常困惑(总感觉"副作用"应该是不好的结果,应该避免),后来看到一个非常好的例子解释了side effect:
let counter = 0
// with side effect
function fn1() {
++counter
return counter
}
// without side effect
function fn2() {
return counter + 1
}
fn1和fn2作用都是返回counter+1的值, 但fn1产生了side effect(counter的值变化了)。side effect(不一定是意外产生的,也不一定是负面的,甚至是我们期待的行为).对应的在react的例子中,例如: 我们通过点击事件修改了count的值,产生了side effect(dom更新了):
import React, { useState } from 'react'
function Example() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
react 组件渲染流程
在了解 hooks 之前我们需要先了解下react functional component的渲染流程:
1 render: 创建react组件(上面的myComponent就是render函数);
2 reconciliation: 也就是virtualDom的前后对比;
3 commit: 在2的前提下更新有变化的dom;
而以下场景会导致re-render(重新执行以上渲染流程),实现dom更新(这里需要注意dom是否更新跟virtualDom有关 ,即使re-render也不一定会更新DOM):
1 props的修改;
2 state的更新;
3 context对象的更新;
4 父元素的re-render;
useState
最简单也是我们最常用的hook,通过useState可以获取一个state初始值和修改state的方法。而修改state会触发组件的re-render(如果修改后的值和修改前是同一个值,则不会触发),达到更新dom的目的。
re-render的优化是react框架本身性能优化中最重要的一条,后面在讲优化的时候再详细说明。
useEffect
useEffect是react提供给我们使用side effect的钩子,useEffect接受两个参数,第一个可能包含side effect的函数,第二个是触发第一个参数的依赖项:
一般可以在以下场景使用:
1 初始化获取服务端数据(componentDidMount, 首次渲染完成后触发), e.g
const [name, setName] = useState('')
useEffect(() => {
//get async data
getData()
.then(data => {
// doSomething
})
},[])
2 监听某个(或多个)state的修改做出对应的动作,e.g.
const [count, setCount] = useState(0)
useEffect(() => {
console.log(`current count is ${count}`)
}, [count])
3 在组件销毁之前(componentWillUnmount)将一些跟组件相关的事件销毁(e.g. 轮询,滚动事件监听等), e.g.
const [count, setCount] = useState(0)
useEffect(() => {
let timmer = setInterval(() => {
// doSomething
}, 1000)
return () => {
clearInterval(timmer)
}
}, [count])
4 想在每次组件更新(componentDidUpdate)的时候都触发, e.g.
const [count, setCount] = useState(0)
useEffect(() => {
console.log(`current count is ${count}`)
})
useContext
useContext接收上层组件传递context的hook。
是react提供给组件间通信的 hook。父组件创建createContext并初始化并导出,通过provider向下传递value ,子孙组件引入父组件的context对象,并通过useContext消费value,e.g.
// 父组件 Parent
export const SomeContext = createContext(null)
const [name, setName] = useState('vb')
const [count, setCount] = useState(0)
return (
<SomeContext.Provider value={{name, count}}>
<Children />
</SomeContext.Provider>
)
// 某个子孙组件
import {SomeContext} from 'Parent'
let {name, count} = useContext(SomeContext)
通过父元素导出context对象会非常奇怪,一般会在父目录下建一个context manager文件对context对象进行统一管理。
useReducer
An alternative to useState。
也是修改state的一个hook, useState的另外一种用法,可以提前内置一些state的方法,e.g.
function reducer(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>
</>
)
}
useCallback && useMemo && memo
useCallback和useMemo都是优化性能的hook。区别是useCallback返回一个memoized函数,useMemo返回一个memoized值。
memo是react提供的HOC(高阶组件 higher order component),主要作用是提供组件的 memoized。
之所以将memo(不属于hooks)放到这里面来讲,是因为他们(memo, useMemo)都可以用来缓存组件。e.g.
// memo
function Children(props) {
return (
<div>my name is ${props.name}</div>
)
}
function areEqual(prevProps, nextProps) {
// 计算并返回是否需要渲染的布尔值
if(prevProps.name === nextProps.name) {
return true
}
return false
}
// areEqual方法可以不传,默认是浅比较之前的props和之后的props是否一致
export default React.memo(Children,areEqual)
// useMemo
function Children(props) {
return useMemo(() => (
<div>my name is ${props.name}</div>
), [props.name])
}
而useMemo也可以替代useCallback实现缓存函数的功能,e.g.
useCallback(fn, deps) === useMemo(() => fn, deps)
我通过以下问题和场景解释下如何使用useMemo, useCallback,memo:
1 首先的问题就是memoized怎么读(mem-o-ize);
2 memoization是什么?
可以先简单理解为缓存。memoization的篇幅太长,也不只是react优化的手段,在所有项目中都可以使用的性能优化手段,我另开了一篇文章有详细介绍。
3 为什么要缓存函数(useCallback)?
缓存值我能理解,re-render时候不用再执行该函数/render,减少计算损耗。但不管是否有缓存这个函数,始终都是要再执行一遍,缓存有什么意义呢?其实这里的主要作用还是为了解决react对依赖项浅比较结果的问题,e.g.
// parent
function Parent() {
const [name, setName] = useState('vb')
const onChildClick = useCallback(() => {
console.log('Children clicked')
}, [name])
return (
<Child click={onChildClick}>
)
}
// children
function Children({onChildClick}) {
return (
<div onClick={onChildClick}></div>
)
}
export default React.memo(Children)
如果不用useCallback的话,父组件的每次re-render都会导致子组件的re-render,因为父组件每次渲染时候onChildClick都会创建一个新的引用。
3 为什么需要优化?
当组件re-render时候,某些组件(或者组件里面的某个计算, 或者子组件)我们可能并不需要全部重新执行一遍,额外的re-render会导致性能下降。
4 分别在什么场景使用(useCallback, useMemo, memo)?
其实我觉得跟Array对象(forEach, map, filter, etc)一样,我们需要的是在不同的语境下使用对应的方法。
memo: 缓存组件;
useMemo: 缓存计算值;
useCallback: 缓存函数;
useRef
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。返回的 ref 对象在组件的整个生命周期内持续存在。
整个生命周期的意思是即使render也会可以读取到之前设置的值,常用于两种场景:
一、缓存初始化(上一个render)的计算值,在自定义hook中也非常常用。e.g. 组件用来展示与上一次的距离值:
function MyComponent(props) {
// 初始值
const preCounter = useRef(0)
const [distance, setDistance] = useState(0)
useEffect(() => {
setDistance(props.count - preCounter)
preCounter.current = props.count
}, [])
return (
<div>与上一次的距离为: {distance}</div>
)
}
二、 用于接受DOM的ref值,e.g.
const inputRef = useRef()
return (
<div ref={inputRef}></div>
)
当DOM更新时候的时候,React都会将ref对象的.current属性设置为相应的DOM节点.
useImperativeHandle
useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用。
其实看到这个定义有点懵,我们先从前因分析下, 首先 functional component 是不支持直接在子组件上挂载refs的,e.g.
// error: Property 'ref' does not exist on type 'IntrinsicAttributes & MyTestProps & { children?: ReactNode; }'
function Parent() {
const childrenDom = createRef()
return (
<Children ref={childrenDom}></Children>
)
}
我们需要通过forwardRef来创建子组件接受refs转发,e.g.
// children
cosnt Children = forwardRef((props, ref) => {
return (
<div ref={ref}>my name is vb</div>
)
})
// parent
function Parent() {
const childrenRef = createRef()
return (
<Children ref={childrenRef}></Children>
)
}
在回到useImperativeHandle,当我们希望父组件调用子组件的方法或者实例时候,就可以:
// children
cosnt Children = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
test: () => {console.log('my name is vb')}
myName: 'vb'
}))
return (
<div ref={ref}>my name is vb</div>
)
})
// parent
function Parent() {
const childrenRef = useRef()
useEffect(() => {
childrenRef.current.test() // my name is vb
console.log(childrenRef.current.myName) // vb
}, [])
return (
<Children ref={childrenRef}></Children>
)
}
useLayoutEffect
类似useEffect,两者主要有两个差别:
1 userEffect是异步的,useLayoutEffect是同步执行的;
2 执行时机的区别:
useEffect: 在浏览器渲染完成后执行;
useLayoutEffect: 在浏览器渲染之前执行,并在同步执行完毕后再执行渲染。
其实就是useLayoutEffect会阻塞渲染流程,并在得到最终计算结果后再渲染出来;而useEffect不会阻塞,但可能会出现闪烁(闪烁有可能是正常的需求,e.g. 时间展示),但不会阻塞浏览器的渲染流程。
官方建议尽量使用useEffect。我暂时没在业务上使用过useLayoutEffect,如果后面有遇到再深入说明下。
useDebugValue
需要安装一个谷歌插件“React Developer Tools”,方便在chrome上调试。
custom hook
自定义hook只有两个规则:
1 use开头的函数;
2 在自定义hook的顶层调用react hooks;
凡是涉及到状态的公用函数,我们都可以自定义hook(使用自定义hook会更加符合react的开发风格)。
1 获取前一个state的值, e.g.
function usePrevious(state) {
const preValRef = React.useRef()
React.useEffect(() => {
preValRef.current = state
}, [])
return preValRef.current
}
2 我们需要区分一般的函数和自定义hook函数,在hook函数中的state变化是会更新到调用者身上的,e.g. 异步请求的自定义hook:
function getData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({name: 'vb'})
}, 3000)
})
}
function useRequest() {
const [data, setData] = useState({})
useEffect(() => {
getData()
.then(data => {
setData(data)
})
}, [])
return data
}
function MyComponent() {
const data = useRequest()
useEffect(() => {
console.log(data.name) // 先打印 undefined, 3000ms后再打印 vb
}, [data])
return (
<div>{data.name}</div>
)
}
自定义hook注意问题:
1 不同的组件调用同一个hook,并不会共享状态,都是单独的实例;
注意事项
1 不要用useState/useReduce等hook去将props属性初始化给一个state。
只会在初始化的时候执行,在props修改的时候不会修改state,同时也会造成额外的负担。
2 在异步方法中,需要注意可能不会合并处理多个state的修改;e.g.
const onClick = async () => {
setLoading(true)
setData(null)
// batched render happen
const res = await getData()
setLoading(false)
// render with bad state
setData(res)
};
console.log("render", loading, data)
//out put
render false null
// click
render true null
render false null // <-- bad render call
render false {time: 1545903880314}
// click
render true null
render false null // <-- bad render call
render false {time: 1545904102818}
问题记录
2 为什么react hooks不再实现componentWillMount的生命周期钩子,让我们在渲染之前请求异步数据后再进行渲染?
我们习惯于在初始化的时候获取数据(e.g. react的componentWillMount,vue的beforeCreated),这其实是一个思维陷阱,习惯让我们以为在render之前请求数据就只渲染一遍,但其实异步获取的数据,不管网络和服务器速度多快,仍会在初始化渲染之后再根据异步数据重新渲染一遍。
3 为什么对于基本类型使用useState hook修改不会导致useEffect死循环(第二个参数为空,useEffect会执行两次), 但对object却会出现死循环?
// side effect执行两次
const [name, setName] = useState('')
useEffect(() => {
console.log(name) // '', 'vb'
setName('vb')
})
// 无限循环
const [name, setName] = useState({name: ''})
useEffect(() => {
setName({name: 'vb'})
})
再试验一下,其实这个问题是不对的,如果object是同一个引用的话,也不会死循环, e.g
let user = {name: ''}
const [name, setName] = useState(user)
useEffect(() => {
user.name = 'vb'
setName(user)
})
这里的问题在于是否会触发side effect,关于这块没找到有相关的介绍文章,只能翻源码(react现在最新版本各种fiber,lane,而且还是Flow写的,对我来说太难阅读了),看到在 dispatchAction 方法下有一个解释:
If the new state is the same as the current state, we may be able to bail out entirely.
如果新的state和当前state是一样的,就可能(还有null值的特殊处理)会放弃
其实就是在side effect之前会先进行比对(Object.is方法进行比对,如果是引用类型,需要是同一个引用对象,如果是基本类型,则需要===),如果是没有变化的话就不会触发。
6 为什么 react 不在框架内部实现re-render相关优化,而要开发者手动处理?
暂时没找到有说明的文章 #TODO
// 可能原因
1 计算所有可能导致re-render的方法(props, state,context对象是否有变化)计算量很大,会影响react性能。
2 re-render在大部分场景下性能消耗不是问题(最后还是有virtual阻止性能消耗很大的dom渲染);
2 可能 hooks 的开发方式无法实现部分re-render的检查(例如父元素的更新是否触发所有子元素的更新);
7 我是否该用useCallback包裹所有的inline-function?
之前听到某个童鞋说不用判断,直接用useCallback包裹所有的inline-function,其实不太需要(还会造成额外的负担)。useCallback只是缓存函数避免浅比较导致re-render。只有在需要浅比较且不希望该函数导致re-render才需要usecallback;
8 关于useEffect的应用场景,我之前偶尔会用useEffect做序列化的工作。e.g.
const [name, setName] = useState('')
useEffect(() => {
setName(serialize(name))
},[name])
return (
<div>my name is {name}</div>
)
除了造成额外的re-render之外,最重要的问题还是要理解side effect应该是state修改后导致的场景。
参考文档
1 stackoverflow.com/questions/4…
2 A Complete Guide to useEffect: overreacted.io/a-complete-…
3 React Hooks: async function in the useEffect: dev.to/danialdezfo…
4 React Hook 中 createContext & useContext 跨组件透传上下文与性能优化: www.ptbird.cn/react-creat…
5 React Hooks - Understanding Component Re-renders: medium.com/@guptagarud…
6 Your Guide to React.useCallback(): dmitripavlutin.com/dont-overus…