关于前端:深入理解React-RouterContextHooksRefsMemo特性讲解

2次阅读

共计 19466 个字符,预计需要花费 49 分钟才能阅读完成。

前言

鉴于读者对 React 有肯定的意识,且本书所有案例均应用 React Hooks 编写,以及在 React Router 源码中应用了 Context 等 React 个性,因而本章仅对 React 的 Context、Hooks 等局部个性进行介绍。对于其余 React 相干个性,读者可查阅相干材料进行学习。

Context

在 React 中,父组件通常将数据作为 props 传递给子组件。如果须要跨层级传递数据,那么应用 props 逐层传递数据将会使开发变得复杂。同时,在理论开发中,许多组件须要一些雷同的货色,如国际化语言配置、利用的主题色等,在开发组件的过程中也不心愿逐级传递这些配置信息。

在这种状况下,能够应用 React 的 Context 个性。Context 被翻译为上下文,如同字面意思,其蕴含了逾越以后层级的信息。

Context 在许多组件或者开发库中有着宽泛的利用,如 react-redux 应用 Context 作为 Provider,提供全局的 store,以及 React Router 通过 Context 提供路由状态。把握 Context 将会对了解 React Router 起到极大的帮忙作用。这里以图 3 - 1 来阐明 Context 如何跨组件传递数据。

在图 3 - 1 中,左侧组件树应用了逐层传递 props 的形式来传递数据,即便组件 B、组件 C 不须要关怀某个数据项,也被迫须要将该数据项作为 props 传递给子组件。而应用 Context 来实现组件间跨层级的数据传递,数据可间接从组件 A 传递到组件 D 中。

在 React v16.3 及以上版本中,可应用 React.createContext 接口创立 Context 容器。基于生产者 - 消费者模式,创立容器后可应用容器提供方(个别称为 Provider)提供某跨层级数据,同时应用容器生产方(个别称为 Consumer)生产容器提供方所提供的数据。示例如下:

// 传入 defaultValue
// 如果 Consumer 没有对应的 Provider,则 Consumer 所取得的值为传入的 1
const CountContext = React.createContext(1);
class App extends React.Component {state = { count: 0};
    render() {console.log('app render');
        return (<CountContext.Provider value={this.state.count}>
                
                <Toolbar />
                <button onClick={() => this.setState(state => ({ count: state.count + 1}))}>
                    
                    更新
                </button>
            </CountContext.Provider>
        );
    }
}

通过 setState 扭转 count 的值,触发 render 渲染,Context.Provider 会将最新的 value 值传递给所有的 Context.Consumer。

class Toolbar extends React.Component {render() {console.log('Toolbar render');
        return (
            <div>
                
                <Button />
            </div>
        );
    }
}
class Button extends React.Component {render() {console.log('Button outer render');
        return (
            // 应用 Consumer 跨组件生产数据
            <CountContext.Consumer>
                
                {count => {
                    // 在 Consumer 中,受到 Provider 提供数据的影响
                    console.log('Button render');
                    return <div>{count}</div>;
                }}
            </CountContext.Consumer>
        );
    }
}

在上例中,顶层组件 App 应用 CountContext.Provider 将 this.state.count 的值提供给后辈组件。App 的子组件 Toolbar 不生产 Provider 所提供的数据,Toolbar 的子组件 Button 应用 CountContext.Consumer 取得 App 所提供的数据 count。中间层的 Toolbar 组件对数据跨层级传递没有任何感知。在单击“更新”按钮触发数据传递时,Toolbar 中的“Toolbar render”信息不会被打印。每次单击“更新”按钮时,仅会打印“app render”与“Button render”,这是因为在 Provider 所提供的值扭转时,仅 Consumer 会渲染,所以 Toolbar 中的“Toolbar render”不会被打印。

如果在 Toolbar 中也应用 Provider 提供数据,如提供的 value 为 500:

class Toolbar extends React.Component {render() {console.log('Toolbar render');
        return (<CountContext.Provider value={500}>
                
                <Button />
            </CountContext.Provider>
        );
    }
}

则 Button 中的 Consumer 失去的值将为 500。起因在于当有多个 Provider 时,Consumer 将生产组件树中最近一级的 Provider 所提供的值。这作为 React 的一个重要个性,在 React Router 源码中被大量利用。

留神,如果不设置 Context.Provider 的 value,或者传入 undefined,则 Consumer 并不会取得创立 Context 时的 defaultValue 数据。创立 Context 时的 defaultValue 数据次要提供给没有匹配到 Provider 的 Consumer,如果去掉 App 中的 Provider,则 Consumer 所取得的值为 1。

