前言

鉴于读者对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所取得的值为传入的1const 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  0effect fire and value is 1// 第二次单击effect cleanup and value is  1effect fire and value is 2// 第三次单击effect cleanup and value is  2effect 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函数的自定义Hookexport 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...