likes
comments
collection
share

Preact 核心源码逐行解析

作者站长头像
站长
· 阅读数 2

了解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。

组成结构

  1. 状态(State):组件的当前数据状态。这通常是控制组件渲染的关键数据。例如,一个计数器组件可能有一个名为count的状态,用来保存当前的计数值。

  2. 属性(Props):从父组件传递下来的数据。属性在组件的整个生命周期内都是不可变的,任何对属性的修改都会被忽略。

  3. 生命周期方法(Lifecycle Methods):一些特殊的方法,组件在其生命周期的某些阶段会自动调用这些方法。例如,componentDidMount方法会在组件第一次被渲染到DOM后被调用。

  4. 事件处理方法(Event Handlers):用来响应用户的交互,如点击、滑动等。这些方法通常会改变组件的状态和/或通知父组件。

  5. 渲染方法(Render Method):每一个Preact组件都需要一个render方法,这个方法返回组件当前的DOM结构。渲染方法应该是一个纯函数,仅依赖于this.props和this.state。

下面是一个简单的Preact组件的例子,Preact也是基于jsx来作为描述组件结构的载体。jsx 是 ECMAScript 的类似 XML 的语法扩展,用于承载Preact组件的结构和内容,并且通过类似HTML的语法提供了一种直观和声明式的方式来创建虚拟DOM元素。并且在编译过程中,JSX元素会被编译为常规JavaScript函数调用和对象。


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在其中担任的作用是

  1. 将JSX转换为JavaScript:最重要的作用是将JSX语法转为Preact可以理解的普通JavaScript代码。在Preact中,JSX经常被转换成 h 函数(hyperscript)调用来创建虚拟DOM元素。

  2. 编译新的JavaScript特性:Babel还能将ECMAScript 2015+(ES6+)的语法转换成当前和较老版本的浏览器都能理解的ES5代码。这意味着开发者可以在项目中使用最新的JavaScript语言特性,而无需担心兼容性问题。

  3. 插件和预设:Babel可以利用插件(plugins)和预设(presets)来扩展其功能。比如一下

    预设(presets)

    1. @babel/preset-env:

    2. @babel/preset-react 或 @babel/preset-preact:

    插件 (Plugins)

    1. @babel/plugin-transform-arrow-functions:

    2. @babel/plugin-proposal-class-properties:

    3. @babel/plugin-transform-react-jsx:

  4. 优化代码:Babel不仅可以转换代码,还可以利用插件来优化代码,减少冗余,提升性能。

转译后的代码

"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

