Skip to content

第6章:元素生命周期与渲染

核心问题:一个元素从创建到销毁经历了哪些阶段?

在无限画布中,每个可见的图形元素(图片、文字、形状等)都由对应的 ViewModel 管理其渲染。本章将深入探讨元素的完整生命周期,从数据模型创建到最终渲染销毁的每个阶段,以及各类具体元素的实现细节。


目录


6.1 生命周期概述

6.1.1 双重生命周期体系

无限画布中的元素具有双重生命周期

  1. 数据模型生命周期(Model Lifecycle):由业务层驱动,响应用户操作
  2. 视图模型生命周期(ViewModel Lifecycle):由渲染层驱动,响应状态变化
┌─────────────────────────────────────────────────────────────────┐
│                    数据模型生命周期                               │
│  ┌───────────┐  ┌───────────┐  ┌───────────┐  ┌───────────┐    │
│  │modelCreated│→│modelUpdated│→│ modelMoved │→│modelRemoved│    │
│  └─────┬─────┘  └─────┬─────┘  └─────┬─────┘  └─────┬─────┘    │
│        │              │              │              │           │
│        ▼              ▼              ▼              ▼           │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                    VmEngine                              │   │
│  │              (触发器 / 协调者)                            │   │
│  └─────────────────────────────────────────────────────────┘   │
│        │              │              │              │           │
│        ▼              ▼              ▼              ▼           │
│  ┌───────────┐  ┌───────────┐  ┌───────────┐  ┌───────────┐    │
│  │ shouldUpdate│→│beforeUpdate│→│   render   │→│afterUpdate │    │
│  └───────────┘  └───────────┘  └───────────┘  └───────────┘    │
│                    视图模型生命周期                               │
└─────────────────────────────────────────────────────────────────┘

设计意图

  • 解耦数据与渲染:数据变化通知 ViewModel,但不直接控制渲染细节
  • 优化渲染性能:ViewModel 可以通过 shouldUpdate 跳过不必要的渲染
  • 异步资源处理:数据模型生命周期可以触发异步资源加载

6.1.2 生命周期时序图

以创建一个图片元素为例,完整的生命周期时序:

用户操作          VmEngine              ImageVm              PixiJS
   │                 │                    │                    │
   │ 添加图片元素     │                    │                    │
   │────────────────►│                    │                    │
   │                 │                    │                    │
   │                 │ new ImageVm()      │                    │
   │                 │───────────────────►│                    │
   │                 │                    │ constructor()      │
   │                 │                    │ onConstruct()      │
   │                 │                    │                    │
   │                 │ element.context    │                    │
   │                 │───────────────────►│                    │
   │                 │                    │                    │
   │                 │ element.setModel() │                    │
   │                 │───────────────────►│                    │
   │                 │                    │                    │
   │                 │ modelCreated()     │                    │
   │                 │───────────────────►│                    │
   │                 │                    │ updateTransform()  │
   │                 │                    │ setState()         │
   │                 │                    │   │                │
   │                 │                    │   │ shouldUpdate() │
   │                 │                    │   │ beforeUpdate() │
   │                 │                    │   │ render()───────┼────► 创建 Sprite
   │                 │                    │   │ afterUpdate()  │
   │                 │                    │                    │
   │                 │ parent.addChild()  │                    │
   │                 │───────────────────►│                    │
   │                 │                    │ 添加到渲染树        │
   │                 │                    │────────────────────┼────► Container.addChild()
   │                 │                    │                    │
   │                 │                    │ 异步加载图片        │
   │                 │                    │····················│····► fetch image
   │                 │                    │                    │
   │                 │                    │ complete()         │
   │                 │                    │───────────────────►│ 渲染完成

6.2 数据模型生命周期

数据模型生命周期由 IBaseElementLifecycle 接口定义:

代码位置infinite-renderer/src/types/element-vm.ts

typescript
/**
 * 数据模型生命周期
 */
export interface IBaseElementLifecycle<P extends BaseElementModel | PageModel = BaseElementModel> {
    /**
     * 模型初始化
     * @param model 数据模型
     * @param context 全局上下文
     */
    modelCreated?(model: P, context: IContext): void;

    /**
     * 模型数据更新
     * @param model 数据模型
     * @param context 全局上下文
     */
    modelUpdated?(model: P, context: IContext): void;

    /**
     * 模型被移动
     * @param model 数据模型
     * @param parent 新的父元素
     * @param index 在父容器中的位置
     * @param context 全局上下文
     */
    modelMoved?(model: P, parent: ParentModel, index: number, context: IContext): void;

    /**
     * 模型被移除
     * @param model 数据模型
     * @param context 全局上下文
     */
    modelRemoved?(model: P, context: IContext): void;
}

6.2.1 modelCreated - 模型创建

触发时机VmEngine.buildVmVmEngine.createVm 创建完 ViewModel 后调用。

职责

  1. 初始化元素的变换属性(位置、旋转、缩放)
  2. 设置元素的渲染状态
  3. 注册必要的资源

典型实现(以 ImageVm 为例):

typescript
// ImageVm.modelCreated
modelCreated(model: ImageModel): void {
    // 1. 更新状态(会触发 setState → render)
    this.updateState(model);
    
    // 2. 设置视图名称(便于调试)
    this.view.name = model.type;
}

// updateState 方法
protected updateState(model: ImageModel) {
    // 更新变换矩阵
    this.updateTransform(model);
    
    // 更新水印图层
    this.updateWatermark(model);
    
    // 更新蒙版信息
    this.updateMaskInfo(model);
    
    // 更新 AI 水印
    this.updateAiWatermark(model);
    
    // 兼容拖拽场景
    if (model.$readyForDrop) {
        const alpha = model.$insideDropArea ? 0 : 0.3;
        this.view.alpha *= alpha;
    }

    // 检测是否为动图
    const isAnimated = checkIsAnimatedImage(model);

    // 设置渲染状态
    this.setState({
        uuid: model.uuid,
        url: model.url,
        width: model.width,
        height: model.height,
        naturalWidth: model.naturalWidth,
        naturalHeight: model.naturalHeight,
        mosaic: simplify(model.mosaic),
        resourceType: model.resourceType,
        isAnimated,
        filter: simplify(model.filter),
        // ... 更多属性
    });

    // 更新碰撞区域
    this.hitArea.width = model.width;
    this.hitArea.height = model.height;
}

