前言
文档目录:
- 实例引用 Refs
- 上下文 Context
- 高阶组件 Higher-Order Components
- 钩子 Hooks
一、实例引用 Refs
Refs
提供了一种方式,允许我们访问 DOM 节点 或在 render 方法中创建的 React 元素。
使用流程如下:
- 创建实例。创建一个 Refs 实例,譬如
this.myRef = React.createRef()
- 挂载实例。通过标签中的 ref 属性将上面创建的实例挂载到目标元素,譬如
<input ref={this.myRef}/>
- 访问实例。通过访问 Refs 实例上的 current 属性来获取元素,譬如
this.myRef.current.focus()
从 Refs 实例的 创建 方式来划分,有以下几种:
React.createRef
(在 ClassComponent 中使用,React 16.3 引入)React.useRef
(在 FunctionComponent 中使用,React 16.8 引入)- 回调 Refs
- 字符串 Refs(已经过时了,忘掉她吧)
React.createRef
React.createRef
仅能在 ClassComponent 中使用,因为该 api 并没有 Hooks 的效果,其值会随着 FunctionComponent 重复执行而不断被初始化。
下面用一个例子快速掌握如何通过 React.createRef
去 创建 和 访问
class MyClassComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef(); // 实例化一个 ref 对象
}
handleInputFocus = () => {
if (this.myRef.current) {
/**
* 在整个流程中,myRef.current 不一定有值
* 是因为譬如
* - 你实例化了一个 ref 对象,但是你并没有将该对象挂载到对应的元素上
* - 或者此刻该元素被移除了
* 所以访问 ref 的时候,一般都是要先判断是否存在的哦~
*/
this.myRef.current.focus(); // 聚焦
// 此时 current 为一个 HTMLElement(你可以理解为事件监听里的 e.target)
console.log(this.myRef.current);
}
}
render() {
return (
<div>
<input
/**
* 通过元素上的 ref 属性将 myRef 传入
* 在元素初始化或者重新渲染时会更新 myRef 的值
* myRef 你可以理解为一个对象,其中有一个 current 属性,元素发生变化时就是更新这个 current 属性
*
* p.s. “元素”可以是“DOM节点”或者“React组件”
*/
ref={this.myRef}
/>
<span
// 点中 span 时聚焦上方的 input
onClick={this.handleInputFocus}
>聚焦</span>
</div>
);
}
}
React.useRef
React.useRef
为 Hooks,仅能在 FunctionComponent 中使用,因为 Hooks 不能用在 ClassComponent 中。
let outterRef = null;
function MyFunctionComponent() {
const [count, setCount] = React.useState(0);
const innerRef = React.useRef(null);
useEffect(
() => {
// 初始化时执行
outterRef = innerRef
},
[]
);
useEffect(
() => {
/**
* 这里始终会输出 true
* 因为 Hooks 的特性,即使当前不断重新渲染,也就是不断调用 React.useRef 后,获取的实例仍然是同一个
*/
console.log(outterRef === innerRef)
},
[count]
)
return (
<>
<input ref={innerRef}/>
<button onClick={() => setCount(count+1)}>{count}</button>
</>
)
}
回调 Refs
给 ref
属性传递一个回调函数,React 在不同时机调用该回调函数,并将元素(或组件)作为参数传入:
- 挂载前
- 触发更新前
- 卸载前(传 null)
// 在 ClassComponent 中使用
class MyClassComponent extends React.Component {
constructor(props) {
super(props);
this.inputRef = null;
}
componentDidMount() {
this.inputRef && this.inputRef.focus();
}
setMyRef = (ref) => {
this.inputRef = ref;
}
render() {
return (
<input type="text" ref={this.setMyRef}/>
)
}
}
// 在 FunctionComponent 中使用
function MyFuncComponent (props) {
let inputRef = null;
const handleClick = () => {
inputRef && inputRef.focus();
}
const setMyRef = (ref) => {
inputRef = ref
}
return (
<div>
<input type="text" ref={setMyRef}/>
<button onClick={handleClick}>聚焦</button>
</div>
)
}
二、上下文 Context
Context
提供了一个无需为每层组件手动添加 props
,就能在组件树间进行数据传递的方法。一般情况下,祖先组件想要将某一个值传递给后代组件,都是通过 props
一层层的向下传递,但是这种方式极其繁琐。而 Context
提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props
目前有两种使用 Context 的方式:
- 广播模式(慎用,这个东西你把握不了)
- 生产者/消费者(React 16.3 引入)
广播模式
使用方式分两步:
- 提供:祖先组件中设置
childContextTypes
和getChildContext
- 获取:后代组件中声明
contextType
不多bb,来个例子:
import React from "react";
import PropTypes from "prop-types"; // prop-types 是 react 中自带的库
// 祖父组件
class CompGrandFather extends React.Component {
constructor(props) {
super(props);
this.state = {
products: []
}
}
componentDidMount() {
this.setState({
products: [1, 2, 3, 4]
})
}
// 通过 childContextTypes 定义往下传递的 context 中数据的类型
static childContextTypes = {
myContextProducts: PropTypes.array,
// myContextFunc: PropTypes.func // prop-types 库中还有各种各样的类型哦
};
// 在 getChildContext 方法中返回对应的数据(对象)
getChildContext() {
return {
myContextProducts: this.state.products,
};
}
render() {
return (
<div>
<CompFather>
<CompChild/>
</CompFather>
</div>
)
}
}
// 父亲组件
function CompFather(props){
console.log('CompFather 重新渲染')
return (
<div>{props.children}</div>
)
}
// 孩子组件
class CompChild extends React.Component {
/**
* 后代组件通过 contextTypes 来定义所能接受到的 context
* 组件中需要获取 context 中哪些数据,就需要在这里声明,否则获取不到的
**/
static contextTypes = {
myContextProducts: PropTypes.array,
};
render() {
return (
<div>
{this.context.myContextProducts
.map(id => (
<p>{id}</p>
))}
</div>
)
}
}
相信你尝试照着这个例子来写过之后,已经基本掌握了使用方式~
但是这种方式官方并不建议在项目中使用哦,原因有下面几点:
- 破坏了 React 的分形架构思想
- 组件没办法随意复用(组件中如果使用到 Context 就意味着祖先组件必须要传递相应的 Context)
- 数据的来源难以溯源(React 是可以在任意一层祖先组件中提供 Context,并且当前组件如果重复提供同样的 Context,是会覆盖祖先传递下来的 Context 的,最终后代组件是获得距离其“最近”的祖先组件提供的 Context,这样子是根本没办法明确的找到 Context 数据的来源到底是哪一个)
- 传递流程可能会被中断(Context 传递过程中某个组件在 shouldComponentUpdate 中返回 false 时,下面的组件将无法触发 rerender,从而导致新的 Context 值无法更新到下面的组件)
- 性能问题
- 无法通过 React 的复用算法进行复用(一旦有一个节点提供了 Context,那么他的所有子节点都会被视为有 side effect 的,因为 React 本身并不判断子节点是否有使用 Context,以及提供的 Context 是否有变化,所以一旦检测到有节点提供了 Context,那么他的子节点则将会被认为需要更新)
生产者/消费者
这是 Context 二代目,在 React 16.3 引入,可以看到一代目的方式是通过给组件本身提供一些特性,用以拓展组件的功能,而且这些拓展是有副作用的(side effect),但是其实我们使用 Context 只是为了解决数据透穿的问题,所以就有人提出用组件的形式来实现数据的传递,分别为生产者(Provider)组件和消费者(Consumer)组件,改变 Provider 中提供的数据发生改变只会触发 Consumer 的重新渲染。
涉及到的关键字先来预览一下:
React.createContext
Context.Provider
Context.Consumer
Class.contextType
下面用二代目 context 来写一个例子:
/**
* 通过 createContext 方法创建一个 Context 对象
* 其接受一个参数,可以任意值,我这里比较建议传一个对象,因为这样比较容易拓展
* p.s. 此时传入的值是会作为“默认值”的哦,并非初始化值
*
* 此时 MyCtx 有两个属性,是一个组件来的,分别是:
* - MyCtx.Provider 生产者
* - MyCtx.Consumer 消费者
**/
const MyCtx = React.createContext({
innerProducts: [],
innerName: '默认名字',
innerAge: 0
})
// 祖父组件
class CompGrandFather extends React.Component {
constructor(props) {
super(props);
this.state = {
products: []
}
}
componentDidMount() {
this.setState({
products: [1, 2, 3, 4]
})
}
render() {
return (
<MyCtx.Provider
value={{
// 将想要传递的值放到 value 属性上
innerProducts: this.state.products,
// innerName: '张大炮', // 这里少传一个参数时,将会使用默认值
innerAge: 18
}}
>
<CompFather>
<CompChild/>
</CompFather>
</MyCtx.Provider>
)
}
}
// 父亲组件
function CompFather(props) {
// 有意思的是,CompGrandFather 修改 context 并不会触发 CompFather 的重新渲染
console.log('CompFather 重新渲染')
return (
<div>{props.children}</div>
)
}
// 孩子组件
function CompChild(props) {
return (
<div>
<MyCtx.Consumer>
{(ctx) => {
/**
* Consumer 的 props.children 是一个方法,并且会接受一个参数,参数就是 Provider 那边传递过来的 value 值
* 当 Provider 的 value 发生变化时,Consumer 会重新调用 props.children,并传递新的值
**/
return (
<div>
{ctx.innerProducts
.map(id => (
<p>{id}</p>
))}
<p>默认值演示:{ctx.innerName}</p>
</div>
)
}}
</MyCtx.Consumer>
</div>
)
}
总结一下,使用起来就三个流程:
- 使用
React.createContext
创建一个(独立于组件的)状态机实例,同时定义默认值,实例中有两个属性,他们都是一个 React 组件,分别是Provider
和Consumer
- 在祖先组件中使用
Provider
组件,并向其传递数据 - 在后代组件中使用
Consumer
组件,从中获取数据
Class.contextType
在上面的例子中,孩子组件中如果要获取数据都是需要通过 Consumer
组件,这里 React 还提供了一种方式,就是 Class.contextType
。
挂载在 class 上的 contextType
属性会被重赋值为一个由 React.createContext()
创建的 Context 对象。此属性能让你使用 this.context
来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。
下面用 Class.contextType
的方式重新实现上面的孩子组件:
class CompChild extends React.Component {
static contextType = MyCtx; // 在 contextType 静态属性中声明关联的 Context
componentDidMount() {
console.log("在生命周期中也能获取到context哦", this.context)
}
render() {
const {innerProducts, innerName} = this.context;
return (
<div>
<div>
{innerProducts
.map(id => (
<p>{id}</p>
))}
<p>默认值演示:{innerName}</p>
</div>
</div>
)
}
}
三、高阶组件 HOC
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
高阶组件其实就是一个函数,其接受组件作为参数,然后返回一个新的组件。也就是说其实高阶组件就是一个高阶函数嘛:
- 将组件(函数)作为参数被传递
- 组件(函数)作为返回值输出
组件工厂
HOC 的实现方式主要有两种:
- 属性代理(函数返回一个我们自己定义的组件,代理上层传递过来的 props)
- 反向继承(返回一个继承原组件的组件,并且通过 super 访问原组件的 render 来进行渲染)
下面就通过一个例子来演示如何通过“属性代理”创建一个 HOC 并使用:
// 有一把武器(普通组件)
function Weapon(props) {
return (
<div>
<p>名字:{props.name}</p>
<p>等级:{props.level}</p>
<p>标签:{props.effect}</p>
</div>
)
}
/**
* 给增加点特效(高阶组件)
* 这个高阶组件接受两个参数,其中 NormalComp 为组件
**/
function WithEffectHOC(NormalComp, effect) {
// 返回一个新的组件
return function(props) {
/**
* 对 props 进行代理
* 这里只是通过 {...props} 写法将上层传递的 props 进行解构并原封不动地将其全部往下传递
* 下面的写法中,先写 effect 再写 props 的解构,如此如果上层所传递的 props 中也含有 effect 属性的话,将会覆盖前面写的 effect 哦~
*
* p.s. 这里只是单纯地全部传递,但是实际使用中,一般会对 props 做各种处理啥的
**/
return (
<NormalComp
effect={effect}
{...props}
/>
)
}
}
/**
* 通过 WithEffectHOC 对 Weapon 进行不同的“拓展”
* 最后得到两个新的组件
**/
const WeaponLight = WithEffectHOC(Weapon, '发光的')
const WeaponDark = WithEffectHOC(Weapon, '黑暗版')
function App() {
return (
<div>
<WeaponLight name="武器A" level="99"/>
<WeaponDark name="武器B" level="10"/>
<WeaponLight name="武器C" level="98" effect="不是一般的发光"/>
</div>
)
}
功能增强
将一些公共逻辑提取出来,构造一个高阶组件,然后根据业务的需要来决定普通组件是否需要通过该高阶组件进行“升级”,譬如:
- 额外的生命周期
- 额外的事件
- 额外的业务逻辑
举一个简单的例子,就是埋点:
function WithSentryHOC (InnerComp) {
return class extends React.Component {
myDivRef = React.createRef()
componentDidMount() {
this.myDivRef.current.addEventListener('click', this.handleClick)
}
componentWillUnmount() {
this.myDivRef.current.removeEventListener('click', this.handleClick)
}
handleClick = () => {
console.log(`发送埋点:点击了${this.props.name}组件`)
}
render() {
return (
<div ref={this.myDivRef}>
<InnerComp {...this.props}/>
</div>
)
}
}
}
function MyNormalComp (props) {
return (
<div>普通组件</div>
)
}
/**
* 给 MyNormalComp 组件“升级”一下
* 每次点击这个组件都会 console.log 一下
* 对于 MyNormalComp 组件来说,这个功能它是“不知道”的
**/
const MyCompWithSentry = WithSentryHOC(MyNormalComp);
function App(){
return (
<MyCompWithSentry name="我的一个组件"/>
)
}
渲染劫持
HOC 里面不单单可以对原组件进行功能拓展,还能增加条件判断,来修改渲染结果
下面使用一个简单的 demo 来演示一下如果做到延时渲染的:
function WithDelayRenderHOC (InnerComp) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
show: false
}
}
componentDidMount(){
window.setTimeout(() => {
this.setState({
show: true
})
}, 3000)
}
render() {
// 当某些条件下渲染的不再是 InnerComp
if (!this.state.show) {
return <div>等待中...</div>
}
return <InnerComp {...this.props}/>
}
}
}
总结
目前只是抽几个比较典型的场景来演示,在实际使用中,设计一个 HOC 往往不会如此简单。这又涉及到 面向切面编程(AOP) 思想,AOP 的主要作用就是把一些和核心业务逻辑模块无关的功能抽取出来,然后再通过“动态织入”的方式掺到业务模块种。
四、钩子 Hooks
Hooks 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
以往使用 Class Component 来编写组件会有以下问题:
- 在组件之间复用状态逻辑很难
- 复杂组件变得难以理解
从前的项目代码中往往是以组件的生命周期来划分成一座座“代码山”,现在将组件中相互关联的部分拆分成更小的函数(就像 Mobx store 一样),其中还能通过 React 提供各种 Hooks 来实现诸如生命周期监听等操作,如此则将代码以业务逻辑进行分割。
下面用较短的篇幅简单演示几种常用 Hooks
的使用方式:
React.useState
状态钩子React.useEffect
副作用钩子React.useCallback
回调函数钩子React.useContext
上下文钩子(前面讲过了)React.useRef
访问钩子(前面讲过了)
React.useState
通过调用 React.useState
方法,并向其传入参数作为默认值,返回一个数组,数组第一个元素为当前值,第二个元素为 set 方法
class MyClassComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
}
}
handleClick = () => {
this.setState({
count: this.state.count + 1
})
}
render(){
return (
<div>
<p>你点击了{this.state.count}次</p>
<button onClick={this.handleClick}>点击</button>
</div>
)
}
}
// 下面同时使用 Hooks 的方式来编写一个效果一摸一样的组件
function MyFuncComponent() {
// 声明一个叫 “count” 的 state 变量。
const [count, setCount] = React.useState(0);
return (
<div>
<p>你点击了{count}次</p>
<button onClick={() => setCount(count + 1)}>点击</button>
</div>
);
}
React.useEffect
用法如下:
React.useEffect(() => {
// do something
return () => {
// trigger when unmount
}
}, [dependencies])
React.useEffect
接受两个参数:
- 函数,会在特定时机被触发
- 数组,为依赖项,也就是当依赖项中数据发生变化时,会触发第一个参数所传递的函数
- 不传递参数,每次重新渲染时都会执行
- 传递非空数组,当其中一项发生变化就会执行
- 传递空数组,仅在组件挂载和卸载时执行
function Welcome(props) {
useEffect(() => {
// 每次组件重新渲染时都会再次执行本函数
document.title = '加载完成';
});
return <p>Hello</p>;
}
React.useCallback
返回一个 memoized 回调函数。
function MyFuncComp(props){
const [count, setCount] = React.useState(0);
const handleClick = () => setCount(count + 1)
return (
<div>
<p>你点击了{count}次</p>
<button onClick={handleClick}>点击</button>
</div>
);
}
上面的例子中,每次 MyFuncComp 重新渲染时,里面的 handleClick 都会被重新声明,最致命的是,这样每次 div 上绑定的 onClick 都不一样了,这样将会导致不必要的重新渲染!
既然 React 都推崇使用 FunctionComponent 的方式写编写组件了,那么其肯定得解决这个问题咯,所以 React.useCallback
等一系列有 memoized 特性的 Hook 就应运而生。
再来改写一下刚刚的例子:
function MyFuncComp(props){
const [count, setCount] = React.useState(0);
const handleClick = React.useCallback(
() => setCount(count + 1),
[count],
);
return (
<div>
<p>你点击了{count}次</p>
<button onClick={handleClick}>点击</button>
</div>
);
}
自定义 Hook
通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。
譬如“获取当前浏览器尺寸(同时监听 resize)”这部分逻辑封装成一个自定义 Hook,供不同的组件同时使用:
/**
* 封装一个获取 client 的 Hook
*
* p.s. Hook 内部也可以使用别的 Hook 的,不断套娃
**/
function useWindowSize() {
// 使用 React.useState 声明一个变量
const [windowSize, setWindowSize] = React.useState<IWindowSize>({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
});
// 使用 React.useCallback 声明一个回调函数
const onResize = React.useCallback(() => {
setWindowSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
});
}, []);
// 使用 React.useEffect 来触发事件绑定
React.useEffect(() => {
window.addEventListener('resize', onResize);
return () => {
// unmount 时还要移除监听哦~
window.removeEventListener('resize', onResize);
};
}, [onResize]);
return windowSize; // 只返回值(不用返回 set 方法)
}
// 组件A
function MyCompA() {
const windowSize = useWindowSize();
return (
<div>
<p>组件A</p>
<p>宽度:{windowSize.width}</p>
<p>高度:{windowSize.height}</p>
</div>
)
}
// 组件B,跟别的 Hook 一起使用
function MyCompB(props){
const [count, setCount] = React.useState(0);
const handleClick = React.useCallback(
() => setCount(count + 1),
[count],
);
const windowSize = useWindowSize();
return (
<div>
<p>你点击了{count}次</p>
<button onClick={handleClick}>点击</button>
<p>宽度:{windowSize.width}</p>
<p>高度:{windowSize.height}</p>
</div>
);
}
如果将不同的业务或者功能逻辑都封装成一个个 Hook,然后组件中只需一个个调用,而无需关心内部逻辑,则可实现逻辑平铺的编码风格~
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!