React 学习

266 阅读9分钟

学习内容

一、React API介绍

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

二、React中的更新创建

  • ReactDOM.render
  • Fiber
  • UpdateQueue
  • FiberRoot
  • Update
  • expirationTime

三、Fiber Scheduler ⭐(React创建完更新之后的调度过程)

  • schedulerWork
  • batchedUpdates
  • performWork
  • performUnitOfWork
  • requestWork
  • react scheduler
  • renderRoot

四、开始更新

  • beginWork以及优化
  • 各类组件的更新过程
  • 调和子节点的过程

五、完成各个节点的更新

  • completeUnitOfWork
  • completeWork
  • unwindWork
  • 虚拟 DOM对比
  • 错误捕获处理
  • 完成整棵树更新

六、提交更新(将之前计算的结果提交到真正的 DOM树进行更新)

  • commitRoot整体流程
  • 提交快照
  • 提交 DOM更新
  • 提交所有生命周期
  • 开发时的帮助方法
  • 提交 DOM插入
  • 提交 DOM删除

七、各种功能的实现过程

  • context的实现过程
  • ref的实现过程
  • hydrate的实现过程
  • React的事件体系

八、Suspense

  • 更新优先级的概念
  • Suspense组件更新
  • retry重新尝试渲染
  • 更新挂起的概念
  • timeout处理
  • lazy组件更新

九、Hooks

  • 核心原理
  • useState
  • useEffect
  • useContext
  • 其它 Hooks API

总结:React的源码实现

  • Fiber、Update、Scheduler等核心着重讲解
  • 学习过程中要结合 流程图和 结果图

准备工作

目录结构:

需要关注的核心文件

|- packages
  |- events // 自己实现了事件传播的机制
  |- react // react包的核心代码,定义节点和表现行为的包
  |- react-dom // 代码主体部分
  |- react-reconciler
  |- scheduler // 核心包,调度,实现了 react异步渲染的方式
  |- shared // 涉及公用代码

React和 React-Dom的关系:

  • React主要负责定义节点和表现行为的包
  • React-Dom主要负责 React代码主体部分

Flow Type:静态检查工具,类似于 TS

  • 静态检查工具,主要用于检查类型,类似于 TypeScript

一. 基础讲解

JSX 到 JavaScript 的转化

  • JSX 与 JS的唯一区别是,前者可以写类似于 html的标签
  • 通过 Babel将 JSX 转化为 JS,具体实例如下:

JSX -> JS 转化实例:

  • JSX
<div id="test" class="testClass">
	test内容
  	<span key="13">123</span>
</div>

function Comp () {
	return <a>javascript:;</a>
};

// 此时的 Com函数 被当作一个变量处理
<Comp id="test" class="testClass">
	test内容
  	<span key="13">123</span>
</Comp>

/**
注意:目前 React根据变量的首字母是否大写,
来判断是翻译成一个 字符串还是一个变量(组件),
如果是字符串,React会认为这是一个原生的 Dom节点,
所以自定义的组件必须使用 首字母大写开头的形式,
否则在运行时会报错
**/
  • JS(由Babel转化)
"use strict";

/*#__PURE__*/
React.createElement("div", {
  id: "test",
  class: "testClass"
}, "test\u5185\u5BB9", /*#__PURE__*/React.createElement("span", {
  key: "13"
}, "123"));

function Comp() {
  return /*#__PURE__*/React.createElement("a", null, "javascript:;");
}

; // 此时的 Com函数 被当作一个变量处理

/*#__PURE__*/
React.createElement(Comp, {
  id: "test",
  class: "testClass"
}, "test\u5185\u5BB9", /*#__PURE__*/React.createElement("span", {
  key: "13"
}, "123"));
/**
注意:目前 React根据变量的首字母是否大写,
来判断是翻译成一个 字符串还是一个变量(组件),
如果是字符串,React会认为这是一个原生的 Dom节点,
所以自定义的组件必须使用 首字母大写开头的形式,
否则在运行时会报错
**/

注意事项:

目前 React根据变量(组件)的首字母是否大写, 来判断是翻译成一个 字符串还是一个变量(组件), 如果是字符串,React会认为这是一个原生的 Dom节点, 所以自定义的组件必须使用 首字母大写开头的形式, 否则在运行时会报错。

ReactElement.js

// packages/react/ReactElement.js

createElement:创建并返回 ReactElement方法

ReactElement通过 createElement创建,调用该方法需要传入三个参数:type、config、children。

使用到的 JS方法:

  • hasOwnProperty.call(config, propName):返回一个布尔值,指示对象自身属性中是否具有指定的属性

  • Array(childrenLength)

  • for (propName in config) {}:以任意顺序遍历一个对象的除[Symbol]以外的[可枚举]属性

  • 当只考虑遍历对象自身属性时,可以使用 for...in 和 hasOwnProperty方法结合