关键点

  • modelCreated 通常只调用一次,在元素首次创建时
  • 应该完成所有初始化工作,包括设置初始渲染状态
  • 可以触发异步资源加载

6.2.2 modelUpdated - 模型更新

触发时机Processor.processUpdateElements 处理更新 Action 时调用。

职责

  1. 响应数据变化,更新渲染状态
  2. 重新计算变换矩阵
  3. 触发必要的重新渲染

典型实现

typescript
// ImageVm.modelUpdated
modelUpdated(model: ImageModel): void {
    // 复用 updateState 方法
    this.updateState(model);
}

// GroupVm.modelUpdated
modelUpdated(model: GroupElementModel): void {
    this.updateTransform(model);
    this.updateWatermark(model);
    this.updateMaskInfo(model);
}

// ShapeVm.modelUpdated
modelUpdated(model: ShapeElementModel): void {
    this.updateState(model);
}

设计模式

  • 大多数元素的 modelCreatedmodelUpdated 实现相似
  • 通过提取公共的 updateState 方法避免代码重复
  • 状态驱动:只需更新状态,渲染逻辑由 render 方法处理

6.2.3 modelMoved - 模型移动

触发时机:元素在元素树中移动位置时调用。

场景示例

  • 将元素从一个组移动到另一个组
  • 调整元素在层级中的顺序
  • 拖拽元素到新的父容器

实现示例

typescript
// Processor.processMoveElements
processMoveElements(action: MoveElements) {
    const { index, parent, elements } = action;

    const parentVm = this.vmEngine.getVm(parent);

    for (const [i, element] of elements.entries()) {
        const elementVm = this.vmEngine.getVm(element);

        let idx = 0;

        // 添加到新位置
        if (typeof index === 'number') {
            parentVm.addChildAt(elementVm, index + i);
            idx = index + i;
        } else {
            parentVm.addChild(elementVm);
            idx = parentVm.children.length - 1;
        }

        // 调用移动生命周期
        if (typeof elementVm.modelMoved === 'function') {
            elementVm.modelMoved(element, parent, idx, this.context);
        }
    }
}

注意

  • 移动操作会自动从原父容器中移除元素
  • modelMoved 可以用于更新依赖父容器的状态

6.2.4 modelRemoved - 模型移除

触发时机VmEngine.removeVm 从缓存中移除 ViewModel 前调用。

职责

  1. 清理元素持有的资源
  2. 取消进行中的异步操作
  3. 移除事件监听

实现示例

typescript
// VmEngine.removeElement
private removeElement<T extends BaseElementModel>(
    model: T,
    options?: IRemoveOptions | boolean,
): IBaseElementVm<T> | null {
    const element = this.getVm(model);

    if (!element) {
        return null;
    }

    // ... 处理子元素

    // 调用生命周期
    if (typeof element.modelRemoved === 'function') {
        element.modelRemoved(model, this.context);
    }

    // 从缓存中删除
    this.elementMap.delete(model.uuid);

    // 销毁元素
    if (destroy) {
        element.destroy({
            texture: true,
            baseTexture: true,
            children: removeChildren,
        });
    }

    return element;
}

// ImageVm.destroy
destroy(options?: boolean | IDestroyOptions): void {
    // 销毁子视图模型
    this.maskVm?.destroy(options);
    this.dynamicImage?.destroy(options);
    this.aiWatermark?.destroy(options);
    
    // 调用父类销毁
    super.destroy(options);
}

6.3 视图模型生命周期

视图模型生命周期由 IBaseVmLifecycle 接口定义,控制渲染行为:

代码位置infinite-renderer/src/types/vm.ts

typescript
export interface IBaseVmLifecycle<T> {
    /**
     * 捕获错误
     */
    catch?(error: any): void;

    /**
     * 是否需要更新(优化用)
     */
    shouldUpdate?(nextState: T): boolean;

    /**
     * Render 之前
     */
    beforeUpdate?(nextState: T): void;

    /**
     * Render 之后
     */
    afterUpdate?(prevState: T): void;

    /**
     * 渲染方法
     */
    render?(prevState: T, nextState: T): void;
}

6.3.1 shouldUpdate - 更新判断

作用:判断是否需要执行渲染,返回 false 可跳过本次渲染。

典型场景

  • 状态值未发生实际变化
  • 某些条件下不需要渲染

实现示例

typescript
// ShapeVm.shouldUpdate
shouldUpdate(nextState: ShapeState): boolean {
    // 使用深比较判断状态是否变化
    return !fastEqual(this._state, nextState);
}

性能优化原理

typescript
// 假设连续三次 setState
element.setState({ width: 100 });  // 状态变化,需要渲染
element.setState({ width: 100 });  // 状态相同,跳过渲染
element.setState({ width: 100 });  // 状态相同,跳过渲染

// shouldUpdate 返回 false 时,render 不会被调用

6.3.2 beforeUpdate - 更新前钩子

作用:在状态更新和渲染之前执行,可用于准备工作。

典型场景

  • 缓存旧状态用于比较
  • 准备渲染所需的数据

实现示例

typescript
beforeUpdate(nextState: TextState): void {
    // 记录上一次的文本状态,用于连接线计算
    this._prevStatus = {
        width: this._state.width,
        height: this._state.height,
        writingMode: this._state.element?.writingMode,
    };
}

6.3.3 render - 渲染方法

作用:执行实际的渲染逻辑,将状态转换为可视图形。

设计原则

  1. 纯渲染:只负责根据状态生成图形,不处理业务逻辑
  2. 幂等性:相同状态应该产生相同的渲染结果
  3. 增量更新:尽量复用已有的渲染对象

典型实现(ShapeVm):

typescript
// ShapeVm.render
render(): void {
    const {
        shapeType,
        width,
        height,
        fill,
        radius,
        stroke,
        strokeWidth,
        strokeAlignment,
        strokeStyle,
        dashArray,
    } = this._state;

    // 清空画布
    this.graphics.clear();

    // 绘制形状
    if (shapeType) {
        drawShape(this.graphics, {
            shapeType,
            width,
            height,
            fill,
            radius,
            stroke,
            strokeWidth,
            strokeAlignment,
            strokeStyle,
            dashArray,
        });
    } else {
        // 绘制占位符
        drawNever(this.graphics, {
            width,
            height,
        });
    }
}

