鉴于读者对React有必定的认识,且本书全部案例均使用React Hooks编写,以及在React Router源码中使用了Context等React特性,所以本章仅对React的Context、Hooks等部分特性进行介绍。对于其余React相关特性,读者可查阅相关资料进行学习。html
在React中,父组件一般将数据做为props传递给子组件。若是须要跨层级传递数据,那么使用props逐层传递数据将会使开发变得复杂。同时,在实际开发中,许多组件须要一些相同的东西,如国际化语言配置、应用的主题色等,在开发组件的过程当中也不但愿逐级传递这些配置信息。前端
在这种状况下,可使用React的Context特性。Context被翻译为上下文,如同字面意思,其包含了跨越当前层级的信息。node
Context在许多组件或者开发库中有着普遍的应用,如react-redux使用Context做为Provider,提供全局的store,以及React Router经过Context提供路由状态。掌握Context将会对理解React Router起到极大的帮助做用。这里以图3-1来讲明Context如何跨组件传递数据。react
在图3-1中,左侧组件树使用了逐层传递props的方式来传递数据,即便组件B、组件C不须要关心某个数据项,也被迫须要将该数据项做为props传递给子组件。而使用Context来实现组件间跨层级的数据传递,数据可直接从组件A传递到组件D中。git
在React v16.3及以上版本中,可以使用React.createContext接口建立Context容器。基于生产者-消费者模式,建立容器后可以使用容器提供方(通常称为Provider)提供某跨层级数据,同时使用容器消费方(通常称为Consumer)消费容器提供方所提供的数据。示例以下:github
// 传入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。redux
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:promise
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时一致。
React Hooks是React v16.8正式引入的特性,旨在解决与状态有关的逻辑重用和共享等问题。
在React Hooks诞生前,随着业务的迭代,在组件的生命周期函数中,充斥着各类互不相关的逻辑。一般的解决办法是使用Render Props动态渲染所需的部分,或者使用高阶组件提供公共逻辑以解耦各组件间的逻辑关联。可是,不管是哪种方法,都会形成组件数量增多、组件树结构修改或者组件嵌套层数过多的问题。在Hooks诞生后,它将本来分散在各个生命周期函数中处理同一业务的逻辑封装到了一块儿,使其更具移植性和可复用性。使用Hooks不只使得在组件之间复用状态逻辑更加容易,也让复杂组件更易于阅读和理解;而且因为没有类组件的大量polyfill代码,仅须要函数组件就可运行,Hooks将用更少的代码实现一样的效果。
React提供了大量的Hooks函数支持,如提供组件状态支持的useState、提供反作用支持的useEffect,以及提供上下文支持的useContext等。
在使用React Hooks时,须要遵照如下准则及特性要求。
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的执行顺序一致。
在计算机科学中,若是某些操做、函数或表达式在其局部环境以外修改了一些状态变量值,则称其具备反作用(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中的清除函数及对应的反作用函数。若每次从新渲染都执行一遍这些函数,则显然不够经济,在某些状况下甚至会形成反作用的死循环。这时,可利用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该如何执行,与生命周期的思考模型须要区分开。
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的反作用。
在使用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方法。
做为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),所以这里再也不赘述。
若但愿在函数组件中使用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是一个函数,其名称约定以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中后,代码将更具备可维护性。
前文曾介绍过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属性将得到类组件的实例,于是也能够调用该组件实例的公共方法。
引用传递(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元素的引用。
为了提升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进行函数式组件开发将对读者有极大的帮助。