react源码解析(二)

393 阅读11分钟

React API:react API和各个API的作用

本节不用关心这些是如何映射到dom中,本节先介绍每一项api含义

  • JSX=>JS
  • createElement
  • Component
  • Ref
  • createContext
  • ConcurrentMode
  • Suspense
  • Hooks

1.JSX=>JS (props&函数组件&类组件&组件为什么要一定大写开头)

在线试一试: www.babeljs.cn/repl

jsx→js

jsx

<div id="dxx" key="key">
  <span id="xyl">1</span>
  <span class="xka">2</span>
</div>

js

"use strict";

React.createElement("div", {
  id: "dxx",
  key: "key"
}, React.createElement("span", {
  id: "xyl"
}, "1"), React.createElement("span", {
  class: "xka"
}, "2"));
大写→变量

jsx

function Comp(){
	return <a>123</a>
}
<Comp id="dxx" key="key">
  <span id="xyl">1</span>
  <span class="xka">2</span>
</Comp>

js

"use strict";

function Comp() {
  return React.createElement("a", null, "123");
}

React.createElement(Comp, {
  id: "dxx",
  key: "key"
}, React.createElement("span", {
  id: "xyl"
}, "1"), React.createElement("span", {
  class: "xka"
}, "2"));
小写→翻译成字符串

jsx

function comp(){
	return <a>123</a>
}
<comp id="dxx" key="key">
  <span id="xyl">1</span>
  <span class="xka">2</span>
</comp>

js

"use strict";

function comp() {
  return React.createElement("a", null, "123");
}

React.createElement("comp", {
  id: "dxx",
  key: "key"
}, React.createElement("span", {
  id: "xyl"
}, "1"), React.createElement("span", {
  class: "xka"
}, "2"));

2.React.createElement() & ReactElement & cloneElement & createFactory & isValidElement解析

React.createElement()

首先我们从github克隆下来react的源码库,我们先来分析下react源码库的文件布局

react工程根目录下有packages文件夹,其间放置的是react的各个包,我们暂时把着力点放于react目录下。内部是react源码实现。

抛出去一些非必要的检测,和warn代码,核心的react代码其实只有几百行。react源码本身并不复杂,负责渲染的react-dom才是最复杂的。

createElement方法位于ReactElement.js文件内,实现如下:

/**
 * dxx 1.2-02 四》》React.createElement() 解析
 * @param {*} type 节点类型 (原生节点:字符串,自己声明主键:classComponent/functionalComponent,react原生组件:就是个标志Fragment、StrictMode、Suspense)
 * @param {*} config 所有写在html标签上的 attr属性 都会变成key value的形式存到这个里面,我们要从这里面筛选出真的是props的内容以及key、ref这些属性
 * @param {*} children 标签中间放置的内容
 */
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;
  //hasValidRef和hasValidKey方法用来校验config中是否存在ref和key属性,有的话就分别赋值给key和ref变量
  if (config != null) {
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }
  //将config.__self和config.__source分别赋值给self和source变量,如果不存在则为null
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // Remaining properties are added to a new props object
    //并通过一个for in循环,把config中的属性添加的props对象中,添加的时候会过滤掉key、ref、__self、__source这几个值
    for (propName in config) {
      if (
        
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)//判断是不是内键的props
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // Children can be more than one argument, and those are transferred onto
  // the newly allocated props object.
  //分别对组件的children和defaultProps进行处理。
  const childrenLength = arguments.length - 2;//arguments.length来获取参数个数,arguments.length-2是为了排除掉type和config这两个属性
  //React.createElement会把第2个参数之后的所有参数都看做是子元素,并最终赋值给props.children属性
  //因此React.createElement方法也支持React.createElement(type,config,child,child,...child)这样的方法调用
  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__) {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    props.children = childArray;//props中的children属性,在这一步会被覆盖掉
  }

  // Resolve default props
  //createElement方法中对于defaultProps的处理
  //如果存在defaultProps,那么遍历他并判断props中是否存在该属性,如果props中该属性的值是undefined,就把defaultProps中的值赋给props
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      //判断如果在开发模式下就会调用Object.defineProperty方法去定义props对象下的key和ref属性
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  
  if (__DEV__) {
    if (key || ref) {
      const displayName =
        typeof type === 'function'
          ? type.displayName || type.name || 'Unknown'
          : type;
      if (key) {
        defineKeyPropWarningGetter(props, displayName);
      }
      if (ref) {
        defineRefPropWarningGetter(props, displayName);
      }
    }
  }
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

这里面有一些开发环境下检测,和外部调用方法,可能会使阅读者精力分散,我们来稍微改动精简下代码,使功能一致,同时更好阅读:

export function createElement(type, config, ...children) {
  const {ref = null, key = null} = config || {};
  const {current} = ReactCurrentOwner;
  const {defaultProps} = type || {};
  const props = assignProps(config, defaultProps, children);

  return new ReactElement({
    type,
    key: '' + key,
    ref,
    current,
    props,
  });
}

