该文件提供了 Component 的操作方法
- setComponentProps:设置组件的 props,并可能重新渲染 Component
- renderComponent:渲染 Component
- buildComponentFromVNode:从 VNode 创建 Component
- unmountComponent:卸载 Component
import { SYNC_RENDER, NO_RENDER, FORCE_RENDER, ASYNC_RENDER, ATTR_KEY } from '../constants';
import options from '../options';
import { extend, applyRef } from '../util';
import { enqueueRender } from '../render-queue';
import { getNodeProps } from './index';
import { diff, mounts, diffLevel, flushMounts, recollectNodeTree, removeChildren } from './diff';
import { createComponent, recyclerComponents } from './component-recycler';
import { removeNode } from '../dom/index';
// 设置组件 props
export function setComponentProps(component, props, renderMode, context, mountAll) {
if (component._disable) return;
// 更新中,此时组件不可用
component._disable = true;
// 单独设置 ref 和 key
component.__ref = props.ref;
component.__key = props.key;
delete props.ref;
delete props.key;
// see: https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
if (typeof component.constructor.getDerivedStateFromProps === 'undefined') {
// �如果组件还未被挂载,调用 componentWillMount 钩子
if (!component.base || mountAll) {
if (component.componentWillMount) component.componentWillMount();
}
// �如果组件已经被挂载,调用 componentWillReceiveProps 钩子
else if (component.componentWillReceiveProps) {
component.componentWillReceiveProps(props, context);
}
}
// 更新 component.context
if (context && context!==component.context) {
if (!component.prevContext) component.prevContext = component.context;
component.context = context;
}
// 更新 component.props
if (!component.prevProps) component.prevProps = component.props;
component.props = props;
// 更新结束,组件恢复可用
component._disable = false;
if (renderMode!==NO_RENDER) {
// 进行同步渲染的几种情况
if (renderMode===SYNC_RENDER || options.syncComponentUpdates!==false || !component.base) {
renderComponent(component, SYNC_RENDER, mountAll);
}
// 异步队列渲染
else {
enqueueRender(component);
}
}
// 应用新 component.__ref
applyRef(component.__ref, component);
}
// 渲染组件
/**
* 渲染 Component, 触发必要的生命周期事件,同时高阶组件也在考虑之中
* @param {import('../component').Component} component The component to render
* @param {number} [renderMode] 渲染模式, 选项见 constants.js.
* @param {boolean} [mountAll] 是否直接挂载所有组件
* @param {boolean} [isChild] ?
* @private
*/
export function renderComponent(component, renderMode, mountAll, isChild) {
if (component._disable) return;
let props = component.props,
state = component.state,
context = component.context,
previousProps = component.prevProps || props,
previousState = component.prevState || state,
previousContext = component.prevContext || context,
isUpdate = component.base, // 存在 component.base 表示已经初始化渲染过了,这次是更新
nextBase = component.nextBase,
initialBase = isUpdate || nextBase, // 初始化渲染时,nextBase 可以作为备用 DOM
initialChildComponent = component._component,
skip = false,
snapshot = previousContext,
rendered, inst, cbase;
// 调用 getDerivedStateFromProps 钩子,它会返回一个对象来更新状态
if (component.constructor.getDerivedStateFromProps) {
state = extend(extend({}, state), component.constructor.getDerivedStateFromProps(props, state));
component.state = state;
}
// 如果 Component 已经初始化渲染过了,这次就是更新,需要调用更新的钩子
if (isUpdate) {
// 调用 shouldComponentUpdate 时需要新旧值的比较
// 因此要先恢复 component 的 props,state,context 为旧值
component.props = previousProps;
component.state = previousState;
component.context = previousContext;
// 如果 render 模式不是 FORCE_RENDER
// 且 shouldComponentUpdate() 返回值为 false 则跳过这次渲染
if (renderMode!==FORCE_RENDER
&& component.shouldComponentUpdate
&& component.shouldComponentUpdate(props, state, context) === false) {
skip = true;
}
// 否则调用 componentWillUpdate 钩子
else if (component.componentWillUpdate) {
component.componentWillUpdate(props, state, context);
}
component.props = props;
component.state = state;
component.context = context;
}
component.prevProps = component.prevState = component.prevContext = component.nextBase = null;
component._dirty = false;
if (!skip) {
// 从 component.render 获取需要渲染的内容
rendered = component.render(props, state, context);
// context to pass to the child, can be updated via (grand-)parent component
if (component.getChildContext) {
context = extend(extend({}, context), component.getChildContext());
}
// see: https://react.docschina.org/docs/react-component.html#getsnapshotbeforeupdate
// getSnapshotBeforeUpdate 返回的值将被作为参数传递给 componentDidUpdate
if (isUpdate && component.getSnapshotBeforeUpdate) {
snapshot = component.getSnapshotBeforeUpdate(previousProps, previousState);
}
let childComponent = rendered && rendered.nodeName,
toUnmount, base;
// 如果 render 返回的 rendered.nodeName 为 function,表示这是一个高阶组件
if (typeof childComponent==='function') {
// 建立高阶组件链接
let childProps = getNodeProps(rendered);
inst = initialChildComponent;
// 检查 component._component 是否存在,且是否是相同的构造函数,是否有相同的 key
// 以上都通过,表示这只是一次更新,只修改 props 就可以了
if (inst && inst.constructor===childComponent && childProps.key==inst.__key) {
setComponentProps(inst, childProps, SYNC_RENDER, context, false);
}
// 否则卸载旧的组件实例 inst,重新创建子组件实例,更新 component._component
// 同时递归调用 renderComponent 渲染新子组件实例
else {
toUnmount = inst;
// 创建 render 返回的子组件 childComponent
component._component = inst = createComponent(childComponent, childProps, context);
inst.nextBase = inst.nextBase || nextBase;
inst._parentComponent = component;
setComponentProps(inst, childProps, NO_RENDER, context, false);
renderComponent(inst, SYNC_RENDER, mountAll, true);
}
base = inst.base;
}
else {
cbase = initialBase;
// destroy high order component link
// 如果不是高阶组件,但是之前有子组件,说明之前是高阶组件,但现在不是了
// unmount 之前的子组件,并清空 component._component 和 cbase
toUnmount = initialChildComponent;
if (toUnmount) {
cbase = component._component = null;
}
// 存在 base 或 nextBase 且是 SYNC_RENDER,立即执行 diff 算法获取新的 DOM 返回到 base 里
if (initialBase || renderMode===SYNC_RENDER) {
if (cbase) cbase._component = null;
base = diff(cbase, rendered, context, mountAll || !isUpdate, initialBase && initialBase.parentNode, true);
}
}
// 如果计算得到的新 DOM 和原来的 DOM 不一样,则使用 replaceChild 来替换 DOM
if (initialBase && base!==initialBase && inst!==initialChildComponent) {
let baseParent = initialBase.parentNode;
if (baseParent && base!==baseParent) {
baseParent.replaceChild(base, initialBase);
if (!toUnmount) {
initialBase._component = null;
recollectNodeTree(initialBase, false);
}
}
}
if (toUnmount) {
unmountComponent(toUnmount);
}
// 设置 component.base 为新的 DOM
component.base = base;
// render 时不是作为子组件渲染的,需要向上追溯设置 _parentComponent.base
if (base && !isChild) {
let componentRef = component,
t = component;
while ((t=t._parentComponent)) {
(componentRef = t).base = base;
}
base._component = componentRef;
base._componentConstructor = componentRef.constructor;
}
}
// ?
if (!isUpdate || mountAll) {
mounts.push(component);
}
else if (!skip) {
// 确保子组件的 componentDidMount 在父组件的 componentDidUpdate 之前调用
// Note: disabled as it causes duplicate hooks, see https://github.com/developit/preact/issues/750
// flushMounts();
if (component.componentDidUpdate) {
component.componentDidUpdate(previousProps, previousState, snapshot);
}
if (options.afterUpdate) options.afterUpdate(component);
}
// 渲染结束,执行 _renderCallbacks 里的所有渲染回调
while (component._renderCallbacks.length) component._renderCallbacks.pop().call(component);
// 当前 diff 递归在第一层,并且 Component 非子组件
// flushMounts 会排队调用已挂载的组件列表的 componentDidMount 方法
// 但此处似乎和 diff 方法里的 flushMounts 重复了?目前还未确认
if (!diffLevel && !isChild) flushMounts();
}
diff.js
包含了 VNode 的更新比较算法,为了尽量减少在更新时对 DOM 的操作,提升性能,diff 的算法是至关重要的部分。
由于不是每次 diff 后立即对应调用 componentDidMount
方法,要等到本次 diff 的所有子节点递归都结束了,才会调用 flushMounts
按顺序依次调用,因此把具体的比较算法剥离开来放在 idiff
方法里。
import { ATTR_KEY } from '../constants';
import { isSameNodeType, isNamedNode } from './index';
import { buildComponentFromVNode } from './component';
import { createNode, setAccessor } from '../dom/index';
import { unmountComponent } from './component';
import options from '../options';
import { applyRef } from '../util';
import { removeNode } from '../dom/index';
// 已挂载并正在等待 componentDidMount 的组件队列
export const mounts = [];
// diff 递归计数,用于跟踪 diff 循环的结束
export let diffLevel = 0;
// 全局标志,指示当前是否在 diff SVG 中
let isSvgMode = false;
// 全局标志,指示 diff 是否执行 hydration
let hydrating = false;
// 排队调用已挂载的组件列表的 componentDidMount 方法
export function flushMounts() {
let c, i;
for (i=0; i<mounts.length; ++i) {
c = mounts[i];
if (options.afterMount) options.afterMount(c);
if (c.componentDidMount) c.componentDidMount();
}
mounts.length = 0;
}
// 将 VNode 的差异(以及它的深层子节点)应用到实际的 DOM 节点
export function diff(dom, vnode, context, mountAll, parent, componentRoot) {
// diffLevel 在这里为 0 表示初始进入 diff
if (!diffLevel++) {
// 当第一次启动 diff 时,检查是在比较 SVG 还是在 SVG 中比较
isSvgMode = parent!=null && parent.ownerSVGElement!==undefined;
// hydration 表示目前的元素将在没有 prop 缓存的情况下进行 diff
// 如果 dom 中存在 ATTR_KEY 这个属性,则关闭 hydration
hydrating = dom!=null && !(ATTR_KEY in dom);
}
// 调用内部 diff 算法,返回更新好的 DOM 元素
let ret = idiff(dom, vnode, context, mountAll, componentRoot);
// 如果新旧父元素不一样,直接 appendChild 到新父元素下面
if (parent && ret.parentNode!==parent) parent.appendChild(ret);
// diffLevel 降低到 0 意味着正在结束 diff
if (!--diffLevel) {
hydrating = false;
// 排队调用已挂载的组件列表的 componentDidMount 方法
if (!componentRoot) flushMounts();
}
return ret;
}
// diff 的内容具体实现,剥离开来以允许绕过 flushMounts
function idiff(dom, vnode, context, mountAll, componentRoot) {
let out = dom,
prevSvgMode = isSvgMode;
// empty values (null, undefined, booleans) render as empty Text nodes
if (vnode==null || typeof vnode==='boolean') vnode = '';
// Fast case: Strings & Numbers create/update Text nodes.
if (typeof vnode==='string' || typeof vnode==='number') {
// update if it's already a Text node:
if (dom && dom.splitText!==undefined && dom.parentNode && (!dom._component || componentRoot)) {
/* istanbul ignore if */ /* Browser quirk that can't be covered: https://github.com/developit/preact/commit/fd4f21f5c45dfd75151bd27b4c217d8003aa5eb9 */
if (dom.nodeValue!=vnode) {
dom.nodeValue = vnode;
}
}
else {
// it wasn't a Text node: replace it with one and recycle the old Element
out = document.createTextNode(vnode);
if (dom) {
if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
recollectNodeTree(dom, true);
}
}
out[ATTR_KEY] = true;
return out;
}
// If the VNode represents a Component, perform a component diff:
let vnodeName = vnode.nodeName;
if (typeof vnodeName==='function') {
return buildComponentFromVNode(dom, vnode, context, mountAll);
}
// Tracks entering and exiting SVG namespace when descending through the tree.
isSvgMode = vnodeName==='svg' ? true : vnodeName==='foreignObject' ? false : isSvgMode;
// If there's no existing element or it's the wrong type, create a new one:
vnodeName = String(vnodeName);
if (!dom || !isNamedNode(dom, vnodeName)) {
out = createNode(vnodeName, isSvgMode);
if (dom) {
// move children into the replacement node
while (dom.firstChild) out.appendChild(dom.firstChild);
// if the previous Element was mounted into the DOM, replace it inline
if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
// recycle the old element (skips non-Element node types)
recollectNodeTree(dom, true);
}
}
let fc = out.firstChild,
props = out[ATTR_KEY],
vchildren = vnode.children;
if (props==null) {
props = out[ATTR_KEY] = {};
for (let a=out.attributes, i=a.length; i--; ) props[a[i].name] = a[i].value;
}
// Optimization: fast-path for elements containing a single TextNode:
if (!hydrating && vchildren && vchildren.length===1 && typeof vchildren[0]==='string' && fc!=null && fc.splitText!==undefined && fc.nextSibling==null) {
if (fc.nodeValue!=vchildren[0]) {
fc.nodeValue = vchildren[0];
}
}
// otherwise, if there are existing or new children, diff them:
else if (vchildren && vchildren.length || fc!=null) {
innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML!=null);
}
// Apply attributes/props from VNode to the DOM Element:
diffAttributes(out, vnode.attributes, props);
// restore previous SVG mode: (in case we're exiting an SVG namespace)
isSvgMode = prevSvgMode;
return out;
}