var triangle = {a: 1, b: 2, c: 3};

function ColoredTriangle() {
  this.color = 'red';
}
ColoredTriangle.prototype = triangle;

var obj = new ColoredTriangle();

for (var prop in obj) {
  if (obj.hasOwnProperty(prop)) {
    console.log(`obj.${prop} = ${obj[prop]}`);
  }
}

// "obj.color = red"

createElement的作用:

  1. 将 config中的属性放到 props中去,除去 ref、key属性

  2. 通过 arguments参数,处理 createElement方法中的参数,将第三个参数及之后的所有参数全部放到 数组中,最后赋值给 props.children保存;如果是刚好有三个参数,则直接将第三个参数赋值给 props.children保存

  3. 处理默认值:type.defaultProps

  4. 将处理好的数据去创建 ReactElement方法

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;

  // 1.将 config中的属性放到 props中去 -----------------------
  if (config != null) {
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      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];
      }
    }
  }

  // 2.处理 props.children----------------------------------
  // 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__) {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    props.children = childArray;
  }

  // 3.处理 type.defaultProps默认值------------------------------
  // Resolve default props
  // class Comp extends React.Component
  // Comp.defaultProps = { value: 1, }
  // Comp给属性 value设置默认值,
  // 如果组件使用时没有设置 value的值,就使用默认值
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  // 4.创建并返回 ReactElement方法-------------------------------- 
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

ReateElement方法

  • 返回一个对象,里边存放了该组件的数据
  • 其中一个属性,$$typeof: REACT_ELEMENT_TYPE 表示该组件的类型
  • 与之类似的有 REACT_PORTAL_TYPE,它是由 ReactDOM.createPortal创建的
const ReactElement = function(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,
  };

  // 一些判断处理

  return element;
};

Component & PureComponent

  1. Component.prototype.setState = function(partialState, callback) {...}:数据更新函数。

  2. Component.prototype.forceUpdate = function(callback) {...}:强制 React-Dom更新。

  3. ComponentDummy.prototype = Component.prototype; 将 ComponentDummy方法的原型对象改为 Component方法的原型对象。

  4. const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy()); 将 PureComponent的原型对象改为 ComponentDummy构造函数的实例。

  5. pureComponentPrototype.isPureReactComponent = true; isPureReactComponent表示当前组件是 PureComponent,在后续更新中 React-Dom会主动判断此属性。

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import invariant from 'shared/invariant';
import lowPriorityWarning from 'shared/lowPriorityWarning';

import ReactNoopUpdateQueue from './ReactNoopUpdateQueue';

const emptyObject = {};
if (__DEV__) {
  Object.freeze(emptyObject);
}

/**
 * Base class helpers for the updating state of a component.
 */
function Component(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;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};

/**
 *
 * @param {object|function} partialState Next partial state or function to
 *        produce next partial state to be merged with current state.
 * @param {?function} callback Called after state is updated.
 * @final
 * @protected
 */
 
// 1.数据更新函数 --------------------------------------------------

Component.prototype.setState = function(partialState, callback) {
  // 类型检查,不重要
  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.',
  );
  // 重要部分,在 React-Dom中使用
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

/**
 * Forces an update. This should only be invoked when it is known with
 * certainty that we are **not** in a DOM transaction.
 *
 * You may want to call this when you know that some deeper aspect of the
 * component's state has changed but `setState` was not called.
 *
 * This will not invoke `shouldComponentUpdate`, but it will invoke
 * `componentWillUpdate` and `componentDidUpdate`.
 *
 * @param {?function} callback Called after update is complete.
 * @final
 * @protected
 */
 
// 2.强制 React.Component更新 -----------------------------------

Component.prototype.forceUpdate = function(callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};

/**
 * Deprecated APIs. These APIs used to exist on classic React classes but since
 * we would like to deprecate them, we're not going to move them over to this
 * modern base class. Instead, we define a getter that warns if it's accessed.
 */
 
// ...

function ComponentDummy() {}
// 3.将 ComponentDummy方法的原型对象改为 Component方法的原型对象 -----------
ComponentDummy.prototype = Component.prototype;

/**
 * Convenience component with default shallow equality check for sCU.
 */
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;
}

// 4.将 PureComponent的原型对象改为 ComponentDummy构造函数的实例 ----------

const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);

// 5.isPureReactComponent表示当前组件是 PureComponent,在后续更新中 React-Dom会主动判断此属性 -----------------------------------------------------------
pureComponentPrototype.isPureReactComponent = true;

export {Component, PureComponent};

createRef & ref

ref的三种使用方式

ref不能单纯的用在 Function Component上,因为函数组件没有实例,但是可以借助 React.forwardRef(),在 函数组件中使用 ref。

  • string ref(废弃)
  • function ref:Dom实例 或 组件实例
  • createRef:React提供的 API,将 实例挂载到对象的 current属性上
import React from 'react'