典型实现(ImageVm):

typescript
// ImageVm.render
render(prevState: ImageState, nextState: ImageState): void {
    const {
        uuid,
        url,
        mask,
        width,
        height,
        naturalWidth,
        naturalHeight,
        mosaic,
        resourceType,
        hasEffects,
        hasFilters,
        imageTransform,
        // ... 更多属性
    } = nextState;

    // 兼容异常数据
    if (!(width > 0 && height > 0)) {
        this.complete();
        return;
    }

    // 判断是否使用特效图渲染
    const isEffected = hasEffects || hasFilters;

    if (
        !isAnimated &&
        isEffected &&
        !this.isDissimilarResizing &&
        url
    ) {
        // 使用 DynamicImage 渲染特效图
        const image = this.useDynamicImage();
        image.update(this._model);
        image.resize(width, height);
    } else {
        // 使用 MaskVm 渲染普通图片
        const maskVm = this.useMaskVm();
        maskVm.setState({
            uuid,
            url,
            mask,
            width,
            height,
            naturalWidth,
            naturalHeight,
            // ... 更多属性
        });
    }
}

渲染策略

  • 简单元素(如 ShapeVm):直接在 Graphics 上绘制
  • 复杂元素(如 ImageVm):委托给专门的子视图模型处理

6.3.4 afterUpdate - 更新后钩子

作用:在渲染完成后执行,可用于清理工作或触发后续操作。

典型场景

  • 加载额外资源
  • 触发完成事件
  • 更新缓存

实现示例

typescript
// TextVm.afterUpdate
afterUpdate(prevState: TextRenderState): void {
    const preUrl = prevState.ninePatch?.url;
    const nextUrl = this._state.ninePatch?.url;

    // 九宫格背景图变化时加载新图片
    if (preUrl !== nextUrl && nextUrl) {
        this._ninePatchLoadingPromise = settings.RESOURCE_ADAPTOR.fetchImage(nextUrl).then(
            (result) => {
                // 并发加载情况下取最后一个
                if (nextUrl !== this._state.ninePatch?.url || !result) {
                    return;
                }
                this._image = result;
                // 强制更新以应用新图片
                this.forceUpdate();
            },
        );
    }
}

6.4 BaseVm 状态管理机制

6.4.1 状态驱动渲染

BaseVm 采用类似 React 的状态驱动渲染模式:

typescript
abstract class BaseVm<T extends object = object> {
    // 组件状态
    protected abstract _state: T;

    // 设置状态,触发更新
    setState(state: Partial<T>): void {
        this.batchState = {
            ...this.batchState,
            ...state,
        };
        if (!this._isBatchDisabled) {
            this.batchUpdate();
        } else {
            this.forceUpdate();
        }
    }

    // 获取当前状态
    getState(): T {
        return this._state;
    }
}

状态更新流程

setState({ x: 100 })


合并到 batchState


batchUpdate() ─── 是否禁用批量更新?
    │                     │
    │ 否                  │ 是
    ▼                     ▼
延迟到微任务          forceUpdate()
    │                     │
    ▼                     │
flush() ◄─────────────────┘


shouldUpdate(nextState) ─── 返回 false?
    │                            │
    │ 返回 true                  ▼
    ▼                        跳过渲染
_internal_update(nextState)

    ├── beforeUpdate(nextState)
    ├── this._state = nextState
    ├── render(prevState, nextState)
    └── afterUpdate(prevState)

6.4.2 批量更新优化

问题:如果每次 setState 都立即渲染,多次连续更新会导致性能问题。

解决方案:批量更新,将多次 setState 合并为一次渲染。

typescript
// 合并更新 state
private batchState: Partial<T> | null = null;

// 当前是否正在 batch 更新中
private isBatching = false;

// 当前组件是否需要更新
private isDirty = false;

/**
 * 合并更新组件
 */
batchUpdate() {
    this.isDirty = true;
    if (!this.isBatching) {
        this.isBatching = true;
        // 延迟到微任务队列
        Promise.resolve().then(() => {
            this.isBatching = false;
            // 若 batch 更新时是 dirty 状态,则驱动更新
            if (this.isDirty) {
                this.flush();
            }
        });
    }
}

批量更新示例

typescript
// 三次 setState 只触发一次 render
element.setState({ x: 100 });   // 标记 dirty,启动 batch
element.setState({ y: 200 });   // 合并状态
element.setState({ z: 300 });   // 合并状态

// 微任务执行时,合并后的状态为 { x: 100, y: 200, z: 300 }
// 只调用一次 render

6.4.3 更新流程源码分析

代码位置infinite-renderer/src/vms/base/base-vm.ts

typescript
private _internal_update(nextState: T) {
    // 1. 更新前钩子
    if (typeof this.beforeUpdate === 'function') {
        this.beforeUpdate(nextState);
    }

    // 2. 递增更新 ID(用于追踪更新次数)
    this._updateID++;

    // 3. 更新状态
    const prevState = this._state;
    this._state = nextState;

    // 4. 执行渲染
    if (typeof this.render === 'function') {
        this.render(prevState, nextState);
    }

    // 5. 清理 batch 状态
    this.batchState = null;
    this.isDirty = false;

    // 6. 更新后钩子
    if (typeof this.afterUpdate === 'function') {
        this.afterUpdate(prevState);
    }
}

/**
 * 强制更新组件
 */
forceUpdate() {
    // 已销毁的组件不执行更新
    if (this.destroyed) {
        console.warn('View has been destroyed.');
        return;
    }

    const nextState = {
        ...this._state,
        ...this.batchState,
    };

    try {
        this._internal_update(nextState);
    } catch (error) {
        this.catch(error);
    } finally {
        this.batchState = null;
        this.isDirty = false;
    }
}

