React Hooks

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>
}
  1. setCount代替之前 this.setState的功能,修改state数据,其实也是reducer的功能,useState也是useReducer实现的
  2. useEffect实现 componentDidMount 的功能;return 的回调函数实现 componentWillUnmount 的功能

2) State Hooks

  1. 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>
}
  1. 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设计的目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题(黑夜模式)或首选语言(简体中文、英语...)。
  1. /lib下新建my-context.js
import React from 'react'

export default React.createContext('')
//=> 创建一个Context对象,初始值为''
//=> 当React渲染一个订阅了这个Context对象的组件,这个组件会从组件树中离自身最近的那个匹配的Provider中读取到当前的context值
  1. _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>
    )
}
  1. 组件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>
    )
}

React Hooks

  • 问题:当前组件,无论是状态中的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?

  1. count变化时,props中的config变化,Child组件中的color属性和text都变化,必然导致重新渲染,可为什么name变化,也会重新渲染Child?
  2. name是MyCountFunc组件的state,输入框中的name变化 => 触发onChange事件,setName函数执行 => name值改变,MyCountFunc组件会重新渲染 => MyCountFunc函数重新执行 => 形成一个新的函数闭包 => 形成与之对应的新的config对象(新的堆内存) => Child子组件的props发生变化 => Child组件重新渲染

所以,props还是变了。但要想不重新渲染,onButtonClick和config不能改变。如何实现呢?


  1. useMemo
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还是改变了,必然会重新渲染。
下一步就是要优化箭头函数,使之


  1. useCallback
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>
    )
}

优化后的结果

React Hooks

上一篇:React Hooks useContext + useReducer实现简易Redux


下一篇:1