从 Preact 源码一窥 React 原理(三):组件

从 Preact 源码一窥 React 原理(三):组件

  • 前言
  • 组件
    • Component 类
    • 组件渲染
    • Component 提供的函数
      • forceUpdate 函数
      • setState 函数
  • Context API
    • Legacy Context
    • New Context
  • 总结
  • 参考资料

系列文章:

  1. 从 Preact 源码一窥 React 原理(一):JSX 渲染
  2. 从 Preact 源码一窥 React 原理(二):Diff 算法
  3. 从 Preact 源码一窥 React 原理(三):组件(本文)

前言

本文大抵是《从 Preact 源码一窥 React 原理》系列的收尾篇了,算上这一篇的组件原理 Preact 的核心源码中的部分也已经基本涵盖了。Preact 中的诸多细节自然是无法面面俱到的,还请诸位自行阅读罢。
剩余还有 Hook 机制、Preact 新增的 Linked State 特性等等,由于其未被 Preact 的核心所包含,因此可能会放在后续进行介绍(有缘再见吧 )。

组件

React 组件是一部分独立的、且可复用的 UI 单元,根据应用方式的不同可以分为类组件以及函数组件。
以下是 Preact 中组件渲染的一个示例:

import { h, render, Component } from 'preact';

// 类组件
class Clock extends Component {
	render() {
		let time = new Date();
		return <time datetime={time.toISOString()}>{ time.toLocaleTimeString() }</time>;
	}
}

// 函数组件
function Clock () {
	let time = new Date();
	return <time datetime={time.toISOString()}>{ time.toLocaleTimeString() }</time>;
}

// render an instance of Clock into :
render(<Clock />, document.body);

如果你曾使用过 React 进行开发,那么你对上述的代码应当很熟悉了。让我们首先着眼于类组件,可以看到,所有的类组件都必须由 Component 基类派生而来。
这也就引出了我们所讨论的第一个重点:Component 类。

Component 类

Preact 中 Component 类的定义没有使用 ES6 语法的 class (尽管在其他代码中使用了 ES6 的相关语法),而是使用了 ES5 的方式。
由于 JavaScript 的灵活性,Component 中的属性在构造函数以外动态添加,因此 Preact 贴心的为 Component 中的几个属性提供了注释,其代码如下所示:

export function Component(props, context) {
	this.props = props;
	this.context = context;
	// this.constructor // When component is functional component, this is reset to functional component
	// if (this.state==null) this.state = {};
	// this.state = {};
	// this._dirty = true;
	// this._renderCallbacks = []; // Only class components

	// Other properties that Component will have set later,
	// shown here as commented out for quick reference
	// this.base = null;
	// this._context = null;
	// this._ancestorComponent = null; // Always set right after instantiation
	// this._vnode = null;
	// this._nextState = null; // Only class components
	// this._prevVNode = null;
	// this._processingException = null; // Always read, set only when handling error
}

其中,各个属性值各自的含义为:

  • props:组件接收的 props 值;
  • context:组件获得的 context 值,根据其使用的 API 不同可能是 Legacy Context 的值或是 New Context 的值;
  • state:组件内部状态;
  • _dirty:标记组件内部状态是否发生改变,也即是组件是否需要进行重新 Diff;
  • _renderCallbacks:组件渲染完成的回调函数;
  • base:调用 render 函数渲染之后得到的真实 DOM;
  • _context:保存了自父组件获得的 Legacy Context 值;
  • _ancestorComponent:最近的父组件;
  • _vnode:组件实例所对应的 VNode;
  • _nextState:在 Diff 执行前,暂时保存组件 state 变化后的值;
  • _prevVNode:使用 render 函数之后得到的 VNode
  • _processingExecption:需要由组件实例处理的上抛的错误;
  • _depth:在 Component 类中未提及,但在 diff 函数中被使用,代表组件的深度;
  • _parentDom:同样在 Component 类中未提及,但在 diff 函数中被使用,代表组件的父真实 DOM。

组件渲染