export default class RefDemo extends React.Component {
  constructor() {
    super()
    this.objRef = React.createRef() // { current: null }

  }

  componentDidMount() {
    
    setTimeout(() => {
      this.refs.stringRef.textContent = 'string ref got'
      this.methodRef.textContent = 'method ref got'
      this.objRef.current.textContent = 'obj ref got'
    }, 1000)
  }

  render() {
    return (
      <>
        <p ref="stringRef">span1</p>
        <p ref={ele => (this.methodRef = ele)}>span3</p>
        <p ref={this.objRef}>span3</p>
      </>
    )
  }
}

createRef

createRef 在编码上只是简单的返回了一个属性 current 为 null的对象。

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 * @flow
 */

import type {RefObject} from 'shared/ReactTypes';

// an immutable object with a single mutable value
export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  if (__DEV__) {
    Object.seal(refObject);
  }
  return refObject;
}

forwardRef

  • forwardRef返回一个对象,其中一个属性是 $$typeof: REACT_FORWARD_REF_TYPE,
  • 但 createElement调用后 此对象的 $$typeof 仍然是 REACT_ELEMENT_TYPE,这才是此组件的类型
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import {REACT_FORWARD_REF_TYPE} from 'shared/ReactSymbols';

import warningWithoutStack from 'shared/warningWithoutStack';

export default function forwardRef<Props, ElementType: React$ElementType>(
  render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
) {

  // ...一些校验逻辑
  
  return {
    $$typeof: REACT_FORWARD_REF_TYPE,
    render,
  };
}
  • 简单使用:
import React from 'react'

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

export default class Comp extends React.Component {
  constructor() {
    super()
    this.ref = React.createRef()
  }

  componentDidMount() {
    this.ref.current.value = 'ref get input'
  }

  render() {
    return <TargetComponent ref={this.ref} />
  }
}

Context 两种方式

childContextType(废弃 V117.xxx)

使用方法:

  1. 父组件要显示声明 context的值和方法
// MessageList
// 父组件显示声明 context的方法,方法名固定
getChildContext() {    return {color: "purple"};  }
// 父组件显示声明 context的值
MessageList.childContextTypes = { color: PropTypes.string };
  1. 子组件(消费组件),要显示订阅 context的值
// Button
// 显示订阅
Button.contextTypes = { color: PropTypes.string };
// 使用
<button style={{background: this.context.color}}>

完整实例:

import PropTypes from 'prop-types';

class Button extends React.Component {
  render() {
    return (
      <button style={{background: this.context.color}}>        {this.props.children}
      </button>
    );
  }
}

Button.contextTypes = { color: PropTypes.string };
class Message extends React.Component {
  render() {
    return (
      <div>
        {this.props.text} <Button>Delete</Button>      
      </div>
    );
  }
}

class MessageList extends React.Component {
  getChildContext() {    return {color: "purple"};  }
  render() {
    const children = this.props.messages.map((message) =>
      <Message text={message.text} />
    );
    return <div>{children}</div>;
  }
}

MessageList.childContextTypes = { color: PropTypes.string };

createContext

/packages/react/src/ReactContext.js

使用实例:

  1. 父组件通过 Privider 提供 value属性

  2. 子组件(类组件)通过 方式一:contextType 或 static contextType使用 context的值

  3. 子组件(函数组件)通过 Consumer使用 context的值

    function Child1(props) {
      return <Consumer>{value => <p>newContext: {value}</p>}</Consumer>
    }
    
  • 完整实例
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。// 为当前的 theme 创建一个 context(“light”为默认值)。

const ThemeContext = React.createContext('light');
class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。    // 无论多深,任何组件都能读取这个值。    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。    return (
      <ThemeContext.Provider value="dark">        
          <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。  
  // React 会往上找到最近的 theme Provider,然后使用它的值。  
  // 在这个例子中,当前的 theme 值为 “dark”。 
  
  // 子组件(类组件)使用方式二:
  // static contextType = ThemeContext;
  
  render() {
    return <Button theme={this.context} />;  }
}
// 子组件(类组件)使用方式一:
ThemedButton.contextType = ThemeContext;

源码:

  • createContext方法返回一个对象 context,对象中包含 Provider 和 Consumer

  • 当 Provider上的属性 value 变化时,会记录在(记录最新的值) _currentValue和 _currentValue2上

  • 可以看出属性 Consumer 就是返回的对象 context:context.Consumer = context;

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';

import type {ReactContext} from 'shared/ReactTypes';

import warningWithoutStack from 'shared/warningWithoutStack';
import warning from 'shared/warning';

