最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 深入浅出 React -- Hooks 的动机和概览

    正文概述 掘金(lyn-ho)   2020-12-10   727
    • 完全可选的。 你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。
    • 100% 向后兼容的。 Hook 不包含任何破坏性改动。
    • 现在可用。 Hooks 已发布于 v16.8.0。

    初识动机

    React Hooks 是 React 团队在开发实践中,逐渐认识到的改进点。它背后的思考涉及到类组件函数组件的对比。所以,我们首先需要知道,什么是类组件、函数组件以及两者的比较。

    类组件(Class Component)

    基于 ES6 Class ,通过继承 React.Component 定义组件。

    示例:

    // 代码源自 “深入浅出搞定 React -- 修言”
    
    class DemoClass extends React.Component {
      // 初始化类组件的 state
      state = {
        text: ''
      }
    
      // 编写生命周期方法 didMount
      componentDidMount() {
        // 省略业务逻辑
      }
    
      // 编写自定义的实例方法
      changeText = (newText) => {
        // 更新 state
        this.setState({
          text: newText
        })
      }
    
      // 编写生命周期方法 render
      render() {
        return (
          <div className="demoClass">
            <p>{this.state.text}</p>
            <button onClick={this.changeText}>点我修改</button>
          </div>
        )
      }
    }
    

    函数组件/无状态组件(Function Component/Stateless Component)

    以函数形式定义的组件。在早期没有 React-Hooks 的时候,函数组件内部无法定义和维护 state,所以它也叫无状态组件。

    示例:

    // 代码源自 “深入浅出搞定 React -- 修言”
    
    function DemoFunction(props) {
      const { text } = props
    
      return (
        <div className="demoFunction">
          <p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p>
        </div>
      )
    }
    

    类组件与函数组件的对比

    通过上面两个示例,除了在写法上的区分。还有一些用法上的区别包括但不限于:

    • 类组件需要继承;函数组件不需要
    • 类组件可以访问生命周期方法;函数组件不可以
    • 类组件可以获取实例化后的 this,并可以基于 this 做各种操作;函数组件不可以
    • 类组件可以定义并维护 state ;函数组件不可以
    • ...

    我们很容易看出,类组件的能力明显强于函数组件(在 Hook 出现之前),但这并不说明“类组件强于函数组件”。同理,也不应鼓吹函数组件的轻量优雅,会在将来干掉类组件。这两种形式并没有优劣之别,我们更应该关注两者的不同,进而在不同的场景使用相应的组件,这样才能获得一个全面、辩证的认知。

    深入理解类组件

    类组件是典型的面向对象编程。对于面向对象这个概念,我们总会想到 “封装” 和 “继承”

    再次思考下类组件示例:

    // 代码源自 “深入浅出搞定 React -- 修言”
    
    class DemoClass extends React.Component {
      // 初始化类组件的 state
      state = {
        text: ''
      }
    
      // 编写生命周期方法 didMount
      componentDidMount() {
        // 省略业务逻辑
      }
    
      // 编写自定义的实例方法
      changeText = (newText) => {
        // 更新 state
        this.setState({
          text: newText
        })
      }
    
      // 编写生命周期方法 render
      render() {
        return (
          <div className="demoClass">
            <p>{this.state.text}</p>
            <button onClick={this.changeText}>点我修改</button>
          </div>
        )
      }
    }
    

    从中可以看出,React 类组件内置了很多属性/方法;你只需继承 React.Component 就可以获得它们。

    类组件的强大毋庸置疑,但“多”就是“好”吗?其实未必。

    React 类组件提供了多少东西,你就需要学习多少东西。假如对属性/方法的使用理解不够透彻甚至错误,会出现很大的问题。大而全的背后,是巨大的学习成本

    类组件还有一个问题,它太重了,对于解决很多简单的问题,编写一个类组件就显得过于复杂。复杂度高就带来了高昂的理解成本。

    更为致命的是,类组件的内部逻辑难以复用。如果一定要这么做需要学习更复杂的实现(比如高阶组件、Render Props 等),这是一个更高的学习成本

    深入理解函数组件

    我们再来看下函数组件示例:

    // 代码源自 “深入浅出搞定 React -- 修言”
    
    function DemoFunction(props) {
      const { text } = props
    
      return (
        <div className="demoFunction">
          <p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p>
        </div>
      )
    }
    

    函数组件不止可以做渲染,也可以实现复杂的交互逻辑:

    // 代码源自 “深入浅出搞定 React -- 修言”
    
    function DemoFunction(props) {
      const { text } = props 
    
      const showAlert = ()=> {
        alert(`我接收到的文本是${text}`)
      } 
    
      return (
        <div className="demoFunction">
          <p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p>
          <button onClick={showAlert}>点击弹窗</button>
        </div>
      )
    }
    

    相较于类组件,函数组件轻量、灵活、易于组织和维护、较低的学习成本。

    更重要的是,React 作者 Dan Abramov 写过一篇关于 函数式组件与类组件有何不同 的文章。整篇文章论证了一件事:

    React的函数式组件和类组件之间是否有任何根本上的区别?当然有 —— 在心智模型上。是面向对象和函数式编程的两套不同的设计思想之间的差异。

    函数组件更加契合 React 框架的设计理念

    UI = f(data)
    

    React 组件本身的定位就是函数,能够把数据变成视图的函数。

    作为开发者,我们所编写的是声明式的代码,而 React 框架的主要工作就是把声明式的代码转换为命令式的 DOM 操作,把数据映射到用户可见的视图。所以,React 的数据应该和渲染绑在一起,而类组件做不到这一点。

    为什么类组件做不到?我们来看一下上面文章的示例代码:

    • 类组件:
    import React from 'react';
    
    class ProfilePage extends React.Component {
      showMessage = () => {
        alert('Followed ' + this.props.user);
      };
    
      handleClick = () => {
        setTimeout(this.showMessage, 3000);
      };
    
      render() {
        return <button onClick={this.handleClick}>Follow</button>;
      }
    }
    
    export default ProfilePage;
    
    • 函数组件:
    import React from 'react';
    
    function ProfilePage(props) {
      const showMessage = () => {
        alert('Followed ' + props.user);
      };
    
      const handleClick = () => {
        setTimeout(showMessage, 3000);
      };
    
      return (
        <button onClick={handleClick}>Follow</button>
      );
    }
    
    export default ProfilePage;
    

    尝试按照以下顺序来分别使用这两个按钮:

    1. 点击 其中某一个 Follow 按钮。
    2. 在3秒内 切换 选中的账号。
    3. 查看 弹出的文本。

    你将看到一个奇特的区别:

    • 当使用 函数式组件 实现的 ProfilePage, 当前账号是 Dan 时点击 Follow 按钮,然后立马切换当前账号到 Sophie,弹出的文本将依旧是 'Followed Dan'

    • 当使用 类组件 实现的 ProfilePage, 弹出的文本将是 'Followed Sophie'

    • 类组件的结果:

    深入浅出 React -- Hooks 的动机和概览

    类组件的结果让人感到困惑:user 的内容是通过 props 传递的,而 props 是不可变的,为什么弹出的文字变了(从 Dan 变成 Sophie)?

    this.props.user 读取数据。在 React 中 props 是不可变的(immutable),但是 this 是永远可变的(mutable)。事实上,这就是类组件 this 存在的意义。React 本身会随着时间的推移而改变,以便可以在 render 方法及生命周期方法中获得最新的实例。

    如果我们说UI在概念上是当前应用状态的一个函数,那么事件处理程序则是渲染结果的一部分 —— 就像视觉输出一样。我们的事件处理程序“属于”一个拥有特定 props 和 state 的特定渲染。

    然而,调用一个回调函数读取 this.props 的 timeout 会打断这种关联。我们的 showMessage 回调并没有与任何一个特定的渲染“绑定”在一起,所以它“失去”了正确的 props。从 this 中读取数据的这种行为,切断了这种联系。

    • 函数组件的结果:

    深入浅出 React -- Hooks 的动机和概览

    props 会在 ProfilePage 函数执行的时候就被捕获,而 props 本身是不可变的,所以我们可以充分保证从函数执行开始,到之后的任何时机读取的 props 都是最初捕获的 props

    当父组件传入新的 props 重新渲染,本质上是基于新的 props 发起一次全新的函数调用,并不会影响上一次调用的 props

    也就是说:函数式组件捕获了渲染所用的值。

    经过编程实践,React 团队认识到了,函数组件更加符合设计理念、也更有利于逻辑拆分和复用。接下来就是“用脚投票”,用实际行动支持开发者编写函数组件。于是,React-Hooks 便应运而生。

    Hooks 的本质

    一套能够使得函数组件更强大、更灵活的“钩子”。让函数组件拥有类组件的能力,并且保留轻量、优雅的特性。

    Hooks 是使用你已经知道的 React 特性的一种更直接的方式 —— 比如 state,生命周期,context,以及 refs。它们并没有从根本上改变 React 的工作方式,你对组件,props, 以及自顶向下的数据流的知识并没有改变。

    Hooks 核心 API

    useState()

    为函数组件引入状态(state

    有了 useState 之后,我们就可以直接在函数组件中引入 state

    示例对比:

    import React, { Component } from "react";
    
    class Example extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          count: 0
        };
      }
      
      changeCount = () => {
        this.setState({
          count: this.state.count + 1
        })
      }
    
      render() {
        return (
          <div>
            <p>You clicked {this.state.count} times</p>
            <button onClick={this.changeCount}>
              Click me
            </button>
          </div>
        );
      }
    }
    
    import React, { useState } from 'react';
    
    function Example() {
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }
    

    同样逻辑的函数组件相比类组件而言,复杂度要低得多得多

    useState 做了什么

    useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。

    useState 参数

    useState() 方法里面唯一的参数就是初始 state。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。

    useState 返回值

    返回值为:当前 state 以及更新 state 的函数。

    this.state = {
      text: 'abc',
      num: 3,
      arr: [1, 2, 3]
    }
    

    使用 useState

    const [text, setText] = useState('abc')
    
    const [num, setNum] = useState(3)
    
    const [arr, setArr] = useState([1, 2, 3])
    

    你还可以定义布尔值、对象等,它就像类组件中 state 对象的某一个属性一样,对应着一个单独的状态,允许存储任意类型的值。

    useEffect()

    useEffect 在一定程度上弥补了函数组件的生命周期的缺失。我们可以将在 componentDidMountcomponentDidUpdatecomponentWillUnmount 三个生命周期里来做的事情,放到 useEffect 里来做。比如操作 DOM、绑定/解绑事件、网络请求 等。

    示例对比:

    class Example extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          count: 0
        };
      }
    
      componentDidMount() {
        document.title = `You clicked ${this.state.count} times`;
      }
      componentDidUpdate() {
        document.title = `You clicked ${this.state.count} times`;
      }
    
      render() {
        return (
          <div>
            <p>You clicked {this.state.count} times</p>
            <button onClick={() => this.setState({ count: this.state.count + 1 })}>
              Click me
            </button>
          </div>
        );
      }
    }
    
    import React, { useState, useEffect } from 'react';
    
    function Example() {
      const [count, setCount] = useState(0);
    
      // Similar to componentDidMount and componentDidUpdate:
      useEffect(() => {
        // Update the document title using the browser API
        document.title = `You clicked ${count} times`;
      });
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }
    

    useEffect 和生命周期的关系

    useEffect 和生命周期方法并不完全相等。它们在心智模型上是不同的,useEffect 更接近于实现状态同步,而不是响应生命周期事件。

    当我不再透过熟悉的class生命周期方法去窥视useEffect 这个Hook的时候,我才得以融会贯通。

    effect 分类

    副作用分为两类:需要清除和不需要清除

    useEffect 参数

    两个参数,回调函数和依赖数组

    useEffect(callback, [])

    第一个参数,要么返回一个能清除副作用的函数,要么无返回值

    useEffect(() => {
      // 副作用逻辑
    
      return () => {
        // 清除副作用
      }
    }, [])
    

    清除副作用会在卸载阶段执行,且与依赖数组无关。

    第二个参数:依赖数组

    • 如果不传入依赖数组,每一次渲染后都执行副作用
    useEffect(callback)
    
    • 依赖数组为空数组,仅在挂载阶段执行一次副作用
    useEffect(callback, [])
    
    • 依赖数组为非空数组,React 会在新的一次渲染后去对比前后两次的渲染,查看依赖数组内的变量是否发生更新,只要有一个发生更新就会执行副作用
    useEffect(callback, [a, b, c])
    

    扩展阅读

    由 React 作者的文章: useEffect 完整指南

    Why Hooks

    • 类(Class)组件难以理解
    • 类组件业务逻辑难以拆分
    • 类组件的状态逻辑难以复用
    • 函数组件从设计思想上更契合 React

    难以理解的类组件

    类组件难以理解的背后是 this 和生命周期的理解

    • this 问题

    ?:

    class Example extends React.Component {
      state = {
        count: 0
      }
      
      changeCount() {
        this.setState({
          count: this.state.count + 1
        })
      }
    
      render() {
        return (
          <div>
            <p>You clicked {this.state.count} times</p>
            <button onClick={this.changeCount}>
              Click me
            </button>
          </div>
        );
      }
    }
    

    当点击 button 按钮时,我们希望 count + 1 ,但实际是程序会报错。原因很简单,changeCount 方法里面拿不到组件实例 this。为了解决这个问题,我们使用 bind 或箭头函数。但不管哪种方法,本质上都是在实践层面的约束来解决设计层面的问题

    • 生命周期的问题
      • 学习成本
      • 不合理的逻辑规划

    Hooks 实现更好的逻辑拆分

    类组件中业务逻辑与生命周期耦合在一起

    componentDidMount() {
      // 1. 这里发起异步调用
      // 2. 这里从 props 里获取某个数据,根据这个数据更新 DOM
      // 3. 这里设置一个订阅
      // 4. 这里随便干点别的什么 
      // ...
    }
    
    componentWillUnMount() {
      // 在这里卸载订阅
    }
    
    componentDidUpdate() {
      // 1. 在这里根据 DidMount 获取到的异步数据更新 DOM
      // 2. 这里从 props 里获取某个数据,根据这个数据更新 DOM(和 DidMount 的第2步一样)
    }
    

    将没有关联的逻辑塞进同一个生命周期里,将强关联的逻辑分散到不同的生命周期里;导致生命周期复杂,逻辑混乱。

    Hooks 可以实现关注点分离,将组件中相互关联的部分拆分成更小的函数。帮助我们实现业务逻辑的聚合,避免复杂的组件和冗余的代码。

    状态复用

    类组件的逻辑复用需要使用高阶组件(HOC) 和 Render Props 组件设计模式。在实现逻辑复用的同时,也破坏了组件的结构;其中最常见的就是 “嵌套地狱” 现象

    Hooks 可以通过自定义 Hook 实现逻辑复用,这么做既不破坏组件结构,也能实现逻辑复用的目的。

    Hooks 不是万能的

    • Hooks 还没有完全为函数组件补齐类组件的能力:比如 getSnapshotBeforeUpdatecomponentDidCatch 这些生命周期
    • 函数组件给了我们一定程度的自由,但也对开发者的水平提出更高的要求
    • Hooks 在使用上有着严格的规则约束

    起源地下载网 » 深入浅出 React -- Hooks 的动机和概览

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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