在上文《从 Preact 源码一窥 React 原理(二):Diff 算法》中我们了解了 Diff 算法的原理以及具体实现,但是略过了 diff 函数中关于组件的部分代码。现在我们就将这部分代码补上:

export function diff(dom, parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent, force, oldDom) {
	// If the previous type doesn't match the new type we drop the whole subtree
	// 如果 oldVnode 与 newVnode 的类型或者 key 值不匹配则将整个原先的 DOM 树抛弃
	if (oldVNode==null || newVNode==null || oldVNode.type!==newVNode.type || oldVNode.key!==newVNode.key) {
		if (oldVNode!=null) unmount(oldVNode, ancestorComponent);
		if (newVNode==null) return null;
		dom = null;
		oldVNode = EMPTY_OBJ;
	}

	if (options.diff) options.diff(newVNode);

	let c, p, isNew = false, oldProps, oldState, snapshot,
		newType = newVNode.type;

	/** @type {import('../internal').Component | null} */
	let clearProcessingException;

	try {
		// outer: 是标签语法,较为少见,详见 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/label
		// VNode 类型为 Fragment ,其为子节点的聚合,无需向 DOM 添加额外节点
		outer: if (oldVNode.type===Fragment || newType===Fragment) {
			balabala...
		}
		// VNode 类型为类组件或者函数组件
		else if (typeof newType==='function') {
			// PART 1
			// 判断组件是否使用了 New Context API,当使用了 New Context API 则从 provider 获取 context 值,
			// 否则从 diff 函数的参数中获取从父组件传递的 context 值。
			let cxType = newType.contextType;
			let provider = cxType && context[cxType._id];
			let cctx = cxType != null ? (provider ? provider.props.value : cxType._defaultValue) : context;

			// PART 2
			// 当 oldVNode 中已经存在 Component 的实例时,将该实例赋值给 newVNode
			if (oldVNode._component) {
				// 将 oldVNode 的 _component,_dom 赋给 newVNode
				c = newVNode._component = oldVNode._component;
				clearProcessingException = c._processingException;
				dom = newVNode._dom = oldVNode._dom;
			}
			// PART 3
			// oldVNode 中不存在 Component 实例时,则为其创建一个
			else {
				// 分别通过类组件以及函数组件的方式创建组件的实例
				if (newType.prototype && newType.prototype.render) {
					newVNode._component = c = new newType(newVNode.props, cctx); // eslint-disable-line new-cap
				}
				else {
					newVNode._component = c = new Component(newVNode.props, cctx);
					// 对于函数组件,需要将创建实例的 constructor 指向函数
					c.constructor = newType;
					// doRender 函数也就是调用 this.constructor
					c.render = doRender;
				}
				c._ancestorComponent = ancestorComponent;
				// 对于使用了 New Context API 的组件(存在 provider),需要调用 sub 函数订阅 context 的更新
				if (provider) provider.sub(c);

				// 为新创建的组件实例设置一些初值
				c.props = newVNode.props;
				if (!c.state) c.state = {};
				// 注意:这里可以看到 Component 中的 context 属性为组件使用的 context 值,其可能为 Legacy Context 或是 New Context,
				// 而 _context 属性为 Legacy Context 的值,保存这一值的目的是在组件进行更新时,需要从组件中获得 Context 的值,即使其本身并不需要该值。
				// 这也展示了 Legacy Context API 的缺陷:本质而言,其与 props 相同,仍是沿着树依次向下传递的。
				c.context = cctx;
				c._context = context;
				isNew = c._dirty = true;
				c._renderCallbacks = [];
			}

			// 更新 _vnode 值
			c._vnode = newVNode;

			// PART 4
			// 调用新的生命周期 getDerivedStateFromProps 更新 state
			// 相关资料参见:https://reactjs.org/docs/react-component.html
			let s = c._nextState || c.state;
			if (newType.getDerivedStateFromProps!=null) {
				oldState = assign({}, c.state);
				// 改成这样比较清晰:if (s===c.state) s = c._nextState = assign({}, c.state);
				if (s===c.state) s = c._nextState = assign({}, s);
				assign(s, newType.getDerivedStateFromProps(newVNode.props, s));
			}

			// PART 5
			// 调用预渲染的生命周期方法
			if (isNew) {
				if (newType.getDerivedStateFromProps==null && c.componentWillMount!=null) c.componentWillMount();
				// 将需要调用 componentDidMount 的组件暂存入 mounts 中,等待 diff 结束后通过 commitRoot 进行调用
				if (c.componentDidMount!=null) mounts.push(c);
			}
			else {
				// 如果不存在 getDerivedStateFromProps 时,则调用 componentWillReceiveProps
				if (newType.getDerivedStateFromProps==null && force==null && c.componentWillReceiveProps!=null) {
					c.componentWillReceiveProps(newVNode.props, cctx);
					s = c._nextState || c.state;
				}

				// 通过 force 和 shouldComponentUpdate 来判断是否执行更新
				if (!force && c.shouldComponentUpdate!=null && c.shouldComponentUpdate(newVNode.props, s, cctx)===false) {
					// 如果 shouldComponentUpdate 返回 false 则执行一些赋值操作后直接结束主逻辑 (break outer)
					dom = newVNode._dom;
					c.props = newVNode.props;
					c.state = s;
					c._dirty = false;
					newVNode._lastDomChild = oldVNode._lastDomChild;
					break outer;
				}

				if (c.componentWillUpdate!=null) {
					c.componentWillUpdate(newVNode.props, s, cctx);
				}
			}

			// PART 6
			// 保存下 oldProps 和 oldState,其会在后续的一些钩子函数或者生命周期函数中被使用
			oldProps = c.props;
			if (!oldState) oldState = c.state;
			// 更新组件的各个属性
			c.context = cctx;
			c.props = newVNode.props;
			c.state = s;

			if (options.render) options.render(newVNode);

			// PART 7
			// 调用组件的 render 函数得到 vnode
			let prev = c._prevVNode;
			let vnode = c._prevVNode = coerceToVNode(c.render(c.props, c.state, c.context));
			c._dirty = false;

			// PART 8
			// getChildContext 为 Legacy Context API ,如果其存在,则通过其更新 context 的值
			// 需要注意的是,这里被更新的为 context 而不是 cctx 也就是说 Legacy Context 和 New Context 各管各,不会混在一块
			if (c.getChildContext!=null) {
				context = assign(assign({}, context), c.getChildContext());
			}

			// PART 9
			// getSnapshotBeforeUpdate 同样是新的生命周期
			// 相关资料参见:https://reactjs.org/docs/react-component.html
			if (!isNew && c.getSnapshotBeforeUpdate!=null) {
				snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
			}

			// 更新组件深度
			c._depth = ancestorComponent ? (ancestorComponent._depth || 0) + 1 : 0;
			// 对 render 返回的 VNode 执行 diff
			c.base = dom = diff(dom, parentDom, vnode, prev, context, isSvg, excessDomChildren, mounts, c, null, oldDom);

			if (vnode!=null) {
				// 如果返回 vnode 为 Fragment,那么其 _lastDomChild 非空,此时将其赋值给 newVNode
				newVNode._lastDomChild = vnode._lastDomChild;
			}

			c._parentDom = parentDom;

			// 更新 ref
			if (newVNode.ref) applyRef(newVNode.ref, c, ancestorComponent);
		}
		// VNode 类型为元素节点
		else { balabala... }

		newVNode._dom = dom;

		// PART 10
		// 完成 diff 之后,调用 _renderCallbacks 中的回调函数以及 componentDidUpdate 生命周期
		if (c!=null) {
			while (p=c._renderCallbacks.pop()) p.call(c);

			// Don't call componentDidUpdate on mount or when we bailed out via
			// `shouldComponentUpdate`
			if (!isNew && oldProps!=null && c.componentDidUpdate!=null) {
				c.componentDidUpdate(oldProps, oldState, snapshot);
			}
		}

		if (clearProcessingException) {
			c._processingException = null;
		}

		if (options.diffed) options.diffed(newVNode);
	}
	catch (e) {
		catchErrorInComponent(e, ancestorComponent);
	}

	return dom;
}