export function createContext<T>(
  defaultValue: T,
  // calculateChangedBits:用于计算 新老 context的变化,后期使用
  calculateChangedBits: ?(a: T, b: T) => number,
): ReactContext<T> {

  // ...
  
  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    _calculateChangedBits: calculateChangedBits,
    
    // 1.当 Provider上的属性 value变化时,会记录在(记录最新的值) _currentValue和 _currentValue2上,
    // 这两个属性的值相同只是使用的平台不同 ---------------------------------
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    // These are circular
    Provider: (null: any),
    Consumer: (null: any),
  };

  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };

  let hasWarnedAboutUsingNestedContextConsumers = false;
  let hasWarnedAboutUsingConsumerProvider = false;

  if (__DEV__) {
    // A separate object, but proxies back to the original context object for
    // backwards compatibility. It has a different $$typeof, so we can properly
    // warn for the incorrect usage of Context as a Consumer.
    const Consumer = {
      $$typeof: REACT_CONTEXT_TYPE,
      _context: context,
      _calculateChangedBits: context._calculateChangedBits,
    };
    // $FlowFixMe: Flow complains about not setting a value, which is intentional here
    Object.defineProperties(Consumer, {
      Provider: {
        get() {
          if (!hasWarnedAboutUsingConsumerProvider) {
            hasWarnedAboutUsingConsumerProvider = true;
            warning(
              false,
              'Rendering <Context.Consumer.Provider> is not supported and will be removed in ' +
                'a future major release. Did you mean to render <Context.Provider> instead?',
            );
          }
          return context.Provider;
        },
        set(_Provider) {
          context.Provider = _Provider;
        },
      },
      _currentValue: {
        get() {
          return context._currentValue;
        },
        set(_currentValue) {
          context._currentValue = _currentValue;
        },
      },
      _currentValue2: {
        get() {
          return context._currentValue2;
        },
        set(_currentValue2) {
          context._currentValue2 = _currentValue2;
        },
      },
      Consumer: {
        get() {
          if (!hasWarnedAboutUsingNestedContextConsumers) {
            hasWarnedAboutUsingNestedContextConsumers = true;
            warning(
              false,
              'Rendering <Context.Consumer.Consumer> is not supported and will be removed in ' +
                'a future major release. Did you mean to render <Context.Consumer> instead?',
            );
          }
          return context.Consumer;
        },
      },
    });
    // $FlowFixMe: Flow complains about missing properties because it doesn't understand defineProperty
    context.Consumer = Consumer;
  } else {
    // 2.可以看出 Consumer就是 context本身-----------------------------
    context.Consumer = context;
  }

  if (__DEV__) {
    context._currentRenderer = null;
    context._currentRenderer2 = null;
  }
  // 3.返回一个对象
  return context;
}

ConcurrentMode(V16.xxx)

/packages/shared/ReactSymbols.js

  • 暂时无法理解

让我们能去去区分任务的优先级高低,使得在 React更新时优先执行优先级高的任务,当浏览器空闲时再去执行优先级低的任务。

export const REACT_CONCURRENT_MODE_TYPE = hasSymbol
  ? Symbol.for('react.concurrent_mode')
  : 0xeacf;

Suspense & lazy

在 Suspense中的多个组件,只有当每个组件都加载完成后,才会统一展示各个组件中的内容,否则在此之前,只展示 Suspense上的属性 fallback中的值

  1. 结合 lazy 实现异步加载组件
  2. 加载异步数据(待完善)

实例:

import React, { Suspense, lazy } from 'react'

// 加载异步组件
const LazyComp = lazy(() => import('./lazy.js'))

let data = ''
let promise = ''
function requestData() {
  if (data) return data
  if (promise) throw promise
  promise = new Promise(resolve => {
    setTimeout(() => {
      data = 'Data resolved'
      resolve()
    }, 2000)
  })
  throw promise
}

function SuspenseComp() {
  // 加载异步数据
  const data = requestData()

  return <p>{data}</p>
}

export default () => (
  <Suspense fallback="loading data">
    <SuspenseComp />
    <LazyComp />
  </Suspense>
)
// lazy.js
export default () => <p>Lazy Comp</p>

Suspense源码

/packages/shared/ReactSymbols.js

  • 暂时无法理解
export const REACT_SUSPENSE_TYPE = hasSymbol
  ? Symbol.for('react.suspense')
  : 0xead1;

lazy源码

/package/react/src/ReactLazy.js

  • _status:表示异步加载组件当前的状态
  • _result:保存异步加载的结果组件
import type {LazyComponent, Thenable} from 'shared/ReactLazyComponent';

import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';

export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {
  return {
    $$typeof: REACT_LAZY_TYPE,
    _ctor: ctor,
    // React uses these fields to store the result.
    // 表示异步加载组件当前的状态
    _status: -1,
    // _result:保存异步加载的结果组件
    _result: null,
  };
}

Hooks

  • 暂时不理解

useState

  • ReactCurrentOwner:是一个全局对象,当 React-DOM 渲染的过程当中才会去赋值 ReactCurrentOwner
  • current:表示目前正在渲染的实例
  • currentDispatcher:当前 current实例对应的 dispatcher
