学习内容
一、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的作用:
-
将 config中的属性放到 props中去,除去 ref、key属性
-
通过 arguments参数,处理 createElement方法中的参数,将第三个参数及之后的所有参数全部放到 数组中,最后赋值给 props.children保存;如果是刚好有三个参数,则直接将第三个参数赋值给 props.children保存
-
处理默认值:type.defaultProps
-
将处理好的数据去创建 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
-
Component.prototype.setState = function(partialState, callback) {...}:数据更新函数。
-
Component.prototype.forceUpdate = function(callback) {...}:强制 React-Dom更新。
-
ComponentDummy.prototype = Component.prototype; 将 ComponentDummy方法的原型对象改为 Component方法的原型对象。
-
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy()); 将 PureComponent的原型对象改为 ComponentDummy构造函数的实例。
-
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)
使用方法:
- 父组件要显示声明 context的值和方法
// MessageList
// 父组件显示声明 context的方法,方法名固定
getChildContext() { return {color: "purple"}; }
// 父组件显示声明 context的值
MessageList.childContextTypes = { color: PropTypes.string };
- 子组件(消费组件),要显示订阅 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
使用实例:
-
父组件通过 Privider 提供 value属性
-
子组件(类组件)通过 方式一:contextType 或 static contextType使用 context的值
-
子组件(函数组件)通过 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中的值
- 结合 lazy 实现异步加载组件
- 加载异步数据(待完善)
实例:
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(进行递归)处理数组节点
- map方法,返回一维 ReactElement新数组
- forEach方法,改变当前 children数组,变为 一维 ReactElement数组
- toArray方法,返回将 children展开的一维 ReactElement新数组
- 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中的更新创建
创建更新的方式:
-
ReactDOM.render || hydrate: 将整个应用初次渲染在页面上使用
-
setState: 后续更新应用
-
forceUpdate: 强制更新应用
ReactDOM.render
/packages/react-dom/src/client/ReactDOM.js
步骤:
-
创建 ReactRoot, 包含 React整个应用的最顶点的对象
-
创建 FiberRoot 和 RootFiber (
重要) -
创建更新, 使应用进入更新调度的阶段
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里面也只有类似的能力来帮助我们记录整个树型结构的状态, 将每个节点串联起来
- return: Fiber; 指向它在 Fiber节点树中的 parent, 用来处理完这个节点后向上返回
- child: Fibler; 指向自己的第一个子节点
- sibling: Fiber; 指向自己的兄弟结构
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);
},
};