了解Preact的核心概念,组件构成(Components & jsx)、渲染机制、Virtual DOM、 diff algorithm(比较算法)等,解析其源码,理清楚Preact的原理。
Preact 版本 v10.19.3
Component
从Preact 的component入手学习组件构成、渲染&更新机制。
在Preact中,Component是构建用户界面的基本单元。是一个JavaScript类或者函数,其中包含一些特定的方法和属性,用于定义如何渲染界面以及如何响应用户的交互。Preact遵循了React的Component模型,但是具有更小的体积和更快的速度。每一个Preact的Component都需要包含一个名为render的方法。这个方法负责返回一个虚拟DOM元素,用于映射这个Component在界面上的DOM。
组成结构
-
状态(State):组件的当前数据状态。这通常是控制组件渲染的关键数据。例如,一个计数器组件可能有一个名为count的状态,用来保存当前的计数值。
-
属性(Props):从父组件传递下来的数据。属性在组件的整个生命周期内都是不可变的,任何对属性的修改都会被忽略。
-
生命周期方法(Lifecycle Methods):一些特殊的方法,组件在其生命周期的某些阶段会自动调用这些方法。例如,componentDidMount方法会在组件第一次被渲染到DOM后被调用。
-
事件处理方法(Event Handlers):用来响应用户的交互,如点击、滑动等。这些方法通常会改变组件的状态和/或通知父组件。
-
渲染方法(Render Method):每一个Preact组件都需要一个render方法,这个方法返回组件当前的DOM结构。渲染方法应该是一个纯函数,仅依赖于this.props和this.state。
下面是一个简单的Preact组件的例子,Preact也是基于jsx来作为描述组件结构的载体。jsx 是 ECMAScript 的类似 XML 的语法扩展,用于承载Preact组件的结构和内容,并且通过类似HTML的语法提供了一种直观和声明式的方式来创建虚拟DOM元素。并且在编译过程中,JSX元素会被编译为常规JavaScript函数调用和对象。
TypeScript
import { h, Component,render } from 'preact';
class MyComponent extends Component {
// 状态
state = { count: 0 };
// 生命周期方法
componentDidMount() {
console.log('Component mounted');
}
// 事件处理方法
increment = () => {
this.setState({ count: this.state.count + 1 });
};
// 渲染方法
render(props, state) {
return (
<div>
<p>Hello, {props.name}!</p> {/* 属性 */}
<p>Count: {state.count}</p> {/* 状态 */}
<button onClick={this.increment}>Increment</button> {/* 事件处理方法 */}
</div>
);
}
}
// 函数组件
function MyComponent(props) {
// useState Hook 用于添加状态
const [count, setCount] = useState(0);
// useEffect Hook 用于生命周期方法
useEffect(() => {
console.log('Component mounted');
// 注意,空数组作为依赖项传递给 useEffect,意味着这个effect只在组件挂载时运行一次
}, []);
// 事件处理函数
const increment = () => {
setCount(count + 1);
};
// 渲染函数
return (
<div>
<p>Hello, {props.name}!</p> {/* 属性 */}
<p>Count: {count}</p> {/* 状态 */}
<button onClick={increment}>Increment</button> {/* 事件处理方法 */}
</div>
);
}
// 渲染MyComponent到id为root的Div
render(<MyComponent name="world" />, document.getElementById('app'));
以上,就是一个基本的Preact组件的主要组成部分。
和React一样Preact也是经过Babel转换为浏览器可以识别的代码,Babel在其中担任的作用是
-
将JSX转换为JavaScript:最重要的作用是将JSX语法转为Preact可以理解的普通JavaScript代码。在Preact中,JSX经常被转换成 h 函数(hyperscript)调用来创建虚拟DOM元素。
-
编译新的JavaScript特性:Babel还能将ECMAScript 2015+(ES6+)的语法转换成当前和较老版本的浏览器都能理解的ES5代码。这意味着开发者可以在项目中使用最新的JavaScript语言特性,而无需担心兼容性问题。
-
插件和预设:Babel可以利用插件(plugins)和预设(presets)来扩展其功能。比如一下
预设(presets)
-
@babel/preset-env:
-
@babel/preset-react 或 @babel/preset-preact:
插件 (Plugins)
-
@babel/plugin-transform-arrow-functions:
-
@babel/plugin-proposal-class-properties:
-
@babel/plugin-transform-react-jsx:
-
等
-
-
优化代码:Babel不仅可以转换代码,还可以利用插件来优化代码,减少冗余,提升性能。
转译后的代码
TypeScript
"use strict";
exports.__esModule = true;
var preact_1 = require("preact");
var MyComponent = /** @class */ (function (_super) {
__extends(MyComponent, _super);
function MyComponent() {
var _this = _super !== null && _super.apply(this, arguments) || this;
// 状态
_this.state = { count: 0 };
// 事件处理方法
_this.increment = function () {
_this.setState({ count: _this.state.count + 1 });
};
return _this;
}
// 生命周期方法
MyComponent.prototype.componentDidMount = function () {
console.log('Component mounted');
};
// 渲染方法
MyComponent.prototype.render = function (props, state) {
return (preact_1.h("div", null,
preact_1.h("p", null,
"Hello, ",
props.name,
"!"),
preact_1.h("p", null,
"Count: ",
state.count),
preact_1.h("button", { onClick: this.increment }, "Increment")));
};
return MyComponent;
}(preact_1.Component));
// 渲染MyComponent到id为root的Div
preact_1.render(preact_1.h(MyComponent, { name: "world" }), document.getElementById('app'));
源码解析
从类组件出发在例子中可以看到自定义的类组件都继承自Component类class MyComponent extends Component ,因此理解Component类也就理解了组件,让我们一步步顺藤摸瓜。
Component
找到源码位置src/index.js -> src/component.js
TypeScript
/**
* Base Component class. Provides `setState()` and `forceUpdate()`, which
* trigger rendering
* 基础组件类。提供 `setState()` 和 `forceUpdate()` 方法,它们会触发渲染。
* @param {object} props The initial component props 初始化组件的 props
* @param {object} context The initial context from parent components'
* getChildContext 初始上下文来自父组件的getChildContext。
*/
export function BaseComponent(props, context) {
this.props = props;
this.context = context;
}
可以看到这个基类*BaseComponent** *接收props 和 context
-
Props: 向组件传递数据和回调函数
-
Context: 提供了在组件树上的所有层级都能访问到的共享数据(通过父组件的getChildContext获取),例如下面这个例子
TypeScript
class MyContext extends Component {
getChildContext() {
return { color: "purple" };
}
render() {
return (
<ChildComponent />
);
}
}
而我们自定义的组件就是继承这个基类并添加状态(State)、属性(Props)、生命周期方法(Lifecycle Methods)、事件处理方法(Event Handlers)、渲染方法(Render Method)。
Render
了解了结构下面我们来看Preact是怎么将这个jsx文件代码渲染到页面上的,也从这让我们了解vnode。
首次渲染
从以上将jsx通过bable转译后的代码看我们调用了render将h 函数基于组件创建的vnode挂载到了 id 为 root 的 Div 上,所以我们直接找到render的实现。
TypeScript
import {render } from 'preact';
...
// 渲染MyComponent到id为root的Div
preact_1.render(preact_1.h(MyComponent, { name: "world" }), document.getElementById('app'));
源码解析
所以我们重点看h和render
H(createElement)