flush() {
    // 已销毁的组件不执行更新
    if (this.destroyed) {
        console.warn('View has been destroyed.');
        return;
    }

    try {
        if (this.batchState) {
            const nextState = {
                ...this._state,
                ...this.batchState,
            };

            // 判断是否需要 rerender
            const shouldUpdate = this.shouldUpdate 
                ? this.shouldUpdate(nextState) 
                : true;

            if (shouldUpdate) {
                this._internal_update(nextState);
            }
        }
    } catch (error) {
        this.catch(error);
    } finally {
        this.batchState = null;
        this.isDirty = false;
    }
}

6.5 BaseElementVm 详解

6.5.1 类继承体系

EventEmitter                    ← 事件系统


BaseVm<T>                       ← 状态管理


BaseContainerVm<T, C>           ← 容器管理


BaseElementVm<P, T>             ← 元素基类

    ├── ImageVm<P extends ImageModel>
    ├── TextVm<P extends TextModel>
    ├── ShapeVm<P extends ShapeModel>
    ├── GroupVm<P extends GroupModel>
    └── ... 其他具体元素

各层职责

职责核心能力
EventEmitter事件发布订阅emit, on, off
BaseVm状态管理setState, render, forceUpdate
BaseContainerVm子元素管理addChild, removeChild, 坐标转换
BaseElementVm元素通用功能变换、包围盒、碰撞检测、图层结构

6.5.2 图层结构设计

每个 BaseElementVm 包含三层结构:

代码位置infinite-renderer/src/vms/base/base-element-vm.ts

typescript
constructor() {
    super();
    
    // 内容图层:元素自身的渲染内容(Sprite、Graphics 等)
    this.contentLayer = this.createContentLayer();
    this.contentLayer.name = 'contentLayer';
    
    // 子元素图层:子元素的容器
    this.childrenLayer = this.createChildrenLayer();
    this.childrenLayer.name = 'childrenLayer';
    
    // 水印图层:元素水印
    this.watermarkLayer = new WatermarkVm();
    this.watermarkLayer.emitter.on('complete', () => {
        if (this._isCompleted) {
            this.complete();
        }
    });
    this.watermarkLayer.emitter.on('error', (error) => {
        this.catch(error);
    });
    this.watermarkLayer.view.name = 'watermarkLayer';
    
    // 组装图层顺序(从下到上)
    this.view.addChild(
        this.contentLayer,
        this.childrenLayer,
        this.watermarkLayer.view
    );
    
    this.onConstruct();
}

图层可视化

┌─────────────────────────────────────────┐
│              view (Container)            │
│                                          │
│  ┌─────────────────────────────────────┐ │
│  │      watermarkLayer (最上层)         │ │
│  │      ├── 版权水印                    │ │
│  │      └── AI 标识                     │ │
│  ├─────────────────────────────────────┤ │
│  │      childrenLayer                   │ │
│  │      ├── child1 (IBaseElementVm)    │ │
│  │      ├── child2 (IBaseElementVm)    │ │
│  │      └── ...                         │ │
│  ├─────────────────────────────────────┤ │
│  │      contentLayer (最下层)           │ │
│  │      ├── Sprite (图片)               │ │
│  │      ├── Graphics (形状)             │ │
│  │      └── TextSprite (文字)           │ │
│  └─────────────────────────────────────┘ │
│                                          │
└─────────────────────────────────────────┘

设计意图

  • 层级分离:内容、子元素、水印各自独立
  • 可扩展:子类可以重写 createContentLayer 自定义内容图层
  • 水印覆盖:水印始终在最上层,不会被子元素遮挡

6.5.3 核心属性与方法

核心属性

typescript
abstract class BaseElementVm<P, T> {
    // PixiJS 渲染容器
    view: Container<DisplayObject> = new Container();

    // 全局上下文
    context: IContext | null = null;

    // 事件发射器
    emitter: EventEmitter<BaseElementVmEvents> = new EventEmitter();

    // 父元素引用
    parent: IBaseElementVm | IPageVm | null = null;

    // 自定义变换矩阵(附加在元素变换之上)
    transform: Matrix | null = null;

    // 唯一标识
    protected _key = uniqueId();

    // 数据模型
    protected _model!: P;

    // 渲染完成状态
    protected _isCompleted = true;
}

变换更新方法

typescript
/**
 * 更新元素 Transform 变换数据
 */
updateTransform(model: P = this._model): void {
    // 设置可见性
    this.view.visible = !model.hidden && !model.$hidden;

    // 分解变换矩阵
    decomposeTransform(model, this.transform, this.view.transform);

    // 更新透明度
    this.updateOpacity(model);
}

/**
 * 更新元素透明度
 */
updateOpacity(model: P = this._model): void {
    const { opacity = 1 } = model;
    this.view.alpha = opacity;
}

包围盒计算方法

typescript
// 获取渲染包围盒(世界坐标)
getBounds(skipUpdate?: boolean, newRect?: Rectangle): Rectangle {
    return this.view.getBounds(skipUpdate, newRect);
}

// 获取局部包围盒
getLocalBounds(newRect?: Rectangle): Rectangle {
    return this.view.getLocalBounds(newRect);
}

// 获取有向包围盒多边形(用于旋转后的精确碰撞检测)
getOrientedPolygon(skipUpdate = false, points?: number[]): number[] {
    if (!skipUpdate) {
        this.view._recursivePostUpdateTransform();
        this.view.updateTransform();
    }

    const rect = this.getLocalBounds(RECTANGLE);
    const matrix = this.view.worldTransform;

    const vert = getPoints(rect, tempPoints);
    const poly = transform(matrix, vert, points);

    return poly;
}

// 获取模型包围盒(基于 model 的 width/height)
getModelBounds(skipUpdate?: boolean, newRect?: Rectangle): Rectangle {
    const poly = this.getModelOrientedPolygon(skipUpdate, tempPoints);
    return getBounds(poly, newRect);
}

getModelLocalBounds(newRect = new Rectangle()): Rectangle {
    const model = this._model;

    newRect.x = 0;
    newRect.y = 0;
    newRect.width = model.width;
    newRect.height = model.height;

    return newRect;
}

碰撞检测方法

typescript
/**
 * 检测元素是否包含点
 */
