All articles

Deep dive into the React

Nov 07, 2022

最近刚把 TypeScript 从底层学习了一遍,空闲再来,外加由于公司现在用的是 Vue,导致对 React 不是很熟练,所以打算认真看下 React 相关内容。

路线大致是:

  1. 《深入 React 技术栈》
  2. 《The Road To React》

这两本书配合着官方文档一起学习,并且将写一系列博客记录这个过程。

⚠️:博客内容只是一个记录,并非是教学,因此内容主要是一些去要注意的地方。

深入 React 技术栈

这本书写于 2016 年,已经是六年前了,书中用的是 React15 的版本。这期间,版本快速迭代,有许多内容已经不能和现在的版本进行兼容,尤其是 React15React16 升级的过程,更别说今年 Facebook 已经发布了 React18 的正式版。虽然迭代了这么多的版本,但其核心仍然是没变的,因此这本书将会让我重新认识 React 核心。

0x01

简介

ReactAngular 等框架不同,其不是一个完整的 MVC/MVVM 框架,它主要专注于提供清晰、简介的 View(视图)层的解决方案;而又与模板引擎不同,React 不仅仅专注于解决 View 层的问题,又包括的 ViewController 的库。

React 会把真实的 DOM 树转换成 JavaScript 对象树,也就是 VirtualDOM,虚拟 DOMReactVue 一样,不直接操作真实 DOM,通过维护 VirtualDOM 的变化,去操作真实 DOM,从而提高 React 的性能。

JSX

React 为方便 View 层组件化,承载了构建 HTML 结构化页面的指责。但其和其他 JavaScript 模版语言不同的是通过创建与更新细腻元素来管理整个 VirtualDOM

这里用书中一个简单的例子来说明一下。

const DeleteAccount = () => (
  <div>
    <p>Are you sure?</p>
    <DangerButton>Confirm</DangerButton>
    <Button color="blue">Cancel</Button>
  </div>
);

上述 DeleteAccount 并不是真实转换,会考虑实际场景中构建的安全等因素,会由 React 内部方法创建虚拟元素。

JSX 就是将 HTML 语法直接加入到 JavaScript 代码中,再转换到纯 JavaScript 后由浏览器执行。

  • XML 基本语法,定义标签时,只允许一个标签包裹,也就是上面代码需要在标签外面包裹上小括号,并且标签一定要闭合。
  • 元素类型。DOM 元素如 div,组件元素如 MyComponent。首字母小写对应 DOM 元素,首字母大写对应组件元素。
  • 元素属性。DOM 元素和组件元素都有属性,不过 DOM 的属性是标准规范属性,两个例外,classfor
    • class 改为 className
    • for 改为 htmlFor
  • 自定义 HTML 属性。需要加上 data- 前缀,否则不渲染。
  • HTML 转译。React 为了防止 XSS 会将所有显示到 DOM 的字符串转义。

组件

传统组件越来越无法满足开发者的需求,引入了分层思想,出现了 MVC 框架。View 只关心怎么输出变量。

Web Components 规范是同意 Web 端关于组件的定义。

  • HTML Template 定义了之前模版的概念。
  • Custom Elements 定义了组件的展现形式。
  • Shadow DOM 定义了组件的作用域范围。
  • HTML Imports 提出了新的引入方式。

React 组件基本上由属性(props)、状态(state)以及生命周期方法。0.14 版本之前使用 React.createClass 构建组件是最传统的方法。

React 的所有组件都继承自顶层类 React.Component。它的定义非常简洁,只是初始化了 React.Component 方法,声明了 propscontextrefs 等,并在原型上定义了 setStateforceUpdate 方法。

ES6 classes 构建,这里主要用这种方式进行构建。

import React, { Component, PropTypes } from 'react';
class Tabs extends Component {
  constructor(props) {
    super(props);
  }
  // ...
  render() {
    return <div className="ui-tabs"></div>;
  }
};
export defaults Tabs;

当然还有无状态函数构建。无状态组件只传入 propscontext 两个参数;也就是说,它不存在 state,也没有生命周期方法,组件本身即上面两种 React 组件构建方法中的 render 方法。

function Button({
  color = "blue",
  text = "Confirm",
}) {
  return (
    <button className={`btn btn-${color}`}>
      <em>{text}</em>
    </button>
  );
}

数据流