经过精简和简化后,createElement仅有30行代码。我们来逐行解析下。

/**
 * 
 * @param type {string | function | object}  
 *        如果type是字符串,那就是原生dom元素,比如div
 *        如果是function或者是Component的子类 则是React组件
 *        object 会是一些特殊的type 比如fragment
 * @param config {object}
 *        props 和key 还有ref 其实都是在config里了
 * @param children
 *        就是由其他嵌套createElement方法返回的ReactElement实例
 * @returns {ReactElement}
 * 
 */
export function createElement(type, config, ...children) {
    
  // 给config设置一个空对象的默认值
  // ref和key 默认为null
  const {ref = null, key = null} = config || {};
  // ReactCurrentOwner负责管理当前渲染的组件和节点
  const {current} = ReactCurrentOwner;
  // 如果是函数组件和类组件 是可以有defaultProps的
  // 比如
  // function A({age}) {return <div>{age}</div>}
  // A.defaultProps = { age:123 }
  const {defaultProps} = type || {};
  // 把defaultProps和props 合并一下
  const props = assignProps(config, defaultProps, children);
  // 返回了一个ReactElement实例
  return new ReactElement({
    type,
    key: '' + key,
    ref,
    current,
    props,
  });
}


ref和key不用多说,大家都知道是干啥的。之前我在想,key明明传的是数字,为啥最后成了字符串,症结就在上面的ReactELement构造函数传参的key那里了,key:''+key。

assignProps是抽象了一个方法,合并defaultProps和传入props的方法,提供代码,其实在cloneElement方法里,也有一段类似代码,但是react并没有抽象出来,相对来说,会有代码冗余,暂且提炼出来。

重点在new ReactElement()。

react的代码里,ReactElement是个工厂函数,返回一个对象。但是我个人觉得比较奇怪。

第一、工厂函数生成实例,这个工厂函数不该大写开头。 第二、使用构造函数或者类来声明ReactElement难道不是一个更好,更符合语义的选择? 在这里,为了便于理解,把ReactElement从工厂函数,改变成了一个类,createElement返回的就是一个ReactElement类的实例。

ReactElement

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // This tag allows us to uniquely identify this as a React Element
    /**
     * 用来标识我们的Element是什么类型的,我们在写jsx代码时候,所有的节点都是通过ReactElement创建的,
     * 那么他的$$typeof就永远都是REACT_ELEMENT_TYPE,这个要记住,因为后续的源码解析中会经常被用到
     * 那是不是所有的节点都是REACT_ELEMENT_TYPE?
     * 大部分情况是的,但是也是会有一些特殊情况的,这些特殊情况会和平台有关,eg,在reactDom里面有一个api叫React.createTotal返回的对象,他的type就是totalType不是REACT_ELEMENT_TYPE
     */
    $$typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    //记录节点类型:class function 
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner,
  };

  if (__DEV__) {
    // The validation flag is currently mutative. We put it on
    // an external backing store so that we can freeze the whole object.
    // This can be replaced with a WeakMap once they are implemented in
    // commonly used development environments.
    element._store = {};

    // To make comparing ReactElements easier for testing purposes, we make
    // the validation flag non-enumerable (where possible, which should
    // include every environment we run tests in), so the test framework
    // ignores it.
    Object.defineProperty(element._store, 'validated', {
      configurable: false,
      enumerable: false,
      writable: true,
      value: false,
    });
    // self and source are DEV only properties.
    Object.defineProperty(element, '_self', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: self,
    });
    // Two elements created in two different places should be considered
    // equal for testing purposes and therefore we hide it from enumeration.
    Object.defineProperty(element, '_source', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: source,
    });
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }

  return element;
};

2、3的重点在于,在react中,jsx标签的本质就是ReactElement,createElement会对组件或者dom的type和props经过一层封装处理,最后返回了ReactElement的实例。

总结

createElement可谓是React中最重要的API了,他是用来创建ReactElement的,但是很多同学却从没见过也没用过,这是为啥呢?因为你用了JSX,JSX并不是标准的js,所以要经过编译才能变成可运行的js,而编译之后,createElement就出现了:

// jsx
<div id="app">content</div>

// js
React.createElement('div', { id: 'app' }, 'content')

cloneElement就很明显了,是用来克隆一个ReactElement的

createFactory是用来创建专门用来创建某一类ReactElement的工厂的,

export function createFactory(type) {
  const factory = createElement.bind(null, type);
  factory.type = type;
  return factory;
}

他其实就是绑定了第一个参数的createElement,一般我们用JSX进行编程的时候不会用到这个API

isValidElement顾名思义就是用来验证是否是一个ReactElement的,基本也用不太到

3.Component vs PureComponent:组件

