1) what is Hooks?
之前也有函数式组件,但是没有状态,无法保存数据,所以一直用类式组件
class MyCount extends Component {
state = {
count: 0,
}
componentDidMount() {
this.interval = setInterval(() => {
this.setState({ count: this.state.count + 1 })
}, 1000)
}
componentWillUnmount() {
if (this.interval) {
clearInterval(this.interval)
}
}
render() {
return <span>{this.state.count}</span>
}
}
- 引入Hooks函数,重写上述组件
function MyCountFunc() {
const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setCount(x => x + 1)
}, 1000)
return () => clearInterval(interval)
})
return <span>{count}</span>
}
- setCount代替之前 this.setState的功能,修改state数据,其实也是reducer的功能,useState也是useReducer实现的
- useEffect实现 componentDidMount 的功能;return 的回调函数实现 componentWillUnmount 的功能
2) State Hooks
- useState -- 值类型,每次传入的都是新的值
function MyCountFunc() {
const [count, setCount] = useState(0)
// setCount两种用法
// setCount(value)
// setCount(callback)
useEffect(() => {
const interval = setInterval(() => {
// setCount(count + 1) 值固定为1,不会变化
setCount(x => x + 1)
}, 1000)
return () => clearInterval(interval)
}, [])
return <span>{count}</span>
}
- useReducer -- 引用类型
如果state是一个对象,每次要求传递的state是一个新的对象,而且这个对象比较复杂,就不能像useState中的函数那样来修改,否则可能实现不了修改的目的(setCount(count + 1));就像redux中 Object.assign()、JSON.parse(JSON.stringify())
function countReducer(state, action) {
switch(action.type) {
case 'add':
return state + 1
case 'minus':
return state - 1
default:
return state
}
}
function MyCountFunc() {
const [count, dispatchCount] = useReducer(countReducer, 0)
useEffect(() => {
const interval = setInterval(() => {
dispatchCount({ type: 'minus' })
}, 1000)
return () => clearInterval(interval)
}, [])
return <span>{count}</span>
}
3) Effect Hook
function MyCountFunc() {
const [count, dispatchCount] = useReducer(countReducer, 0)
const [name, setName] = useState('firm')
/*
* 不添加 dependencies,每次组件内的 state(这里就是count、name)变化,都会update component
* 1. 所以第一次Mount Component,会输出 'effect invoked'
* 2. 之后每次Update Component,就会先执行上一次状态中useEffect的return 回调函数 => 'effect deteched' => 再执行这一次状态中的useEffect => 'effect invoked'
* 3. 切换组件时,Unmount Component,就只有 => 'effect deteched'
*/
useEffect(() => {
console.log('effect invoked')
return () => console.log('effect deteched')
}, [])
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<button onClick={() => dispatchCount({ type: 'add' })}>{count}</button>
</div>
)
}
/*
* useEffect和useLayoutEffect
* 1. 在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用
* 2. 可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。 它会在所有的 DOM 变更之后同步调用 effect。
* 所以,组件挂载时,Layout effect invoked => effect invoked
* 组件更新时, Layouteffect deteched、Layouteffect invoked => effect deteched、effect invoked
* 组件卸载时, effect deteched => Layouteffect deteched
*/
useEffect(() => {
console.log('effect invoked')
return () => console.log('effect deteched')
}, [count])
useLayoutEffect(() => {
console.log('Layout effect invoked')
return () => console.log('Layout effect deteched')
}, [count])
3) context Hook
- Context设计的目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题(黑夜模式)或首选语言(简体中文、英语...)。
- /lib下新建my-context.js
import React from 'react'
export default React.createContext('')
//=> 创建一个Context对象,初始值为''
//=> 当React渲染一个订阅了这个Context对象的组件,这个组件会从组件树中离自身最近的那个匹配的Provider中读取到当前的context值
- _app文件下
render() {
const { Component, pageProps } = this.props
return (
<Container>
<Layout>
<MyContext.Provider value={this.state.context}>
<Component {...pageProps} />
<button onClick={() => this.setState({ context: `${this.state.context}111`})}>update context</button>
</MyContext.Provider>
</Layout>
</Container>
)
}
- 组件b
const context = useContext(MyContext)
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<button onClick={() => dispatchCount({ type: 'add' })}>{count}</button>
<p>{context}</p>
</div>
)
4) Ref Hook
- 类式组件中ref的使用,获取DOM元素的节点
class MyCount extends Component {
constructor() {
super();
// 创建一个 ref 来存储 spanRef 的 DOM 元素
this.spanRef = React.createRef()
}
state = {
count: 0,
}
componentDidMount() {
// React 会在组件挂载时给 current 属性传入 DOM 元素,并在组件卸载时传入 null 值
this.refs.current
this.interval = setInterval(() => {
this.setState({ count: this.state.count + 1 })
}, 1000)
}
componentWillUnmount() {
if (this.interval) {
clearInterval(this.interval)
}
}
render() {
// 告诉 React 我们想把 <span> ref 关联到构造器里创建的 `spanRef` 上
return <span ref={this.spanRef}>{this.state.count}</span>
}
}
- 无状态组件引入ref,有了useRef就可以存储ref数据了
function MyCountFunc() {
// ....
const inputRef = useRef()
useEffect(() => {
// ...
console.log(inputRef)
// return
}, [])
return (
<div>
<input ref={inputRef} value={name} onChange={e => setName(e.target.value)} />
{/* ... */}
</div>
)
}
5) Hooks渲染优化
function MyCountFunc() {
const [count, dispatchCount] = useReducer(countReducer, 0)
const [name, setName] = useState('firm')
const config = {
text: `conut is ${count}`,
color: count > 3 ? 'red' : 'blue',
}
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<Child
config={config}
onButtonClick={() => dispatch({ type: 'add' })}
/>
</div>
)
}
function Child({ onButtonClick, config }) {
console.log('child render');
return (
<button onClick={onButtonClick} style={{ color: config.color }}>
{config.text}
</button>
)
}
问题
:当前组件,无论是状态中的count还是name发生变化,都会打印出'child render' ,为什么会这样?
Child是一个无状态组件,是否重新渲染是看传给它的props(即onButtonClick和config)是否变化
解决
:使用memo对Child组件进行优化一下
- React.memo
React.memo是一个高阶组件。
如果你的组件给定了一样的props得到同样的渲染结果,可以调用React.memo()将其包起来通过依赖项提升某些方面的性能。这就意味着React将跳过重新渲染过程,复用上次的渲染结果。
React.memo只影响props的改变。如果你包裹在memo中的组件有用到useState或者useContext,那么当state或者context变化是,组件仍将重新渲染。
默认情况下,它只会对props对象中的复杂对象进行浅层比较。若想要控制整个比较过程,可以自定义比较函数作为第二个参数传入。
- 注: memo的功能和类式组件中的shouldComponentUpdate()函数很像,但是比较函数在props相等时返会true,不等时返回false。
const Child = memo(function Child({ onButtonClick, config }) {
console.log('child render');
return (
<button onClick={onButtonClick} style={{ color: config.color }}>
{config.text}
</button>
)
})
再次测试,结果仍然是,无论count或者name变化时都输出'child props',why?
- count变化时,props中的config变化,Child组件中的color属性和text都变化,必然导致重新渲染,可为什么name变化,也会重新渲染Child?
- name是MyCountFunc组件的state,输入框中的name变化 => 触发onChange事件,setName函数执行 => name值改变,MyCountFunc组件会重新渲染 => MyCountFunc函数重新执行 => 形成一个新的函数闭包 => 形成与之对应的新的config对象(新的堆内存) => Child子组件的props发生变化 => Child组件重新渲染
所以,props还是变了。但要想不重新渲染,onButtonClick和config不能改变。如何实现呢?
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
返回一个备忘值。
传入一个"创建"备忘值的函数,useMemo只会在依赖项变化时重新计算备忘值。这个优化能够避免渲染时的高昂开销。如果不提供依赖项,每次渲染都会得到一个新值。
在渲染是,传给useMemo的函数会执行,所以不要再渲染时做一些一般不做的事情,避免带来side effect,那是useEffect做的事情,要区分开。
useMemo是用来做性能优化的,不是作为semantic guarantee.在将来,React可能会忘记一些之前备忘的值并在下一次渲染时重新计算它们,例如为屏幕外的组件释放内存。写出的代码应该在没有useMemo时也能工作,然后用useMemo优化性能。
注意
依赖项数组并不是作为参数传给了数组。不过从概念上说,它们表示的是这意思:函数中引用的值也应出现在依赖项数组中。以后,编译器会足够高级,自动创建依赖项数组。
我们推荐在eslint-plugin-react-hooks包下使用exhaustive-deps规则。它会在不正确的指定依赖项时发出警告并建议修复。
const config = useMemo(() => ({
text: `count is ${count}`,
color: count > 3 ? 'red' : 'blue',
}), [count])
此时,再检测,点击按钮时,count变化 => 'child props';但是,输入框中改变name时,还会输出'child props',说明MyCountFunc重新渲染了,why?
onButtonClick传入的是箭头函数,而箭头函数的this是由它所定义的词法作用域决定,所以MyCountFunc重新渲染时,会生成一个新的MyCountFunc执行上下文,不同于之前的MyCountFunc,其中包含的箭头函数由于词法作用域不同,当然不同于之前的箭头函数,所以props的OnButtonClick还是改变了,
必然会重新渲染。
下一步就是要优化箭头函数,使之
const memorizedCallback = useCallback(
() => {
doSomething(a, b)
},
[a, b])
返回一个备忘的回调函数。
传入内联回调和依赖项数组。useCallback返回一个只在依赖项改变时才改变的备忘版回调。这在将回调传递给依靠引用相等(两个变量引用完全相等的对象)来避免不必要渲染(例如shouldComponentUpdate)的优化子组件时非常有用。
useCallback(fn, deps)相当于useMemo(() => fn, deps)
Note
依赖项数组并不是作为参数传给了数组。不过从概念上说,它们表示的是这意思:函数中引用的值也应出现在依赖项数组中。以后,编译器会足够高级,自动创建依赖项数组。
我们推荐在eslint-plugin-react-hooks包下使用exhaustive-deps规则。它会在不正确的指定依赖项时发出警告并建议修复。
function MyCountFunc() {
const [count, dispatchCount] = useReducer(countReducer, 0)
const [name, setName] = useState('firm')
const config = useMemo(() => ({
text: `count is ${count}`,
color: count > 3 ? 'red' : 'blue',
}), [count])
const buttonClick = useCallback(() => {
dispatchCount({ type: 'add' })
}, [])
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<Child
config={config}
onButtonClick={buttonClick}
/>
</div>
)
}
优化后的结果