function doRender(props, state, context) {
	// 直接调用函数组件的 constructor 也即是对应的函数
	return this.constructor(props, context);
}

如果你阅读了前一篇文章《从 Preact 源码一窥 React 原理(二):Diff 算法》那么你应当对于 diff 函数的核心思路和实现逻辑很清楚了,这里就不再赘述。
上述代码中略去了已在前文中讨论的一些部分,让我们直接进入组件处理的正题:

  • PART 1:根据 newType.contextType 判断当前的使用 Context API 类型,并获取相应的 Context 值,具体内容将在 Context 一节中介绍;
  • PART 2:当 oldVNode 中存在组件的实例时,将该组件的实例赋值给 newVNode
  • PART 3:当 oldVNode 中存在组件的实例时,则为 newVNode 创建一个组件实例。
    如果为类组件则直接通过 new 关键字创建对应类的实例;如果为函数组件则创建一个 Component 的实例,并将其 constructor 设为该函数,实例的 render 方法设为 doRender 函数,其也即是直接调用对应的函数。
    如果组件使用了 New Context API 则在通过 provider 调用 sub 函数以订阅 context 的变化。
    之后为其设置一些初始值,例如 state_dirty 等;
  • PART 4:在完成组件的创建或者拷贝后,调用新的 React 生命周期 getDerivedStateFromProps 对组件的 state 进行更新,关于该生命周期的介绍请参见 React - Component;
  • PART 5:之后再调用传统几个的预渲染阶段 React 生命周期函数。
    如果是新创建的组件,则调用 componentWillMount ,并将该组件放入 mounts 数组中,等待 Diff 完成后通过 commitRoot 函数调用对应的 componentDidMount
    如果不是新创建的组件,则调用 componentWillReceiveProps 并通过 force 以及 shouldComponentUpdate 判断组件是否执行更新操作;
  • PART 6:保存下 oldProps 以及 oldState 从而后续的一些钩子函数或者生命周期函数中使用,并对组件中的各个属性进行更新;
  • PART 7:调用组件的 render 函数,获取其生成的 VNode
  • PART 8:如果声明了 Legacy API 的函数 getChildContext ,那么调用其更新父组件传递下来的 Context 值;
  • PART 9:调用新的生命周期 getSnapshotBeforeUpdate 并执行 diff 函数对 render 函数生成的 VNode 进行更新。完成之后最后对组件一些辅助的值进行更新,例如 base_parentDom 等;
  • PART 10:结束组件的渲染过程,对保存在组件 _renderCallbacks 中的回调函数进行调用,并调用生命周期函数 componentDidUpdate,完成渲染过程。