contains(point: IPointData): boolean {
    // 优先检测 mask
    if (this.view.mask) {
        const maskObject = this.getMaskObject();
        if (maskObject instanceof Graphics) {
            return !!maskObject.containsPoint(point);
        }
    }

    // 检测 hitArea
    if (this.view.hitArea) {
        const pos = this.getLocalPoint(point, tempPoint);
        return !!this.view.hitArea.contains(pos.x, pos.y);
    }

    return false;
}

/**
 * 检测元素是否与矩形碰撞(精确检测)
 */
hits(rect: Rectangle): boolean {
    const bounds = this.view.getBounds();

    // 快速排除:包围盒不相交
    if (!bounds.intersects(rect)) {
        return false;
    }

    // 检测 mask
    if (this.view.mask) {
        const maskObject = this.getMaskObject();
        if (maskObject instanceof Graphics) {
            return !!doesGraphicsIntersectRect(maskObject, rect);
        }
    }

    // 检测 hitArea
    if (this.view.hitArea && this.view.hitArea instanceof Rectangle) {
        return doesHitAreaIntersectRect(this.view.hitArea, rect, this.view.worldTransform);
    }

    return false;
}

/**
 * 检测元素是否与矩形相交(基于包围盒)
 */
intersects(rect: Rectangle): boolean {
    const bounds = this.view.getBounds(false, RECTANGLE);

    // 包围盒不相交
    if (!rect.intersects(bounds)) {
        return false;
    }

    // 完全包含
    if (rect.containsRect(bounds)) {
        return true;
    }

    // 精确检测
    // ...
}

6.6 具体元素实现

6.6.1 ImageVm - 图片元素

代码位置infinite-renderer/src/vms/image/image-vm.ts

ImageVm 是最复杂的元素之一,支持:

  • 普通图片渲染
  • 动图播放
  • 图片滤镜/特效
  • 马赛克
  • 蒙版
  • AI 水印

状态定义

typescript
export interface ImageState {
    uuid?: string;
    url?: string;
    width: number;
    height: number;
    naturalWidth: number;
    naturalHeight: number;
    mosaic?: Mosaic | null;
    resourceType?: string;
    isAnimated?: boolean;
    filter?: Filter | null;
    filterInfo?: FilterInfo;
    hasFilters?: boolean;
    imageEffects?: ImageEffect[];
    hasEffects?: boolean;
    shadows?: Shadow[];
    fillOpacity?: number;
    mask?: string;
    imageTransform?: Transform;
    opacity?: number;
    maskPath?: MaskShape;
    // ... 更多
}

装饰器模式

ImageVm 使用多个装饰器扩展功能:

typescript
@HiddenOnEditing()        // 编辑时隐藏
@PrimaryElement()         // 主要元素标识
@PreferImage<ImageVm>({   // 优先使用结果图
    condition(model) {
        // 动图不使用 imageUrl
        if (checkIsAnimatedImage(model)) {
            return false;
        }
        // 有特效时使用 imageUrl
        return model.hasEffects || model.hasFilters;
    },
    onImageLoaded() {
        this.complete();
        // 预热缓存
        settings.RESOURCE_ADAPTOR.fetchImage(this._model.url);
    },
    // ...
})
@EmitError()              // 错误事件发射
class ImageVm extends BaseElementVm<ImageModel, ImageState> {
    // ...
}

渲染策略

typescript
render(prevState: ImageState, nextState: ImageState): void {
    const { hasEffects, hasFilters, isAnimated, url } = nextState;

    // 图片是否含有特效类属性
    const isEffected = hasEffects || hasFilters;

    if (
        !isAnimated &&
        isEffected &&
        !this.isDissimilarResizing &&
        url
    ) {
        // 使用 DynamicImage 渲染特效图
        // DynamicImage 会加载预处理的特效结果图
        const image = this.useDynamicImage();
        image.update(this._model);
        image.resize(width, height);
    } else {
        // 使用 MaskVm 渲染普通图片
        // MaskVm 处理原始图片的加载、蒙版、马赛克等
        const maskVm = this.useMaskVm();
        maskVm.setState({
            uuid,
            url,
            mask,
            width,
            height,
            // ...
        });
    }
}

变换特殊处理

typescript
override updateTransform(model: ImageModel = this._model): void {
    this.view.visible = !model.hidden && !model.$hidden;

    // 获取可能被覆盖的属性
    const {
        left = model.left,
        top = model.top,
        width = model.width,
        height = model.height,
    } = model.$override || model;
    
    const { a, b, c, d, tx, ty } = model.transform.localTransform;

    const matrix = tempMatrix.set(a, b, c, d, tx, ty);

    // 附加额外变换
    if (this.transform) {
        matrix.prepend(this.transform);
    }

    // 分解变换到 PixiJS Transform
    decomposeLocalTransform(matrix, width, height, this.view.transform);

    // 应用偏移
    this.view.transform.position.x += left;
    this.view.transform.position.y += top;
    this.view.alpha = model.opacity;
}

6.6.2 TextVm - 文本元素

代码位置infinite-renderer/src/vms/text/text-vm.ts

TextVm 处理富文本渲染,支持:

  • 多样式文本(颜色、字体、大小)
  • 文字特效
  • 自动换行和排版
  • 列表样式
  • 九宫格背景

状态定义

typescript
interface TextState extends Partial<TextHolder> {
    width: number;
    height: number;
    textAlign: string;
    ninePatch?: NinePatchFilling | null;
    $resizing?: boolean;
}

export interface TextRenderState {
    width: number;
    height: number;
    color: string;
    effectScale: number;
    fontFamily?: string;
    fontStyle: string;
    fontWeight: number;
    fontSize: number;
    lineHeight: number;
    letterSpacing: number;
    textDecoration: string;
    writingMode: string;
    shadows: BaseShadow[];
    textAlign: string;
    verticalAlign: string;
    contents?: TextContentModel[] | null;
    textEffects: TextEffect[];
    // ...
}

文本排版流程

