Tergel

Preact源码了解

现代GUI开发的lights bulb时刻

概述

这次打算基于Preact v8.5版本来了解一下React,或者说声明式UI框架和虚拟dom树的底层设计。

Preact代码不多,主要由 Component, VNode 还有 diff 算法组成。

  • 知识延伸1

    现代 JavaScript(ES6 及之后)已经有了模块化机制,区别于过去只有全局变量和函数作用域的情况:

    • ES Module (import/export)
      • 每个文件是独立作用域,不会把变量挂载到 windowglobalThis
      • 若要对外暴露,需要显式 export;若要使用外部模块,需要显式 import
      • 浏览器中 <script type="module">,或 Node.js 中 .mjs 文件,会开启模块作用域。
    • CommonJS (require/module.exports)
      • Node.js 早期采用的模块化方案。
    • AMD、UMD
      • ES6 模块普及前的加载方案,现在使用较少
  • 知识延伸2

    • Preact工程源码使用 Flow 进行静态类型检查,项目中有 .flowconfig 配置文件。
    • 发布到 npm 时附带 preact.js.flow 文件,供使用 Flow 的项目自动识别类型。
    • 现代工程常见做法
      • 大多数项目已经转向 TypeScript
        • 内联类型标注更直观自然(x: number)。
        • 类型声明文件(.d.ts)生态健全。
        • 工具链和 IDE 支持全面。
      • JSDoc + TypeScript
        • 在纯 JavaScript 项目中,配合 TypeScript 的 checkJs 模式,JSDoc 注释也能触发类型检查。
    • **.d.ts
      • d.ts是类型声明文件,本质是给没有类型的js代码补上类型签名信息以方便ts调用

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 树的一个节点,通常包含 nodeNamechildrenkeyprops/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.jsrender() → 框架入口,负责把整棵 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();
}