组件的渲染过程介绍的较细,并且对每一段代码均附上了相应的注释,理解起来应当较为容易。
其中还有一部分内容未介绍的非常详尽,也就是 Context API 相关的代码,这一内容将在 Context API 一节中进行分析。

Component 提供的函数

上一节内容介绍了如何对组件进行渲染,但是如果想要对组件执行更新应当如何做呢?
此时就需要通过 Component 类所提供的两个函数来完成相应的工作,其分别为:forceUpdate 函数和 setState 函数。

forceUpdate 函数

开发者可以通过调用 forceUpdate 函数对组件进行强制更新,强制更新的本质就在于通过 force 参数来取消 shouldComponentUpdate 的判断。事实上,同样可以通过 forceUpdate 进行非强制的更新,只需要将其参数设置为 false 即可。
以下是 forceUpdate 的实现:

Component.prototype.forceUpdate = function(callback) {
	let vnode = this._vnode, dom = this._vnode._dom, parentDom = this._parentDom;
	if (parentDom) {
		// 如果 callback === false 则为非强制更新
		const force = callback!==false;

		// 与 render 函数相同,均是先调用 diff 执行更新后调用 commitRoot 执行 componentDidMount 回调
		let mounts = [];
		dom = diff(dom, parentDom, vnode, vnode, this._context, parentDom.ownerSVGElement!==undefined, null, mounts, this._ancestorComponent, force, dom);
		if (dom!=null && dom.parentNode!==parentDom) {
			parentDom.appendChild(dom);
		}
		commitRoot(mounts, vnode);
	}
	if (callback) callback();
};

