React-Hooks 设计动机与工作模式(上)

React-Hooks 设计动机与工作模式(上)

  提起 React-Hooks,可能很多人的第一反应,都会是 useStateuseEffectuseContext这些琐碎且繁多的 API。似乎 React-Hooks 就是一坨没有感情的工具性代码,压根没有啥玄妙的东西在里面,那些面试官天天让咱聊 React-Hooks,到底是想听啥呢?

  当我们由浅入深地认知一样新事物的时候,往往需要遵循“Why→What→How”这样的一个认知过程。这三者是相辅相成、缺一不可的:当我们了解了具体的“What”和“How”之后,往往能够更加具象地回答理论层面“Why”的问题;而我们对“Why”的探索和认知,也必然会反哺到对“What”的理解和对“How”的实践。

  对于一个工程师来说,对"why"的执着程度,很大程度上能够反映职业天花板的高度。

React-Hooks 设计动机初探

  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组件。早期并没有 React-Hooks 的加持,函数组件内部无法定义和维护 state,因此它还有一个别名叫“无状态组件”。以下是一个典型的函数组件:

function DemoFunction(props) {
  const { text } = props
  return (
    <div className="demoFunction">
      <p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p>
    </div>
  );
}

函数组件与类组件的对比:无关“优劣”,只谈“不同”

  基于上面的两个 Demo,从形态上对两种组件做区分。它们之间肉眼可见的区别就包括但不限于:

  • 类组件需要继承 class,函数组件不需要;
  • 类组件可以访问生命周期方法,函数组件不能;
  • 类组件中可以获取到实例化后的 this,并基于这个 this 做各种各样的事情,而函数组件不可以;
  • 类组件中可以定义并维护 state(状态),而函数组件不可以;

  根据以上推论,是否意味着类组件比函数组件更好呢,答案显然是否定的。 在 React-Hooks 出现之前的世界里,类组件的能力边界明显强于函数组件,但要进一步推导“类组件强于函数组件”,未免显得有些牵强。 讨论这两种组件形式时,不应怀揣“孰优孰劣”这样的成见,而应该更多地去关注两者的不同,进而把不同的特性与不同的场景做连接。

重新理解类组件:包裹在面向对象思想下的“重装战舰”

  类组件是面向对象编程思想的一种表征。面向对象是一个老生常谈的概念了,当我们应用面向对象的时候,总是会有意或无意地做这样两件事情。

  1. 封装:将一类属性和方法,“聚拢”到一个 Class 里去。
  2. 继承:新的 Class 可以通过继承现有 Class,实现对某一类属性和方法的复用。

React 类组件也不例外。我们再次审视一下这个典型的类组件 Case:

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 类组件内部预置了相当多的“现成的东西”等着你去调度/定制,state 和生命周期就是这些“现成东西”中的典型。要想得到这些东西,难度也不大,你只需要轻轻地继承一个 React.Component 即可。这种感觉就好像是你不费吹灰之力,就拥有了一辆“重装战舰”,该有的枪炮导弹早已配备整齐,就等你操纵控制台上的一堆开关了。

  毋庸置疑,类组件给到开发者的东西是足够多的,但“多”就是“好”吗?其实未必。React 类组件,也有同样的问题——它提供了多少东西,你就需要学多少东西。假如背不住生命周期,你的组件逻辑顺序大概率会变成一团糟。“大而全”的背后,是不可忽视的学习成本

  类组件太重了,对于解决许多问题来说,编写一个类组件实在是一个过于复杂的姿势。复杂的姿势必然带来高昂的理解成本,这也是我们所不想看到的。类组件固然强大, 但它绝非万能

深入理解函数组件:呼应 React 设计思想的“轻巧快艇”

  我们再来看这个函数组件的 case:

function DemoFunction(props) {
  const { text } = props
  return (
    <div className="demoFunction">
      <p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p>
    </div>
  );
}

  你以为函数组件的简单是因为它只能承担渲染这一种任务,那可就太小瞧它了。它同样能够承接相对复杂的交互逻辑,像这样:

function DemoFunction(props) {
  const { text } = props 
  const showAlert = ()=> {
    alert(`我接收到的文本是${text}`)
  } 
  return (
    <div className="demoFunction">
      <p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p>
      <button onClick={showAlert}>点击弹窗</button>
    </div>
  );
}

  相比于类组件,函数组件肉眼可见的特质自然包括轻量、灵活、易于组织和维护、较低的学习成本等。这些要素毫无疑问是重要的,它们也确实驱动着 React 团队做出改变。

  React 作者 Dan 早期特意为类组件和函数组件写过的一篇非常棒的对比文章,这篇文章很长,但是通篇都在论证这一句话:

函数组件会捕获 render 内部的状态,这是两类组件最大的不同。

  类组件和函数组件之间,纵有千差万别,但最不能够被我们忽视掉的,是心智模式层面的差异,是面向对象和函数式编程这两套不同的设计思想之间的差异,函数组件更加契合 React 框架的设计理念。

 React 组件本身的定位就是函数,一个吃进数据、吐出 UI 的函数。作为开发者,我们编写的是声明式的代码,而 React 框架的主要工作,就是及时地把声明式的代码转换为命令式的 DOM 操作,把数据层面的描述映射到用户可见的 UI 变化中去。这就意味着从原则上来讲,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>;
  }
}

  看起来好像没啥毛病,但是如果你在这个在线 Demo中尝试点击基于类组件形式编写的 ProfilePage按钮后 3s 内把用户切换为 Sophie。

  这个现象必然让许多人感到困惑:user的内容是通过 props下发的,props作为不可变值,为什么会从 Dan变成 Sophie呢?

  虽然 props 本身是不可变的,但 this 却是可变的,this 上的数据是可以被修改的this.props 的调用每次都会获取最新的 props,而这正是 React 确保数据实时性的一个重要手段。

  多数情况下,在 React 生命周期对执行顺序的调控下,this.propsthis.state的变化都能够和预期中的渲染动作保持一致。但在这个案例中,我们通过 setTimeout 将预期中的渲染推迟了 3s,打破了 this.props 和渲染动作之间的这种时机上的关联,进而导致渲染时捕获到的是一个错误的、修改后的 this.props

  但如果我们把 ProfilePage改造为一个像这样的函数组件:

function ProfilePage(props) {
  const showMessage = () => {
    alert('Followed ' + props.user);
  };
  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };
  return (
    <button onClick={handleClick}>Follow</button>
  );
}

  props 会在 ProfilePage函数执行的一瞬间就被捕获,而 props本身又是一个不可变值,因此我们可以充分确保从现在开始,在任何时机下读取到的 props,都是最初捕获到的那个 props。当父组件传入新的 props来尝试重新渲染 ProfilePage时,本质上是基于新的 props入参发起了一次全新的函数调用,并不会影响上一次调用对上一个 props的捕获。这样一来,我们便确保了渲染结果确实能够符合预期。

Hooks 的本质:一套能够使函数组件更强大、更灵活的“钩子”

  React-Hooks 是一套能够使函数组件更强大、更灵活的“钩子”。函数组件比起类组件“少”了很多东西,比如生命周期、对 state的管理等。这就给函数组件的使用带来了非常多的局限性,导致我们并不能使用函数这种形式,写出一个真正的全功能的组件。React-Hooks 的出现,就是为了帮助函数组件补齐这些(相对于类组件来说)缺失的能力。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值