如果心愿应用 this.context 形式获取 Provider 所提供的值,则可申明类的动态属性 contextType(React v16.6.0)。contextType 的值为创立的 Context,如:

const MyContext = React.createContext();
class MyClass extends React.Component {
    static contextType = MyContext;
    render() {
        // 获取 Context 的值
        let value = this.context;
    }
}

在 React v16.3 以前,不反对通过 createContext 的形式创立上下文,可应用社区的 polyfill 计划,如 create-react-context 等。

留神,组件的优化办法如 shouldComponentUpdate 或者 React.memo 不能影响 Context 值的传递。若在 Button 中引入 shouldComponentUpdate,则会阻止 Button 更新:

shouldComponentUpdate() {
    // 返回 false 阻止了 Button 组件的渲染,然而 Provider 提供的数据仍然会提供到
    //Consumer 中
    // 不受此影响
    return false;
};

扭转 Provider 所提供的值后,仍然会触发 Consumer 的从新渲染,后果与未引入 shouldComponentUpdate 时统一。

Hooks

React Hooks 是 React v16.8 正式引入的个性,旨在解决与状态无关的逻辑重用和共享等问题。

在 React Hooks 诞生前,随着业务的迭代,在组件的生命周期函数中,充斥着各种互不相干的逻辑。通常的解决办法是应用 Render Props 动静渲染所需的局部,或者应用高阶组件提供公共逻辑以解耦各组件间的逻辑关联。然而,无论是哪一种办法,都会造成组件数量增多、组件树结构批改或者组件嵌套层数过多的问题。在 Hooks 诞生后,它将本来扩散在各个生命周期函数中解决同一业务的逻辑封装到了一起,使其更具移植性和可复用性。应用 Hooks 不仅使得在组件之间复用状态逻辑更加容易,也让简单组件更易于浏览和了解;并且因为没有类组件的大量 polyfill 代码,仅须要函数组件就可运行,Hooks 将用更少的代码实现同样的成果。

React 提供了大量的 Hooks 函数反对,如提供组件状态反对的 useState、提供副作用反对的 useEffect,以及提供上下文反对的 useContext 等。

在应用 React Hooks 时,须要恪守以下准则及个性要求。

  • 只在顶层应用 Hooks。不要在循环、条件或嵌套函数中调用 Hooks,确保总是在 React 函数组件的顶层调用它们。
  • 不要在一般的 JavaScript 函数中调用 Hooks。仅在 React 的函数组件中调用 Hooks,以及在自定义 Hook 中调用其余 Hooks。

useState

useState 相似于 React 类组件中的 state 和 setState,可保护和批改以后组件的状态。

useState 是 React 自带的一个 Hook 函数,应用 useState 可申明外部状态变量。useState 接管的参数为状态初始值或状态初始化办法,它返回一个数组。数组的第一项是以后状态值,每次渲染其状态值可能都会不同;第二项是可扭转对应状态值的 set 函数,在 useState 初始化后该函数不会变动。

useState 的类型为:

function useState<S>(initialState: S | (() => S)): [S, Dispatch <SetStateAction <S>>];

initialState 仅在组件初始化时失效,后续的渲染将疏忽 initialState:

const [inputValue, setValue] = useState("react");const [react, setReact] = useState(inputValue);

如上例中的 inputValue,当初始值传入另一个状态并初始化后,另一个状态函数将不再依赖 inputValue 的值。

应用 Hooks 的形式非常简单,引入后在函数组件中应用:

import {useState} from 'react';
function Example() {const [count, setCount] = useState(0);
    return (
        <div>
            <p> 您点击了 {count} 次 </p>
            <button onClick={() => setCount(count + 1)}> 单击触发更新 </button>
        </div>
    );
}

相似于 setState,单击按钮时调用 setCount 更新了状态值 count。当调用 setCount 后,组件会从新渲染,count 的值会失去更新。

当传入初始状态为函数时,其仅执行一次,相似于类组件中的构造函数:

const [count, setCount] = useState(() => {
    // 可执行初始化逻辑
    return 0;
});

此外,useState 返回的更新函数也可应用函数式更新:

setCount(preCount => preCount + 1)

如果新的 state 须要依赖先前的 state 计算得出,那么能够将回调函数当作参数传递给 setState。该回调函数将接管先前的 state,并将返回的值作为新的 state 进行更新。

留神,React 规定 Hooks 需写在函数的最外层,不能写在 if…else 等条件语句中,以此来确保 Hooks 的执行程序统一。