所以H函数本质就是createElement
TypeScript
// src/create-element.js
/**
* Create an virtual node (used for JSX)
* 创建虚拟node
* @param {VNode["type"]} type The node name or Component constructor for this
* virtual node 虚拟节点的节点类型或组件构造函数
* @param {object | null | undefined} [props] 虚拟节点的属性
* @param {Array<import('.').ComponentChildren>} [children] 虚拟节点的子节点
* @returns {VNode}
*/
export function createElement(type, props, children) {
let normalizedProps = {},
key,
ref,
i;
// 标准化 props
for (i in props) {
if (i == 'key') key = props[i];
else if (i == 'ref') ref = props[i];
else normalizedProps[i] = props[i];
}
// 处理传入子节点的情况
if (arguments.length > 2) {
normalizedProps.children =
arguments.length > 3 ? slice.call(arguments, 2) : children;
}
// 如果是组件VNode,则检查并应用defaultProps
//注意:在开发中类型可能未定义,此处绝不能出错
if (typeof type == 'function' && type.defaultProps != null) {
for (i in type.defaultProps) {
if (normalizedProps[i] === undefined) {
normalizedProps[i] = type.defaultProps[i];
}
}
}
return createVNode(type, normalizedProps, key, ref, null);
}
/**
* 创建一个内部使用的 VNode
* @param {VNode["type"]} type The node name or Component
* Constructor for this virtual node
* @param {object | string | number | null} props这个虚拟节点的属性。
* 如果这个虚拟节点代表一个文本节点,那么这就是节点的文本(字符串或数字)。
* @param {string | number | null} 这个虚拟节点的key,用于与其子节点进行比较时使用。
* @param {VNode["ref"]} ref属性是对其创建的子节点的引用
* @returns {VNode}
*/
export function createVNode(type, props, key, ref, original) {
/** @type {VNode} */
const vnode = {
type,
props,
key,
ref,
_children: null,
_parent: null,
_depth: 0,
_dom: null,
_nextDom: undefined,
_component: null,
constructor: undefined,
_original: original == null ? ++vnodeId : original,
_index: -1,
_flags: 0
};
// Only invoke the vnode hook if this was *not* a direct copy:
if (original == null && options.vnode != null) options.vnode(vnode);
return vnode;
}
步骤解析
-
进行props的标准化后调用createVNode,创建VNode
-
在createVNode中基于component创建VNode,把component存入到vnode.type里
-
返回VNode
VNode
这里我们来解析一下VNode的结构,直接看VNode的ts结构
TypeScript
export type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;
export interface VNode<P = {}> {
type: ComponentType<P> | string;
props: P & { children: ComponentChildren };
key: Key;
ref?: Ref<any> | null;
/**
* 此"vnode"开始渲染的时间。仅在连接了开发工具时才会设置。默认值为"0"。
*/
startTime?: number;
/**
* 此"vnode"完成渲染的时间。仅在连接了开发工具时才会设置。
*/
endTime?: number;
}
export interface VNode<P = {}> extends preact.VNode<P> {
type: (string & { defaultProps: undefined }) | ComponentType<P>;
props: P & { children: ComponentChildren };
ref?: Ref<any> | null;
_children: Array<VNode<any>> | null;
_parent: VNode | null;
// 渲染层级用于排序渲染优先级
_depth: number | null;
/**
* 一个VNode的第一个(用于Fragments)DOM子节点
*/
_dom: PreactElement | null;
_nextDom: PreactElement | null | undefined;
_component: Component | null;
constructor: undefined;
_original: number;
_index: number;
_flags: number;
}
-
type: 这个属性可以是一个字符串,表示 HTML 标签的名称,比如 'div'、'span'等。也可以是一个函数组件或类组件,表示用户自定义的组件。
-
props: 这是一个对象,包含了所有传递给组件的props(包括children)。如果 type 是一个 HTML 标签,props也可能包含了一些处理事件的函数(如 onClick)或者 DOM 属性(如 value、checked等)。
-
ref: 这是一个引用,可以用来访问实际的 DOM 节点或者组件实例,如果有的话。
-
_children: 这是一个数组,包含了这个 VNode 所有的子 VNodes。如果没有子 VNode,这个属性为 null。
-
_parent: 这是这个 VNode 的父 VNode。如果没有父 VNode,这个属性为 null。
-
_depth: 这个数字表示 VNode 在 VNode 树中的深度。用于排序和确定渲染优先级。
-
_dom: 这是这个 VNode 对应的真实 DOM 元素。如果这个 VNode 还没有被渲染到真实 DOM,这个属性为 null。
-
_nextDom: 这是Fragment组件或者返回Fragment的组件的最后一个DOM子节点。
-
_component: 如果这个 VNode 是一个组件,这个属性就是这个组件的实例。
-
constructor: 这个属性应纯背景是 undefined。可能是为了防止 VNode 被当作 JavaScript 的普通对象来使用。
-
_original: 这个属性存储了 VNode 被创建时的原始版本。
-
_index: 这个属性保存了这个 vnode 在其父 vnode 的 _children 数组中的索引位置。
-
_flags: 这是一个数字,表示了这个 vnode 的一些元信息,比如它是不是一个组件,是否具有 key 等等。
在Preact中_开头的变量名都是内部变量。
我们可以通过Preact Developer Tools 开启tools点击组件的debug就会在控制台打印查看上面我们preact 组件的VNode