const ReactCurrentOwner = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: (null: null | Fiber),
  currentDispatcher: (null: null | Dispatcher),
};
export default ReactCurrentOwner;

function resolveDispatcher() {
  const dispatcher = ReactCurrentOwner.currentDispatcher;
  invariant(
    dispatcher !== null,
    'Hooks can only be called inside the body of a function component.',
  );
  return dispatcher;
}

export function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

useEffect

const ReactCurrentOwner = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: (null: null | Fiber),
  currentDispatcher: (null: null | Dispatcher),
};
export default ReactCurrentOwner;

function resolveDispatcher() {
  const dispatcher = ReactCurrentOwner.currentDispatcher;
  invariant(
    dispatcher !== null,
    'Hooks can only be called inside the body of a function component.',
  );
  return dispatcher;
}

export function useEffect(
  create: () => mixed,
  inputs: Array<mixed> | void | null,
) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, inputs);
}

实际应用:

useEffect,在没有第二个数组参数时, 类似于 update生命周期,每次有数据改变都会去执行 update, 当有有了第二个数组参数后,只有数组中值变化了, 才会去执行 useEffect; 当绑定 DOM操作后,可以返回一个函数用于执行解绑 DOM, 当有返回函数时,在每次执行 useEffect时,都会去先执行上次返回的函数, 再去执行其它代码; 如果第二个数组参数为空,那 useEffect只会执行一次

/**
 * 必须要react和react-dom 16.7以上
 */

import React, { useState, useEffect } from 'react'

export default () => {
  const [name, setName] = useState('jokcy')

  /**
   * useEffect,在没有第二个数组参数时,
   * 类似于 update生命周期,每次有数据改变都会去执行 update,
   * 当有有了第二个数组参数后,只有数组中值变化了,
   * 才会去执行 useEffect;
   * 当绑定 DOM操作后,可以返回一个函数用于执行解绑 DOM,
   * 当有返回函数时,在每次执行 useEffect时,都会去先执行上次返回的函数,
   * 再去执行其它代码;
   * 如果第二个数组参数为空,那 useEffect只会执行一次
   */
  useEffect(() => {
    console.log('component update')

    return () => {
      console.log('unbind')
    }
  }, [])

  return (
    <>
      <p>My Name is: {name}</p>
      <input type="text" value={name} onChange={e => setName(e.target.value)} />
    </>
  )
}

React Children

/packages/react/src/ReactChildren.js

使用实例:

import React from 'react'

function ChildrenDemo(props) {
  console.log(props.children)
  /**
   * [1, 1, 1, 2, 2, 2]
   * React.Children.map中的返回结果如果有数组,会将其全部展开
   */
  console.log(React.Children.map(props.children, c => [c, [c, c]]))
  return props.children
}

export default () => (
  <ChildrenDemo>
    <span>1</span>
    <span>2</span>
  </ChildrenDemo>
)
// 111222

源码:

  • traverseContextPool:类似于对象池的概念,减少常用对象频繁的创建、销毁操作
  • mapIntoWithKeyPrefixInternal:用于开始解析 map、forEach等参数
  • traverseAllChildrenImpl(递归操作):将children数组解析成一个个的child
  • mapSingleChildIntoContext:对每个 child 调用 map中的回调函数,判断返回的是否为数组,如果是单个节点,则将其包装成 ReactElement,如果是 数组,则调用 mapIntoWithKeyPrefixInternal(进行递归)处理数组节点
  1. map方法,返回一维 ReactElement新数组
  2. forEach方法,改变当前 children数组,变为 一维 ReactElement数组
  3. toArray方法,返回将 children展开的一维 ReactElement新数组
  4. only方法,判断 children是否是单个合理的节点

import invariant from 'shared/invariant';
import warning from 'shared/warning';
import {
  getIteratorFn,
  REACT_ELEMENT_TYPE,
  REACT_PORTAL_TYPE,
} from 'shared/ReactSymbols';

import {isValidElement, cloneAndReplaceKey} from './ReactElement';
import ReactDebugCurrentFrame from './ReactDebugCurrentFrame';

const SEPARATOR = '.';
const SUBSEPARATOR = ':';

/**
 * @param {string} key to be escaped.
 * @return {string} the escaped key.
 */
function escape(key) {
  const escapeRegex = /[=:]/g;
  const escaperLookup = {
    '=': '=0',
    ':': '=2',
  };
  const escapedString = ('' + key).replace(escapeRegex, function(match) {
    return escaperLookup[match];
  });

  return '$' + escapedString;
}

/**
 * TODO: Test that a single child and an array with one item have the same key
 * pattern.
 */

let didWarnAboutMaps = false;

const userProvidedKeyEscapeRegex = /\/+/g;
function escapeUserProvidedKey(text) {
  return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/');
}

