【前端丛林】React这样服用,效果更佳(7)

87 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情

前言

哈喽大家好,我是Lotzinfly,一位前端小猎人。欢迎大家再次来到前端丛林,在这里你将会遇到各种各样的前端猎物,我希望可以把这些前端猎物统统拿下,嚼碎了服用,并成为自己身上的骨肉。今天是国庆假期第七天,也是我们冒险的第七天,短暂的七天假期就这样结束了,不知道大家在这七天里有没有收获呢?

昨天我们介绍了useState这一钩子函数,今天我们会继续深入学习React高级用法,继续学习React的重点React HooksHook 是 React 16.8 的新增特性,学会React Hooks将大大提高我们的开发效率,所以一定要掌握好React Hooks。在国庆假期的最后一天,让我们稳住继续学习,学会React实现弯道超车。你们准备好了吗?那么开始我们的冒险之旅吧!

1.useReducer

  • useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法
  • 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等

1.1 基本用法

const [state, dispatch] = useReducer(reducer, initialArg, init);
const initialState = 0;
function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return { number: state.number + 1 };
        case 'decrement':
            return { number: state.number - 1 };
        default:
            throw new Error();
    }
}
function init(initialState) {
    return { number: initialState };
}
function Counter() {
    const [state, dispatch] = useReducer(reducer, initialState, init);
    return (
        <>
            Count: {state.number}
            <button onClick={() => dispatch({ type: 'increment' })}>+</button>
            <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
        </>
    )
}

2.useContext

  • 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
  • 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
  • useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者<MyContext.Consumer>
  • useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context
const CounterContext = React.createContext();
function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return { number: state.number + 1 };
        case 'decrement':
            return { number: state.number - 1 };
        default:
            throw new Error();
    }
}
function Counter() {
    let { state, dispatch } = useContext(CounterContext);
    return (
        <>
            <p>{state.number}</p>
            <button onClick={() => dispatch({ type: 'increment' })}>+</button>
            <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
        </>
    )
}
function App() {
    const [state, dispatch] = useReducer(reducer, { number: 0 });
    return (
        <CounterContext.Provider value={{ state, dispatch }}>
            <Counter />
        </CounterContext.Provider>
    )
}

3.useEffect

  • 在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI的一致性
  • 使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道
  • useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的componentDidMount 、 componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API
  • 该 Hook 接收一个包含命令式、且可能有副作用代码的函数
useEffect(didUpdate);

3.1 通过class实现修标题

class Counter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            number: 0
        };
    }
    componentDidMount() {
        document.title = `你点击了${this.state.number}次`;
    }
    componentDidUpdate() {
        document.title = `你点击了${this.state.number}次`;
    }
    render() {
        return (
            <div>
                <p>{this.state.number}</p>
                <button onClick={() => this.setState({ number: this.state.number + 1 })}>
                    +
                </button>
            </div>
        );
    }
}

在这个 class 中,我们需要在两个生命周期函数中编写重复的代码,这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。我们希望它在每次渲染之后执行,但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。useEffect会在第一次渲染之后和每次更新之后都会执行。

3.2 通过effect实现

import React, { Component, useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
function Counter() {
    const [number, setNumber] = useState(0);
    // 相当于 componentDidMount 和 componentDidUpdate:
    useEffect(() => {
        // 使用浏览器的 API 更新页面标题
        document.title = `你点击了${number}次`;
    });
    return (
        <>
            <p>{number}</p>
            <button onClick={() => setNumber(number + 1)}>+</button>
        </>
    )
}
ReactDOM.render(<Counter />, document.getElementById('root'));

每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect 属于一次特定的渲染。

3.3 跳过 Effect 进行性能优化

  • 如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可
  • 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行
function Counter() {
    const [number, setNumber] = useState(0);
    // 相当于componentDidMount 和 componentDidUpdate
    useEffect(() => {
        console.log('开启一个新的定时器')
        const $timer = setInterval(() => {
            setNumber(number => number + 1);
        }, 1000);
    }, []);
    return (
        <>
            <p>{number}</p>
        </>
    )
}

3.4 清除副作用

  • 副作用函数还可以通过返回一个函数来指定如何清除副作用
  • 为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染,则在执行下一个effect 之前,上一个 effect 就已被清除
import React, { useEffect, useState, useReducer } from 'react';
import ReactDOM from 'react-dom';
function Counter() {
    const [number, setNumber] = useState(0);
    useEffect(() => {
        console.log('开启一个新的定时器')
        const $timer = setInterval(() => {
            setNumber(number => number + 1);
        }, 1000);
        return () => {
            console.log('销毁老的定时器');
            clearInterval($timer);
        }
    });
    return (
        <>
            <p>{number}</p>
        </>
    )
}
function App() {
    let [visible, setVisible] = useState(true);
    return (
        <div>
            {visible && <Counter />}
            <button onClick={() => setVisible(false)}>stop</button>
        </div>
    )
}
ReactDOM.render(<App />, document.getElementById('root'));

4.useRef

  • useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)
  • 返回的 ref 对象在组件的整个生命周期内保持不变
const refContainer = useRef(initialValue);

4.1 useRef

import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
function Parent() {
    let [number, setNumber] = useState(0);
    return (
        <>
            <Child />
            <button onClick={() => setNumber({ number: number + 1 })}>+</button>
        </>
    )
}
let input;
function Child() {
    const inputRef = useRef();
    console.log('input===inputRef', input === inputRef);
    input = inputRef;
    function getFocus() {
        inputRef.current.focus();
    }
    return (
        <>
            <input type="text" ref={inputRef} />
            <button onClick={getFocus}>获得焦点</button>
        </>
    )
}
ReactDOM.render(<Parent />, document.getElementById('root'));

4.2 forwardRef

  • 将ref从父组件中转发到子组件中的dom元素上
  • 子组件接受props和ref作为参数
function Child(props, ref) {
    return (
        <input type="text" ref={ref} />
    )
}
Child = forwardRef(Child);
function Parent() {
    let [number, setNumber] = useState(0);
    const inputRef = useRef();
    function getFocus() {
        inputRef.current.value = 'focus';
        inputRef.current.focus();
    }
    return (
        <>
            <Child ref={inputRef} />
            <button onClick={() => setNumber({ number: number + 1 })}>+</button>
            <button onClick={getFocus}>获得焦点</button>
        </>
    )
}

4.3 useImperativeHandle

  • useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值
  • 在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与forwardRef 一起使用
function Child(props, ref) {
    const inputRef = useRef();
    useImperativeHandle(ref, () => (
        {
            focus() {
                inputRef.current.focus();
            }
        }
    ));
    return (
        <input type="text" ref={inputRef} />
    )
}
Child = forwardRef(Child);
function Parent() {
    let [number, setNumber] = useState(0);
    const inputRef = useRef();
    function getFocus() {
        console.log(inputRef.current);
        inputRef.current.value = 'focus';
        inputRef.current.focus();
    }
    return (
        <>
            <Child ref={inputRef} />
            <button onClick={() => setNumber({ number: number + 1 })}>+</button>
            <button onClick={getFocus}>获得焦点</button>
        </>
    )
}

结尾

好啦,这期的前端丛林大冒险先到这里啦!这期我们介绍了React HooksuseReducer、useContext、useEffect、useRef这几个钩子函数,相信让大家对React Hooks有了一定的了解,大家一定要好好啃下来嚼烂嚼透。希望大家可以好好品尝并消化,迅速升级,接下来我们才更好地过五关斩六将!好啦,我们下期再见。拜拜!