forceUpdate 函数的实现较为简单,其与 render 函数想类似,均是先调用 diff 函数执行更新后调用 commitRoot 函数执行 componentDidMount 回调。

setState 函数

除了通过调用 forceUpdate 来强制更新组件,开发者还可以调用 setState 函数对组件的 state 进行修改。其随后会以异步的方式对组件进行更新。
setState 函数实现如下:

Component.prototype.setState = function(update, callback) {
	// 当 _nextState 指向 state 时会对 state 进行一次拷贝,以防止更新过程影响当前 state
	let s = (this._nextState!==this.state && this._nextState) || (this._nextState = assign({}, this.state));

	// update 可以是函数或者对象
	if (typeof update!=='function' || (update = update(s, this.props))) {
		// 通过 assign 对 _nextState 进行更新
		assign(s, update);
	}

	// update 的函数返回值为 null 则不需要执行组件更新
	if (update==null) return;

	// 当组件需要更新时,将其放入待更新队列中
	if (this._vnode) {
		if (callback) this._renderCallbacks.push(callback);
		enqueueRender(this);
	}
};

如果组件当前的 _nextState 属性指向 state 属性的值,则 setState 函数会首先对 state 进行一次拷贝。后续的多次 setState 操作均会在拷贝后的 _nextState 值上进行更新。
传入 setState 函数的参数 update 可以是函数或者是对象,其均会通过 assign_nextState 进行更新。如果 update 函数的返回值为 null 则说明该操作不需要执行后续的 diff;否则,将对当前组件调用 enqueueRender 函数,将当前组件放入待更新队列中。

enqueueRender 函数的相关实现如下:

export function enqueueRender(c) {
	// 设置组件的 _dirty 为 true 并将组件加入待更新队列中
	// 如果待更新队列长度为 1 则通过 defer 延迟调用 process
	if (!c._dirty && (c._dirty = true) && q.push(c) === 1) {
		(options.debounceRendering || defer)(process);
	}
}

// defer 可以通过 Promise 或者 setTimeout 实现
const defer = typeof Promise=='function' ? Promise.prototype.then.bind(Promise.resolve()) : setTimeout;

function process() {
	let p;
	// 首先根据深度对队列中的组件进排序,首先更新深度较高的组价
	q.sort((a, b) => b._depth - a._depth);
	while ((p=q.pop())) {
		// 通过调用 forceUpdate 来实现更新,此时参数为 false 也即是非强制更新
		if (p._dirty) p.forceUpdate(false);
	}
}

enqueueRender 函数将组件的 _dirty 值设为 true,并将其放入待更新的队列中。如果该队列中仅有一个值,则会通过 defer 函数延迟调用 process 函数。
defer 函数通过 Promise 或者 setTimeout 将函数推迟到下一个 Tick 执行。
process 函数首先对队列中的组件进行排序,并按照深度从高到低的顺序调用 forceUpdate 进行组件的更新。

上述的三个函数也就构成了 Preact 所实现的批量更新特性。在一个 Tick 内所有因 setState 而改变了状态,需要执行更新的组件均会被待更新队列存储下来,并在下一个 Tick 中一起更新。

Context API

在 Context API 的实现原理之前,首先做一些简单的概念铺垫。如果您想要进一步理解 Legacy Context API 和 New Context API ,请参阅 React 的官方文档 React - Context 。

Context API 是什么?

Context 通过组件树提供了一个传递数据的方法,从而避免了在每一个层级手动的传递 props 属性。
来自 React - Context