useEffect

副作用

在计算机科学中,如果某些操作、函数或表达式在其部分环境之外批改了一些状态变量值,则称其具备副作用(side effect)。副作用能够是一个与第三方通信的网络申请,或者是内部变量的批改,或者是调用具备副作用的任何其余函数。副作用并无好坏之分,其存在可能影响其余环境的应用,开发者须要做的是正确处理副作用,使得副作用操作与程序的其余部分隔离,这将使得整个软件系统易于扩大、重构、调试、测试和保护。在大多数前端框架中,也激励开发者在独自的、松耦合的模块中治理副作用和组件渲染。

对于函数来说,无副作用执行的函数称为纯函数,它们接管参数,并返回值。纯函数是确定性的,意味着在给定输出的状况下,它们总是返回雷同的输入。但这并不意味着所有非纯函数都具备副作用,如在函数内生成随机值会使纯函数变为非纯函数,但不具备副作用。

React 是对于纯函数的,它要求 render 污浊。若 render 不污浊,则会影响其余组件,影响渲染。但在浏览器中,副作用无处不在,如果心愿在 React 中解决副作用,则可应用 useEffect。useEffect,顾名思义,就是执行有副作用的操作,其申明如下:

useEffect(effect: React.EffectCallback, inputs?: ReadonlyArray<any> | undefined)

函数的第一个参数为副作用函数,第二个参数为执行副作用的依赖数组,这将在上面的内容中介绍。示例如下:

const App = () => {const [value, setValue] = React.useState(0);
    // 引入 useEffect
    React.useEffect(function useEffectCallBack() {
        // 可执行副作用
        // 在此进行数据申请、订阅事件或手动更改 DOM 等操作
        const nvDom = document.getElementById('content');
        console.log('color effect', nvDom.style.color);
    });
    console.log('render');
    return (
        <div
            id="content"
            style={{color: value === 1 ? 'red' : ''}}
            onClick={() => setValue(c => c + 1)}
        >
            {' '}
            value: {value}{' '}
        </div>
    );
};

当上述组件初始化后,在打印 render 后会打印一次 color effect,表明组件渲染之后,执行了传入的 effect。而在单击 ID 为 content 的元素后,将更新 value 状态,触发一次渲染,打印 render 之后会打印 color effect red。这一流程表明 React 的 DOM 曾经更新结束,并将控制权交给开发者的副作用函数,副作用函数胜利地获取到了 DOM 更新后的值。事实上,上述流程与 React 的 componentDidMount、componentDidUpdate 生命周期相似,React 首次渲染和之后的每次渲染都会调用一遍传给 useEffect 的函数,这也是 useEffect 与传统类组件能够类比的中央。一般来说,useEffect 可类比为 componentDidMount、componentDidUpdate、componentWillUnmount 三者的汇合,但要留神它们不齐全等同,次要区别在于 componentDidMount 或 componentDidUpdate 中的代码是“同步”执行的。这里的“同步”指的是副作用的执行将妨碍浏览器本身的渲染,如有时候须要先依据 DOM 计算出某个元素的尺寸再从新渲染,这时候生命周期办法会在浏览器真正绘制前产生。

而 useEffect 中定义的副作用函数的执行不会妨碍浏览器更新视图,也就是说这些函数是异步执行的。所谓异步执行,指的是传入 useEffect 的回调函数是在浏览器的“绘制”阶段之后触发的,不“同步”妨碍浏览器的绘制。在通常状况下,这是比拟正当的,因为大多数的副作用都没有必要妨碍浏览器的绘制。对于 useEffect,React 应用了一种非凡伎俩保障 effect 函数在“绘制”阶段后触发:

const channel = new MessageChannel();
channel.port1.onmessage = function () {
    // 此时绘制完结,触发 effect 函数
    console.log('after repaint');
};
requestAnimationFrame(function () {console.log('before repaint');
    channel.port2.postMessage(undefined);
});

requestAnimationFrame 与 postMessage 联合应用以达到这一类目标。

简而言之,useEffect 会在浏览器执行完 reflow/repaint 流程之后触发,effect 函数适宜执行无 DOM 依赖、不妨碍主线程渲染的副作用,如数据网络申请、内部事件绑定等。

革除副作用

当副作用对外界产生某些影响时,在再次执行副作用前,应先革除之前的副作用,再从新更新副作用,这种状况能够在 effect 中返回一个函数,即 cleanup(革除)函数。

每个 effect 都能够返回一个革除函数。作为 useEffect 可选的革除机制,其能够将监听和勾销监听的逻辑放在一个 effect 中。