const POOL_SIZE = 10;
const traverseContextPool = [];
/**
 * traverseContextPool:类似于对象池的概念,------------------------------
 * 这个对象池中的对象需要经常创建、销毁,
 * 这种操作非常消耗性能,浪费 CPU资源,
 * 所以提前在对象池中先声明一定数量的对象,供程序使用,
 * 用完后再放回次对象池;
 */
function getPooledTraverseContext(
  mapResult,
  keyPrefix,
  mapFunction,
  mapContext,
) {
  if (traverseContextPool.length) {
    const traverseContext = traverseContextPool.pop();
    traverseContext.result = mapResult;
    traverseContext.keyPrefix = keyPrefix;
    traverseContext.func = mapFunction;
    traverseContext.context = mapContext;
    traverseContext.count = 0;
    return traverseContext;
  } else {
    return {
      result: mapResult,
      keyPrefix: keyPrefix,
      func: mapFunction,
      context: mapContext,
      count: 0,
    };
  }
}

function releaseTraverseContext(traverseContext) {
  traverseContext.result = null;
  traverseContext.keyPrefix = null;
  traverseContext.func = null;
  traverseContext.context = null;
  traverseContext.count = 0;
  if (traverseContextPool.length < POOL_SIZE) {
    traverseContextPool.push(traverseContext);
  }
}

/**
 * @param {?*} children Children tree container.
 * @param {!string} nameSoFar Name of the key path so far.
 * @param {!function} callback Callback to invoke with each child found.
 * @param {?*} traverseContext Used to pass information throughout the traversal
 * process.
 * @return {!number} The number of children in this subtree.
 */
function traverseAllChildrenImpl(
  children,
  nameSoFar,
  callback,
  traverseContext,
) {
  const type = typeof children;

  if (type === 'undefined' || type === 'boolean') {
    // All of the above are perceived as null.
    children = null;
  }

  let invokeCallback = false;

  if (children === null) {
    invokeCallback = true;
  } else {
    switch (type) {
      case 'string':
      case 'number':
        invokeCallback = true;
        break;
      case 'object':
        switch (children.$$typeof) {
          case REACT_ELEMENT_TYPE:
          case REACT_PORTAL_TYPE:
            invokeCallback = true;
        }
    }
  }

  if (invokeCallback) {
    callback(
      traverseContext,
      children,
      // If it's the only child, treat the name as if it was wrapped in an array
      // so that it's consistent if the number of children grows.
      nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }

  let child;
  let nextName;
  let subtreeCount = 0; // Count of children found in the current subtree.
  const nextNamePrefix =
    nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;

  if (Array.isArray(children)) {
    for (let i = 0; i < children.length; i++) {
      child = children[i];
      nextName = nextNamePrefix + getComponentKey(child, i);
      subtreeCount += traverseAllChildrenImpl(
        child,
        nextName,
        callback,
        traverseContext,
      );
    }
  } else {
    const iteratorFn = getIteratorFn(children);
    if (typeof iteratorFn === 'function') {
      if (__DEV__) {
        // Warn about using Maps as children
        if (iteratorFn === children.entries) {
          warning(
            didWarnAboutMaps,
            'Using Maps as children is unsupported and will likely yield ' +
              'unexpected results. Convert it to a sequence/iterable of keyed ' +
              'ReactElements instead.',
          );
          didWarnAboutMaps = true;
        }
      }

      const iterator = iteratorFn.call(children);
      let step;
      let ii = 0;
      while (!(step = iterator.next()).done) {
        child = step.value;
        nextName = nextNamePrefix + getComponentKey(child, ii++);
        subtreeCount += traverseAllChildrenImpl(
          child,
          nextName,
          callback,
          traverseContext,
        );
      }
    } else if (type === 'object') {
      let addendum = '';
      if (__DEV__) {
        addendum =
          ' If you meant to render a collection of children, use an array ' +
          'instead.' +
          ReactDebugCurrentFrame.getStackAddendum();
      }
      const childrenString = '' + children;
      invariant(
        false,
        'Objects are not valid as a React child (found: %s).%s',
        childrenString === '[object Object]'
          ? 'object with keys {' + Object.keys(children).join(', ') + '}'
          : childrenString,
        addendum,
      );
    }
  }

  return subtreeCount;
}

/**
 * @param {?*} children Children tree object.
 * @param {!function} callback To invoke upon traversing each child.
 * @param {?*} traverseContext Context for traversal.
 * @return {!number} The number of children in this subtree.
 */
function traverseAllChildren(children, callback, traverseContext) {
  if (children == null) {
    return 0;
  }

  return traverseAllChildrenImpl(children, '', callback, traverseContext);
}

/**
 * Generate a key string that identifies a component within a set.
 *
 * @param {*} component A component that could contain a manual key.
 * @param {number} index Index that is used if a manual key is not provided.
 * @return {string}
 */
function getComponentKey(component, index) {
  // Do some typechecking here since we call this blindly. We want to ensure
  // that we don't block potential future ES APIs.
  if (
    typeof component === 'object' &&
    component !== null &&
    component.key != null
  ) {
    // Explicit key
    return escape(component.key);
  }
  // Implicit key determined by the index in the set
  return index.toString(36);
}

function forEachSingleChild(bookKeeping, child, name) {
  const {func, context} = bookKeeping;
  func.call(context, child, bookKeeping.count++);
}

/**
 *
 * @param {?*} children Children tree container.
 * @param {function(*, int)} forEachFunc
 * @param {*} forEachContext Context for forEachContext.
 */
function forEachChildren(children, forEachFunc, forEachContext) {
  if (children == null) {
    return children;
  }
  const traverseContext = getPooledTraverseContext(
    null,
    null,
    forEachFunc,
    forEachContext,
  );
  traverseAllChildren(children, forEachSingleChild, traverseContext);
  releaseTraverseContext(traverseContext);
}

function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  const {result, keyPrefix, func, context} = bookKeeping;

  let mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {
    if (isValidElement(mappedChild)) {
      /**
       * 此处 cloneAndReplaceKey方法直接返回一个 ReactElement
       */
      mappedChild = cloneAndReplaceKey(
        mappedChild,
        keyPrefix +
          (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/'
            : '') +
          childKey,
      );
    }
    result.push(mappedChild);
  }
}