当你需要在两个层次深度间隔很大的组件之间传递数据时,可以使用连续传递 props 来实现这一需求,但是一旦需求改变,就需要开发者修改每一个中间组件,这样的开发代价是不可接受的。
为此,React 提供了 Context API 可以在任意层级深度间隔的组件之间传递需要的数据。
在 16.x 版本的 React 中,React 提供了两种 Context API:

  • Legacy Context API
  • New Context API

Legacy Context

Legacy Context 也即是 React 16.x 前所一直提供的 Context API。
虽然 Legacy Context 被例如 Redux 等第三方库所广泛使用,但是事实上其一直仅仅是实验性的 API。目前其仍然存在 React 中,但在未来的版本中将会被移除。
既然 React / Preact 目前仍然支持这一 API ,那么我们还是有必要了解一下这一 API。

事实上,Legacy Context 在 Preact 中的实现与 Props 类似(在 React 中也是同样),均是沿着树状结构依次向下传递。
首先需要关注的是 diffdiffChildren 两个函数的参数,这两个函数的相互调用构成了的 Preact 中 Diff 算法的主体:

function diff(dom, parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent, force, oldDom)
function diffChildren(parentDom, newParentVNode, oldParentVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent, oldDom)

可以看到两个函数中均包含了 context 参数,正如之前所提,这一参数也就是 Legacy Context API 所创建的 Context 值。

Legacy Context 值的源头在于 getChildContex 函数,其在 diff 函数的 PART 8 中被调用,见如下代码:

if (c.getChildContext!=null) {
	context = assign(assign({}, context), c.getChildContext());
}

如果组件申明了 getChildContex 函数,则将其添加到 当前 Legacy Context 的拷贝值之上,生成新的 Legacy Context 并继续向下传递。

Legacy Context 值的获取从 diff 函数的参数中来。在 diff 函数的 PART 1 中,函数会判断当前组件所使用的 Context API 类型,如果是 Legacy Context 则将其赋值给 cctx,其也就是组件实际所使用的 Context 值。
无论组件是否需要使用 Context 值,其内部也会由 _context 属性保存当前组件所接收的 Legacy Context 值,从而使得组件在通过 setState 或者 forceUpdate 进行更新时能够为子组件传递 Legacy Context 的值。

Preact 中对于 Context 相关变量的声明很不清晰,如果将变量名做如下更改则会更易阅读:

  • cctx 改为 currentEnabledContext;
  • Component.context 改为 Component.currentEnabledContext
  • context 改为 legacyContext;
  • Component._context 改为 Component._LegacyContext。

New Context

New Context API 是 React 在 16.x 版本中所推出的新 Context API。与 Legacy Context API 不同,其不再是实验性质的 API ,而是稳定的受到良好支持的 API。
关于 New Context 的介绍在 React 官方文档中介绍的非常详细,其核心就在于 Provider - 提供者以及 Customer - 消费者的概念(具体内容请参阅 React - Context)。

New Context 中的 Provider 以及 Customer 均需要通过入口函数 createContext 进行创建,其也是 New Context API 实现的核心内容,其代码如下:

export function createContext(defaultValue) {
	const id = '__cC' + i++;

	// context 中保存的是唯一的 id 以及默认值
	let context = {
		_id: id,
		_defaultValue: defaultValue
	};

	// Consumer 的渲染不额外创建 node ,直接将子结果进行渲染,同时传递一个 context 参数
	function Consumer(props, context) {
		// 注意,这里的 children 必然为一个函数
		return props.children(context);
	}
	Consumer.contextType = context;
	context.Consumer = Consumer;

	let ctx = { [id]: null };

	// comp 指的是 Provider 实例化后得到的组件实例
	function initProvider(comp) {
		// 已订阅的消费者组件
		const subs = [];
		// 用于在 Consumer 中取到 Provider
		comp.getChildContext = () => {
			ctx[id] = comp;
			return ctx;
		};
		// 每次 shouldComponentUpdate 被调用时,均会通知所有已订阅的消费者,对其进行更新
		comp.shouldComponentUpdate = props => {
			subs.map(c => {
				// New Context 的值实际上仍需要从 Provider 的 props.value 上取
				if (c._parentDom) {
					c.context = props.value;
					enqueueRender(c);
				}
			});
		};
		// 将消费者组件订阅到数组中
		comp.sub = (c) => {
			subs.push(c);
			let old = c.componentWillUnmount;
			// 当消费者卸载时,同时在订阅中删除对应组件
			c.componentWillUnmount = () => {
				subs.splice(subs.indexOf(c), 1);
				old && old();
			};
		};
	}

	// 只有 Provider 组件渲染了才会初始化 provider
	function Provider(props) {
		if (!this.getChildContext) initProvider(this);
		return props.children;
	}
	context.Provider = Provider;

	return context;
}

