最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 学习 React 第二节课:进阶

    正文概述 掘金(tellyourmad)   2021-06-23   448

    前言

    文档目录:

    1. 实例引用 Refs
    2. 上下文 Context
    3. 高阶组件 Higher-Order Components
    4. 钩子 Hooks

    一、实例引用 Refs

    Refs 提供了一种方式,允许我们访问 DOM 节点 或在 render 方法中创建的 React 元素

    使用流程如下:

    1. 创建实例。创建一个 Refs 实例,譬如 this.myRef = React.createRef()
    2. 挂载实例。通过标签中的 ref 属性将上面创建的实例挂载到目标元素,譬如 <input ref={this.myRef}/>
    3. 访问实例。通过访问 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 引入)

    广播模式

    使用方式分两步:

    • 提供:祖先组件中设置 childContextTypesgetChildContext
    • 获取:后代组件中声明 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>
      )
    }
    

    总结一下,使用起来就三个流程:

    1. 使用 React.createContext 创建一个(独立于组件的)状态机实例,同时定义默认值,实例中有两个属性,他们都是一个 React 组件,分别是 ProviderConsumer
    2. 在祖先组件中使用 Provider 组件,并向其传递数据
    3. 在后代组件中使用 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,然后组件中只需一个个调用,而无需关心内部逻辑,则可实现逻辑平铺的编码风格~


    起源地下载网 » 学习 React 第二节课:进阶

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元