前言
文档目录:
- 实例引用 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.createContextContext.ProviderContext.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介绍。
 
- 提示下载完但解压或打开不了?
 
- 找不到素材资源介绍文章里的示例图片?
 
- 模板不会安装或需要功能定制以及二次开发?
 
                    
    
发表评论
还没有评论,快来抢沙发吧!