/**
 * 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获取),例如下面这个例子

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的实现。

import {render } from 'preact';
...
// 渲染MyComponent到id为root的Div
preact_1.render(preact_1.h(MyComponent, { name: "world" }), document.getElementById('app'));

源码解析

所以我们重点看h和render

H(createElement)

Preact 核心源码逐行解析

所以H函数本质就是createElement

// 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;
}

步骤解析

  1. 进行props的标准化后调用createVNode,创建VNode

  2. 在createVNode中基于component创建VNode,把component存入到vnode.type里

  3. 返回VNode

VNode

这里我们来解析一下VNode的结构,直接看VNode的ts结构

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;
  }
  1. type: 这个属性可以是一个字符串,表示 HTML 标签的名称,比如 'div'、'span'等。也可以是一个函数组件或类组件,表示用户自定义的组件。

  2. props: 这是一个对象,包含了所有传递给组件的props(包括children)。如果 type 是一个 HTML 标签,props也可能包含了一些处理事件的函数(如 onClick)或者 DOM 属性(如 value、checked等)。

  3. ref: 这是一个引用,可以用来访问实际的 DOM 节点或者组件实例,如果有的话。

  4. _children: 这是一个数组,包含了这个 VNode 所有的子 VNodes。如果没有子 VNode,这个属性为 null。

  5. _parent: 这是这个 VNode 的父 VNode。如果没有父 VNode,这个属性为 null。

  6. _depth: 这个数字表示 VNode 在 VNode 树中的深度。用于排序和确定渲染优先级。

  7. _dom: 这是这个 VNode 对应的真实 DOM 元素。如果这个 VNode 还没有被渲染到真实 DOM,这个属性为 null。

  8. _nextDom: 这是Fragment组件或者返回Fragment的组件的最后一个DOM子节点。

  9. _component: 如果这个 VNode 是一个组件,这个属性就是这个组件的实例。

  10. constructor: 这个属性应纯背景是 undefined。可能是为了防止 VNode 被当作 JavaScript 的普通对象来使用。

  11. _original: 这个属性存储了 VNode 被创建时的原始版本。

  12. _index: 这个属性保存了这个 vnode 在其父 vnode 的 _children 数组中的索引位置。

  13. _flags: 这是一个数字,表示了这个 vnode 的一些元信息,比如它是不是一个组件,是否具有 key 等等。

在Preact中_开头的变量名都是内部变量。

我们可以通过Preact Developer Tools 开启tools点击组件的debug就会在控制台打印查看上面我们preact 组件的VNode

Preact 核心源码逐行解析

Preact 核心源码逐行解析

Preact 核心源码逐行解析

那么为什么需要将DOM转化为VNode呢。

原因是操作真实DOM非常消耗资源,而操作JavaScript对象则相对更快。VNode让我们能在JS层进行DOM操作,之后再通过高效的Diff算法将变化应用到真实DOM,从而提升性,可以说虚拟DOM是真实DOM的抽象表达,它可以帮助我们在不直接操作DOM的情况下描述DOM应该是什么样子。VNode也就是这个虚拟DOM的每一个节点。

主要作用如下:

  1. 提升性能:操作真实DOM非常消耗资源,而操作JavaScript对象则相对更快。VNode让我们能在JS层进行DOM操作,之后再通过高效的Diff算法将变化应用到真实DOM,从而提升性能。

  2. 跨平台:因为VNode是JavaScript对象,它可以在不同环境下(web, node, native mobile)运行。这让例如React Native这样的跨平台解决方案得以实现。

  3. 简化API: VNode为我们提供了一种简单,直观的方式来描述我们希望的DOM结构。

  4. 为组件化打基础:在Preact等库中,组件其实就返回一颗VNode树,每个组件都可以看作是一个返回VNode的函数。这也是我们能通过组合不同组件构建复杂应用的基础。

总的来说,VNode是让preact能以更简单、更声明式、更性能的方式来操作DOM,是我们通过preact搭建页面的基石。

Render

我们已经了解到了Preact会通过H(createElement)创建组件的VNode,接下来我们来看Render是如何将VNode渲染到页面上的

// 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: 在函数组件中附加一个在渲染之前调用的钩子,主要用于检查参数。

Preact 核心源码逐行解析

  • isHydrating:判断是否为Hydration 模式

步骤

  1. 判断是否是直接渲染已经处理好完整的 HTML

  2. 基于vnode创建 element

  3. 通过diff渲染更新dom

  4. 执行commit,里面会执行一些更新后的回调

对于diff和commitRoot的解析我们放到后面diff模块(可以直接手动路由过去看),因为重渲染一样会使用这两个方法,我们先知道他们的作用就行。

  • diff:执行生命周期,对比新旧VNode渲染更新dom

  • commitRoot*:里面会执行一些更新后的回调*

先来看重渲染的逻辑

重渲染(更新)

组件的渲染基本使用两种方式 setState(修改组件状态,进而触发更新)和 forceUpdate(直接重新渲染),所以我们直接看代码实现

源码解析

Component & setState forceUpdate

找到源码位置src/index.js -> src/component.js

/**
 * 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: 这是组件当前的状态对象,在组件重新渲染前,它包含了组件最近一次渲染时使用的状态值。这是开发者通常交互的状态对象,用于读取当前状态。比如
this.state = {
  count: 0
};
  • this._nextState: 用来存下一个(新的)状态的引用,当调用this.setState时,Preact 将调度状态的更新。在状态更新过程中,this._nextState 可能会与 this.state 不同,this._nextState 会持有即将应用于组件的下一个状态值。比如
setCount(count + 1);

this._nextState = {
  count: 1
};
  • _stateCallbacks :它存储了与组件状态更新关联的回调函数

  • _force: 来告诉Preact的渲染器,不管当前的状态或属性与之前是否相同,组件都应该重新渲染

还有很多其他的内部属性当我们遇到的时候再一个个探讨。

setstate和forceUpdate最后会走到*enqueueRender**,*进行状态更新的入队,等待渲染。

EnqueueRender

将需要渲染的组件入栈,等待render的时候执行

/**
 * 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

执行步骤

  1. 修改dirty,将组件入栈到rerenderQueue(需要重新渲染的组件都存在这里面,在commit的时候执行渲染)

  2. 通过 prevDebounce 调用process 重新渲染函数。

Precoss

重新渲染所有排队的组件,清空渲染队列rerenderQueue

/**
 * @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;
}

函数步骤

  1. 将需要渲染的组件按照渲染层次排序

  2. 循环执行组件渲染renderComponent

    1. 若是最后一个组件,则直接执行commit
  3. 提交渲染commitRoot,在 DOM 更新发生之后执行一系列清理工作和回调函数调用(_renderCallbacks)

这些步骤中可以看到两个重要的环节执行渲染和提交渲染。

/**
 * 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 上,并且在这个过程中调用生命周期函数。

大致步骤如下:

  1. 比较元素类型:新旧 VNode 的类型不同,Preact 将删除旧元素(真实 DOM),然后创建并插入新元素。

  2. 比较元素属性:当在类型相同的元素上比较属性时,Preact 会逐个检查新旧属性的差异。如果新的 VNode 有一些属性在旧的 VNode 中不存在,Preact 将添加这些属性到真实的 DOM 上;反过来,如果旧的 VNode 有一些属性在新的 VNode 中不存在,Preact 将从真实 DOM 上删除这些属性。

  3. 比较子元素:Preact 将递归地检查新旧 VNode 的所有子节点。如果有新节点需要添加,它将创建新的 DOM 节点并插入到正确的位置;如果有旧节点需要删除,它将删除相应的 DOM 节点。

  4. 组件状态与生命周期:当处理组件时,Preact 可能会调用各类组件生命周期方法,比如 componentDidMount,componentDidUpdate等。这些方法可能导致 DOM 的更新。此外,如果组件的状态(state)发生变化,Preact 将重新渲染组件并更新相应的真实DOM节点。

源码解析

Diff

// 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;
}

函数步骤

  1. diff新旧VNode更新NewVNode

  2. 执行各个生命周期hooks

  3. 循环更新子组件的VNode

  4. 最终都会走到diffElementNodes** ,进行实际dom的更新

DiffElementNodes

// 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;
}

函数的主要流程如下:

  1. 函数首先检查是否有多余的DOM节点(excessDomChildren),如果存在并且与新的虚拟DOM节点匹配,那么就会把它们从多余的节点列表中移除,并用它们来代替需要创建的新节点。

  2. 如果没有找到可复用的DOM节点,函数会根据新的节点类型(nodeType)创建一个新的DOM节点。

  3. 函数接下来会比对新旧节点的属性,找出需要增加、删除或修改的属性,实际的操作DOM

  4. 如果新的节点有dangerouslySetInnerHTML属性,函数会直接设置这个新的HTML,否则,函数会对比新旧节点的子节点,并对子节点进行递归的diff操作,根据新旧子节点的差异进行相应的更新。

  5. 函数最后会处理特殊的value和checked属性,因为这两个属性在某些元素上的行为和其他属性不同。

OK以上我们就知道了整个component 创建 -> Bable转译 -> 解析 -> 创建VNode -> diff(渲染 diff & 新建更新销毁 DOM) -> commit 整体流程

整体流程图

总结

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