typescript
protected async updateText(model: T, complete = false) {
    const textUpdateID = ++this._textUpdateID;
    
    // 获取排版工具实例
    const typetool = await getTypeToolIns();

    // 若 updateID 相等,则执行排版
    if (textUpdateID === this._textUpdateID) {
        // 1. 转换数据模型
        const text = this.context?.modelAdaptor?.transformTextModel(model);

        // 2. 执行排版
        const autoWidth = text.autoAdaptive & 0b10;
        const autoHeight = text.autoAdaptive & 0b01;

        let layout = typetool.shape(text, {
            width: autoWidth ? 0 : text.width,
            height: autoHeight ? 0 : text.height,
        });

        // 3. 获取排版结果
        const bbox = layout.bbox();

        // 4. 更新模型尺寸(如果变化)
        if (!isSame(bbox.width, model.width) || !isSame(bbox.height, model.height)) {
            this.context.modelAdaptor.resizeTextModel(model, {
                width: bbox.width,
                height: bbox.height,
            });
        }

        // 5. 构建渲染数据
        const textHolder = this.buildTextHolder(model, text, typetool, layout);

        // 6. 设置状态
        this.setState(textHolder);
        this.forceUpdate();

        if (complete) {
            this.complete();
        }
    }
}

渲染实现

typescript
render(): void {
    const { element, layout, position } = this._state;
    const text = this._textSprite;
    let { width, height } = this._state;

    if (element && layout) {
        const { renderRect, ninePatch, $resizing } = this._state;
        const hasEffects = element.textEffects?.some(item => item.enable);
        const hasHighlight = getHasHighLight(element);
        const hasBackground = !!element.background?.enable;

        // 更新文字精灵
        text.update({
            element,
            width,
            height,
            position,
            layout,
            renderRect,
            ninePatch,
            image: this._image,
            $resizing,
            hasBackground,
            hasEffects,
            hasHighlight,
        });

        text.dirty();
    } else {
        text.resize(width, height);
    }

    // 设置位置
    if (position) {
        text.x = position.x;
        text.y = position.y;
    }
}

6.6.3 ShapeVm - 形状元素

代码位置infinite-renderer/src/vms/shape/shape-vm.ts

ShapeVm 是相对简单的元素,主要使用 Graphics 绘制基础图形。

状态定义

typescript
export interface ShapeState {
    shapeType?: ShapeType;
    fill?: string;
    radius?: number;
    stroke?: string;
    strokeWidth?: number;
    strokeAlignment?: ShapeStrokeAlignment;
    strokeStyle?: ShapeStrokeStyle | '';
    dashArray?: number[];
    width: number;
    height: number;
}

继承关系

typescript
class ShapeVm extends GraphicsElementVm<ShapeElementModel, ShapeState> {
    // GraphicsElementVm 提供了 this.graphics 属性
}

完整实现

typescript
class ShapeVm extends GraphicsElementVm<ShapeElementModel, ShapeState> {
    protected _state: ShapeState = {
        width: 0,
        height: 0,
    };

    constructor() {
        super();
        this.view.name = 'shape';
    }

    private updateState(model: ShapeElementModel) {
        this.updateTransform(model);
        this.updateWatermark(model);

        this.setState({
            shapeType: model.shapeType,
            width: model.width,
            height: model.height,
            radius: model.radius,
            fill: model.fill,
            stroke: model.stroke,
            strokeWidth: model.strokeWidth,
            strokeAlignment: model.strokeAlignment || undefined,
            strokeStyle: model.strokeStyle,
            dashArray: model.dashArray,
        });
    }

    // 自定义锚点计算(三角形特殊处理)
    protected buildAnchorPoints(rect: Rectangle, anchors: AnchorPoints): AnchorPoints {
        const { shapeType, radius } = this._state;

        if (shapeType === 'triangle-up') {
            return buildTriangleAnchorPoints(rect, anchors, radius);
        }

        return super.buildAnchorPoints(rect, anchors);
    }

    // 碰撞检测:考虑子元素
    contains(point: IPointData): boolean {
        const bounds = this.view.getBounds();

        if (!bounds.contains(point.x, point.y)) {
            return false;
        }

        // 图形区域包含点
        if (super.contains(point)) {
            return true;
        }

        // 子元素包含点
        for (const child of this.children) {
            if (child.contains(point)) {
                return true;
            }
        }

        return false;
    }

    modelCreated(model: ShapeElementModel): void {
        this.updateState(model);
    }

    modelUpdated(model: ShapeElementModel): void {
        this.updateState(model);
    }

    // 优化:状态无变化时跳过渲染
    shouldUpdate(nextState: ShapeState): boolean {
        return !fastEqual(this._state, nextState);
    }

    render(): void {
        const {
            shapeType,
            width,
            height,
            fill,
            radius,
            stroke,
            strokeWidth,
            strokeAlignment,
            strokeStyle,
            dashArray,
        } = this._state;

        // 清空画布
        this.graphics.clear();

        if (shapeType) {
            // 使用绘制函数绘制形状
            drawShape(this.graphics, {
                shapeType,
                width,
                height,
                fill,
                radius,
                stroke,
                strokeWidth,
                strokeAlignment,
                strokeStyle,
                dashArray,
            });
        } else {
            // 绘制占位符
            drawNever(this.graphics, {
                width,
                height,
            });
        }
    }
}

6.6.4 GroupVm - 组元素

代码位置infinite-renderer/src/vms/group/group-vm.ts

GroupVm 是最简单的元素之一,主要作为子元素的容器。

完整实现

typescript
class GroupVm
    extends BaseGroupElementVm<GroupElementModel>
    implements IBaseElementVm<GroupElementModel>
{
    protected _state: object = {};

    constructor() {
        super();
        this.view.name = 'group';
    }

    private updateState(model: GroupElementModel) {
        this.updateTransform(model);
        this.updateWatermark(model);
        this.updateMaskInfo(model);
    }

    // 使用支持蒙版的容器
    protected createChildrenLayer(): Container<DisplayObject> {
        return new MaskContainer();
    }

    // 碰撞检测:基于包围盒
    contains(point: IPointData): boolean {
        const bounds = this.getBounds();
        return bounds.contains(point.x, point.y);
    }

    hits(rect: Rectangle): boolean {
        return this.intersects(rect);
    }

    intersects(rect: Rectangle): boolean {
        const bounds = this.getBounds();
        return bounds.intersects(rect);
    }

    modelCreated(model: GroupElementModel): void {
        this.updateState(model);
    }

    modelUpdated(model: GroupElementModel): void {
        this.updateState(model);
    }
}