createContext 中的几个核心部分分别为:

  • id:用作标识的唯一 ID;
  • context:保存了 ID 信息以及默认的 Context 值,需要注意的是其并不是 New Context 的值;
  • ctx:保存了对 Provider 组件的索引,其同样并不是 New Context 的值;
  • Customer:消费者组件只是简单的对 children 的函数进行执行;
  • Provider:当 Provider 组件渲染时,其会执行对该组件的初始化操作,为其添加三个函数。
    其中,getChildContext 函数用于将 ctx 的值添加到 Legacy Context 中,从而使得 Customer 能够获取 Provider 的实例;
    shouldComponentUpdate 生命周期函数会在 New Context 的值更新时,通知所有订阅的 Customer,另其执行更新操作;
    sub 函数会将 Customer 订阅到 Provider 上,并截获其 componentWillUnmount 生命周期,当 Customer 卸载时,会取消其订阅。

事实上,虽然 createContext 的实现中存在名为 contextctx 的变量,但是实际上其均不是 New Context 的值。真正被使用的 New Context 值仍需要从 Provider 的 props 中取到。

让我们再回过头来看 diff 函数中的相关实现,其 PART 1 代码如下所示:

// 判断组件是否使用了 New Context API,当使用了 New Context API 则从 provider 获取 context 值,
// 否则从 diff 函数的参数中获取从父组件传递的 context 值。
let cxType = newType.contextType;
let provider = cxType && context[cxType._id];
let cctx = cxType != null ? (provider ? provider.props.value : cxType._defaultValue) : context;

现在来看这段代码就更为明白了:通过 newType.contextType 可以判断当前组件是否为 Customer,如果是,则通过合并到 Legacy Context 上的 ctx 来获取 Provider 的实例。得到 Provider 实例之后,也就能获取到 provider.props.value 的值,也即是 New Context 的值。

这一一来,我们就能理清 New Context 的工作流程:

  1. 调用 createContext 创建 Provider 以及 Customer
  2. Provider 组件渲染,调用 initProvider 执行初始化操作,通过 getChildContext 函数将 ctx 也即是对 Provider 实例的引用放入Legacy Context 中;
  3. Customer 组件渲染,通过 Legacy Context 获取 Provider 的实例,从而获取 New Context 值,并调用 sub 函数保持订阅;
  4. Provider 组件 props.value 更新,通过生命周期函数 shouldComponentUpdate 通知所有 Customer 进行更新;
  5. Customer 组件卸载时,通过被 Provider 截获的生命周期函数 componentWillUnmount 取消订阅。

顺便一提,可以看出在 Preact 中 New Context 在 Preact 中的实现也依赖于通过 Legacy Context 来获取 Provider 的实例。这样的方式并不优雅,尤其是在 Legacy Context 将会在未来被废弃的情况下。Preact 的开发人员或许需要重新考虑 New Context 的实现方式了。

总结

本文分析了 Preact 组件相关的具体实现,至此,Preact 核心部分的分析已经基本涵盖。
虽然囿于水平,无法面面俱到,但也算是费心费力整理的干货。若有任何错误,烦请留言指出。

参考资料

  1. Preact - Github
  2. React - Context
  3. React - Component

你可能感兴趣的:(前端杂物筐,React,Preact,组件,React,原理)