上次说到 React 事件机制 是基于 Virtual Dom 实现的,这次来学习下 Virtual DOM 的源码
为什么需要 Virtual DOM
1、直接操作 DOM 效率低,复杂而频繁的 DOM 操作是产生性能瓶颈的原因之一
2、通过 js 抽象出来 Virtual DOM 可以降低复杂度
3、Virtual DOM diff 算法提供了仅更新差异原生 DOM 的基础,保证高效渲染
如果没有 Virtual DOM,简单来说就是直接重置 innerHTML。这样操作,在一个大型列表所有数据都变了的情况下,还算是合理,但当只有一行数据发生变化时,它也需要重置整个 innerHTML,这时候就造成了大量浪费。
比较 innerHTML 和 Virtual DOM 的重绘过程如下:
innerHTML: render html string + 重新创建所有 DOM 元素
Virtual DOM: render Virtual DOM + diff + 必要的 DOM 更新
Virtual DOM
Virtual DOM 是 React 的根基,几乎所有工作都是基于 Virtual DOM 完成的,它通过 js 实现了一套 DOM API,它有完整的 Virtual DOM 标签,生命周期的管理,以及 Virtual DOM 的高效 diff 算法 和将 Virtual DOM 展示为原生 DOM 的 Patch 方法,这让我们可以无需担心性能问题随时刷新整个页面。
React 相对于直接操作原生 DOM 有很大的性能优势,很大程度上都要归功于 virtual DOM 的 batching 和 diff。batching 把所有的 DOM 操作搜集起来,一次性提交给真实的 DOM。diff 算法时间复杂度也从标准的的 diff 算法的 O(n^3) 降到了 O(n)。
ReactElement
Virtual DOM 中的虚拟节点称为 ReactNode,包含 ReactElement ReactFragment ReactText,ReactElement 分为 ReactComponentElement 和 ReactDOMElement 两种。但它们都有共同的基础元素:type key props ref children ...
ReactElement 文件地址:packages/react/src/ReactElement.js
const ReactElement = function(type, key, ref, ..., owner, props) {
const element = {
// 唯一标识 ReactElement
$$typeof: REACT_ELEMENT_TYPE,
// 元素预置属性
type: type,
key: key,
ref: ref,
props: props,
// 记录负责创建此元素的组件
_owner: owner,
};
// ...
return element;
};
创建 React 元素
我们通常使用 JSX 来编写你的 UI 组件。每个 JSX 元素都是调用 React.createElement() 的语法糖。一般来说,如果你使用了 JSX,就不再需要调用以下方法。
createElement()
createFactory()
createElement 文件地址:packages/react/src/ReactElement.js
// 参数修正,创建并返回一个 ReactElement 实例.
export function createElement(type, config, children) {
let propName;
// 提取保留名称
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
// 如果 config 存在,则提取里边的内容
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;
// 复制 config 里的内容到 props 对象上,比如 id style
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// 处理 children,全部挂载到 props 的 children 属性上。
// children 可以是多个参数,如果只有一个参数,直接赋值给 children,如果是多个就需要做合并梳理
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;
}
// 如果某个 prop 为空,且设置了默认的 prop,将默认的 prop 赋值给当前的 prop
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
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);
}
}
}
// 返回一个 ReactElement 的实例对象
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
React 组件
// packages/react/src/ReactBaseClasses.js
// 一个构造函数,通过它构造的实例对象有三个私有属性
function Component(props, context, updater) {
this.props = props;
this.context = context;
// require('fbjs/lib/emptyObject');
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
// setState 和 forceUpdate 这两个方法挂载Component(组件构造器)的原型上
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.',
);
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
Component.prototype.forceUpdate = function(callback) {
this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};
// 辅助组件构造器
// 让 ComponentDummy 的原型指向 Component 的原型,这样它也能访问原型上面的共有方法和属性了,
// 比如 setState 和 forceUpdate
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;
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;
}
// 辅助组件构造器 ComponentDummy 实例化出来一个对象 pureComponentPrototype,然后把这个对象的 constructor 属性又指向了 PureComponent,因此 PureComponent 也成为了一个构造器,也就是上面的第二种组件基类
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// 避免这些方法额外的原型跳,把 Component 的原型跟 PureComponent 的原型合并
Object.assign(pureComponentPrototype, Component.prototype);
// 会有不同的属性分别挂在上面 isReactComponent 和 isPureReactComponent,用来判断是纯组件还是有状态组件和无状态组件,isPureReactComponent 的时候会执行 state 和 props 的浅层判断来调用 shouldUpdate
pureComponentPrototype.isPureReactComponent = true;
Diff 算法
传统 diff 算法通过循环递归的方法对节点进行操作,算法复杂度 为O(n3),其中n为树中节点的总数。这种指数型时间复杂度的算法对前端渲染场景代价太大。结合前端 DOM Tree 的特点,可以对传统 diff 算法进行有针对性的优化。
1、Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计
2、拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
3、对于同一层级的一组子节点,他们可以通过唯一 id 来区分。
链表的每一个节点是 Fiber,而不是在 16 之前的虚拟DOM 节点。Diff 主要是构建 currentInWorkProgress 的过程,最终生成 commitHookEffectList。
React 15.x 的 Diff 算法
tree diff
基于上边说的第一点,tree diff 对树进行分层比较,通过 updatedepth 对 Virtual DOM 树进行层级控制,只会对相同层级的 DOM 节点进行比较,当发现节点已经不存在时,则直接删除该节点及其子节点,不会继续进行比较,这样只需要对树进行一次遍历即可完成整改 DOM 树的比较。
component diff
如果不是同一类型的组件,直接替换整个组件下的所以子节点。
如果是同一类型的组件,继续比较 Virtual DOM 树。React 允许用户通过 shouldComponentUpdate 来判断该组件是否需要进行 diff 算法分析。
element diff
当节点处于同一层级时, diff 提供了 3 种节点操作,分别为插入、移动、删除。
插入:新的组件类型不在旧集合里时需要插入
移动:新旧集合里都存在,且 element 是可更新的类型
删除:旧组件不在新集合里,或者旧组件在新集合里但是对于的 element 是不可更新的类型
对于同层级的同组子节点,如果位置发生变化,单纯的 diff 会进行繁琐低效的删除创建操作,React 针对这一现象做了优化,允许开发者对同层级的同组子节点设置唯一 key。优化后 diff 时可以通过 key 知道新旧集合中的节点是否发生变化,如果只是位置变化,可以通过移动完成,不再需要频繁的删除插入。
_updateChildren: function(nextNestedChildrenElements, transaction, context) {
var prevChildren = this._renderedChildren;
var removedNodes = {};
var nextChildren = this._reconcilerUpdateChildren(
prevChildren,
nextNestedChildrenElements,
removedNodes,
transaction,
context
);
// nextChildren 和 prevChildren 不存在时不需要 diff
if (!nextChildren && !prevChildren) {
return;
}
var updates = null;
var name;
// nextIndex 是 nextChildren 中每个节点的索引,
// lastIndex 是 prevChildren 中最后的索引
var lastIndex = 0;
var nextIndex = 0;
var lastPlacedNode = null;
for (name in nextChildren) {
if (!nextChildren.hasOwnProperty(name)) {
continue;
}
var prevChild = prevChildren && prevChildren[name];
var nextChild = nextChildren[name];
if (prevChild === nextChild) {
// 需要移动节点
updates = enqueue(
updates,
this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex)
);
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
prevChild._mountIndex = nextIndex;
} else {
if (prevChild) {
// Update `lastIndex` before `_mountIndex` gets unset by unmounting.
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
// 遍历 removedNodes 删除子节点 prevChild
}
// 初始化并创建节点
updates = enqueue(
updates,
this._mountChildAtIndex(
nextChild,
lastPlacedNode,
nextIndex,
transaction,
context
)
);
}
nextIndex++;
lastPlacedNode = ReactReconciler.getNativeNode(nextChild);
}
// 父节点不存在了,直接删除该节点及其子节点
for (name in removedNodes) {
if (removedNodes.hasOwnProperty(name)) {
updates = enqueue(
updates,
this._unmountChild(prevChildren[name], removedNodes[name])
);
}
}
// 有需要更新的节点,处理更新队列
if (updates) {
processQueue(this, updates);
}
this._renderedChildren = nextChildren;
},
React 16.x 的 Diff 算法
React 16.x 将整体的数据结构从树改为了链表结构。所以相应的 Diff 算法也得改变,因为以前的 Diff 算法是基于树的。
链表的每一个节点是 Fiber,而不是在 16.x 之前的虚拟DOM 节点。
React 16.x 的 diff 策略采用从链表头部开始比较的算法,是层次遍历,算法是建立在一个节点的插入、删除、移动等操作都是在节点树的同一层级中进行的。 对于 diff, 新老节点的对比,我们以新节点为标准,然后来构建整个 currentInWorkProgress,对于新的 children 会有四种情况。
1、TextNode(包含字符串和数字) 2、单个 React Element(通过该节点是否有 ?typeof 区分) 3、数组 4、可迭代的 children,跟数组的处理方式差不多
入口: reconcileChildren
// packages/react-reconciler/src/ReactFiberBeginWork.js
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderExpirationTime: ExpirationTime,
) {
if (current === null) {
// 首次渲染 current 为空,全部添加到子节点上
// mountChildFibers 创建子节点的 Fiber 实例
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
// diff,然后得出 effect list
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderExpirationTime,
);
}
}
// packages/react-reconciler/src/ReactChildFiber.js
function ChildReconciler(shouldTrackSideEffects) {
// ...
}
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
reconcileChildFibers 和 mountChildFibers 都是通过 ChildReconciler 来的,只是参数不同,该参数主要是用来优化初次渲染的,因为初次渲染没有更新操作。
TextNode Diff
对于 diff TextNode 会有两种情况。
1、currentFirstNode 是 TextNode
2、currentFirstNode 不是 TextNode
// packages/react-reconciler/src/ReactChildFiber.js
function reconcileSingleTextNode(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
textContent: string,
expirationTime: ExpirationTime,
): Fiber {
// currentFirstChild 是一个 TextNode
if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
const existing = useFiber(currentFirstChild, textContent, expirationTime);
existing.return = returnFiber;
return existing;
}
// currentFirstChild 不是一个 TextNode,那么就代表这个节点不能复用,所以就从 currentFirstChild 开始删掉剩余的节点
// deleteRemainingChildren 是标记删除,在 commit 时才真正删除
deleteRemainingChildren(returnFiber, currentFirstChild);
const created = createFiberFromText(
textContent,
returnFiber.mode,
expirationTime,
);
created.return = returnFiber;
return created;
}
]useFiber 是复用节点的方法,deleteRemainingChildren 是删除剩余节点的方法,具体可以看 附录 ChildReconciler 完整源码
React Element Diff
// packages/react-reconciler/src/ReactChildFiber.js
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
expirationTime: ExpirationTime,
): Fiber {
const key = element.key;
let child = currentFirstChild;
// 找 currentFirstChild 的所有兄弟节点中有没有可以复用的节点
// 找到 key 相同的节点,就会复用当前节点
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
if (
child.tag === Fragment
? element.type === REACT_FRAGMENT_TYPE
: child.elementType === element.type ||
// Keep this check inline so it only runs on the false path:
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false)
) {
// 为什么要删除老的节点的兄弟节点?
// 因为当前节点是只有一个节点,而老的如果是有兄弟节点是要删除的,是多于的。删掉了之后就可以复用老的节点了
deleteRemainingChildren(returnFiber, child.sibling);
// 复用当前节点
const existing = useFiber(
child,
element.type === REACT_FRAGMENT_TYPE
? element.props.children
: element.props,
expirationTime,
);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
} else {
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
// 如果没有可以复用的节点,就把这个节点删除
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 前面的循环已经把该删除的已经删除了,接下来就开始创建新的节点了
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
expirationTime,
element.key,
);
created.return = returnFiber;
return created;
} else {
const created = createFiberFromElement(
element,
returnFiber.mode,
expirationTime,
);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
Array Diff
// packages/react-reconciler/src/ReactChildFiber.js
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
expirationTime: ExpirationTime,
): Fiber | null {
// 由于我们在 fibers 上没有反向指针,因此无法通过两端搜索来进行优化。 我正在尝试查看该模型可以达到的程度。
// 如果最终不值得权衡,我们可以稍后添加。
// 即使进行了两端优化,我们也希望针对变化不多的情况进行优化,并强行进行比较,而不是使用 Map。
// 它想探索的是仅在向前模式下首先沿该路径行驶,并且只有在我们注意到我们需要大量的前瞻性之后才选择 Map。
// 这不能处理逆转以及两端搜索,但这是不寻常的。 此外,为了使两端优化都能在 Iterables 上运行,我们需要复制整个集合。
// 在第一个迭代中,我们将为每个插入/移动命中最坏的情况(将所有内容添加到Map)。
// 如果更改此代码,则还更新使用相同算法的reconcileChildrenIterator()。
if (__DEV__) {
// First, validate keys.
let knownKeys = null;
for (let i = 0; i < newChildren.length; i++) {
const child = newChildren[i];
knownKeys = warnOnInvalidKey(child, knownKeys);
}
}
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
// currentFirstChild 只有在更新时才不为空 他是当前 fiber 的 child 属性,也是下一个要调度的 fiber
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0; // 上次放置的索引, 更新时placeChild() 根据这个索引值决定新组件的插入位置
let newIdx = 0; // 新数组的索引
let nextOldFiber = null;
// 这里只会比对 index 相对应的 key 是否可以复用
// 找到第一个不能复用的节点,就跳出循环
// oldFiber === null 的时候直接插入
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) { // 位置不匹配
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
expirationTime,
);
// newFiber 为 null,表示没有找到复用的节点,跳出循环,
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// 匹配 slot 后不存在 fiber,即该节点没有被复用,需要删除旧的节点
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// 删除剩余的老节点,追加剩余的新节点
// 第一轮遍历完成后,如果是新节点已遍历完成,就将剩余的老节点批量删除;
// 如果是老节点遍历完成仍有新节点剩余,则将新节点批量插入老节点末端
if (newIdx === newChildren.length) {
// 新的 children 长度已经够了,所以把剩下的删除掉
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
if (oldFiber === null) {
// 如果老的节点已经被复用完了,对剩下的新节点进行操作
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
expirationTime,
);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// Add all children to a key map for quick lookups.
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 删除未匹配的元素
// 如果在第一轮遍历中发现key值不相等的情况,则直接跳出以上步骤,按照 key 值进行遍历更新,
// 最后再删除没有被上述情况涉及的元素(添加 key 值是有助于提升 diff 算法效率的)
// 这里要注意的是,在遍历 newChildren 的时候,newIdx 有可能不是从 0 开始的了。
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
expirationTime,
);
// newFiber 为 true ,代表可以复用这个节点
if (newFiber !== null) {
if (shouldTrackSideEffects) {
// newFiber.alternate !== null 表示这个节点已经被复用了,
if (newFiber.alternate !== null) {
// 复用了之后就要用 existingChildren 中删除掉,避免下次受影响
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
// 留下来的 fiber 对象,都是没有复用的
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
附录 ChildReconciler 完整源码
本段内容可略过
// packages/react-reconciler/src/ReactChildFiber.js
// 这个包装函数的存在是因为希望克隆每个路径中的代码,
// 以便能够通过早期的分支来单独优化每个路径(深度优先遍历里面的路径).
// 这需要编译器或者手动完成,不需要该分支的助手可以存在这个函数之外
function ChildReconciler(shouldTrackSideEffects) {
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
if (!shouldTrackSideEffects) {
// Noop.
return;
}
// 删除按相反顺序添加,因此我们将其添加到前面。
// 此时,return Fiber 的 effect lis 为空,
// 将删除附加到列表中。
// 在完成阶段之前不会添加 effect。 一旦实施恢复,就可能会出错。
const last = returnFiber.lastEffect;
if (last !== null) {
last.nextEffect = childToDelete;
returnFiber.lastEffect = childToDelete;
} else {
returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
}
// 对于要删除的节点,后面的副作用就都没有用了,将 nextEffect 置为空
childToDelete.nextEffect = null;
childToDelete.effectTag = Deletion;
}
// 标记删除 children 中剩余的节点
function deleteRemainingChildren(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
): null {
if (!shouldTrackSideEffects) {
// Noop.
return null;
}
// TODO: For the shouldClone case, this could be micro-optimized a bit by
// assuming that after the first child we've already added everything.
let childToDelete = currentFirstChild;
while (childToDelete !== null) {
deleteChild(returnFiber, childToDelete);
childToDelete = childToDelete.sibling;
}
return null;
}
function mapRemainingChildren(
returnFiber: Fiber,
currentFirstChild: Fiber,
): Map<string | number, Fiber> {
// 将剩余的子级添加到临时 Map 中,以便我们可以通过 key 快速找到它们。 key 为空的时候使用其索引添加到此集合中。
const existingChildren: Map<string | number, Fiber> = new Map();
let existingChild = currentFirstChild;
while (existingChild !== null) {
if (existingChild.key !== null) {
existingChildren.set(existingChild.key, existingChild);
} else {
existingChildren.set(existingChild.index, existingChild);
}
existingChild = existingChild.sibling;
}
return existingChildren;
}
// 复用节点的方法
function useFiber(
fiber: Fiber, // 就是当前节点 currentFirstChild
pendingProps: mixed, // 如果是 TextNode,那么就是 text
expirationTime: ExpirationTime,
): Fiber {
// 我们目前将 sibling 设置为 null,并将 index 设置为0,因为在返回它之前很容易忘记做。 例如。 只有一个 children 的情况。
const clone = createWorkInProgress(fiber, pendingProps, expirationTime);
clone.index = 0;
clone.sibling = null;
return clone;
}
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
newFiber.index = newIndex;
// 初次渲染的情况不需要 diff
if (!shouldTrackSideEffects) {
// Noop.
return lastPlacedIndex;
}
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// 移动位置
newFiber.effectTag = Placement;
return lastPlacedIndex;
} else {
// 不需要移动
return oldIndex;
}
} else {
// 插入
newFiber.effectTag = Placement;
return lastPlacedIndex;
}
}
function placeSingleChild(newFiber: Fiber): Fiber {
// This is simpler for the single child case. We only need to do a
// placement for inserting new children.
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.effectTag = Placement;
}
return newFiber;
}
function updateTextNode(
returnFiber: Fiber,
current: Fiber | null,
textContent: string,
expirationTime: ExpirationTime,
) {
if (current === null || current.tag !== HostText) {
// Insert
const created = createFiberFromText(
textContent,
returnFiber.mode,
expirationTime,
);
created.return = returnFiber;
return created;
} else {
// Update
const existing = useFiber(current, textContent, expirationTime);
existing.return = returnFiber;
return existing;
}
}
function updateElement(
returnFiber: Fiber,
current: Fiber | null,
element: ReactElement,
expirationTime: ExpirationTime,
): Fiber {
if (
current !== null &&
(current.elementType === element.type ||
// Keep this check inline so it only runs on the false path:
(__DEV__ ? isCompatibleFamilyForHotReloading(current, element) : false))
) {
// Move based on index
const existing = useFiber(current, element.props, expirationTime);
existing.ref = coerceRef(returnFiber, current, element);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
} else {
// Insert
const created = createFiberFromElement(
element,
returnFiber.mode,
expirationTime,
);
created.ref = coerceRef(returnFiber, current, element);
created.return = returnFiber;
return created;
}
}
function updatePortal(
returnFiber: Fiber,
current: Fiber | null,
portal: ReactPortal,
expirationTime: ExpirationTime,
): Fiber {
if (
current === null ||
current.tag !== HostPortal ||
current.stateNode.containerInfo !== portal.containerInfo ||
current.stateNode.implementation !== portal.implementation
) {
// 插入
const created = createFiberFromPortal(
portal,
returnFiber.mode,
expirationTime,
);
created.return = returnFiber;
return created;
} else {
// Update
const existing = useFiber(current, portal.children || [], expirationTime);
existing.return = returnFiber;
return existing;
}
}
function updateFragment(
returnFiber: Fiber,
current: Fiber | null,
fragment: Iterable<*>,
expirationTime: ExpirationTime,
key: null | string,
): Fiber {
if (current === null || current.tag !== Fragment) {
// Insert
const created = createFiberFromFragment(
fragment,
returnFiber.mode,
expirationTime,
key,
);
created.return = returnFiber;
return created;
} else {
// Update
const existing = useFiber(current, fragment, expirationTime);
existing.return = returnFiber;
return existing;
}
}
function createChild(
returnFiber: Fiber,
newChild: any,
expirationTime: ExpirationTime,
): Fiber | null {
if (typeof newChild === 'string' || typeof newChild === 'number') {
// Text nodes don't have keys. If the previous node is implicitly keyed
// we can continue to replace it without aborting even if it is not a text
// node.
const created = createFiberFromText(
'' + newChild,
returnFiber.mode,
expirationTime,
);
created.return = returnFiber;
return created;
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
const created = createFiberFromElement(
newChild,
returnFiber.mode,
expirationTime,
);
created.ref = coerceRef(returnFiber, null, newChild);
created.return = returnFiber;
return created;
}
case REACT_PORTAL_TYPE: {
const created = createFiberFromPortal(
newChild,
returnFiber.mode,
expirationTime,
);
created.return = returnFiber;
return created;
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
const created = createFiberFromFragment(
newChild,
returnFiber.mode,
expirationTime,
null,
);
created.return = returnFiber;
return created;
}
throwOnInvalidObjectType(returnFiber, newChild);
}
if (__DEV__) {
if (typeof newChild === 'function') {
warnOnFunctionType();
}
}
return null;
}
// 对比新老的 key 是否相同,来查看是否可以复用老的节点。
function updateSlot(
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
expirationTime: ExpirationTime,
): Fiber | null {
// Update the fiber if the keys match, otherwise return null.
// oldFiber 不为空 oldFiber.key 为空有可能 oldFiber 是 Fragment
const key = oldFiber !== null ? oldFiber.key : null;
if (typeof newChild === 'string' || typeof newChild === 'number') {
// 对于新节点如果是 string 或者 number,是没有 key 的,
// 如果老节点有 key 则不能复用,直接返回 null。
// 老节点 key 为 null 的话,代表老的节点是文本节点,可以复用
if (key !== null) {
return null;
}
// 更新 fiber
return updateTextNode(
returnFiber,
oldFiber,
'' + newChild,
expirationTime,
);
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
if (newChild.type === REACT_FRAGMENT_TYPE) {
return updateFragment(
returnFiber,
oldFiber,
newChild.props.children,
expirationTime,
key,
);
}
return updateElement(
returnFiber,
oldFiber,
newChild,
expirationTime,
);
} else {
return null;
}
}
case REACT_PORTAL_TYPE: {
if (newChild.key === key) {
return updatePortal(
returnFiber,
oldFiber,
newChild,
expirationTime,
);
} else {
return null;
}
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
if (key !== null) {
return null;
}
return updateFragment(
returnFiber,
oldFiber,
newChild,
expirationTime,
null,
);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
if (__DEV__) {
if (typeof newChild === 'function') {
warnOnFunctionType();
}
}
return null;
}
function updateFromMap(
existingChildren: Map<string | number, Fiber>,
returnFiber: Fiber,
newIdx: number,
newChild: any,
expirationTime: ExpirationTime,
): Fiber | null {
if (typeof newChild === 'string' || typeof newChild === 'number') {
// 对于字符串和数字来说,没有key,所以是用 index 来匹配
const matchedFiber = existingChildren.get(newIdx) || null;
return updateTextNode(
returnFiber,
matchedFiber,
'' + newChild,
expirationTime,
);
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
const matchedFiber =
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null;
if (newChild.type === REACT_FRAGMENT_TYPE) {
return updateFragment(
returnFiber,
matchedFiber,
newChild.props.children,
expirationTime,
newChild.key,
);
}
return updateElement(
returnFiber,
matchedFiber,
newChild,
expirationTime,
);
}
case REACT_PORTAL_TYPE: {
const matchedFiber =
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null;
return updatePortal(
returnFiber,
matchedFiber,
newChild,
expirationTime,
);
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
const matchedFiber = existingChildren.get(newIdx) || null;
return updateFragment(
returnFiber,
matchedFiber,
newChild,
expirationTime,
null,
);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
if (__DEV__) {
if (typeof newChild === 'function') {
warnOnFunctionType();
}
}
return null;
}
/**
* Warns if there is a duplicate or missing key
*/
function warnOnInvalidKey(
child: mixed,
knownKeys: Set<string> | null,
): Set<string> | null {
if (__DEV__) {
if (typeof child !== 'object' || child === null) {
return knownKeys;
}
switch (child.$$typeof) {
case REACT_ELEMENT_TYPE:
case REACT_PORTAL_TYPE:
warnForMissingKey(child);
const key = child.key;
if (typeof key !== 'string') {
break;
}
if (knownKeys === null) {
knownKeys = new Set();
knownKeys.add(key);
break;
}
if (!knownKeys.has(key)) {
knownKeys.add(key);
break;
}
warning(
false,
'Encountered two children with the same key, `%s`. ' +
'Keys should be unique so that components maintain their identity ' +
'across updates. Non-unique keys may cause children to be ' +
'duplicated and/or omitted — the behavior is unsupported and ' +
'could change in a future version.',
key,
);
break;
default:
break;
}
}
return knownKeys;
}
// 新老节点对比
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
expirationTime: ExpirationTime,
): Fiber | null {
// This algorithm can't optimize by searching from both ends since we
// don't have backpointers on fibers. I'm trying to see how far we can get
// with that model. If it ends up not being worth the tradeoffs, we can
// add it later.
// Even with a two ended optimization, we'd want to optimize for the case
// where there are few changes and brute force the comparison instead of
// going for the Map. It'd like to explore hitting that path first in
// forward-only mode and only go for the Map once we notice that we need
// lots of look ahead. This doesn't handle reversal as well as two ended
// search but that's unusual. Besides, for the two ended optimization to
// work on Iterables, we'd need to copy the whole set.
// In this first iteration, we'll just live with hitting the bad case
// (adding everything to a Map) in for every insert/move.
// If you change this code, also update reconcileChildrenIterator() which
// uses the same algorithm.
// 由于我们在 fibers 上没有反向指针,因此无法通过两端搜索来进行优化。 我正在尝试查看该模型可以达到的程度。
// 如果最终不值得权衡,我们可以稍后添加。
// 即使进行了两端优化,我们也希望针对变化不多的情况进行优化,并强行进行比较,而不是使用 Map。
// 它想探索的是仅在向前模式下首先沿该路径行驶,并且只有在我们注意到我们需要大量的前瞻性之后才选择 Map。
// 这不能处理逆转以及两端搜索,但这是不寻常的。 此外,为了使两端优化都能在 Iterables 上运行,我们需要复制整个集合。
// 在第一个迭代中,我们将为每个插入/移动命中最坏的情况(将所有内容添加到Map)。
// 如果更改此代码,则还更新使用相同算法的reconcileChildrenIterator()。
if (__DEV__) {
// First, validate keys.
let knownKeys = null;
for (let i = 0; i < newChildren.length; i++) {
const child = newChildren[i];
knownKeys = warnOnInvalidKey(child, knownKeys);
}
}
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
// currentFirstChild 只有在更新时才不为空 他是当前 fiber 的 child 属性,也是下一个要调度的 fiber
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0; // 上次放置的索引, 更新时placeChild() 根据这个索引值决定新组件的插入位置
let newIdx = 0;
let nextOldFiber = null;
// 这里只会比对 index 相对应的 key 是否可以复用
// 找到第一个不能复用的节点,就跳出循环
// oldFiber === null 的时候直接插入
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) { // 位置不匹配
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
expirationTime,
);
// newFiber 为 null,表示没有找到复用的节点,跳出循环,
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// 匹配 slot 后不存在 fiber,即该节点没有被复用,需要删除旧的节点
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// 删除剩余的老节点,追加剩余的新节点
// 第一轮遍历完成后,如果是新节点已遍历完成,就将剩余的老节点批量删除;
// 如果是老节点遍历完成仍有新节点剩余,则将新节点批量插入老节点末端
if (newIdx === newChildren.length) {
// 新的 children 长度已经够了,所以把剩下的删除掉
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
if (oldFiber === null) {
// 如果老的节点已经被复用完了,对剩下的新节点进行操作
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
expirationTime,
);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// Add all children to a key map for quick lookups.
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 删除未匹配的元素
// 如果在第一轮遍历中发现key值不相等的情况,则直接跳出以上步骤,按照 key 值进行遍历更新,
// 最后再删除没有被上述情况涉及的元素(添加 key 值是有助于提升 diff 算法效率的)
// 这里要注意的是,在遍历 newChildren 的时候,newIdx 有可能不是从 0 开始的了。
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
expirationTime,
);
// newFiber 为 true ,代表可以复用这个节点
if (newFiber !== null) {
if (shouldTrackSideEffects) {
// newFiber.alternate !== null 表示这个节点已经被复用了,
if (newFiber.alternate !== null) {
// 复用了之后就要用 existingChildren 中删除掉,避免下次受影响
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
// 留下来的 fiber 对象,都是没有复用的
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
function reconcileChildrenIterator(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildrenIterable: Iterable<*>,
expirationTime: ExpirationTime,
): Fiber | null {
// This is the same implementation as reconcileChildrenArray(),
// but using the iterator instead.
const iteratorFn = getIteratorFn(newChildrenIterable);
invariant(
typeof iteratorFn === 'function',
'An object is not an iterable. This error is likely caused by a bug in ' +
'React. Please file an issue.',
);
if (__DEV__) {
// We don't support rendering Generators because it's a mutation.
// See https://github.com/facebook/react/issues/12995
if (
typeof Symbol === 'function' &&
// $FlowFixMe Flow doesn't know about toStringTag
newChildrenIterable[Symbol.toStringTag] === 'Generator'
) {
warning(
didWarnAboutGenerators,
'Using Generators as children is unsupported and will likely yield ' +
'unexpected results because enumerating a generator mutates it. ' +
'You may convert it to an array with `Array.from()` or the ' +
'`[...spread]` operator before rendering. Keep in mind ' +
'you might need to polyfill these features for older browsers.',
);
didWarnAboutGenerators = true;
}
// Warn about using Maps as children
if ((newChildrenIterable: any).entries === iteratorFn) {
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;
}
// First, validate keys.
// We'll get a different iterator later for the main pass.
const newChildren = iteratorFn.call(newChildrenIterable);
if (newChildren) {
let knownKeys = null;
let step = newChildren.next();
for (; !step.done; step = newChildren.next()) {
const child = step.value;
knownKeys = warnOnInvalidKey(child, knownKeys);
}
}
}
const newChildren = iteratorFn.call(newChildrenIterable);
invariant(newChildren != null, 'An iterable object provided no iterator.');
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
let step = newChildren.next();
for (
;
oldFiber !== null && !step.done;
newIdx++, step = newChildren.next()
) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
step.value,
expirationTime,
);
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
// need to delete the existing child.
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (step.done) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; !step.done; newIdx++, step = newChildren.next()) {
const newFiber = createChild(returnFiber, step.value, expirationTime);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// Add all children to a key map for quick lookups.
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Keep scanning and use the map to restore deleted items as moves.
for (; !step.done; newIdx++, step = newChildren.next()) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
step.value,
expirationTime,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
function reconcileSingleTextNode(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
textContent: string,
expirationTime: ExpirationTime,
): Fiber {
// There's no need to check for keys on text nodes since we don't have a
// way to define them.
if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
// We already have an existing node so let's just update it and delete
// the rest.
deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
const existing = useFiber(currentFirstChild, textContent, expirationTime);
existing.return = returnFiber;
return existing;
}
// The existing first child is not a text node so we need to create one
// and delete the existing ones.
deleteRemainingChildren(returnFiber, currentFirstChild);
const created = createFiberFromText(
textContent,
returnFiber.mode,
expirationTime,
);
created.return = returnFiber;
return created;
}
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
expirationTime: ExpirationTime,
): Fiber {
const key = element.key;
let child = currentFirstChild;
// 找到 key 相同的节点,就会复用当前节点
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
if (
child.tag === Fragment
? element.type === REACT_FRAGMENT_TYPE
: child.elementType === element.type ||
// Keep this check inline so it only runs on the false path:
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false)
) {
// 为什么要删除老的节点的兄弟节点?
// 因为当前节点是只有一个节点,而老的如果是有兄弟节点是要删除的,是多于的。删掉了之后就可以复用老的节点了
deleteRemainingChildren(returnFiber, child.sibling);
// 复用当前节点
const existing = useFiber(
child,
element.type === REACT_FRAGMENT_TYPE
? element.props.children
: element.props,
expirationTime,
);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
} else {
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
// 如果没有可以复用的节点,就把这个节点删除
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 前面的循环已经把该删除的已经删除了,接下来就开始创建新的节点了
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
expirationTime,
element.key,
);
created.return = returnFiber;
return created;
} else {
const created = createFiberFromElement(
element,
returnFiber.mode,
expirationTime,
);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
function reconcileSinglePortal(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
portal: ReactPortal,
expirationTime: ExpirationTime,
): Fiber {
const key = portal.key;
let child = currentFirstChild;
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
if (
child.tag === HostPortal &&
child.stateNode.containerInfo === portal.containerInfo &&
child.stateNode.implementation === portal.implementation
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(
child,
portal.children || [],
expirationTime,
);
existing.return = returnFiber;
return existing;
} else {
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
const created = createFiberFromPortal(
portal,
returnFiber.mode,
expirationTime,
);
created.return = returnFiber;
return created;
}
// 此 API 在协调自己的时候会标记子节点的 side-effect。
// 当访问子节点和父节点的时候将给他们添加 side-effect list
// returnFiber 是 workInProgress Tree
// currentFirstChild 是 currentTree
// newChild 是计算出来的下一个child,是一个 element 不是 fiber
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
expirationTime: ExpirationTime,
): Fiber | null {
// 这个函数不是递归。
// 如果顶级的项是数组,将把它视为一组子项 而不是 fragment
// 另一方面,嵌套数组将被视为 fragement。 递归发生在正常流程。
// 处理顶级无键片段,就好像它们是数组一样。
// 这导致<> {[...]} </>和<> ... </>之间的歧义。
// 我们对模糊的情况进行了同样的处理。
// 判断是否是这种写法 <> ... </>,如果是, isUnkeyedTopLevelFragment 就为 true。
// 为什么要判断这个呢?因为如果是 <> ... </> 写法,进行创建 Fiber 的子节点应该是
// newChild.props.children 而不是 newChild
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
// Handle object types
const isObject = typeof newChild === 'object' && newChild !== null;
// 如果是对象,代表 newChild 是 ReactElement,ReactElment 就会有 ?typeof
if (isObject) {
switch (newChild.?typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
expirationTime,
),
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
expirationTime,
),
);
}
}
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
expirationTime,
),
);
}
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
expirationTime,
);
}
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
expirationTime,
);
}
if (isObject) {
throwOnInvalidObjectType(returnFiber, newChild);
}
if (__DEV__) {
if (typeof newChild === 'function') {
warnOnFunctionType();
}
}
if (typeof newChild === 'undefined' && !isUnkeyedTopLevelFragment) {
// If the new child is undefined, and the return fiber is a composite
// component, throw an error. If Fiber return types are disabled,
// we already threw above.
switch (returnFiber.tag) {
case ClassComponent: {
if (__DEV__) {
const instance = returnFiber.stateNode;
if (instance.render._isMockFunction) {
// We allow auto-mocks to proceed as if they're returning null.
break;
}
}
}
// Intentionally fall through to the next case, which handles both
// functions and classes
// eslint-disable-next-lined no-fallthrough
case FunctionComponent: {
const Component = returnFiber.type;
invariant(
false,
'%s(...): Nothing was returned from render. This usually means a ' +
'return statement is missing. Or, to render nothing, ' +
'return null.',
Component.displayName || Component.name || 'Component',
);
}
}
}
// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
return reconcileChildFibers;
}
总结
我们可以在 React 中任性的随意更新得益于 Virtual DOM,更得益于 diff 算法,尤其是 16.x 基于 React Fiber 架构的 diff 算法。