那么,React 何时革除 effect?effect 的革除函数将会在组件从新渲染之后,并先于副作用函数执行。以一个例子来阐明:

const App = () => {const [value, setValue] = useState(0);
    useEffect(function useEffectCallBack() {expensive();
        console.log('effect fire and value is', value);
        return function useEffectCleanup() {console.log('effect cleanup and value is', value);
        };
    });
    return <div onClick={() => setValue(c => c + 1)}>value: {value}</div>;
};

每次单击 div 元素,都会打印:

// 第一次单击
effect cleanup and value is  0
effect fire and value is 1
// 第二次单击
effect cleanup and value is  1
effect fire and value is 2
// 第三次单击
effect cleanup and value is  2
effect fire and value is 3
// ……

如上例所示,React 会在执行以后 effect 之前对上一个 effect 进行革除。革除函数作用域中的变量值都为上一次渲染时的变量值,这与 Hooks 的 Caputure Value 个性无关,将在上面的内容中介绍。

除了每次更新会执行革除函数,React 还会在组件卸载的时候执行革除函数。

缩小不必要的 effect

如下面内容所说,在每次组件渲染后,都会运行 effect 中的革除函数及对应的副作用函数。若每次从新渲染都执行一遍这些函数,则显然不够经济,在某些状况下甚至会造成副作用的死循环。这时,可利用 useEffect 参数列表中的第二个参数解决。useEffect 参数列表中的第二个参数也称为依赖列表,其作用是通知 React 只有当这个列表中的参数值产生扭转时,才执行传入的副作用函数:

useEffect(() => {document.title = `You clicked ${count} times`;
}, [count]);
// 只有当 count 的值发生变化时,才会从新执行 document.title 这一行

那么,React 是如何判断依赖列表中的值产生了变动的呢?事实上,React 对依赖列表中的每个值,将通过 Object.is 进行元素前后之间的比拟,以确定是否有任何更改。如果在以后渲染过程中,依赖列表中的某一个元素与该元素在上一个渲染周期的不同,则将执行 effect 副作用。

留神,如果元素之一是对象或数组,那么因为 Object.is 将比拟对象或数组的援用,因而可能会造成一些纳闷:

function App({config}) {React.useEffect(() => {}, [config]);
    return <div>{/* UI */}</div>;
}
// 每次渲染都传入 config 新对象
<App config={{a: 1}} />;

如果 config 每次都由内部传入,那么只管 config 对象的字段值都不变,但因为新传入的对象与之前 config 对象的援用不相等,因而 effect 副作用将被执行。要解决此种问题,能够依赖一些社区的解决方案,如 use-deep-compare-effect。

在通常状况下,若 useEffect 的第二个参数传入一个空数组 [](这并不属于非凡状况,它仍然遵循依赖列表的工作形式),则 React 将认为其依赖元素为空,每次渲染比对,空数组与空数组都没有任何变动。React 认为 effect 不依赖于 props 或 state 中的任何值,所以 effect 副作用永远都不须要反复执行,可了解为 componentDidUpdate 永远不会执行。这相当于只在首次渲染的时候执行 effect,以及在销毁组件的时候执行 cleanup 函数。要留神,这仅是便于了解的类比,对于第二个参数传入一个空数组[] 与这类生命周期的区别,可查看上面的注意事项。

注意事项

1)Capture Value 个性

留神,React Hooks 有着 Capture Value 的个性,每一次渲染都有它本人的 props 和 state:

function Counter() {const [count, setCount] = useState(0);
    useEffect(() => {const id = setInterval(() => {console.log('count is', count);
            setCount(count + 1);
        }, 1000);
        return () => clearInterval(id);
    }, []);
    return <h1>{count}</h1>;
}

在 useEffect 中,取得的永远是初始值 0,将永远打印“count is 0”;h1 中的值也将永远为 setCount(0+1)的值,即“1”。若心愿 count 能顺次减少,则可应用 useRef 保留 count,useRef 将在 3.2.4 节介绍。

2)async 函数

useEffect 不容许传入 async 函数,如:

