概述
这次打算基于Preact v8.5版本来了解一下React,或者说声明式UI框架和虚拟dom树的底层设计。
Preact代码不多,主要由 Component, VNode 还有 diff 算法组成。
知识延伸1
现代 JavaScript(ES6 及之后)已经有了模块化机制,区别于过去只有全局变量和函数作用域的情况:
- ES Module (import/export)
- 每个文件是独立作用域,不会把变量挂载到
window或globalThis。 - 若要对外暴露,需要显式
export;若要使用外部模块,需要显式import。 - 浏览器中
<script type="module">,或 Node.js 中.mjs文件,会开启模块作用域。
- 每个文件是独立作用域,不会把变量挂载到
- CommonJS (require/module.exports)
- Node.js 早期采用的模块化方案。
- AMD、UMD
- ES6 模块普及前的加载方案,现在使用较少
- ES Module (import/export)
知识延伸2
- Preact工程源码使用 Flow 进行静态类型检查,项目中有
.flowconfig配置文件。 - 发布到 npm 时附带
preact.js.flow文件,供使用 Flow 的项目自动识别类型。 - 现代工程常见做法
- 大多数项目已经转向 TypeScript:
- 内联类型标注更直观自然(
x: number)。 - 类型声明文件(
.d.ts)生态健全。 - 工具链和 IDE 支持全面。
- 内联类型标注更直观自然(
- JSDoc + TypeScript
- 在纯 JavaScript 项目中,配合 TypeScript 的
checkJs模式,JSDoc 注释也能触发类型检查。
- 在纯 JavaScript 项目中,配合 TypeScript 的
- 大多数项目已经转向 TypeScript:
- **.d.ts
- d.ts是类型声明文件,本质是给没有类型的js代码补上类型签名信息以方便ts调用
- Preact工程源码使用 Flow 进行静态类型检查,项目中有
index.js
在看具体组件如Component, VNode, diff 之前有必要先看一眼index.js,这里隐藏了Preact运行逻辑得一个关键内容
/**
* Properties Preact adds to elements it creates
* @typedef PreactElementExtensions
* @property {string} [normalizedNodeName] A normalized node name to use in diffing
* @property {EventListenerMap} [_listeners] A map of event listeners added by components to this DOM node
* @property {import('../component').Component} [_component] The component that rendered this DOM node
* @property {function} [_componentConstructor] The constructor of the component that rendered this DOM node
*/
上面是Preact定义的扩展属性,Preact会在自己渲染得DOM元素上声明这些属性,方便在执行diff时作为参考。
normalizedNodeName: 格式化(lower_cased)得node name,用于在diff时候快速检查元素类型
_listeners: 记录事件监听映射,在触发响应时从这里检索并触发相应监听
_component: 记录渲染了当前DOM节点得Component组件
_componentConstructor: 记录当前DOM节点对应Component得构造器
/**
* A DOM element that has been extended with Preact properties
* @typedef {Element & ElementCSSInlineStyle & PreactElementExtensions} PreactElement
*/
PreactElement 就是一个普通 DOM 元素,但具备上面那些扩展属性的类型别名
Component
定义了 state, prop 等属性,并定义了 经典的setState 和 render 函数。
当前这个Preact版本还是相对早期的设计,因此开发者自己的组件需要继承Component并实现 render 函数,在render函数中返回一个jsx表达式。
另外setState调用会在经过一些状态判断后触发enqueueRender。
setState
- 用于刷新当前组件。实际上setState/forceUpdate 最终会调用 enqueueRender,把当前组件加入一个模块级单例的更新队列中。这里说模块级单例,是因为enqueueRender定义在render-queue.js文件中,也就是一个独立得模块。里面有一个let items = []。
- 执行时会检查 _dirty 标记,只有此前不是 dirty 的组件才会入队;并且仅当队列从空变为非空(items.push(…) == 1)时,才通过
(options.debounceRendering || defer)(rerender)安排一次延迟的批量刷新。在刷新真正执行前,其它组件仍可继续入队,但不会重复安排新的定时器。 - rerender 运行时会用 pop() 持续取出并渲染队列中的组件(LIFO),调用
renderComponent完成单个组件的刷新,并清除 _dirty。这样实现了一次调度合并一批更新的批处理策略。
VNode
一个轻量得对象,代表虚拟 DOM 树的一个节点,通常包含 nodeName、children、key、props/attrs 等属性。
Preact定义了一个h函数,用来接收标签名(或组件函数)和属性参数,并返回一个 VNode实例。实际上在代码中编写的 JSX 会在编译阶被转译为对 h() 的调用,从而生成 VNode。
const el = <div class="box">Hello</div>;
编译后变成
const el = h(
'div',
{ class: 'box' },
'Hello'
);
因此,JSX 语法在编译后就是生成 VNode 的函数调用,多个 VNode 组合起来就形成了虚拟 DOM 树。
render.js
这个文件只声明了一个render函数,它接收一个组件(或 VNode 树)和目标 DOM 容器。实际上render内部会调用 diff() 来实现渲染目标DOM元素得逻辑。
整个Preact应用的渲染,或者说第一个组件就是通过这个函数完成渲染并插入到root元素中的:
render(<App />, document.getElementById('root'));
和Component.render区分:
render.js的render()→ 框架入口,负责把整棵 VNode 树渲染到实际 DOM 中。- 组件类(
Component)里的render()→ 组件实例的渲染方法,用于返回当前组件对应的 VNode(即 JSX 编译后的结果),描述组件的结构,但不直接生成 DOM。
diff
现在轮到主菜了。
这部分我觉得有必要逐行来看。首先根据注释,diff是用于计算VNode和实际DOM节点的状态差异,并最小化得改动来更新DOM,而diff内部调用了idiff来完成实际的diff操作。
函数签名
function render(vnode, parent, merge) {
return diff(merge, vnode, {}, false, parent, false);
}
一般我们的代码发起点是类似这样:
render(<App />, document.getElementById('root'));
所以在运行时render的参数分别是
render(
{ nodeName: App, attributes: {}, children: [], key: undefined }, // 一个 VNode
document.getElementById('root'), // 真实的 DOMElement
undefined // 第三个参数没传,默认为 undefined
);
接下来看diff的参数类型
function diff(dom, vnode, context, mountAll, parent, componentRoot) {}
运行时diff的参数分别是:
diff(
undefined, // dom
{ nodeName: App, attributes: {}, children: [], key: undefined }, // vnode
{}, // context
false, // mountAll
document.getElementById('root'), // parent
false // componentRoot
)
因为diff最终会返回渲染的DOM元素,因此函数的JSDoc写了: * @returns {import('../dom').PreactElement} The created/mutated element。
基于上面的信息,我们再看实际代码:
export function diff(dom, vnode, context, mountAll, parent, componentRoot) {
// 在处理嵌套组件时,diff会递归执行。而这里通过if判断限制只有首次调用时会进行相关逻辑
if (!diffLevel++) {
// 是否在svg标签里
isSvgMode = parent!=null && parent.ownerSVGElement!==undefined;
// 似乎和元素复用有关,鉴于这里dom是undefined, 所以忽略
hydrating = dom!=null && !(ATTR_KEY in dom);
}
// 实际得diff流程
let ret = idiff(dom, vnode, context, mountAll, componentRoot);
// 将新创建的DOM元素插入父节点
if (parent && ret.parentNode!==parent) parent.appendChild(ret);
// 本轮diff结束
if (!--diffLevel) {
hydrating = false;
// 触发生命周期函数
if (!componentRoot) flushMounts();
}
return ret;
}
现在来看idiff
idiff的函数签名如下:
function idiff(dom, vnode, context, mountAll, componentRoot) {}
而在首次渲染App组件时给idiff传递了这些参数
idiff(
undefined, // dom
{ nodeName: App, attributes: {}, children: [], key: undefined }, // vnode
{}, // context
false, // mountAll
false, // componentRoot
);
接下来基于这个数据来看代码
function idiff(dom, vnode, context, mountAll, componentRoot) {
let out = dom, // 复用的另一个DOM, 这里是undefined
prevSvgMode = isSvgMode; // 是否在svg节点内
// 处理 vnode 特殊情况。
// 这里是为了应对类似 { condition ? <div>Hi</div> : null } 的逻辑。
// 或者是 {condition && <div>Hi</div>} 这种。 condition为false时,返回的是一个false,但false不是一个合法的DOM节点,所以上面这两种情况归一为''
if (vnode==null || typeof vnode==='boolean') vnode = '';
/* 如果vnode类型是数字或者字符串,可以做特殊处理。
比如 jsx中写<div>hello world</div>,
生成的vnode是类似 { type: 'div', props: { children: "hello world" }, key: null, ref: null}
children代表嵌套的vnode,递归到这个children时,他类型就是string
*/
if (typeof vnode==='string' || typeof vnode==='number') {
// 我们现在这个场景下, dom其实是undefined ,所以不会走进这里,但也可以看一下
// 这里判断 dom 存在并且是文本节点,父节点不为空,并且_component不存在 或者 componentRoot为true时才进行
if (dom && dom.splitText!==undefined && dom.parentNode && (!dom._component || componentRoot)) {
// 直接更新文本节点的值
if (dom.nodeValue!=vnode) {
dom.nodeValue = vnode;
}
}
else {
// 上面条件不满足,就是说当前存在的dom元素不能作为文本节点,那么我们需要新创建一个
out = document.createTextNode(vnode);
if (dom) {
// 如果存在dom节点并且在DOM树中, 则直接替换它
if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
recollectNodeTree(dom, true);
}
}
out[ATTR_KEY] = true;
// 直接返回处理过的文本节点
return out;
}
}
let vnodeName = vnode.nodeName;
// 类型为function 说明是一个组件VNode。 组件VNode有专门的函数实现渲染
if (typeof vnodeName==='function') {
return buildComponentFromVNode(dom, vnode, context, mountAll);
}
// 如果当前vnode是'svg, 说明进入了一个svg子树,
// 否则检查是不是foreignObject,它是svg的特殊元素,允许在svg里嵌入HTML或者XML片段,如果vnode是这个类型,就按照普通HTML样式渲染,因此是false
isSvgMode = vnodeName==='svg' ? true : vnodeName==='foreignObject' ? false : isSvgMode;
vnodeName = String(vnodeName);
// dom节点不存在, 或者dom节点上绑定的vnode名字不一样
if (!dom || !isNamedNode(dom, vnodeName)) {
// 创建DOM元素,内部根据是不是svg,决定了调用哪个DOM函数。
// 在创建的同时也设置了normalizedNodeName属性
// 因此这里返回的就是 PreactElement 类型
out = createNode(vnodeName, isSvgMode);
// dom节点存在
if (dom) {
// 将dom节点所有子元素移动到我们新的节点里
while (dom.firstChild) out.appendChild(dom.firstChild);
// 用新的节点替换dom节点
if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
// 这里dom节点已经被移出了DOM树,因此在这里对它做缓存回收
// 里面会根据dom._component决定当前dom是否属于某个组件,如果是就触发相应组件的生命周期函数
recollectNodeTree(dom, true);
}
}
let fc = out.firstChild,
props = out[ATTR_KEY],
vchildren = vnode.children;
// 如果props为空就初始化一个新的,并将现有attributes拷贝到它里面
if (props==null) {
props = out[ATTR_KEY] = {};
for (let a=out.attributes, i=a.length; i--; ) props[a[i].name] = a[i].value;
}
// 快速处理,如果目标是一个纯文本子节点,且当前 DOM 也正好只有一个文本子节点,就地更新文本值
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];
}
}
// vnode拥有子节点,就通过 innerDiffNode 进一步检查DOM子节点
else if (vchildren && vchildren.length || fc!=null) {
innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML!=null);
}
// 用vnode的属性去更新DOM节点
diffAttributes(out, vnode.attributes, props);
// 恢复 isSvgMode
isSvgMode = prevSvgMode;
return out;
总结下来, idiff 分别针对文本节点,组件节点和 常规dom节点分别做了处理:
- 如果是文本节点,直接更新已有节点或者新增文本节点并返回
- 如果是组件节点,执行专门的组件渲染函数完成渲染
- 如果是常规dom节点,如果merge参数不存在,或者dom.nodeName不匹配时创建新的dom元素,并diff属性和子节点。函数第一个已经将dom赋值给了out。因此前面讲的逻辑不执行的话,其实就是基于现有的merge节点(和子节点)进行diff
innerDiffNode
let originalChildren = dom.childNodes,
children = [],
keyed = {},
keyedLen = 0,
min = 0,
len = originalChildren.length,
childrenLen = 0,
vlen = vchildren ? vchildren.length : 0,
j, c, f, vchild, child;
// 当前dom节点存在子元素
if (len!==0) {
// 遍历子元素,根据是否是PreactElement且存在key,分别保存到不同的分组里面 keyed 和 children
for (let i=0; i<len; i++) {
let child = originalChildren[i],
props = child[ATTR_KEY],
key = vlen && props ? child._component ? child._component.__key : props.key : null;
if (key!=null) {
keyedLen++;
keyed[key] = child;
}
else if (props || (child.splitText!==undefined ? (isHydrating ? child.nodeValue.trim() : true) : isHydrating)) {
children[childrenLen++] = child;
}
}
}
// vnode拥有子元素
if (vlen!==0) {
for (let i=0; i<vlen; i++) {
vchild = vchildren[i];
child = null;
let key = vchild.key;
// 用key寻找是否存在匹配的dom节点
if (key!=null) {
if (keyedLen && keyed[key]!==undefined) {
child = keyed[key];
keyed[key] = undefined;
keyedLen--;
}
}
// 没有key,则遍历children寻找相同类型的dom节点
// 所谓相同类型,内部先根据vnode是否为文本节点去检查当前dom子节点是否为文本节点
// 否则检查当前vnode是否为常规dom节点,就判断是否为相同节点
// 如果找到符合要求的就选择复用它
else if (min<childrenLen) {
for (j=min; j<childrenLen; j++) {
if (children[j]!==undefined && isSameNodeType(c = children[j], vchild, isHydrating)) {
child = c;
children[j] = undefined;
if (j===childrenLen-1) childrenLen--;
if (j===min) min++;
break;
}
}
}
// 到这里,已经找到一个可复用的子 DOM 节点 (child),
// 以及对应需要渲染的虚拟节点 (vchild),
// 所以递归调用 idiff 来对比二者并更新/返回最新的子 DOM
child = idiff(child, vchild, context, mountAll);
// 渲染新节点后,对原来的节点做相关处理
// 当前函数开始执行时我们获取了originalChildren拿到了一个备份,因此后续通过idiff对原来dom做了修改以后,也不会影响到originalChildren
// 因此我们可以从originalChildren里取出原来的子节点,和新idiff返回的结果做比对
f = originalChildren[i];
if (child && child!==dom && child!==f) {
//
if (f==null) {
dom.appendChild(child);
}
else if (child===f.nextSibling) {
removeNode(f);
}
else {
dom.insertBefore(child, f);
}
}
}
}
// 下面两步:回收/卸载本轮 diff 结束后在 keyed 和 children 中残留的节点
// 也就是本次子节点 diff 中多余的旧节点
if (keyedLen) {
for (let i in keyed) if (keyed[i]!==undefined) recollectNodeTree(keyed[i], false);
}
while (min<=childrenLen) {
if ((child = children[childrenLen--])!==undefined) recollectNodeTree(child, false);
}
diffAttributes
function diffAttributes(dom, attrs, old) {
let name;
// 把不在存在 (!(attrs && attrs[name] != null)) 的属性从DOM上移除
for (name in old) {
if (!(attrs && attrs[name]!=null) && old[name]!=null) {
setAccessor(dom, name, old[name], old[name] = undefined, isSvgMode);
}
}
//
for (name in attrs) {
if (name!=='children' && name!=='innerHTML' && (!(name in old) || attrs[name]!==(name==='value' || name==='checked' ? dom[name] : old[name]))) {
setAccessor(dom, name, old[name], old[name] = attrs[name], isSvgMode);
}
}
}
export function setAccessor(node, name, old, value, isSvg) {
if (name==='className') name = 'class';
// 忽略key属性
if (name==='key') {
// ignore
}
// 如果是ref属性,调用相应函数完成设置
else if (name==='ref') {
applyRef(old, null);
applyRef(value, node);
}
else if (name==='class' && !isSvg) {
// svg上classname是SVGAnimatedString,所以不能直接设置className,所以这里条件有 !isSvg
node.className = value || '';
}
// 如果是样式表,根据样式的类型采用不同的方法设置给目标节点
else if (name==='style') {
if (!value || typeof value==='string' || typeof old==='string') {
node.style.cssText = value || '';
}
if (value && typeof value==='object') {
if (typeof old!=='string') {
for (let i in old) if (!(i in value)) node.style[i] = '';
}
for (let i in value) {
node.style[i] = typeof value[i]==='number' && IS_NON_DIMENSIONAL.test(i)===false ? (value[i]+'px') : value[i];
}
}
}
else if (name==='dangerouslySetInnerHTML') {
if (value) node.innerHTML = value.__html || '';
}
// 如果是 on 开头,就认为是事件监听器,对应value被设置到了map当中,然后由eventProxy来分发事件
else if (name[0]=='o' && name[1]=='n') {
let useCapture = name !== (name=name.replace(/Capture$/, ''));
name = name.toLowerCase().substring(2);
if (value) {
if (!old) node.addEventListener(name, eventProxy, useCapture);
}
else {
node.removeEventListener(name, eventProxy, useCapture);
}
(node._listeners || (node._listeners = {}))[name] = value;
}
// 不是svg 且 该属性存在于当前node
else if (name!=='list' && name!=='type' && !isSvg && name in node) {
// 优先通过 node[name] 的方式赋值,性能更好
// list 和 type 是表单相关属性,不能直接赋值
try {
node[name] = value==null ? '' : value;
} catch (e) { }
if ((value==null || value===false) && name!='spellcheck') node.removeAttribute(name);
}
else {
// 作为最后的手段,通过 setAttribute设置属性,如果新的value是空就做移除操作
// 这里区分svg调用了不同的函数
let ns = isSvg && (name !== (name = name.replace(/^xlink:?/, '')));
if (value==null || value===false) {
if (ns) node.removeAttributeNS('http://www.w3.org/1999/xlink', name.toLowerCase());
else node.removeAttribute(name);
}
else if (typeof value!=='function') {
if (ns) node.setAttributeNS('http://www.w3.org/1999/xlink', name.toLowerCase(), value);
else node.setAttribute(name, value);
}
}
}
vdom/component.js
这个文件里是组件相关的函数,比如前面提到的 buildComponentFromVNode,自定义组件渲染相关的逻辑在这里
buildComponentFromVNode
export function buildComponentFromVNode(dom, vnode, context, mountAll) {
let c = dom && dom._component,
originalComponent = c,
oldDom = dom,
isDirectOwner = c && dom._componentConstructor===vnode.nodeName,
isOwner = isDirectOwner,
props = getNodeProps(vnode);
// 从里往外找,检查这个组件是否是自己渲染的子组件
while (c && !isOwner && (c=c._parentComponent)) {
isOwner = c.constructor===vnode.nodeName;
}
// 复用已有实例
if (c && isOwner && (!mountAll || c._component)) {
setComponentProps(c, props, ASYNC_RENDER, context, mountAll);
dom = c.base;
}
else {
// 卸载原来组件
if (originalComponent && !isDirectOwner) {
unmountComponent(originalComponent);
dom = oldDom = null;
}
// 创建组件实例
c = createComponent(vnode.nodeName, props, context);
if (dom && !c.nextBase) {
c.nextBase = dom;
oldDom = null;
}
// 设置组件属性
setComponentProps(c, props, SYNC_RENDER, context, mountAll);
dom = c.base;
// 释放原有dom并回收
if (oldDom && dom!==oldDom) {
oldDom._component = null;
recollectNodeTree(oldDom, false);
}
}
return dom;
}
createComponent
export function createComponent(Ctor, props, context) {
let inst, i = recyclerComponents.length;
if (Ctor.prototype && Ctor.prototype.render) {
// 如果是 class 组件,直接new,然后触发了初始化逻辑
inst = new Ctor(props, context);
Component.call(inst, props, context);
}
else {
// 函数组件,会用一个轻量的Component承载,并把render指向 doRender
inst = new Component(props, context);
inst.constructor = Ctor;
inst.render = doRender;
}
// 寻找和Ctor相同的可复用元素,主要是从它里面取到 nextBase
while (i--) {
if (recyclerComponents[i].constructor===Ctor) {
inst.nextBase = recyclerComponents[i].nextBase;
recyclerComponents.splice(i, 1);
return inst;
}
}
return inst;
}
setComponentProps
export function setComponentProps(component, props, renderMode, context, mountAll) {
// 防止重入, 例如在生命周期里setState
if (component._disable) return;
component._disable = true;
// 从props 里剥离ref/key,组件不应该在this.props上看到他们
component.__ref = props.ref;
component.__key = props.key;
delete props.ref;
delete props.key;
// getDerivedStateFromProps 为空,则会尝试进行相关生命周期函数
if (typeof component.constructor.getDerivedStateFromProps === 'undefined') {
if (!component.base || mountAll) {
if (component.componentWillMount) component.componentWillMount();
}
else if (component.componentWillReceiveProps) {
component.componentWillReceiveProps(props, context);
}
}
// 更新context
if (context && context!==component.context) {
if (!component.prevContext) component.prevContext = component.context;
component.context = context;
}
// 保留 props 快照
if (!component.prevProps) component.prevProps = component.props;
// 覆盖 props
component.props = props;
// 解锁
component._disable = false;
// 根据渲染模式执行对应函数
if (renderMode!==NO_RENDER) {
// SYNC_RENDER 同步渲染,
// 或者 options中指定了syncComponentUpdates,
// 或者 尚未挂载 component.base (首次渲染)
if (renderMode===SYNC_RENDER || options.syncComponentUpdates!==false || !component.base) {
renderComponent(component, SYNC_RENDER, mountAll);
}
else {
enqueueRender(component);
}
}
// 为组件 ref 赋值
applyRef(component.__ref, component);
}
renderComponent
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,
nextBase = component.nextBase,
initialBase = isUpdate || nextBase,
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;
}
// 已挂载:进入更新前置流程(旧生命周期)
if (isUpdate) {
// 暂时把实例回拨到“上一轮快照”,以便 SCU/cWU 拿到 prev 值
component.props = previousProps;
component.state = previousState;
component.context = previousContext;
// 不是强制渲染, 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;
}
// 清理上一轮快照与 nextBase,并标记本组件“已不脏”
component.prevProps = component.prevState = component.prevContext = component.nextBase = null;
component._dirty = false;
if (!skip) {
// 执行组件的 render 函数,得到vnode
rendered = component.render(props, state, context);
// 处理向下传递的context
if (component.getChildContext) {
context = extend(extend({}, context), component.getChildContext());
}
// 获取快照
if (isUpdate && component.getSnapshotBeforeUpdate) {
snapshot = component.getSnapshotBeforeUpdate(previousProps, previousState);
}
// 如果上面render返回的是组件,那么这里就会拿到它的构造函数,或者是dom标签名
let childComponent = rendered && rendered.nodeName,
toUnmount, base;
// 如果是组件,
if (typeof childComponent==='function') {
// set up high order component link
let childProps = getNodeProps(rendered);
inst = initialChildComponent;
// 如果旧实例与本次类型/Key 都匹配 就复用并直接更新子组件 props(同步渲染)
if (inst && inst.constructor===childComponent && childProps.key==inst.__key) {
setComponentProps(inst, childProps, SYNC_RENDER, context, false);
}
else {
// 旧实例与新组件不一致,标记旧子组件待卸载
toUnmount = inst;
// 创建新子组件并首渲染
component._component = inst = createComponent(childComponent, childProps, context);
inst.nextBase = inst.nextBase || nextBase;
inst._parentComponent = component;
// 这里只是设置了 props, 因为传递了 NO_RENDER, 所以不会触发渲染
setComponentProps(inst, childProps, NO_RENDER, context, false);
// 同步渲染子组件
renderComponent(inst, SYNC_RENDER, mountAll, true);
}
// 由子组件的 base 决定父组件的 base
base = inst.base;
}
else { // render返回的是元素/文本路径vnode
cbase = initialBase;
// 如果之前有子组件实例,当前却不是组件了,断开并准备卸载它
toUnmount = initialChildComponent;
if (toUnmount) {
cbase = component._component = null;
}
// 有基准DOM(已挂在或者提供了nextBase), 或者要求同步渲染,进行DOM diff
if (initialBase || renderMode===SYNC_RENDER) {
if (cbase) cbase._component = null;
base = diff(cbase, rendered, context, mountAll || !isUpdate, initialBase && initialBase.parentNode, true);
}
}
// 若产生了新的根 DOM,替换旧的 initialBase(如从不同类型切换)
if (initialBase && base!==initialBase && inst!==initialChildComponent) {
let baseParent = initialBase.parentNode;
if (baseParent && base!==baseParent) {
baseParent.replaceChild(base, initialBase);
// 如果没有子组件要卸载,回收旧 DOM 树
if (!toUnmount) {
initialBase._component = null;
recollectNodeTree(initialBase, false);
}
}
}
if (toUnmount) {
// 卸载“被替换”的旧子组件
unmountComponent(toUnmount);
}
// 绑定本组件的 base,并向上把同一个 DOM 赋给父链(父组件 base 指向相同根)
component.base = base;
if (base && !isChild) {
let componentRef = component,
t = component;
while ((t=t._parentComponent)) {
(componentRef = t).base = base;
}
base._component = componentRef;
base._componentConstructor = componentRef.constructor;
}
}
// 首次挂载或强制全量挂载的话加入到 mounts 队列,稍后统一调用 componentDidMount
if (!isUpdate || mountAll) {
mounts.push(component);
}
// 已挂载的正常更新且未跳过,直接触发相应生命周期
else if (!skip) {
if (component.componentDidUpdate) {
component.componentDidUpdate(previousProps, previousState, snapshot);
}
if (options.afterUpdate) options.afterUpdate(component);
}
// 通过setState传入callback注册的回调,会在这里统一执行
while (component._renderCallbacks.length) component._renderCallbacks.pop().call(component);
// 安排触发mounts中组件的生命周期
if (!diffLevel && !isChild) flushMounts();
}