最重要的概念组件:依托于react上的表现React.Component,我们写的组件更多是继承与React.Component

没看源码前觉得:Component这个base class给我们提供了各式各样的功能,帮助我们去运行render function,然后最终把我们写的DOM标签、子组件之类的渲染出来,渲染到浏览器里面,变成想要的页面

看了源码之后发现:不止有Component还有PureComponent:区别就是提供了我们简便的shouldComponentUpdate的一个实现,保证我们组件在props没有任何变化的情况下,能够减少不必要的更新

图一

图二

Component

Component function
/**
 * Base class helpers for the updating state of a component.
 * dxx 1.Component是一个function,类的声明的一个方式,接收三个参数
 * @props 组件内部使用
 * @context 组件内部使用
 * @updater  
 */
function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  //使用过stringRef:最终会把我们想要获取的那个实例挂载ref上面
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};
Component.prototype.setState
/**
 * @param {object|function} partialState:dxx2.我们要更新的state可以是对象或者方法 
 * @param {?function} callback Called after state is updated.
 * @final
 * @protected
 */
Component.prototype.setState = function(partialState, callback) {
  /**
   * dxx 3.这一段代码都是在判断我们这个对象是不是object||function||null
   * 否则就给个提醒
   */
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  /**
   * dxx 4.这里才是重点 调用了我们初始化时候传入的update对象,
   * enqueueSetState方法是在reactDom里面去实现的,和react没有什么关系,因为不同平台更新state走的渲染流程是不一样的,作为参数是为了区分不同平台
   */
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
Component.prototype.forceUpdate:强制ReactComponent去更新一遍
/**
 * @param {?function} callback Called after update is complete.
 * @final
 * @protected
 */
Component.prototype.forceUpdate = function(callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};

PureComponent:继承于Component

}
/**
 * dxx 6ComponentDummy去实现了一个类似于简单的继承的方式
 */
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;

/**
 * Convenience component with default shallow equality check for sCU.
 * dxx 5.继承于Component
 * 参数和Component一致
 */
function PureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}
// 6ComponentDummy去实现了一个类似于简单的继承的方式
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
//把Component.prototype属性copy到pureComponentPrototype上面
Object.assign(pureComponentPrototype, Component.prototype);
//7.唯一的区别:在isPureReactComponent通过这个属性标识是不是PureReactComponent
pureComponentPrototype.isPureReactComponent = true;

总结

这两个类基本相同,唯一的区别是PureComponent的原型上多了一个标识

组件就是一个函数或者一个 Class(当然 Class 也是 function),它根据输入参数,并最终返回一个 React Element,而不需要我们直接手写无聊的 React Element。

if (ctor.prototype && ctor.prototype.isPureReactComponent) {
  return (
    !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
  );
}

这是检查组件是否需要更新的一个判断,ctor就是你声明的继承自Component or PureComponent的类,他会判断你是否继承自PureComponent,如果是的话就shallowEqual比较state和props。

顺便说一下:React中对比一个ClassComponent是否需要更新,只有两个地方。一是看有没有shouldComponentUpdate方法,二就是这里的PureComponent判断

4.createRef & ref

对节点进行实例操作,获取子节点实例

三种使用ref实例:stringRef(废弃)、function、createRef

新的ref用法,React即将抛弃

这种string ref的用法,将来你只能使用两种方式来使用ref

源码位置ReactContext.js

class App extends React.Component{

  constructor() {
    this.ref = React.createRef()
  }

  render() {
    return <div ref={this.ref} />
    // or
    return <div ref={(node) => this.funRef = node} />
  }

}

Ref在更新过程中发挥着什么作用,后续讲解

5.forwardRef :实现ref的传递

ref获取某个节点实例,但是组件是function component的话没有this实例,通过ref传进去就会报错,开源组件,使用人不知道是什么组件

源码地址:forwardRef.js

forwardRef是用来解决HOC组件传递ref的问题的,所谓HOC就是Higher Order Component,比如使用redux的时候,我们用connect来给组件绑定需要的state,这其中其实就是给我们的组件在外部包了一层组件,然后通过...props的方式把外部的props传入到实际组件。forwardRef的使用方法如下:

const TargetComponent = React.forwardRef((props, ref) => (
  <TargetComponent ref={ref} />
))

这也是为什么要提供createRef作为新的ref使用方法的原因,如果用string ref就没法当作参数传递了。

这里只是简单说一下使用方法,后面讲ref的时候会详细分析。

6.Context :跨多层组件进行信息更新

childContextType 17版本被废弃

对下层所有组件影响太大,导致下层所有组件即便是没有任何更新的情况下,他每一次更新的过程当中仍然要进行完整的渲染,所以对性能的损耗会非常大

createContext

源码位置:ReactContext.js

7.ConcurrentMode:让react整体渲染过程进行优先级排比