React 的数据流和 Vue 的数据流管道都是 props 并且自顶向下,既是从父组件到子组件。如果顶层组件初始化 props,那么 React 会向下遍历整棵组件树,重新尝试渲染所有相关的子组件。把组件看成一个函数,那么它接受了 props 作为参数,内部由 state 作为函数的内部参数,返回一个 Virtual DOM 的实现。

setState 方法会让我们改变 state 之后,重新渲染组件,其是一个异步的方法。React 中一个重要的内置 props.children 它代表组件的子组件集合,并且还提供了 React.Children 里面包含处理 props.children 的一些方法。

Dynamic Children(动态子组件)将组件中的子组件通过 React.Children 内的一些方法计算得到。在 props 中包含子组件 props、组件 propsfunciton propspropTypes

注意:propType 主要用与规范 props 的类型和是否是必须状态。在 React15.5 版本之后移入另一个包中,使用 prop-types 库进行代替。

生命周期

生命周期(LifeCycle),用过 Vue 的工程师对这个词语都不会太过于陌生,但它与 React 组件的生命周期还是有所区别,React 组件生命周期分为挂载、渲染和卸载。因此 React 组件的生命周期可以分为两类,一类是挂载和卸载的时候,另一类是组件接收到新数据,组件更新时。

挂载

看了下书中的例子,有些内容已经被修改,这里会结合官方文档进行解释说明。书中提到了两个 APIcomponentWillMountcomponentDidMount。其中 componentWillMount 方法会在 render 方法之前执行,而 componentDidMount 方法会在 render 方法之后执行,分别代表了渲染前后的时刻。在这个阶段主要读取初始的 propsstate,只会在组件初始化时运行一次。

现在来说一下哪些内容在最新版的 React 中已经有所修改。官方文档说明目前在 render 之前主要是执行一个静态方法 getDerivedStateFromProps()。因为componentWillMount这个名称在 React16.3 之后更改为 UNSAFE_componentWillMount。这个方法调用 setState 不会触发额外的渲染,并且在 React17 中正式删除。

卸载

组件的卸载则非常的简单,只需要 componentWillUnmount 这一个卸载前状态。

import React, {
  Component,
  PropTypes,
} from "react";

class App extends Component {
  componentWillUnmount() {
    // ...
  }
  render() {
    return <div>This is a demo</div>;
  }
}

在这个方法中主要用于事件回收如取消网络请求或清除一些定时器。

更新

更新主要是在父组件向下传递 props 或组件自身执行 setState 方法时发生的更新动作。如果组件自身的 state 更新了,那么会依次执行 shouldComponentUpdatecomponentWillUpdaterendercomponentDidUpdate

shouldComponentUpdate 它接收需要更新的 propsstate,让开发者增加必要的条件判断,让其在需要时更新,不需要时不更新。如果 return false 不再执行后面的生命周期,其是一个不常用的生命周期方法。

componentWillUpdate 和前面提到的一样,在 React16.3 中已经修改成 UNSAFE_componentWillUpdate。代表在更新过程中渲染前的时刻。

componentDidUpdate 代表更新过程中渲染后的时刻。提供更新前的 propsstate

如果组件是由父组件更新了 props 而更新的,那么在 shouldComponentUpdate 之前会先执行 componentWillReceiveProps。此方法可以作为 Reactprops 传入后,渲染之前设置 setState 的机会。不过这个生命周期也在 React16.3 中重命名为 UNSAFE_componentWillReceiveProps

因此目前生命周期,更新走的是以下流程:

  • static getDerivedStateFromProps()
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate()
  • componentDidUpdate()

小结

React 的生命周期在 16.3 之后给三个生命周期函数加上了 UNSAFE 示意不推荐去使用,并且在 React17 中正式删除了,同时还新增了两个生命周期函数。针对为什么 React 官方会这么做,其实从这些函数的作用可以看出,会经常在 render 之前对生命周期函数进行一些错误的操作,而具体为什么可以看这篇文章我对 React V16.4 生命周期的理解

React 和 DOM

ReactDOM 中的 API 非常少,只有 findDOMNodeunmountComponentAtNoderender

findDOMNode。在提到前面提到的组件生命周期,DOM 真正被添加是在 componentDidMountcomponentDidUpdate 方法。在这两个方法中可以获取到真实的 DOM

⚠️:严格模式已经废弃。

unmountComponentAtNodeReact18 中已经被 root.unmount() 替代。