function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  let escapedPrefix = '';
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + '/';
  }
  const traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
  releaseTraverseContext(traverseContext);
}

/**
 * @param {?*} children Children tree container.
 * @param {function(*, int)} func The map function.
 * @param {*} context Context for mapFunction.
 * @return {object} Object containing the ordered map of results.
 */
function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}

/**
 * @param {?*} children Children tree container.
 * @return {number} The number of children.
 */
function countChildren(children) {
  return traverseAllChildren(children, () => null, null);
}

function toArray(children) {
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, child => child);
  return result;
}

function onlyChild(children) {
  invariant(
    isValidElement(children),
    'React.Children.only expected to receive a single React element child.',
  );
  return children;
}

export {
  forEachChildren as forEach,
  mapChildren as map,
  countChildren as count,
  onlyChild as only,
  toArray,
};

Others

memo.js

memo的作用是给 Function Component有类似于 PureComponent的作用, 在 props没有变化的情况下, 对于使用了 memo的 Function Component组件可以不渲染

  • 类似于 PurComponent的功能
  • 具体实现要到 React-Dom中查看
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import {REACT_MEMO_TYPE} from 'shared/ReactSymbols';

import isValidElementType from 'shared/isValidElementType';
import warningWithoutStack from 'shared/warningWithoutStack';

export default function memo<Props>(
  type: React$ElementType,
  compare?: (oldProps: Props, newProps: Props) => boolean,
) {

  // ...

  return {
    $$typeof: REACT_MEMO_TYPE,
    type,
    compare: compare === undefined ? null : compare,
  };
}

Fragment

/packages/shared/ReactSymbols.js

  • 代替组件中的根组件出现, 减少不必要的无用根组件的嵌套 <React.Fragment> ... </React.Fragment>, <> ... </>
export const REACT_FRAGMENT_TYPE = hasSymbol
  ? Symbol.for('react.fragment')
  : 0xeacb;

StrictMode

/packages/shared/ReactSymbols.js

StrictMode 是一个用以标记出应用中潜在问题的工具。就像 Fragment ,StrictMode 不会渲染任何真实的UI。它为其后代元素触发额外的检查和警告。

  • StrictMode影响的范围是其子树
export const REACT_STRICT_MODE_TYPE = hasSymbol
  ? Symbol.for('react.strict_mode')
  : 0xeacc;

cloneElement

  • 整体过程类似于 creatElement, 最后会创建一个 ReactElement
/**
 * Clone and return a new ReactElement using element as the starting point.
 * See https://reactjs.org/docs/react-api.html#cloneelement
 */
export function cloneElement(element, config, children) {
  invariant(
    !(element === null || element === undefined),
    'React.cloneElement(...): The argument must be a React element, but you passed %s.',
    element,
  );

  let propName;

  // Original props are copied
  const props = Object.assign({}, element.props);

  // Reserved names are extracted
  let key = element.key;
  let ref = element.ref;
  // Self is preserved since the owner is preserved.
  const self = element._self;
  // Source is preserved since cloneElement is unlikely to be targeted by a
  // transpiler, and the original source is probably a better indicator of the
  // true owner.
  const source = element._source;

  // Owner will be preserved, unless ref is overridden
  let owner = element._owner;

  if (config != null) {
    if (hasValidRef(config)) {
      // Silently steal the ref from the parent.
      ref = config.ref;
      owner = ReactCurrentOwner.current;
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }

    // Remaining properties override existing props
    let defaultProps;
    if (element.type && element.type.defaultProps) {
      defaultProps = element.type.defaultProps;
    }
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        if (config[propName] === undefined && defaultProps !== undefined) {
          // Resolve default props
          props[propName] = defaultProps[propName];
        } else {
          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];
    }
    props.children = childArray;
  }

  return ReactElement(element.type, key, ref, self, source, owner, props);
}

