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> ) }
优化后的结果
来源:https://www.cnblogs.com/wydumn/p/12209068.html