useEffect(async () => {// return 函数将不会被调用}, []);

起因在于 async 函数返回了 promise,这与 useEffect 的 cleanup 函数容易混同。在 async 函数中返回 cleanup 函数将不起作用,若要应用 async 函数,则可进行如下改写:

useEffect(() => {(async () => {// 一些逻辑})(); // 可返回 cleanup 函数}, []);

3)空数组依赖

留神,useEffect 传递空数组依赖容易产生一些问题,这些问题通常容易被忽视,如以下示例:

function ChildComponent({count}) {useEffect(() => {console.log('componentDidMount', count);
        return () => {
            // 永远为 0,由 Capture Value 个性所导致
            alert('componentWillUnmount and count is' + count);
        };
    }, []);
    console.log('count', count);
    return <>count:{count}</>;
}
const App = () => {const [count, setCount] = useState(0);
    const [childShow, setChild] = useState(true);
    return (<div onClick={() => setCount(c => c + 1)}>
            {' '}
            <button onClick={() => setChild(false)}> 销毁 Child 组件 </button>{' '}
            {childShow && <ChildComponent count={count} />}{' '}
        </div>
    );
};

单击“销毁 Child 组件”按钮,浏览器将弹出“componentWillUnmount and count is 0”提示框,无论 setCount 被调用多少次,都将如此,这是由 Capture Value 个性所导致的。而类组件的 componentWillUnmount 生命周期可从 this.props.count 中获取到最新的 count 值。

在应用 useEffect 时,留神其不齐全与 componentDidUpdate、componentWillUnmount 等生命周期等同,应该以“副作用”或状态同步的形式去思考 useEffect。但这也不代表不倡议应用空数组依赖,须要联合上下文场景决定。与其将 useEffect 视为一个性能来经验 3 个独自的生命周期,不如将其简略地视为一种在渲染后运行副作用的形式,可能会更有帮忙。

useEffect 的设计用意是关注数据流的扭转,而后决定 effect 该如何执行,与生命周期的思考模型须要辨别开。

useLayoutEffect

React 还提供了与 useEffect 等同位置的 useLayoutEffect。useEffect 和 useLayoutEffect 在副作用中都可取得 DOM 变更后的属性:

const App = () => {const [value, setValue] = useState(0);
    useEffect(function useEffectCallBack() {const nvDom = document.getElementById('content');
        console.log('color effect', nvDom.style.color);
    });
    useLayoutEffect(function useLayoutEffectCallback() {const nvDom = document.getElementById('content');
        console.log('color layout effect', nvDom.style.color);
    });
    return (
        <div
            id="content"
            style={{color: value === 1 ? 'red' : ''}}
            onClick={() => setValue(c => c + 1)}
        >
            {' '}
            value: {value}{' '}
        </div>
    );
};

单击按钮后会打印“color layout effect red”“color effect red”。可见 useEffect 与 useLayoutEffect 都可从 DOM 中取得其变更后的属性。

从外表上看,useEffect 与 useLayoutEffect 并无区别,但事实上厘清它们的区别须要从副作用的“同步”“异步”动手。3.2.2 节曾介绍过 useEffect 的运行过程是异步进行的,即 useEffect 不妨碍浏览器的渲染;useLayoutEffect 与 useEffect 的区别是 useLayoutEffect 的运行过程是“同步”的,其妨碍浏览器的渲染。

简而言之,useEffect 产生在浏览器 reflow/repaint 操作之后,如果某些 effect 是从 DOM 中取得值的,如获取 clientHeight、clientWidth,并须要对 DOM 进行变更,则能够改用 useLayoutEffect,使得这些操作在 reflow/repaint 操作之前实现,这样有机会防止浏览器破费大量老本,屡次进行 reflow/repaint 操作。以一个例子来阐明:

const App = () => {const [value, setValue] = useState(0);
    useEffect(function useEffectCallBack() {console.log('effect');
    }); // 在下一帧渲染前执行
    window.requestAnimationFrame(() => {console.log('requestAnimationFrame');
    });
    useLayoutEffect(function useLayoutEffectCallback() {console.log('layoutEffect');
    });
    console.log('render');
    return <div onClick={() => setValue(c => c + 1)}>value: {value}</div>;
};

别离在 useEffect、requestAnimationFrame、useLayoutEffect 和 render 过程中进行调试打印,以察看它们的时序。能够看到,当渲染 App 后将按如下程序打印:render、layoutEffect、requestAnimationFrame、effect。由此可知,useLayoutEffect 的副作用都在“绘制”阶段前,useEffect 的副作用都在“绘制”阶段后。通过浏览器调试工具察看 task 的执行,如图 3 - 2 所示。在图 3 - 2 中,①执行了 useLayoutEffectCallback,为 useLayoutEffect 的副作用;②为浏览器的 Paint 流程;在 Paint 流程后,③的执行函数为 useEffectCallBack,执行了 useEffect 的副作用。

useRef

在应用 class 类组件时,通常须要申明属性,用以保留 DOM 节点。借助 useRef,同样能够在函数组件中保留 DOM 节点的援用:

import {useRef} from "React"
function App() {const inputRef = useRef(null);
    return<div>
        <input type="text" ref={inputRef} />
        {/*  通过 inputRef.current 获取节点 */}
        <button onClick={() => inputRef.current.focus()}>focus</button>
    </div>
}// useRef 的签名为:interface MutableRefObject<T> {current: T;}
function useRef<T>(initialValue: T): MutableRefObject<T>;

useRef 返回一个可变的 Ref 对象,其 current 属性被初始化为传递的参数(initialValue)。useRef 返回的可变对象就像一个“盒子”,这个“盒子”存在于组件的整个生命周期中,其 current 属性保留了一个可变的值。

useRef 不仅实用于 DOM 节点的援用,相似于类上的实例属性,useRef 还可用来寄存一些与 UI 无关的信息。useRef 返回的可变对象,其 current 属性能够保留任何值,如对象、根本类型或函数等。所以,函数组件尽管没有类的实例,没有“this”,然而通过 useRef 仍然能够解决数据的存储问题。如在 2.1 节,曾应用过 useRef:

function Example(props) {const { history} = props; // 应用 useRef 保留登记函数
    const historyUnBlockCb = React.useRef < UnregisterCallback > (() => {});
    React.useEffect(() => {return () => {
            // 在销毁组件时调用,登记 history.block
            historyUnBlockCb.current();};
    }, []);
    function block() {
        // 解除之前的阻止
        historyUnBlockCb.current();
        // 从新设置弹框确认,更新登记函数,单击“确定”按钮,失常跳转;单击“勾销”// 按钮,跳转不失效
        historyUnBlockCb.current = history.block('是否持续?');
    }
    return (
        <>
            {' '}
            <button onClick={block}> 阻止跳转 </button>{' '}
            <button
                onClick={() => {historyUnBlockCb.current();
                }}
            >
                解除阻止
            </button>{' '}
        </>
    );
}

上例应用 useRef 返回了可变对象 historyUnBlockCb,通过 historyUnBlockCb.current 保留了 history.block 的返回值。

留神,更改 refObject.current 的值不会导致从新渲染。如果心愿从新渲染组件,则可应用 useState,或者应用某种 forceUpdate 办法。

useMemo

作为 React 内置的 Hooks,useMemo 用于缓存某些函数的返回值。useMemo 应用了缓存,可防止每次渲染都从新执行相干函数。useMemo 接管一个函数及对应的依赖数组,当依赖数组中的一个依赖项发生变化时,将从新计算耗时函数。

function App() {const [count, setCount] = React.useState(0);
    const forceUpdate = useForceUpdate();
    const expensiveCalcCount = count => {console.log('expensive calc');
        let i = 0;
        while (i < 9999999) i++;
        return count;
    }; // 应用 useMemo 记录高开销的操作
    const letterCount = React.useMemo(() => expensiveCalcCount(count), [count]);
    console.log('component render');
    return (<div style={{ padding: '15px'}}>
            {' '}
            <div>{letterCount}</div> <button onClick={() => setCount(c => c + 1)}> 扭转 count</button>{' '}
            <button onClick={forceUpdate}> 更新 </button>{' '}
        </div>
    );
}

在下面的示例中,除了应用了 React.useState,还应用了一个自定义 Hook——useForceUpdate,其返回了 forceUpdate 函数,与类组件中的 forceUpdate 函数性能统一。对于自定义 Hook,将在 3.2.7 节介绍。

在初始渲染 App 时,React.useMemo 中的函数会被计算一次,对应的 count 值与函数返回的后果都会被 useMemo 记录下来。

若单击“扭转 count”按钮,因为 count 扭转,当 App 再次渲染时,React.useMemo 发现 count 有变动,将从新调用 expensiveCalcCount 并计算其返回值。因而,控制台会打印“expensive calc”“component render”。

而若单击“更新”按钮,则调用 forceUpdate 函数再次渲染。因为在再次渲染过程中 React.useMemo 发现 count 值没有扭转,因而将返回上一次 React.useMemo 中函数计算失去的后果,渲染 App 控制台仅打印“component render”。

同时,React 也提供了 useCallback 用以缓存函数:

useCallback(fn, deps)

在实现上,useCallback 等价于 useMemo(() => fn, deps),因而这里不再赘述。

useContext

若心愿在函数组件中应用 3.1 节中所述的 Context,除应用 Context.Consumer 生产外,还可应用 useContext:

const contextValue = useContext(Context);

useContext 接管一个 Context 对象(React.createContext 的返回值)并返回该 Context 的以后值。与 3.1 节中的 Consumer 相似,以后的 Context 值由下层组件中距离最近的 Context.Provider 提供。当更新下层组件中距离最近的 Context.Provider 时,应用 useContext 的函数组件会触发从新渲染,并取得最新传递给 Context.Provider 的 value 值。

调用了 useContext 的组件总会在 Context 值变动时从新渲染,这个个性将会常常应用到。

在函数组件中,应用 useContext 获取上下文内容,无效地解决了之前 Provider、Consumer 须要额定包装组件的问题,且因为其代替了 Context.Consumer 的 render props 写法,这将使得组件树更加简洁。

自定义 Hook

自定义 Hook 是一个函数,其名称约定以 use 结尾,以便能够看出这是一个 Hooks 办法。如果某函数的名称以 use 结尾,并且调用了其余 Hooks,就称其为一个自定义 Hook。自定义 Hook 就像一般函数一样,能够定义任意的入参加出参,惟一要留神的是自定义 Hook 须要遵循 Hooks 的基本准则,如不能在条件循环中应用、不能在一般函数中应用。

自定义 Hook 解决了之前 React 组件中的共享逻辑问题。通过自定义 Hook,可将如表单解决、动画、申明订阅等逻辑形象到函数中。自定义 Hook 是重用逻辑的一种形式,不受外部调用状况的束缚。事实上,每次调用 Hooks 都会有一个齐全隔离的状态。因而,能够在一个组件中应用两次雷同的自定义 Hook。上面是两个罕用自定义 Hook 的示例:

// 获取 forceUpdate 函数的自定义 Hook
export default function useForceUpdate() {const [, dispatch] = useState(Object.create(null));
    const memoizedDispatch = useCallback(() => {
        // 援用变动
        dispatch(Object.create(null));
    }, [dispatch]);
    return memoizedDispatch;
}

获取某个变量上一次渲染的值:

// 获取上一次渲染的值
function usePrevious(value) {const ref = useRef();
    useEffect(() => {ref.current = value;}, [value]);
    return ref.current;
}

可基于根底的 React Hooks 定义许多自定义 Hook,如 useLocalStorage、useLocation、useHistory(将在第 5 章中进行介绍)等。将逻辑形象到自定义 Hook 中后,代码将更具备可维护性。

Refs

createRef

前文曾介绍过 useRef 用以保留 DOM 节点,事实上也能够通过 createRef 创立 Ref 对象:

class MyComponent extends React.Component {constructor(props) {super(props);
        this.myRef = React.createRef();}
    render() {return <div ref={this.myRef} />;
    }
}

当 this.myRef 被传递给 div 元素时,可通过以下形式获取 div 原生节点:

const node = this.myRef.current;

Ref 不仅能够作用于 DOM 节点上,也能够作用于类组件上。在类组件上应用该属性时,Ref 对象的 current 属性将取得类组件的实例,因此也能够调用该组件实例的公共办法。

forwardRef

援用传递(Ref forwading)是一种通过组件向子组件主动传递援用 Ref 的技术。例如,某些 input 组件须要管制其 focus,原本是能够应用 Ref 来管制的,然而因为该 input 已被包裹在组件中,所以这时就须要应用 forwardRef 来透过组件取得该 input 的援用。

import React, {Component} from 'react';
import ReactDOM, {render} from 'react-dom';
const ChildOrigin = (props, ref) => {return <div ref={ref}>{props.txt}</div>;
};
const Child = React.forwardRef(ChildOrigin);
class Parent extends Component {constructor() {super();
        this.myChild = React.createRef();}
    componentDidMount() {console.log(this.myChild.current);
        // 获取的是 Child 组件中的 div 元素
    }
    render() {return <Child ref={this.myChild} txt="parent props txt" />;
    }
}

当对原 ChildOrigin 组件应用 forwardRef 取得了新的 Child 组件后,新 Child 组件的 Ref 将传递到 ChildOrigin 组件外部。在下面的示例中,可通过新 Child 组件的 Ref 值 this.myChild. current 获取到 ChildOrigin 组件外部 div 元素的援用。

Memo

为了进步 React 的运行性能,React v16.6.0 提供了一个高阶组件——React.memo。当 React.memo 包装一个函数组件时,React 会缓存输入的渲染后果,之后当遇到雷同的渲染条件时,会跳过此次渲染。与 React 的 PureComponent 组件相似,React.memo 默认应用了浅比拟的缓存策略,但 React.memo 对应的是函数组件,而 React.PureComponent 对应的是类组件。React.memo 的签名如下:

function memo<P extends object>(  
  Component: SFC<P>,  
  propsAreEqual?: (prevProps: Readonly<PropsWithChildren<P>>, 
  nextProps: Readonly<PropsWithChildren<P>>
) => boolean): NamedExoticComponent<P>;

React.memo 参数列表中的第一个参数接管一个函数组件,第二个参数示意可选的 props 比对函数。React.memo 包装函数组件后,会返回一个新的记忆化组件。以一个示例来阐明,若有一个子组件 ChildComponent,没有通过 React.memo 记忆化:

function ChildComponent({count}) {console.log('childComponent render', count);
    return <>count:{count}</>;
}
const App = () => {const [count] = useState(0);
    const [childShow, setChild] = useState(true);
    return (
        <div>
            {' '}
            <button onClick={() => setChild(c => !c)}> 暗藏 / 展现内容 </button>{' '}
            {childShow && <div> 内容 </div>} <ChildComponent count={count} />{' '}
        </div>
    );
};

当反复单击按钮时,因为触发了从新渲染,ChildComponent 将失去更新,将屡次打印“childComponent render”。若引入 React.memo(ChildComponent)缓存组件,则在渲染组件时,React 将进行查看。如果该组件渲染的 props 与先前渲染的 props 不同,则 React 将触发渲染;反之,如果 props 前后没有变动,则 React 不执行渲染,更不会执行虚构 DOM 差别查看,其将应用上一次的渲染后果。

function ChildComponent({count}) {console.log('childComponent render');
    return <>count:{count}</>;
}
const MemoChildComponent = React.memo(ChildComponent);
const App = () => {const [count] = useState(0);
    const [childShow, setChild] = useState(true);
    return (
        <div>
            {' '}
            <button onClick={() => setChild(c => !c)}> 暗藏 / 展现内容 </button>{' '}
            {childShow && <div> 内容 </div>} <MemoChildComponent count={count} />{' '}
        </div>
    );
};

当单击“暗藏 / 展现内容”按钮时,会导致从新渲染,但因为原组件通过 React.memo 包装过,应用了包装后的组件 MemoChildComponent,在屡次渲染时 props 没有变动,因而这时不会屡次打印“childComponent render”。

同时,React.memo 能够应用第二个参数 propsAreEqual 来自定义渲染与否的逻辑:

const MemoChildComponent = React.memo(ChildComponent, function propsAreEqual(prevProps, nextProps) {return prevProps.count === nextProps.count;});

propsAreEqual 接管上一次的 prevProps 与行将渲染的 nextProps,函数返回的 boolean 值表明前后的 props 是否相等。若返回“true”,则认为前后 props 相等;反之,则认为不相等,React 将依据函数的返回值决定组件的渲染状况(与 shouldComponentUpdate 相似)。因而,可认为函数返回“true”,props 相等,不进行渲染;函数返回“false”则认为 props 有变动,React 会执行渲染。留神,不能把 React.memo 放在组件渲染过程中。

const App = () => {
    // 每次都取得新的记忆化组件
    const MemoChildComponent = React.memo(ChildComponent);
    const [count] = useState(0);
    const [childShow, setChild] = useState(true);
    return (
        <div>
            {' '}
            <button onClick={() => setChild(c => !c)}> 暗藏 / 展现内容 </button>{' '}
            {childShow && <div> 内容 </div>} <MemoChildComponent count={count} />{' '}
        </div>
    );
};

这相当于每次渲染都开拓一块新的缓存,原缓存无奈失去利用,React.memo 的记忆化将生效,开发者须要特地留神。

小结

本章介绍了 Context、Hooks、Refs、Memo 等 React 个性,在 React Router 源码及相干第三方库实现中,都波及以上个性。把握以上个性,对了解 React Router 及应用 React Router 进行实战都有十分大的帮忙。

相比 props 和 state,React 的 Context 个性能够实现跨层级的组件通信。咱们能够在很多框架设计中找到应用 Context 的例子,React Router 也是其一。学习应用 Context 对了解 React Router 非常重要。同时,本章介绍了 React Hooks,作为 React v16.8 的新个性,以及思考到 React Router 今后的演进趋势,学习应用 React Hooks 进行函数式组件开发将对读者有极大的帮忙。

参考文献

  • https://zh-hans.reactjs.org/d…
  • https://en.wikipedia.org/wiki…
  • https://zh-hans.reactjs.org/d…
  • https://github.com/facebook/r…
  • https://developer.mozilla.org…
正文完
 0