createFactory

  • createFactor是对 createElement的封装
/**
 * Return a function that produces ReactElements of a given type.
 * See https://reactjs.org/docs/react-api.html#createfactory
 */
export function createFactory(type) {
  const factory = createElement.bind(null, type);

  factory.type = type;
  return factory;
}

实例:

var factoryLI = React.createFactory("li");
var li1 = factoryLI(null, 'First');
var li2 = factoryLI(null, 'Second');  
var li3 = factoryLI(null, 'Third');  
var factoryUL = React.createFactory("ul");
var ul = factoryUL({className: 'list'}, li1, li2, li3);  
ReactDOM.render(  
    ul,  
    document.getElementById('timeBox')  
);

二. React中的更新创建

创建更新的方式:

  1. ReactDOM.render || hydrate: 将整个应用初次渲染在页面上使用

  2. setState: 后续更新应用

  3. forceUpdate: 强制更新应用

ReactDOM.render

/packages/react-dom/src/client/ReactDOM.js

步骤:

  1. 创建 ReactRoot, 包含 React整个应用的最顶点的对象

  2. 创建 FiberRoot 和 RootFiber (重要)

  3. 创建更新, 使应用进入更新调度的阶段

FiberRoot

/packages/react-reconciler/src/ReactFiberRoot.js

详细参数

  • FiberRoot是整个应用的起点
  • 包含应用挂载的目标节点 root
  • 记录整个应用更新过程中的各种信息 (比如各种不同类型的 expirationTime, 异步调度任务的 callback ...)

Fiber

  • 每个 ReactElement都会对应一个 Fiber对象

  • 记录对应节点的各种状态 (Class Component中的 state, props ...)

  • 当 对应的 Fiber更新后才会更新到 Class Component中的 this.state, props里面, 这些过程都是在 Fiber上操作的, 执行完成后才放到 Class Component上, 这种方式也给了 React实现 Hooks的方便

  • 串联整个应用形成树结构, 如在 ReactElement中通过 props.children属性将整个应用串联起来, 而在 Fiber里面也只有类似的能力来帮助我们记录整个树型结构的状态, 将每个节点串联起来

    1. return: Fiber; 指向它在 Fiber节点树中的 parent, 用来处理完这个节点后向上返回
    2. child: Fibler; 指向自己的第一个子节点
    3. sibling: Fiber; 指向自己的兄弟结构

Fiber节点的遍历查找.png

Update & UpdateQueue

Update

/packages/react-reconciler/ReactFiberReconciler.js

  • 用于记录组件状态的改变的对象

  • 存放于 Fiber对象的 UpdateQueue中,它是一个单向链表的结构,一次整体的更新过程中 UpdateQueue可能存在多个 Update,最终通过这些 Update算出最终的 state

  • 多个 Update可以同时存在(一个事件连续多次调用 setState,都会放到 UpdateQueue中)

UpdateQueue

  • 一个链表,用于存放 Update

expirationTime

当在一个操作内多次调用了一个 setState,即使在很短的时间内的多次调用,那计算出的 expirationTime也是不同的,这也意味着它们任务的优先级不同,最终会导致 React的任务更新执行多次,使得整个应用的性能下降,为了在一定时间内计算出的 expirationTime相同优先级也相同,而设计了这个 expirationTime的算法,expirationTime会在之后的 React更新中多处用到。

不同的 expirationTime

在异步的情况下计算 expirationTime,因为异步任务的优先级较低,所以可以被打断,但为了防止此任务一直被打断,所以 React设置了一个 expirationTime,在此时间内此任务可以被打断,但超过此时间后此任务还没有执行,则此任务就不能再被打断必须执行。

React中不仅有异步任务,大部分是 同步任务,所以会有不同的 expirationTime的存在

  • Sync模式:最高优先级的任务,此类任务创建后必须立刻更新到 DOM中

  • 异步模式:创建之后会进行一个调度,会有复杂的操作在其中,可能会被中断,因此会有一个计算出的过期时间,分为两种情况 500ms、50ms

  • 指定 context情况

setState && forceUpdate

/packages/react-reconciler/src/ReactFiberClassComponent.js

在 React中只有 ReactDOM.render()、setState、forceUpdate去更新 React应用是合理的,并且没有其它什么方式去更新 React应用。

核心

  • 给节点的 Fiber创建更新
  • 更新的类型不同

源码

const classComponentUpdater = {
  isMounted,
  enqueueSetState(inst, payload, callback) {
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  // ...
  enqueueForceUpdate(inst, callback) {
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);
    update.tag = ForceUpdate;

    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'forceUpdate');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
};