从 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 <body>:
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.