React 原理&底层逻辑&源码探析(1)

579 阅读11分钟

React 原理&底层逻辑&源码探析

本系列文章属于根据“修言”大佬的同名视频编写的笔记及理解

JSX 代码是如何转变为 DOM 的

关于 JSX 的三个大问题:

JSX 的本质是什么,它和 JS 之间的关系是什么?

为什么要用 JSX?不用会有什么后果?

JSX 背后的功能模块是什么,这个功能模块都做了哪些事情?

JSX 的本质


🍊 *JSX 是 JavaScript 的一种语法扩展,它和模板语言很接近,但是它充分具备 JavaScript 的能力。*
  • JSX 语法是如何在 JavaScript 中生效的呢?

    Babel 工具会将 JSX 解析为 React.createElement 的调用,换句话说就是 JSX 是 React.createElement 的语法糖

  • 既然 JSX 等价于一次 React.createElement 调用,那么为什么 React 官方为什么不直接引导我们用 React.createElement 来创建元素呢?

    来随便写一段 JSX 代码,看它转化为 React.createElement 形式是怎样的

Untitled.png

看得出来 JSX 代码相对于 React.createElement 代码层次分明,意图清晰。

JSX 是如何映射为 DOM 的


  • 简介

    export function createElement(type, config, children)

    Parameters

    type: 用于标识节点的类型

    config: 以对象形式传入,组件所有的属性都会以键值对的形式存储在 config 对象中

    children: 以对象形式传入,它记录的是组件标签之间嵌套的关系

    Returns

    react.element React 的 DOM 对象,即虚拟DOM

可以看出来,createElement 就是格式化了参数,然后调用 ReactElement 方法,在生成模式下,ReactElement 就是简单的赋值给作为返回值的 element

  • createElement 源码

    export function createElement(type, config, children) {
      let propName;
    
      // Reserved names are extracted
      const props = {};
    
      let key = null;
      let ref = null;
      let self = null;
      let source = null;
    
      if (config != null) {
        if (hasValidRef(config)) {
          ref = config.ref;
    
          if (__DEV__) {
            // ....
          }
        }
        if (hasValidKey(config)) {
          if (__DEV__) {
            // ....
          }
          key = '' + config.key;
        }
    
        self = config.__self === undefined ? null : config.__self;
        source = config.__source === undefined ? null : config.__source;
        // Remaining properties are added to a new props object
        for (propName in config) {
          if (
            hasOwnProperty.call(config, propName) &&
            !RESERVED_PROPS.hasOwnProperty(propName)
          ) {
            props[propName] = config[propName];
          }
        }
      }
    
      // Children can be more than one argument, and those are transferred onto
      // the newly allocated props object.
      const childrenLength = arguments.length - 2;
      if (childrenLength === 1) {
        props.children = children;
      } else if (childrenLength > 1) {
        const childArray = Array(childrenLength);
        for (let i = 0; i < childrenLength; i++) {
          childArray[i] = arguments[i + 2];
        }
        if (__DEV__) {
          // ......
        }
        props.children = childArray;
      }
    
      // Resolve default props
      if (type && type.defaultProps) {
        const defaultProps = type.defaultProps;
        for (propName in defaultProps) {
          if (props[propName] === undefined) {
            props[propName] = defaultProps[propName];
          }
        }
      }
      if (__DEV__) {
        // ....
      }
      return ReactElement(
        type,
        key,
        ref,
        self,
        source,
        ReactCurrentOwner.current,
        props,
      );
    }
    
  • ReactElement 源码

    function ReactElement(type, key, ref, self, source, owner, props) {
      const element = {
        // This tag allows us to uniquely identify this as a React Element
        $$typeof: REACT_ELEMENT_TYPE,
    
        // Built-in properties that belong on the element
        type: type,
        key: key,
        ref: ref,
        props: props,
    
        // Record the component responsible for creating this element.
        _owner: owner,
      };
    
      if (__DEV__) {
        // .......
      }
    
      return element;
    }
    
  • createElememt 函数体拆解

Untitled 1.png

从开发者处先得到简单组织的数据,再解析成清晰的数据结构,再由 ReactElement 创建 react.elment 对象

Untitled 2.png

Untitled 3.png

element 依然是虚拟DOM,想要渲染真实的 DOM,需要调用 ReactDOM.render 方法

render(element: React$Element<any>, container: Container, callback: ?Function)

它的第二个参数是原生的 DOM 节点,充当容器。

总结


Untitled 4.png

为什么React16要更改组件的生命周期