设计特点

  • 不需要自己的渲染状态(_state 为空对象)
  • 不实现 render 方法(无需绘制自身内容)
  • 碰撞检测基于子元素的联合包围盒
  • 使用 MaskContainer 支持组内蒙版

6.7 特殊元素解析

6.7.1 LayoutVm - 画板元素

代码位置infinite-renderer/src/vms/layout/layout-vm.ts

LayoutVm 是画板/画布元素,在设计编辑器中代表一个独立的设计区域。

特殊功能

  • 裁剪蒙版(限制子元素渲染范围)
  • 背景图/背景色
  • 背景遮罩
  • AI 水印
  • 标题显示

装饰器配置

typescript
@HiddenOnEditing()
@PrimaryLayout((element, model) => {
    // 在 plog 模板中作为主画板
    return element.context?.templateType === 'plog' && isTopLevelLayoutElement(model);
})
@IgnorePosition((element, model) => {
    // 设计模式下忽略顶层画板的位置
    return element.context?.stageMode === 'design' && isTopLevelLayoutElement(model);
})
@GlobalBackground((element) => {
    // 非白板模式下渲染全局背景
    return element.context?.stageMode !== 'board';
})
@EmitError()
class LayoutVm extends BaseElementVm<LayoutModel, LayoutState> {
    // ...
}

图层结构

typescript
constructor() {
    super();

    // 创建裁剪蒙版
    this.maskData = new MaskData(this.mask);
    this.view.mask = this.maskData;
    this.view.cullArea = this.hitArea;
    this.view.hitArea = this.hitArea;

    this.mask.name = 'mask';
    this.view.addChild(this.mask);

    // 监听背景加载状态
    this.layoutBackground.emitter.on('loading', () => this.loading());
    this.layoutBackground.emitter.on('complete', () => this.complete());
    this.layoutBackground.emitter.on('error', (error) => this.catch(error));

    // 图层顺序:背景色 < 背景图 < 遮罩 < 水印
    this.contentLayer.addChild(
        this.layoutBackground.view,
        this.layoutBackgroundMask.view
    );
}

裁剪蒙版实现

typescript
private updateState(model: LayoutModel) {
    // ... 其他更新

    let x = 0;
    let y = 0;
    let { width, height } = model;

    // 处理裁剪区域
    if (model._clipping?.enable) {
        const {
            offsetX = x,
            offsetY = y,
            offsetWidth = width,
            offsetHeight = height,
        } = model._clipping;

        x = offsetX;
        y = offsetY;
        width = offsetWidth;
        height = offsetHeight;
    }

    // 绘制裁剪蒙版
    this.mask.clear();
    this.mask.beginFill(0x000000, 1);
    this.mask.drawRect(x, y, width, height);

    // 更新背景
    this.layoutBackground.setState({
        x,
        y,
        width,
        height,
        background,
        // ...
    });
}

6.7.2 ConnectorVm - 连接线

代码位置infinite-renderer/src/vms/connector/connector-vm.ts

ConnectorVm 实现元素间的连接线,支持多种线型和箭头样式。

状态定义

typescript
export interface ConnectorState {
    points: ConnectorPoint[];        // 路径点
    color?: string;                  // 线条颜色
    lineType?: ConnectorLineType;    // 线型:直线/折线/曲线
    lineStyle?: ConnectorLineStyle;  // 样式:实线/虚线
    lineWidth?: number;              // 线宽
    radius?: number;                 // 圆角半径
    startId?: string;                // 起点关联元素
    startStyle?: JoinStyle;          // 起点箭头样式
    endId?: string;                  // 终点关联元素
    endStyle?: JoinStyle;            // 终点箭头样式
    dashArray?: number[];            // 虚线数组
    lineCap?: ConnectorLineCap;      // 线帽样式
}

渲染实现

typescript
render(): void {
    const {
        points,
        color = '#000000',
        lineWidth = 4,
        radius = 0,
        lineType = 'elbowed',
        lineStyle = 'solid',
        startStyle = 'none',
        endStyle = 'none',
        dashArray = [],
        lineCap = 'butt',
    } = this._state;

    this.graphics.clear();
    this.startView.clear();
    this.endView.clear();

    // 非法线段
    if (points.length <= 1) {
        return;
    }

    // 计算箭头偏移
    const startOffset = mapArrowOffset(lineWidth, startStyle);
    const endOffset = mapArrowOffset(lineWidth, endStyle);

    // 配置线条样式
    const lineColor = Color.shared.setValue(color);
    const options: IStrokeStyleOptions = {
        color: lineColor.toNumber(),
        alpha: lineColor.alpha,
        width: lineWidth,
    };

    // 处理虚线
    if (lineStyle !== 'solid') {
        const totalLength = getLineLength(points, lineType, radius);
        const lineDashArray = calculateSymmetricDash(
            totalLength - startOffset - endOffset,
            dashArray,
            2,
        );
        options.dashArray = lineDashArray;
        options.dashOffset = lineDashArray[0] / 2;
    }

    // 设置线帽样式
    switch (lineCap) {
        case 'butt':
            options.cap = STROKE_CAP.BUTT;
            break;
        case 'round':
            options.cap = STROKE_CAP.ROUND;
            break;
        case 'square':
            options.cap = STROKE_CAP.SQUARE;
            break;
    }

    this.graphics.strokeStyle(options);

    // 渲染连线路径
    renderLine(this.graphics, points, lineType, radius, startOffset, endOffset);

    // 渲染起点箭头
    if (startStyle && startStyle !== 'none') {
        const start = lineType === 'straight' ? points[points.length - 1] : points[1];
        const end = points[0];
        this.renderTerminator(
            this.startView,
            lineWidth,
            color,
            startStyle,
            start,
            end,
            this.startMatrix.identity(),
        );
    }

    // 渲染终点箭头
    if (endStyle && endStyle !== 'none') {
        const start = lineType === 'straight' ? points[0] : points[points.length - 2];
        const end = points[points.length - 1];
        this.renderTerminator(
            this.endView,
            lineWidth,
            color,
            endStyle,
            start,
            end,
            this.endMatrix.identity(),
        );
    }
}

箭头绘制