renderReact18 中已经被 root.createRoot() 替代。在提供的 container 里渲染一个 React 元素,并返回对该组件的 refs 引用。初次渲染之后的更新,其不会重新渲染整个组件,会使用 DOM diff 算法来做局部的更新。

0x02

合成事件

React 合成事件是指将原生事件合成一个 React 事件,之所以要封装自己的一套事件机制,目的是为了实现全浏览器的一致性,抹平不同浏览器之间的差异性。

React 的底层,主要对合成事件做了两件事情:事件委派自动绑定

其具有自己的事件代理机制。它并不会把时间处理函数直接绑定到真实的节点上,而是*把所有的事件绑定到解构的最外层,使用一个统一的事件监听器。*存在一个映射保存所有组件内部的时间监听和处理函数。

React 组件中,每个方法的上下文都会指向该组件实例,自动绑定 this 为当前组件。可以通过 bind、构造器声明、箭头函数自动绑定。

原生事件

当然也可以在 React 当中使用原生事件,可以在 componentDidMount 时,进行原生事件的挂载,在 componentWillUnmount 时候进行卸载原生事件,如果不进行手动卸载,可能会导致内存泄漏的问题。

当原生事件与合成事件进行混合使用时,合成事件内部仅对最外层进行了绑定,依赖事件的冒泡机制完成委派。

受控组件和非受控组件

参考文章:

每当表单的状态发生变化时,都会被写入到组件的 state 中,这种组件在 React 中被称为受控组件(controlled component)。

而对于非受控组件的定义,简单地说,如果一个表单组件没有 value props(单选按钮和复选框对应的是 checked prop)时,就可以称为非受控组件。通过 React.createRef(); 获取 DOM 节点上的 value

In most cases, we recommend using controlled components to implement forms. 官方推荐多数情况下,使用受控组件。

组件通信

组件的通信主要是父组件向子组件通信、子组件向父组件通信以及没有嵌套关系之间组件的通信。

父组件向子组件通信是最常用的,也应该是最熟悉的,主要是通过 props 进行向下传值。

子组件向父组件则是通过 callback 或者 自定义事件机制。

// Father
import { useState } from "react";
import Children from "./Children";

function App() {
  const [name, setName] = useState("Smitish");
  const changeName = (res) => {
    setName(res);
  };
  return (
    <div>
      <h1>Hello, {name}</h1>
      <Children changeName={changeName} />
    </div>
  );
}
export default App;
// Children
function Children({ changeName }) {
  return (
    <div>
      <input
        type="text"
        onChange={(e) =>
          changeName(e.target.value)
        }
      />
    </div>
  );
}

export default Children;

如果需要让子组件跨级访问信息,也就是组件层层传递 props,可以用 context 来实现跨级父子通信 在官方的文档中,Context 所表现出来的特征主要是在组件树中共享全局的数据。这里以官方为例,简单的说明了 context 的使用方法。

const ThemeContext = React.createContext("light");
class App extends React.Component {
  render() {
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

高阶组件

在不同的组件之间使用相同的功能,也就是横向切入点问题,主要的解决办法是 Mixin高阶组件。由于在 ReactMixin 破坏了原有组件的封装,可能会带来新的 stateprops,并且可能会有命名冲突,以及难以维护,因此 React 也抛弃了使用 Mixin,使用高阶组件(higherOrder Component)代替。

额外说下,在 Vue2 中的 OptionAPI 支持 Mixin,但是也是很容易发生命名冲突,并且引入多个 Mixin 对象时,很难区分具体方法的来源,因此在 Vue3 中的 CompositionAPI 更加的偏向于函数式编程,从而可以也算是抛弃了 Mixin

高阶组件的基本形式如下:

const EnhancedComponent = higherOrderComponent(
  WrappedComponent
);

a higher-order component is a function that takes a component and returns a new component.

正如官方案例所示,将 CommentListBlogPost 进行包装成新的组件,类似于 JavaScript 中的 Decorator

const CommentListWithSubscription =
  withSubscription(CommentList, (DataSource) =>
    DataSource.getComments()
  );

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) =>
    DataSource.getBlogPost(props.id)
);

注意:不要在 render 中使用 HOCReactdiff 算法使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。

总结

目前来说已经把最基本的 React 核心的内容基本上都已经复习完了,虽然这本书后面还有一些 DiffReduxReact-Router 等等,但是由于版本更新迭代的太快,之后会单独去探讨这些内容。

后续应该就是看 The Road To React 这本书。

antcao.me © 2022-PRESENT

: 0x9aB9C...7ee7d