这里提及两个关键词: “虚拟 DOM” “组件“

虚拟 DOM:核心算法的基石

Untitled 5.png

组件化:工程化的思想在框架中的落地

几乎所有的可见/不可见的内容都可以被抽离为各种各样的组件,每个组件既是“封闭”的,也是“开放”的

**“封闭”:**在组件自身的渲染工作流中每个组件都只处理它内部的渲染逻辑

**“开放”:**React允许开发者基于“单向数据流”的原则,完成组件间的通信,而组件之间的通信又将改变通信双方/某一方内部的数据,进而对渲染结果构成影响

生命周期的本质:组件的“灵魂”与”躯干“


本文将 render 方法比作组件的灵魂,需要注意的是这里的 render 方法并非将虚拟 DOM 转化为真实DOM 的 ReactDOM.render 方法,而是组件生命周期中将组件转化为虚拟 DOM 的 render 方法

那么,render 方法之外的生命周期方法可以理解为是组件的”躯干”

做一个感性的理解:“躯干”未必总是会做具体的事情,倘若“躯干”做了点什么,往往都会直接或间接地影响到“灵魂”

拆解 React 生命周期:从 React 15说起

缩略图:

Untitled 6.png

render 在执行过程中不会去操作真是 DOM,它的职能是把需要渲染的内容返回出来

  • 以下是 React 15 中组件生命周期的演示示例:

    class MyComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = { count: 0 };
        console.log('constructor');
      }
    
      componentWillMount() {
        console.log('componentWillMount');
      }
    
      componentDidMount() {
        console.log('componentDidMount');
      }
    
      componentWillReceiveProps(nextProps) {
        console.log('componentWillReceiveProps');
      }
    
      shouldComponentUpdate(nextProps, nextState) {
        console.log('shouldComponentUpdate');
        return true;
      }
    
      componentWillUpdate(nextProps, nextState) {
        console.log('componentWillUpdate');
      }
    
      componentDidUpdate(prevProps, prevState) {
        console.log('componentDidUpdate');
      }
    
      componentWillUnmount() {
        console.log('componentWillUnmount');
      }
    
      handleClick() {
        this.setState({ count: this.state.count + 1 });
      }
    
      render() {
        console.log('render');
        return (
          <div>
            <p>Count: {this.state.count}</p>
            <button onClick={() => this.handleClick()}>Increment</button>
          </div>
        );
      }
    }
    
    

    上述代码中,MyComponent 组件包含了 React 15 中所有生命周期方法。在每个生命周期方法中,我们使用 console.log() 方法输出相应的生命周期名称。在组件的 render() 方法中,我们渲染了一个包含状态值和一个按钮的 div 元素。当用户点击按钮时,我们会调用 handleClick() 方法来更新组件的状态。

    如果我们将 MyComponent 组件渲染到页面上,并在控制台中查看输出,可以看到组件的生命周期方法被按照顺序依次调用。例如:

    ReactDOM.render(<MyComponent />, document.getElementById('root'));
    
    

    输出:

    constructor
    componentWillMount
    render
    componentDidMount
    
    

    当用户点击按钮时,组件的状态会发生改变,并重新渲染。此时,React 会调用一系列生命周期方法来更新组件。例如:

    shouldComponentUpdate
    componentWillUpdate
    render
    componentDidUpdate
    
    

    如果我们将组件从页面上卸载,可以看到 componentWillUnmount 方法被调用:

    ReactDOM.unmountComponentAtNode(document.getElementById('root'));
    
    

    输出:

    componentWillUnmount
    

上面的示例还有一个周期函数没有展示就是 componentWillReceiveProps,它的触发原因是父组件的重新渲染导致子组件重新渲染。尽管子组件没有使用父组件的 state,子组件依然会跟着进入更新生命周期。

但是 render 函数如果频繁执行,会很费性能。

那么就引出了 shouldComponentUpdate() 生命周期函数的意义,React 组件会根据它的返回值来决定是否执行该方法后的生命周期,进而决定是否对组件进行 re-render

进化的生命周期方法:React 16 生命周期工作流详解

缩略图:

Untitled 7.png

  • 以下是 React 16 中组件生命周期的演示示例:

    class MyComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = { count: 0 };
        console.log('constructor');
      }
    
      static getDerivedStateFromProps(props, state) {
        console.log('getDerivedStateFromProps');
        return null;
      }
    
      componentDidMount() {
        console.log('componentDidMount');
      }
    
      shouldComponentUpdate(nextProps, nextState) {
        console.log('shouldComponentUpdate');
        return true;
      }
    
      getSnapshotBeforeUpdate(prevProps, prevState) {
        console.log('getSnapshotBeforeUpdate');
        return null;
      }
    
      componentDidUpdate(prevProps, prevState, snapshot) {
        console.log('componentDidUpdate');
      }
    
      componentWillUnmount() {
        console.log('componentWillUnmount');
      }
    
      handleClick() {
        this.setState({ count: this.state.count + 1 });
      }
    
      render() {
        console.log('render');
        return (
          <div>
            <p>Count: {this.state.count}</p>
            <button onClick={() => this.handleClick()}>Increment</button>
          </div>
        );
      }
    }
    
    

    上述代码中,MyComponent 组件包含了 React 16 中所有生命周期方法。在每个生命周期方法中,我们使用 console.log() 方法输出相应的生命周期名称。在组件的 render() 方法中,我们渲染了一个包含状态值和一个按钮的 div 元素。当用户点击按钮时,我们会调用 handleClick() 方法来更新组件的状态。

    如果我们将 MyComponent 组件渲染到页面上,并在控制台中查看输出,可以看到组件的生命周期方法被按照顺序依次调用。例如:

    ReactDOM.render(<MyComponent />, document.getElementById('root'));
    

    输出:

    constructor
    getDerivedStateFromProps
    render
    componentDidMount
    
    

    当用户点击按钮时,组件的状态会发生改变,并重新渲染。此时,React 会调用一系列生命周期方法来更新组件。例如:

    getDerivedStateFromProps
    shouldComponentUpdate
    render
    getSnapshotBeforeUpdate
    componentDidUpdate
    
    

    如果我们将组件从页面上卸载,可以看到 componentWillUnmount 方法被调用:

    ReactDOM.unmountComponentAtNode(document.getElementById('root'));
    

    输出:

    componentWillUnmount
    

做一个挂载阶段的对比:

Untitled 8.png

废弃了componentWillMount,新增了getDerivedStateFromProps

🍐 注.React16对render方法也进行了一些改进。React16之前,render方法必须返回单个元素,而React16允许我们返回元素数组和字符串。但本课时我们更加则重讨论的是“工作流”层面的改变

getDerivedStateFromProps 不是 componentWillMount 的替代品

componentWillMount的存在不仅“鸡肋”而且危险因此它并不值得被“代替”,它就应该被废弃

getDerivedStateFromProps 有且仅有一个用途:使用 props 来派生/更新 state

从之前的示例可以看出,getDerivedStateFromProps 在更新和挂载两个阶段都会出镜

可以看出挂载阶段的生命周期改变是一个雄心勃勃的”进化“逻辑

认识 getDerivedFromProps

static getDerivedStateFromProps(props, state)

第一个参数是组件接收的 props ,第二个参数是组件当前的 state

注意,这是静态方法

(对于类组件)当重写这个方法时,需注意必须返回一个对象形式的返回值,以更新 state

getDerivedStateFromProps 方法对 state 的更新动作并非“覆盖”式的更新,而是针对某个属性的定向更新

更新阶段的对比:

Untitled 9.png

🍐 16.4 的更新仅在 getDerivedStateFromProps 上,16.4 中任何原因触发的重新渲染都会触发 getDerivedStateFromProps,16.3 仅有由父组件导致的重新渲染会触发 getDerivedStateFromProps

为什么要用 getDerivedStateFromProps 代替 componentWillReceiveProps?

🍊 与 componentDidUpdate 一起,这个新的生命周期涵盖过时 componentWillReceiveProps 的所有用例。 —— React官方
  • getDerivedStateFromProps 是作为一个试图代替 componentWillReceiveProps 的API而出现的
  • getDerivedStateFromProps 不能完全和componentWillReceiveProps画等号
  • React 16.3 之后,componentWillReceiveProps 被标记为不安全的生命周期函数,因为它可能会导致一些副作用。

React 16在强制推行“只用getDerivedStateFromProps来完成props到state的映射”

消失的 componentWillUpdate 与新增的 getSnapshotBeforeUpdate

getSnapshotBeforeUpdate 的返回值会作为第三个参数给到 componentDidUpdate

它的执行时机是在 render 方法之后,真实DOM更新之前

同时获取到更新前的真实DOM和更新前后的 state&props 信息