typescript
private renderTerminator(
    graphics: Graphics,
    width: number,
    color: string,
    style: JoinStyle,
    start: ConnectorPoint,
    end: ConnectorPoint,
    matrix = Matrix.IDENTITY,
) {
    // 计算方向角度
    const vx = end.x - start.x;
    const vy = end.y - start.y;
    const radians = Math.atan2(vy, vx);
    const zoom = width / 2;

    switch (style) {
        case 'solid-arrow':
        case 'triangle-arrow':
        case 'triangle': {
            const item = ArrowMap[style];
            const pathData = new SVGPathData(item.path).toAbs();
            pathData.scale(zoom, zoom);

            // 变换矩阵:移动到箭头位置,旋转到正确方向
            matrix
                .translate(-item.width * zoom, (-item.height * zoom) / 2)
                .rotate(radians)
                .translate(end.x, end.y);

            graphics.beginFill(color);
            graphics.setMatrix(matrix);
            graphics.drawPath(Path.SVGDataToPath(pathData));
            graphics.endFill();
            break;
        }

        case 'circle': {
            const item = ArrowMap.circle;
            matrix.rotate(radians).translate(end.x, end.y);

            graphics
                .beginFill(color)
                .setMatrix(matrix)
                .drawCircle(0, 0, item.width * 0.5 * zoom)
                .endFill();
            break;
        }

        // ... 其他样式
    }
}

6.8 渲染完成与异步加载

6.8.1 isCompleted 状态管理

许多元素需要加载外部资源(图片、字体等),渲染完成状态用于追踪资源加载进度。

代码位置infinite-renderer/src/vms/base/base-element-vm.ts

typescript
get isCompleted(): boolean {
    return (
        this._isCompleted &&
        // 若水印图层可见,则附加水印完成状态
        (!this.watermarkLayer.view.visible || this.watermarkLayer.isCompleted)
    );
}

protected _isCompleted = true;

ImageVm 的完成状态

typescript
// ImageVm
get isCompleted() {
    return super.isCompleted && this._aiWatermarkCompleted;
}

protected _isCompleted = false;  // 初始为 false,需要加载图片
protected _aiWatermarkCompleted = true;

LayoutVm 的完成状态

typescript
// LayoutVm
get isCompleted(): boolean {
    return (
        super.isCompleted &&
        this.layoutBackground.isCompleted &&
        (!this.aiWatermark || this._aiWatermarkCompleted)
    );
}

6.8.2 loading 与 complete 事件

事件定义

typescript
export interface BaseElementVmEvents {
    loading: [];
    complete: [];
    error: [error: any];
}

状态切换方法

typescript
complete(): void {
    this._isCompleted = true;

    if (this.isCompleted) {
        this.emitter.emit('complete');
    }
}

loading(): void {
    this._isCompleted = false;
    this.emitter.emit('loading');
}

使用示例

typescript
// ImageVm 图片加载完成
this.maskVm.emitter.on('complete', () => {
    this.handleMaskLoaded();
    this.complete();  // 触发完成事件
});

// 外部监听
const vm = vmEngine.buildVm(imageModel);
vm.emitter.on('complete', () => {
    console.log('图片加载完成');
});

6.8.3 级联完成检测

对于复合元素,完成状态需要考虑所有子组件:

typescript
// LayoutVm 需要等待背景加载完成
constructor() {
    super();
    
    this.layoutBackground.emitter.on('loading', () => this.loading());
    this.layoutBackground.emitter.on('complete', () => this.complete());
}

// 完成状态检查所有子组件
get isCompleted(): boolean {
    return (
        super.isCompleted &&
        this.layoutBackground.isCompleted &&
        (!this.aiWatermark || this._aiWatermarkCompleted)
    );
}

级联完成流程

LayoutVm.isCompleted

    ├── super.isCompleted (BaseElementVm)
    │       │
    │       ├── this._isCompleted
    │       │
    │       └── watermarkLayer.isCompleted

    ├── layoutBackground.isCompleted
    │       │
    │       └── 背景图加载状态

    └── aiWatermark 加载状态

6.9 本章小结

生命周期总结

数据模型生命周期

钩子触发时机典型用途
modelCreated元素创建后初始化状态、加载资源
modelUpdated属性变化时更新状态、重新渲染
modelMoved移动到新父容器更新依赖父容器的状态
modelRemoved从缓存移除前清理资源、取消异步操作

视图模型生命周期

钩子触发时机典型用途
shouldUpdatesetState 后判断是否需要渲染(优化)
beforeUpdaterender 前准备工作、缓存旧状态
render状态更新时执行渲染逻辑
afterUpdaterender 后加载额外资源、触发事件

元素实现模式

  1. 简单元素(如 ShapeVm):

    • 直接在 render 中使用 Graphics 绘制
    • 实现 shouldUpdate 优化性能
  2. 复杂元素(如 ImageVm):

    • 委托给专门的子视图模型(MaskVm、DynamicImage)
    • 使用装饰器扩展功能
    • 处理异步资源加载
  3. 容器元素(如 GroupVm):

    • 主要作为子元素容器
    • 不需要自己的渲染逻辑
    • 使用 MaskContainer 支持蒙版
  4. 特殊元素(如 LayoutVm、ConnectorVm):

    • 复杂的图层结构
    • 特殊的变换处理
    • 多组件协作

设计原则

  1. 状态驱动:所有渲染都由状态变化触发
  2. 批量更新:合并多次 setState 为一次渲染
  3. 层级分离:内容、子元素、水印各自独立
  4. 异步友好:完善的加载状态管理
  5. 可扩展:通过继承和装饰器扩展功能

📖 延伸阅读


📝 练习题

  1. 分析题:为什么 ImageVm 的 _isCompleted 初始值是 false,而 BaseElementVm 是 true

  2. 实现题:实现一个简单的 CircleVm,要求:

    • 继承 GraphicsElementVm
    • 支持 fillstrokeradius 属性
    • 实现 shouldUpdate 优化
  3. 设计题:如果需要实现一个"视频元素",它需要支持播放/暂停状态,你会如何设计其生命周期?


下一章:第7章 事件系统与碰撞检测 - 深入了解交互系统的实现


📅 文档更新时间:2026-01-28

👤 维护者:前端团队

如有转载或 CV 的请标注本站原文地址