那么为什么需要将DOM转化为VNode呢。
原因是操作真实DOM非常消耗资源,而操作JavaScript对象则相对更快。VNode让我们能在JS层进行DOM操作,之后再通过高效的Diff算法将变化应用到真实DOM,从而提升性,可以说虚拟DOM是真实DOM的抽象表达,它可以帮助我们在不直接操作DOM的情况下描述DOM应该是什么样子。VNode也就是这个虚拟DOM的每一个节点。
主要作用如下:
-
提升性能:操作真实DOM非常消耗资源,而操作JavaScript对象则相对更快。VNode让我们能在JS层进行DOM操作,之后再通过高效的Diff算法将变化应用到真实DOM,从而提升性能。
-
跨平台:因为VNode是JavaScript对象,它可以在不同环境下(web, node, native mobile)运行。这让例如React Native这样的跨平台解决方案得以实现。
-
简化API: VNode为我们提供了一种简单,直观的方式来描述我们希望的DOM结构。
-
为组件化打基础:在Preact等库中,组件其实就返回一颗VNode树,每个组件都可以看作是一个返回VNode的函数。这也是我们能通过组合不同组件构建复杂应用的基础。
总的来说,VNode是让preact能以更简单、更声明式、更性能的方式来操作DOM,是我们通过preact搭建页面的基石。
Render
我们已经了解到了Preact会通过H(createElement)创建组件的VNode,接下来我们来看Render是如何将VNode渲染到页面上的
TypeScript
// src/render.js
/**
* Render a Preact virtual node into a DOM element
* 将一个 Preact 虚拟节点渲染到一个 DOM 元素中。
* @param {ComponentChild} vnode The virtual node to render
* @param {PreactElement} parentDom The DOM element to render into
* @param {PreactElement | object} [replaceNode] Optional: Attempt to re-use an
* existing DOM tree rooted at `replaceNode`指定dom替换
* replaceNode 参数将于 Preact v11 中移除
*/
export function render(vnode, parentDom, replaceNode) {
// 附加一个在渲染之前调用的钩子,主要用于检查参数。
if (options._root) options._root(vnode, parentDom);
//对于滥用`hydrate()`中的`replaceNode`参数,通过传递`hydrate`函数而不是DOM元素来表示我们是否处 //于hydration模式。
let isHydrating = typeof replaceNode == 'function';
// 为了能够支持在同一个DOM节点上多次调用`render()`,我们需要获取对先前树的引用。我们通过为指向上一 // 个渲染树的DOM节点分配一个新的`_children`属性来实现这一点。默认情况下,此属性不存在,这意味着
// 正在首次挂载新树。
let oldVNode = isHydrating
? null
: (replaceNode && replaceNode._children) || parentDom._children;
// 创建 vnode
vnode = ((!isHydrating && replaceNode) || parentDom)._children =
createElement(Fragment, null, [vnode]);
// List of effects that need to be called after diffing.
let commitQueue = [],
refQueue = [];
// 渲染
diff(
parentDom,
vnode,
oldVNode || EMPTY_OBJ,
EMPTY_OBJ,
parentDom.ownerSVGElement !== undefined,
!isHydrating && replaceNode
? [replaceNode]
: oldVNode
? null
: parentDom.firstChild
? slice.call(parentDom.childNodes)
: null,
commitQueue,
!isHydrating && replaceNode
? replaceNode
: oldVNode
? oldVNode._dom
: parentDom.firstChild,
isHydrating,
refQueue
);
vnode._nextDom = undefined;
commitRoot(commitQueue, vnode, refQueue);
}
内部属性
-
_root: 在函数组件中附加一个在渲染之前调用的钩子,主要用于检查参数。
-
isHydrating:判断是否为Hydration 模式
步骤
-
判断是否是直接渲染已经处理好完整的 HTML
-
基于vnode创建 element
-
通过diff渲染更新dom
-
执行commit,里面会执行一些更新后的回调
对于diff和commitRoot的解析我们放到后面diff模块(可以直接手动路由过去看),因为重渲染一样会使用这两个方法,我们先知道他们的作用就行。
-
diff:执行生命周期,对比新旧VNode渲染更新dom
-
commitRoot*:里面会执行一些更新后的回调*
先来看重渲染的逻辑
重渲染(更新)
组件的渲染基本使用两种方式 setState(修改组件状态,进而触发更新)和 forceUpdate(直接重新渲染),所以我们直接看代码实现
源码解析
Component & setState forceUpdate
找到源码位置src/index.js -> src/component.js
TypeScript
/**
* Base Component class. Provides `setState()` and `forceUpdate()`, which
* trigger rendering
* 基础组件类。提供 `setState()` 和 `forceUpdate()` 方法,它们会触发渲染。
* @param {object} props The initial component props 初始化组件的 props
* @param {object} context The initial context from parent components'
* getChildContext 初始上下文来自父组件的getChildContext。
*/
export function BaseComponent(props, context) {
this.props = props;
this.context = context;
}
// 定义 setState 方法
/**
* 更新组件状态并安排重新渲染。
* @this {Component}
* @param {object | ((s: object, p: object) => object)} update 要使用新值更新的状态属性哈希 * 表或给定当前状态和 props 返回新部分状态的函数
* @param {() => void} [callback] 组件状态更新后要调用的函数
*/
BaseComponent.prototype.setState = function (update, callback) {
// only clone state when copying to nextState the first time.
// 只有在第一次复制到nextState时才克隆状态。
let s;
if (this._nextState != null && this._nextState !== this.state) {
s = this._nextState;
} else {
// 首次赋值state给nextstate
s = this._nextState = assign({}, this.state);
}
// setState 的 update_dispatcher 为函数
if (typeof update == 'function') {
// 执行update,将结果再赋值给update
update = update(assign({}, s), this.props);
}
if (update) {
// 浅覆盖 修改s
assign(s, update);
}
// 判断是否跳过渲染
if (update == null) return;
// _vnode 虚拟dom
if (this._vnode) {
if (callback) {
// 入队到callback队列中,在更新后执行
this._stateCallbacks.push(callback);
}
// 入队渲染
enqueueRender(this);
}
};
/**
* 立即执行组件的同步重新渲染
* @this {Component}
* @param {() => void} [callback] 在组件重新渲染后要调用的函数
*/
BaseComponent.prototype.forceUpdate = function (callback) {
if (this._vnode) {
this._force = true;
if (callback) this._renderCallbacks.push(callback);
enqueueRender(this);
}
};
// src/util.js
export function assign(obj, props) {
// @ts-expect-error We change the type of `obj` to be `O & P`
for (let i in props) obj[i] = props[i];
return /** @type {O & P} */ (obj);
}
从里面我们可以看到setState使用到了很多内部属性比如state 、_nextState、_stateCallbacks。他们的作用如下
- this.state: 这是组件当前的状态对象,在组件重新渲染前,它包含了组件最近一次渲染时使用的状态值。这是开发者通常交互的状态对象,用于读取当前状态。比如
TypeScript
this.state = {
count: 0
};
- this._nextState: 用来存下一个(新的)状态的引用,当调用this.setState时,Preact 将调度状态的更新。在状态更新过程中,this._nextState 可能会与 this.state 不同,this._nextState 会持有即将应用于组件的下一个状态值。比如
TypeScript
setCount(count + 1);
this._nextState = {
count: 1
};
-
_stateCallbacks :它存储了与组件状态更新关联的回调函数
-
_force: 来告诉Preact的渲染器,不管当前的状态或属性与之前是否相同,组件都应该重新渲染
还有很多其他的内部属性当我们遇到的时候再一个个探讨。
setstate和forceUpdate最后会走到*enqueueRender**,*进行状态更新的入队,等待渲染。
EnqueueRender
将需要渲染的组件入栈,等待render的时候执行
TypeScript
/**
* The render queue 需要渲染的组件
* @type {Array<Component>}
*/
let rerenderQueue = [];
// 执行渲染函数的函数 优先微任务
const defer =
typeof Promise == 'function'
? Promise.prototype.then.bind(Promise.resolve())
: setTimeout;
/**
* 将一个组件的渲染入队列
* @param {Component} c The component to rerender
*/
export function enqueueRender(c) {
// 控制在一次渲染中只执行一次,若debounceRendering改了需要重新渲染
if((
!c._dirty && (c._dirty = true) && rerenderQueue.push(c) &&
!process._rerenderCount++) ||
prevDebounce !== options.debounceRendering
) {
/**
* revDebounce这个变量确实被用来存储当前的options.debounceRendering,这是一个控制渲染行为的函数。
* 每次enqueueRender被调用时,prevDebounce会被更新为当前的options.debounceRendering。
* 这样做的目的是,在应用程序的运行过程中,options.debounceRendering可能会被改变。
* 通过将当前的options.debounceRendering保存到prevDebounce中,我们就能确保即便options.debounceRendering在未来被修改了,
* 我们依然可以引用它原来的版本,也就是我们所说的"prev(之前的)Debounce"。
*/
prevDebounce = options.debounceRendering;
// 立即执行prevDebounce
(prevDebounce || defer)(process);
}
}
process._rerenderCount = 0;
内部属性
-
_dirty: 组件是否需要渲染
-
_rerenderCount:!process._rerenderCount++用于保证只有第一次的时候才往后执行(process._rerenderCount = 0的时候)
-
debounceRendering: 定时重新渲染化的函数。在某些情况下,可能会暂时重写这个函数以调整更新逻辑,为了以后能够恢复到Preact的默认行为,会保存一个 prevDebounce 变量来存储原始的 debounceRendering 函数。可以手动改为requestAnimationFrame或者requestIdleCallback
执行步骤
-
修改dirty,将组件入栈到rerenderQueue(需要重新渲染的组件都存在这里面,在commit的时候执行渲染)
-
通过 prevDebounce 调用process 重新渲染函数。
Precoss
重新渲染所有排队的组件,清空渲染队列rerenderQueue
TypeScript
/**
* @param {Component} a
* @param {Component} b
*/
const depthSort = (a, b) => a._vnode._depth - b._vnode._depth;
/** 重新渲染所有排队的组件,清空渲染队列 */
function process() {
let c;
let commitQueue = [];
let refQueue = [];
let root;
// 按照渲染层次排序
rerenderQueue.sort(depthSort);
// 不要立即更新`renderCount`。保持其值为非零,以防止在`queue`仍在被消耗时安排不必要的
// process()调用
// 循环渲染组件
while ((c = rerenderQueue.shift())) {
// 判断是否需要渲染 _dirty是在enqueueRender中设置的
if (c._dirty) {
let renderQueueLength = rerenderQueue.length;
// 渲染组件 返回新的 vnode
root = renderComponent(c, commitQueue, refQueue) || root;
// 如果这是队列中的最后一个组件,则在退出循环之前运行提交回调。
// This is required in order for `componentDidMount(){this.setState()}` to be batched into one flush.这是必需的,以便 `componentDidMount(){this.setState()}` 能够批量处理为一个刷新。
// Otherwise, also run commit callbacks if the render queue was mutated.
// 否则,如果渲染队列被修改,也运行提交回调。
if (renderQueueLength === 0 || rerenderQueue.length > renderQueueLength) {
commitRoot(commitQueue, root, refQueue);
refQueue.length = commitQueue.length = 0;
root = undefined;
// 当重新渲染提供程序时,可以注入额外的新项目,我们希望保持这些新项目从上到下的顺序,以便我们 //可以在单个传递中处理它们。
rerenderQueue.sort(depthSort);
} else if (root) {
if (options._commit) options._commit(root, EMPTY_ARR);
}
}
}
// 负责"提交"阶段,在 DOM 更新发生之后执行一系列清理工作和回调函数调用的过程
if (root) commitRoot(commitQueue, root, refQueue);
// 重置渲染记数
process._rerenderCount = 0;
}
函数步骤
-
将需要渲染的组件按照渲染层次排序
-
循环执行组件渲染renderComponent
- 若是最后一个组件,则直接执行commit
-
提交渲染commitRoot,在 DOM 更新发生之后执行一系列清理工作和回调函数调用(_renderCallbacks)
这些步骤中可以看到两个重要的环节执行渲染和提交渲染。
TypeScript
/**
* Trigger in-place re-rendering of a component.
* 触发组件的原地重新渲染。
* @param {Component} component The component to rerender
*/
function renderComponent(component, commitQueue, refQueue) {
let oldVNode = component._vnode,
oldDom = oldVNode._dom,
parentDom = component._parentDom;
if (parentDom) {
const newVNode = assign({}, oldVNode);
newVNode._original = oldVNode._original + 1;
if (options.vnode) options.vnode(newVNode);
// 执行diff更新dom
diff(
parentDom,
newVNode,
oldVNode,
component._globalContext,
parentDom.ownerSVGElement !== undefined,
oldVNode._flags & MODE_HYDRATE ? [oldDom] : null,
commitQueue,
oldDom == null ? getDomSibling(oldVNode) : oldDom,
!!(oldVNode._flags & MODE_HYDRATE),
refQueue
);
newVNode._parent._children[newVNode._index] = newVNode;
newVNode._nextDom = undefined;
// 检查新的虚拟节点的 _dom 属性(指向相应的实际 DOM 元素)是否与旧的 DOM 相同。
// 以确保父节点的 DOM 指针指向正确的元素。
// 这是为了确保 DOM 的结构与虚拟 DOM 的结构保持一致,从而在接下来的更新中能快速、准确地找到需要更新的元素。
if (newVNode._dom != oldDom) {
updateParentDomPointers(newVNode);
}
return newVNode;
}
}
在*renderComponent** *中会通过diff算法更新vnode并修改dom,下面从这我们展开对Preact diff的介绍
Diff
diff函数是preact操作DOM的核心,这里会比较新旧两虚拟DOM树(VNode树),找到他们的差异,然后将这些差异高效地应用到真实的 DOM 上,并且在这个过程中调用生命周期函数。
大致步骤如下:
-
比较元素类型:新旧 VNode 的类型不同,Preact 将删除旧元素(真实 DOM),然后创建并插入新元素。
-
比较元素属性:当在类型相同的元素上比较属性时,Preact 会逐个检查新旧属性的差异。如果新的 VNode 有一些属性在旧的 VNode 中不存在,Preact 将添加这些属性到真实的 DOM 上;反过来,如果旧的 VNode 有一些属性在新的 VNode 中不存在,Preact 将从真实 DOM 上删除这些属性。
-
比较子元素:Preact 将递归地检查新旧 VNode 的所有子节点。如果有新节点需要添加,它将创建新的 DOM 节点并插入到正确的位置;如果有旧节点需要删除,它将删除相应的 DOM 节点。
-
组件状态与生命周期:当处理组件时,Preact 可能会调用各类组件生命周期方法,比如 componentDidMount,componentDidUpdate等。这些方法可能导致 DOM 的更新。此外,如果组件的状态(state)发生变化,Preact 将重新渲染组件并更新相应的真实DOM节点。
源码解析
Diff
TypeScript
// src/diff/index.js
/**
* 区分两个虚拟节点并对 DOM 应用适当的更改
* @param {PreactElement} parentDom The parent of the DOM element
* @param {VNode} newVNode The new virtual node
* @param {VNode} oldVNode The old virtual node
* @param {object} globalContext The current context object. Modified by
* getChildContext
* @param {boolean} isSvg Whether or not this element is an SVG node
* @param {Array<PreactElement>} excessDomChildren
* @param {Array<Component>} commitQueue List of components which have callbacks
* to invoke in commitRoot
* 具有回调的组件列表在 commitRoot 中调用
* @param {PreactElement}
* @param {boolean} isHydrating Whether or not we are in hydration
* @param {any[]} refQueue an array of elements needed to invoke refs
*/
export function diff(
parentDom,
newVNode,
oldVNode,
globalContext,
isSvg,
excessDomChildren,
commitQueue,
oldDom,
isHydrating,
refQueue
) {
let tmp,newType = newVNode.type;
// 构造函数未定义。 这是为了防止 JSON 注入。
if (newVNode.constructor !== undefined) return null;
// If the previous diff bailed out, resume creating/hydrating.
if (oldVNode._flags & MODE_SUSPENDED) {
isHydrating = !!(oldVNode._flags & MODE_HYDRATE);
oldDom = newVNode._dom = oldVNode._dom;
excessDomChildren = [oldDom];
}
if ((tmp = options._diff)) tmp(newVNode);
//这段都是为了创建component
outer: if (typeof newType == 'function') {
// 处理类&函数组件
try {
let c, isNew, oldProps, oldState, snapshot, clearProcessingException;
let newProps = newVNode.props;
// Necessary for createContext api. Setting this property will pass
// the context value as `this.context` just for this component.
// 获取上下文
tmp = newType.contextType;
let provider = tmp && globalContext[tmp._id];
let componentContext = tmp
? provider
? provider.props.value
: tmp._defaultValue
: globalContext;
// 存在_component时复用组件实例_component给newVnode
if (oldVNode._component) {
// 重用组件实例可以避免进行不必要的创建和销毁组件实例的操作,进而提高性能
c = newVNode._component = oldVNode._component;
// 代码用于处理组件中的异常。当组件在渲染或者处理生命周期事件时发生错误,引发异常状态时,这些异常并不能立即被抛出,反而会被暂时存储起来
clearProcessingException = c._processingException = c._pendingError;
} else {
// 实例化新组件, 判断组件内是否有render函数
if ('prototype' in newType && newType.prototype.render) {
// @ts-expect-error The check above verifies that newType is suppose to be constructed
newVNode._component = c = new newType(newProps, componentContext); // eslint-disable-line new-cap
} else {
// @ts-expect-error Trust me, Component implements the interface we want
newVNode._component = c = new BaseComponent(
newProps,
componentContext
);
c.constructor = newType;
c.render = doRender;
}
if (provider) provider.sub(c);
c.props = newProps;
if (!c.state) c.state = {};
c.context = componentContext;
c._globalContext = globalContext;
isNew = c._dirty = true;
c._renderCallbacks = [];
c._stateCallbacks = [];
}
// Invoke getDerivedStateFromProps
if (c._nextState == null) {
c._nextState = c.state;
}
if (newType.getDerivedStateFromProps != null) {
if (c._nextState == c.state) {
c._nextState = assign({}, c._nextState);
}
assign(
c._nextState,
newType.getDerivedStateFromProps(newProps, c._nextState)
);
}
oldProps = c.props;
oldState = c.state;
c._vnode = newVNode;
// 判断是否是一个全新的组件 调用预渲染生命周期方法
if (isNew) {
if (
newType.getDerivedStateFromProps == null &&
c.componentWillMount != null
) {
c.componentWillMount();
}
if (c.componentDidMount != null) {
c._renderCallbacks.push(c.componentDidMount);
}
} else {
if (
newType.getDerivedStateFromProps == null &&
newProps !== oldProps &&
c.componentWillReceiveProps != null
) {
c.componentWillReceiveProps(newProps, componentContext);
}
if (
!c._force &&
((c.shouldComponentUpdate != null &&
c.shouldComponentUpdate(
newProps,
c._nextState,
componentContext
) === false) ||
newVNode._original === oldVNode._original)
) {
if (newVNode._original !== oldVNode._original) {
c.props = newProps;
c.state = c._nextState;
c._dirty = false;
}
newVNode._dom = oldVNode._dom;
newVNode._children = oldVNode._children;
newVNode._children.forEach(vnode => {
if (vnode) vnode._parent = newVNode;
});
for (let i = 0; i < c._stateCallbacks.length; i++) {
c._renderCallbacks.push(c._stateCallbacks[i]);
}
c._stateCallbacks = [];
if (c._renderCallbacks.length) {
commitQueue.push(c);
}
break outer;
}
if (c.componentWillUpdate != null) {
c.componentWillUpdate(newProps, c._nextState, componentContext);
}
if (c.componentDidUpdate != null) {
c._renderCallbacks.push(() => {
c.componentDidUpdate(oldProps, oldState, snapshot);
});
}
}
c.context = componentContext;
c.props = newProps;
c._parentDom = parentDom;
c._force = false;
let renderHook = options._render,
count = 0;
if ('prototype' in newType && newType.prototype.render) {
c.state = c._nextState;
c._dirty = false;
if (renderHook) renderHook(newVNode);
tmp = c.render(c.props, c.state, c.context);
for (let i = 0; i < c._stateCallbacks.length; i++) {
// commit后回调事件入队
c._renderCallbacks.push(c._stateCallbacks[i]);
}
c._stateCallbacks = [];
} else {
do {
c._dirty = false;
if (renderHook) renderHook(newVNode);
tmp = c.render(c.props, c.state, c.context);
// Handle setState called in render, see #2553
c.state = c._nextState;
} while (c._dirty && ++count < 25);
}
// Handle setState called in render, see #2553
c.state = c._nextState;
if (c.getChildContext != null) {
globalContext = assign(assign({}, globalContext), c.getChildContext());
}
if (!isNew && c.getSnapshotBeforeUpdate != null) {
snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
}
let isTopLevelFragment =
tmp != null && tmp.type === Fragment && tmp.key == null;
let renderResult = isTopLevelFragment ? tmp.props.children : tmp;
// 循环调用更新子组件
diffChildren(
parentDom,
isArray(renderResult) ? renderResult : [renderResult],
newVNode,
oldVNode,
globalContext,
isSvg,
excessDomChildren,
commitQueue,
oldDom,
isHydrating,
refQueue
);
c.base = newVNode._dom;
// We successfully rendered this VNode, unset any stored hydration/bailout state:
newVNode._flags &= RESET_MODE;
if (c._renderCallbacks.length) {
commitQueue.push(c);
}
if (clearProcessingException) {
c._pendingError = c._processingException = null;
}
} catch (e) {
newVNode._original = null;
// if hydrating or creating initial tree, bailout preserves DOM:
if (isHydrating || excessDomChildren != null) {
newVNode._dom = oldDom;
newVNode._flags |= isHydrating
? MODE_HYDRATE | MODE_SUSPENDED
: MODE_HYDRATE;
excessDomChildren[excessDomChildren.indexOf(oldDom)] = null;
// ^ could possibly be simplified to:
// excessDomChildren.length = 0;
} else {
newVNode._dom = oldVNode._dom;
newVNode._children = oldVNode._children;
}
options._catchError(e, newVNode, oldVNode);
}
} else if (
excessDomChildren == null &&
newVNode._original === oldVNode._original
) {
newVNode._children = oldVNode._children;
newVNode._dom = oldVNode._dom;
} else {
// 实际更新dom的地方
newVNode._dom = diffElementNodes(
oldVNode._dom,
newVNode,
oldVNode,
globalContext,
isSvg,
excessDomChildren,
commitQueue,
isHydrating,
refQueue
);
}
if ((tmp = options.diffed)) tmp(newVNode);
}
/**
* Diff the children of a virtual node
* diff虚拟节点的子节点
*/
export function diffChildren(
parentDom,
renderResult,
newParentVNode,
oldParentVNode,
globalContext,
isSvg,
excessDomChildren,
commitQueue,
oldDom,
isHydrating,
refQueue
) {
let i,oldVNode,childVNode,newDom,firstChildDom;
let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR;
let newChildrenLength = renderResult.length;
newParentVNode._nextDom = oldDom;
constructNewChildrenArray(newParentVNode, renderResult, oldChildren);
oldDom = newParentVNode._nextDom;
// 循环更新子组件
for (i = 0; i < newChildrenLength; i++) {
childVNode = newParentVNode._children[i];
if (
childVNode == null ||
typeof childVNode == 'boolean' ||
typeof childVNode == 'function'
) {
continue;
}
if (childVNode._index === -1) {
oldVNode = EMPTY_OBJ;
} else {
oldVNode = oldChildren[childVNode._index] || EMPTY_OBJ;
}
// Update childVNode._index to its final index
childVNode._index = i;
// 更新当前组件的vnode
parentDom,
childVNode,
oldVNode,
globalContext,
isSvg,
excessDomChildren,
commitQueue,
oldDom,
isHydrating,
refQueue
);
// Adjust DOM nodes
newDom = childVNode._dom;
if (childVNode.ref && oldVNode.ref != childVNode.ref) {
if (oldVNode.ref) {
applyRef(oldVNode.ref, null, childVNode);
}
refQueue.push(
childVNode.ref,
childVNode._component || newDom,
childVNode
);
}
if (firstChildDom == null && newDom != null) {
firstChildDom = newDom;
}
if (
childVNode._flags & INSERT_VNODE ||
oldVNode._children === childVNode._children
) {
oldDom = insert(childVNode, oldDom, parentDom);
} else if (
typeof childVNode.type == 'function' &&
childVNode._nextDom !== undefined
) {
// 由于返回 Fragment 或类似 VNode 的组件可能包含多个相同级别的 DOM 节点,因此从此子 VNode 的最后一个 DOM 子节点的兄弟节点继续进行差异比较。
oldDom = childVNode._nextDom;
} else if (newDom) {
oldDom = newDom.nextSibling;
}
childVNode._nextDom = undefined;
// Unset diffing flags
childVNode._flags &= ~(INSERT_VNODE | MATCHED);
}
newParentVNode._nextDom = oldDom;
newParentVNode._dom = firstChildDom;
}
函数步骤
-
diff新旧VNode更新NewVNode
-
执行各个生命周期hooks
-
循环更新子组件的VNode
-
最终都会走到diffElementNodes** ,进行实际dom的更新
DiffElementNodes
TypeScript
// src/diff/index.js
function diffElementNodes(
dom,
newVNode,
oldVNode,
globalContext,
isSvg,
excessDomChildren,
commitQueue,
isHydrating,
refQueue
) {
let oldProps = oldVNode.props;
let newProps = newVNode.props;
let nodeType = /** @type {string} */ (newVNode.type);
/** @type {any} */
let i;
/** @type {{ __html?: string }} */
let newHtml;
/** @type {{ __html?: string }} */
let oldHtml;
/** @type {ComponentChildren} */
let newChildren;
let value;
let inputValue;
let checked;
// 在遍历树时,跟踪进入和退出SVG命名空间的路径
if (nodeType === 'svg') isSvg = true;
// excessDomChildren:多余的 DOM 子节点的列表。
if (excessDomChildren != null) {
for (i = 0; i < excessDomChildren.length; i++) {
value = excessDomChildren[i];
// 如果newVNode与excessDomChildren中的元素匹配,或者dom参数与excessDomChildren中的元素匹配,
// 则从excessDomChildren中删除它,以便在diffChildren中稍后不会被删除。
if (
value &&
'setAttribute' in value === !!nodeType &&
(nodeType ? value.localName === nodeType : value.nodeType === 3)
) {
dom = value;
excessDomChildren[i] = null;
break;
}
}
}
// dom 是 null,那就意味着并没有一个已存在的元素或节点可以被修改或更新,
// 直接创建一个新的元素或节点
if (dom == null) {
if (nodeType === null) {
return document.createTextNode(newProps);
}
if (isSvg) {
dom = document.createElementNS('http://www.w3.org/2000/svg', nodeType);
} else {
dom = document.createElement(nodeType, newProps.is && newProps);
}
excessDomChildren = null;
// 我们正在创建一个新节点,因此我们可以假设这是一个新的子树(在我们进行水合作用的情况下),这会使水合作用失效。
isHydrating = false;
}
if (nodeType === null) {
// During hydration, we still have to split merged text from SSR'd HTML.
if (oldProps !== newProps && (!isHydrating || dom.data !== newProps)) {
dom.data = newProps;
}
} else {
// 如果 excessDomChildren 不为 null,则使用当前元素的子元素重新填充它:
excessDomChildren = excessDomChildren && slice.call(dom.childNodes);
oldProps = oldVNode.props || EMPTY_OBJ;
if (!isHydrating && excessDomChildren != null) {
oldProps = {};
for (i = 0; i < dom.attributes.length; i++) {
value = dom.attributes[i];
oldProps[value.name] = value.value;
}
}
// 更新dom属性,在这会更新DOM
for (i in oldProps) {
value = oldProps[i];
if (i == 'children') {
} else if (i == 'dangerouslySetInnerHTML') {
oldHtml = value;
} else if (i !== 'key' && !(i in newProps)) {
setProperty(dom, i, null, value, isSvg);
}
}
// During hydration, props are not diffed at all (including dangerouslySetInnerHTML)
// @TODO we should warn in debug mode when props don't match here.
for (i in newProps) {
value = newProps[i];
if (i == 'children') {
newChildren = value;
} else if (i == 'dangerouslySetInnerHTML') {
newHtml = value;
} else if (i == 'value') {
inputValue = value;
} else if (i == 'checked') {
checked = value;
} else if (
i !== 'key' &&
(!isHydrating || typeof value == 'function') &&
oldProps[i] !== value
) {
setProperty(dom, i, value, oldProps[i], isSvg);
}
}
// If the new vnode didn't have dangerouslySetInnerHTML, diff its children
if (newHtml) {
// Avoid re-applying the same '__html' if it did not changed between re-render
if (
!isHydrating &&
(!oldHtml ||
(newHtml.__html !== oldHtml.__html &&
newHtml.__html !== dom.innerHTML))
) {
dom.innerHTML = newHtml.__html;
}
newVNode._children = [];
} else {
if (oldHtml) dom.innerHTML = '';
// 循环调用子组件diff
diffChildren(
dom,
isArray(newChildren) ? newChildren : [newChildren],
newVNode,
oldVNode,
globalContext,
isSvg && nodeType !== 'foreignObject',
excessDomChildren,
commitQueue,
excessDomChildren
? excessDomChildren[0]
: oldVNode._children && getDomSibling(oldVNode, 0),
isHydrating,
refQueue
);
// Remove children that are not part of any vnode.
if (excessDomChildren != null) {
for (i = excessDomChildren.length; i--; ) {
if (excessDomChildren[i] != null) removeNode(excessDomChildren[i]);
}
}
}
// As above, don't diff props during hydration
if (!isHydrating) {
i = 'value';
if (
inputValue !== undefined &&
(inputValue !== dom[i] ||
(nodeType === 'progress' && !inputValue) ||
(nodeType === 'option' && inputValue !== oldProps[i]))
) {
setProperty(dom, i, inputValue, oldProps[i], false);
}
i = 'checked';
if (checked !== undefined && checked !== dom[i]) {
setProperty(dom, i, checked, oldProps[i], false);
}
}
}
return dom;
}
函数的主要流程如下:
-
函数首先检查是否有多余的DOM节点(excessDomChildren),如果存在并且与新的虚拟DOM节点匹配,那么就会把它们从多余的节点列表中移除,并用它们来代替需要创建的新节点。
-
如果没有找到可复用的DOM节点,函数会根据新的节点类型(nodeType)创建一个新的DOM节点。
-
函数接下来会比对新旧节点的属性,找出需要增加、删除或修改的属性,实际的操作DOM
-
如果新的节点有dangerouslySetInnerHTML属性,函数会直接设置这个新的HTML,否则,函数会对比新旧节点的子节点,并对子节点进行递归的diff操作,根据新旧子节点的差异进行相应的更新。
-
函数最后会处理特殊的value和checked属性,因为这两个属性在某些元素上的行为和其他属性不同。
OK以上我们就知道了整个component 创建 -> Bable转译 -> 解析 -> 创建VNode -> diff(渲染 diff & 新建更新销毁 DOM) -> commit 整体流程
整体流程图

总结
至此我们了解了Preact组件的构成以及是怎么解析渲染成真的DOM,知道了首次渲染、重渲染的执行逻辑,大致知道了Diff的设计,Preact的核心也大致基本理解了,收工~