getSnapshotBeforeUpdate 的主要作用是在组件更新前获取一些 DOM 相关的信息,例如滚动位置或者输入框的光标位置等。因为在组件更新后获取这些信息可能会出现不准确的情况,所以这个函数可以在组件更新前获取这些信息并在组件更新后将这些信息传递给 componentDidUpdate 函数,从而保证正确性。

这个函数的返回值会被传递给 componentDidUpdate 函数的第三个参数,可以在 componentDidUpdate 函数中使用这个参数来根据前后两个状态的差异来执行一些操作。如果不需要在更新后执行任何操作,这个函数可以返回 null

  • 以下是一个使用 getSnapshotBeforeUpdate 的场景示例:

    假设我们有一个聊天应用,聊天框会不断地更新显示新的聊天信息。我们希望当用户向上滚动查看历史聊天记录时,不会因为新的聊天信息的到来而使滚动位置发生变化。为了实现这个功能,我们可以在 getSnapshotBeforeUpdate 中获取当前的滚动位置,并在 componentDidUpdate 中将滚动位置恢复到之前的位置。

    示例代码如下:

    class ChatBox extends React.Component {
      constructor(props) {
        super(props);
        this.chatBoxRef = React.createRef();
      }
    
      getSnapshotBeforeUpdate(prevProps, prevState) {
        // 获取当前滚动位置
        const chatBox = this.chatBoxRef.current;
        return chatBox.scrollHeight - chatBox.scrollTop;
      }
    
      componentDidUpdate(prevProps, prevState, snapshot) {
        // 恢复滚动位置
        const chatBox = this.chatBoxRef.current;
        chatBox.scrollTop = chatBox.scrollHeight - snapshot;
      }
    
      render() {
        return (
          <div ref={this.chatBoxRef}>
            {/* 渲染聊天信息 */}
          </div>
        );
      }
    }
    
    

    在上面的示例中,我们在 getSnapshotBeforeUpdate 函数中获取了聊天框的滚动位置,将其作为返回值。在 componentDidUpdate 函数中,我们将滚动位置恢复到之前的位置。这样,用户在向上滚动查看历史聊天记录时,即使新的聊天信息到来,也不会影响滚动位置的变化,从而提供了更好的用户体验。

卸载阶段

卸载阶段,React16 与 React15 没有变化

为什么要这样进化组件生命周期呢?

这是为了迎合 Fiber 架构

🍊 Fiber 是 React16 对 React 核心算法的一次重写 Fiber 会使原本同步渲染过程变成异步的

在未使用 Fiber 架构前,React 组件每次更新都会构建一棵新的虚拟 DOM 树,与之前的虚拟 DOM 进行一次 diff,这个过程是递归的,这个漫长且不会被打断的更新过程带来的用户体验极差。

Fiber 会将一个大的更新任务拆解为许多个小任务,每当一个小任务完成时都会把主线程交回去,看看有没有优先级更高的任务,确保不会出现其他任务被饿死的情况

换个角度看生命周期工作流

Fiber架构的重要特征就是可以被打断的异步渲染模式

根据“能否被打断”这一标准,React 16的生命周期被划分为了 render 和 commit 两个阶段,commit 阶段又被分为了 pre-commit commit 两个阶段

render阶段在执行过程中允许被打断,而commit阶段则总是同步执行的

为什么这样安排呢?

因为 render 阶段对于用户而言都是不可视的,打断后不会有任何表现,不影响用户体验,但是 commit 阶段就涉及修改真实 DOM,改变视图了,那么所以更新视图的操作就应该同步

细说生命周期“废旧立新”背后的思考

render 阶段是允许暂停、终止和重启的

这就导致 render 阶段的生命周期都是有可能被重复执行的

现在我们来看看 React 16 打算废弃的是哪些生命周期:

  • componentWillMount
  • componentWillUpdate
  • componentWillReceiveProps

它们都处于 render 阶段,都可能重复被执行

比如在 componentWillMount 当中可能经常做的操作有:setState() fetch 发起异步请求 操作真实DOM….

  • 在Fiber带来的异步渲染机制下,可能会导致非常严重的Bug

    由于render阶段里的生命周期都可以重复执行,在componentWillxxx被打断+重启多次后,就会发出多个付款请求

    避免开发者触碰ths,就是在避免各种危险的骚操作

总的来说,React 16改造生命周期的主要动机是为了配合Fiber架构带来的异步渲染机制,针对生命周期中长期被滥用的部分推行了具有强制性的最佳实践,确保了Fiber机制下数据和视图的安全性,同时也确保了生命周期方法的行为更加纯粹、可控、可预测。