Flutter虚拟DOM管理插件virtual_dom的使用

发布于 1周前 作者 sinazl 来自 Flutter

Flutter虚拟DOM管理插件virtual_dom的使用

virtual_dom

Virtual_DOM是一个小型、轻量级且低级别的虚拟DOM实现。

版本: 0.1.12

这个软件是什么?不是什么?

这个软件不包含任何现成的用户界面元素。
这个软件是基础,并且旨在从零开始实现用户界面元素(应用组件)和客户端Web应用程序。

什么是虚拟DOM?

虚拟化是一个相当复杂的实现,尽管体积小,但涉及多种虚拟节点之间的交互,这些虚拟节点可能处于不同的状态,渲染器,跟踪其状态的机制,它们的识别、比较和基于重建的重用决策。 虚拟DOM是一棵树形结构的虚拟节点,用于构建DOM树。 虚拟树支持虚拟节点的部分更新。 在对树进行最简单的操作(如插入或删除)时,只有部分DOM树被更新。 这使得DOM树的更新非常快速。 当使用唯一的键来标识同一类型的虚拟节点时,这一点尤其正确。 不允许使用非唯一键,否则将抛出异常。 键只需要在同一级别(即父节点级别)内唯一。 同时使用带有和不带有键的虚拟节点是允许的。

虚拟DOM的目的

使用虚拟DOM可以开发快速的客户端浏览器应用,例如管理面板、电子商务应用、聊天室等。 虚拟DOM节点几乎等同于DOM节点。 熟悉HTML和CSS的开发者可以轻松地使用虚拟节点。 但由于仅使用虚拟节点不足以提供充分的功能解决方案,因此提供了虚拟组件以实现全面的工作。 虚拟组件实际上是与虚拟节点相同的虚拟树元素,但有显著的区别。 虚拟组件没有等效的DOM节点。 它们应该生成内容(作为渲染结果),该内容由虚拟节点和/或虚拟组件组成。 更确切地说,有两种类型的组件 - 虚拟组件和应用组件。 虚拟组件包装应用组件。 组件的使用就是应用组件的使用。

虚拟节点键、组件键和组件效果键

虚拟节点键必须在父节点内唯一。 这些键的主要(也是最合理的)用途是索引集合中的元素(类似于数据库主键)。 这是为了能够快速计算树中是否有更改(只需执行键比较操作)。

组件键可用于标识相同类型的组件实例。

组件效果键可用于指示如果此键的先前值与当前值不同,则组件需要重新渲染。通常,效果键的值是从当前参数中获取的,这些参数影响重新渲染过程。

辅助函数

辅助函数意味着一些全局函数,以简化该软件的使用。 这是一个伪概念,并不是一个约定。 目前有几个辅助函数:

  • el
  • h
  • mount
  • styles
  • vHtml
  • vKey

h 函数帮助声明虚拟元素。 使用此函数是可选的,您可以编写自己的类似函数(比这个好一百倍)。 但是,无论如何,该软件的作者认为此函数至少有一点用处,因此认为将其包含在此软件中是可行的。

return h('div', [
  h('div', 'Counter: $count'),
  h('button', 'Click', {
    'click': (Event event) {
      setCount(count + 1);
    }
  }),
]);

mount 函数帮助将虚拟组件挂载到浏览器DOM元素上。 它通常在程序启动时使用。

final app = document.getElementById('app')!;
mount(app, App());

styles 函数帮助创建HTML style 属性。

final style = styles({
  'background-color': 'black',
  'color': 'red',
  'display': 'block',
  'margin': '8px',
  'padding': '8px',
});
return h(
  'div',
  {'style': style},
  h('div', [for (final line in lines) h('div', line)]),
);

vHtml 函数帮助创建 VHtml 节点。 实际上,它只是调用了构造函数,但它扮演了一个重要角色 - 隐藏了实现细节。由于不建议在 Component.render() 方法中直接使用虚拟节点。

Object render() {
  final style = styles({
   'color': color,
  });
  return h('div', {
    'style': style
  }, [
    vHtml('style', _style),
    h('div', {
      'class': 'spinner'
    }, [
      h('div', {'class': 'bounce1'}),
      h('div', {'class': 'bounce2'}),
      h('div', {'class': 'bounce3'}),
    ]),
   ])
    ..useShadowRoot();
}

vKey 函数帮助向虚拟节点添加键(通常是集合元素的键)。

Object render() {
  final changeState = State.change();
  final application = App.get();
  Listener.use(application.windows, changeState);
  final uiFactory = application.uiFactory;
  final list = [];
  final windows = application.windows.value;
  for (final window in windows.values) {
    final windowListItem = uiFactory.createWindowListItem(window);
    final item = h('div', windowListItem);
    list.add(vKey(window.uri, item));
  }
   return h('div', list);
}

应用组件

应用组件(或更简单地说,组件)是一个代表应用程序负责渲染用户界面元素的部分的组件。

import 'dart:html';

import 'package:virtual_dom/features.dart';
import 'package:virtual_dom/virtual_dom.dart';

void main(List<String> args) {
  final app = document.getElementById('app')!;
  mount(app, Counter());
}

class Counter extends Component {
  @override
  Object render() {
    final count = State.get('count', () => 0);
    final setCount = State.set<int>('count');
    return h('div', [
      h('div', 'Counter: $count'),
      h('button', 'Click', {
        'click': (Event event) {
          setCount(count + 1);
        }
      }),
    ]);
  }
}

手动挂载警告。 由于这是一个手动挂载方法,如果需要卸载组件时,你必须显式调用组件的 dispose() 方法(如果需要的话)。 原因是顶级组件没有父节点。只有父节点会调用子节点的 dispose() 方法,或者由父节点调用的渲染器。 在大多数情况下,这并不是必需的,只需关闭浏览器中的页面标签即可。

特性

组件本身并不十分功能化,它只负责返回可以渲染的内容。 特殊类用于给组件增加一些功能。 再次强调,这只是伪概念,并不是一个约定。 目前有几个类实现了这些特性:

  • InheritedProperty
  • InheritedValue
  • Init
  • Listener
  • State
  • ValueListener
  • useErrorReport
  • useValueWatcher
  • useWatcher

当然,没有什么可以阻止独立实现其他特性以扩展功能。 如果你觉得这很难,那其实并不难 - 它非常简单(看看这些类的源代码就足够了)。

InheritedValue 特性允许在父组件中定义一个 Listenable 值(指定其名称和类型),该值将在子元素中通过其名称可用。 当 value 发生变化时,所有使用它的组件将自动重新渲染。

// 父组件
final value = InheritedValue.add('value', () => 0);

// 子组件
final value = InheritedValue.get<int>('value');

Init 特性允许定义 initdisposedispose 是可选的)处理程序,这些处理程序将在适当的时候恰好执行一次。

Init.use(() {
  _count++;
  return () {
    _count--;
  };
});

Listener 特性允许自动添加和移除一个 ChangeNotifier 的监听器,指定的动作会在初始化时添加监听器,并在释放时移除。

final changeState = State.change();
Listener.use(_value, changeState);

State 特性允许定义一个值,该值将存储在组件的内部上下文中直到组件被销毁。 此外,这个特性还允许获取一个改变该值的函数。 对该值的任何更改都会使组件重新渲染。 当然,函数定义无条件地使组件重新渲染。

final step = State.get('step', () => 0);
final setStep = State.set<int>('step');
final changeState = State.change();

ValueListener 特性允许自动添加和移除一个 ValueNotifier 的监听器,指定的动作会在初始化时添加监听器,并在释放时移除。

final val = State.get('val', () => 0);
final setVal = State.set<int>('val');
ValueListener.use(_value, setVal);

使用Shadow DOM

使用Shadow DOM并不比使用普通DOM复杂很多。 换句话说,一切都很简单。 Shadow DOM可以与 VElement 虚拟节点一起使用。 为了表示需要使用Shadow DOM,只需在使用前配置这样的虚拟节点。 为此,使用 VElement.useShadowRoot() 方法。

class _Component1 extends Component {
  final String color;

  final String text;

  _Component1(this.text, this.color) : super(key: '$text#$color');

  @override
  Object render() {
    final style = '''
.specialColor {
  color: $color
}''';

    return h('div', [
      vHtml('style', style),
      h('div', {'class': 'specialColor'}, text),
    ])
      ..useShadowRoot();
  }
}

在这个例子中,..useShadowRoot() 将应用于返回的元素(在这种情况下是最顶层的 h('div')),并最终返回相同的元素(并配置为使用Shadow Root)。 配置已经使用的节点(已经在树中放置)会导致错误的操作。 但这并不意味着不能返回一个新的节点。所有这些状态都是跟踪的,被认为是不同的,一切都会正常工作。

渲染错误报告

在渲染过程中发生错误时,最好的做法是在这种情况下报告此错误。 在这种情况下,“安全”意味着使用try/catch语句。 但这些错误的信息该如何处理? 为此目的有一个特殊的类 ErrorReport。 虚拟组件包含一个字段 ValueNotifier<ErrorReport?>? errorReport。 默认情况下,此字段未初始化,这意味着此虚拟组件不承担错误报告的责任。 但一旦设置了此值(在某个组件中),则意味着该组件接管了显示错误消息的任务。

看起来一切都很简单,但并非如此。 如果在这个组件中发生渲染错误,而该组件本身应显示错误消息怎么办? 因为该组件将不再重新渲染,这将导致无法显示错误报告。 在这种情况下,最好将 ValueNotifier<ErrorReport?> 的值传递给子组件。 因为虚拟化的原则并不妨碍孩子“过自己的生活”(至少渲染一次),他们将继续存在并运行,即使在一个“损坏”的父组件中。 在这种情况下,他们完全有能力显示错误报告。

所有父组件需要做的是初始化 VComponent.errorReport 字段并渲染将接收此值的子组件。 子组件(组件)应跟踪此值的变化(监视)并输出更改(错误报告)。

可以在以下位置找到示例:GitHub链接

这不是完成此任务的唯一方法,但这是一个相当优化的方法。

可以嵌套负责显示错误消息的组件,如果发生错误,将使用最近的一个。 VNode.findErrorReport() 方法负责查找最近或唯一的组件。 它检查当前组件,然后迭代遍历所有父组件直到顶部。

全局错误报告

将全局 ErrorReport 实例添加到全局并不总是好的,但有时如果想了解错误在哪里发生,则可能是必要的。 ErrorReport 类有一个静态字段 ValueNotifier<ErrorReport?> global 可以在任何情况下使用。 但,像往常一样,事情并不那么简单。 问题在于,在UI元素和其他地方发生的错误不能被视为可以通过一种方式捕获的错误。 在UI元素中发生的错误由渲染过程捕获。所有其他错误都不会被渲染过程捕获。 这意味着需要采取一些特殊行动。 一个可能的行动是为此目的创建一个全局的 ErrorReport。 请注意,ErrorReport 本身除了存储错误信息外不做任何事。 也就是说,有必要创建一个UI组件来显示错误报告。 结果,有两个可能的解决方案:

  • 全局错误报告适用于所有情况
  • 全局错误报告适用于所有情况,局部错误报告适用于UI

在UI流程之外使用全局错误报告的示例:

Timer.run() {
  ErrorReport.run(() {
    try 'Some error';
  });
});

Timer.run() async {
  return ErrorReport.runAsync(() async {
    await f();
    try 'Some error';
  });
});

更多关于Flutter虚拟DOM管理插件virtual_dom的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter虚拟DOM管理插件virtual_dom的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在Flutter中,虽然通常不会直接提及“虚拟DOM”这一概念(因为Flutter使用的是一个称为Widget树的机制,而不是传统Web开发中的虚拟DOM),但我们可以借助一些插件或技术来优化Widget的重建和管理。不过,需要注意的是,Flutter社区中并没有一个广泛认可的名为virtual_dom的插件。不过,我们可以讨论如何优化Widget的重建过程,以及如何使用一些现有的库或技术来达到类似的效果。

在Flutter中,优化Widget重建的关键在于理解StateStatefulWidget的使用,以及如何利用keys来避免不必要的重建。此外,还可以使用一些性能分析工具来识别和优化性能瓶颈。

下面是一个简单的示例,展示了如何使用keys来避免不必要的Widget重建:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter Widget Optimization'),
        ),
        body: MyWidgetList(),
      ),
    );
  }
}

class MyWidgetList extends StatefulWidget {
  @override
  _MyWidgetListState createState() => _MyWidgetListState();
}

class _MyWidgetListState extends State<MyWidgetList> {
  final List<String> items = List.generate(20, (i) => "Item $i");
  bool toggle = false;

  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        ElevatedButton(
          onPressed: _toggle,
          child: Text('Toggle State'),
        ),
        ListView.builder(
          itemCount: items.length,
          itemBuilder: (context, index) {
            // 使用UniqueKey来避免每个item在setState时被重建
            return ListTile(
              key: UniqueKey(),
              title: Text(items[index]),
            );
          },
        ),
      ],
    );
  }
}

在上面的示例中,我们创建了一个包含多个ListTileListView。如果每个ListTile都使用默认的key(即没有显式指定key),那么在调用setState时,所有的ListTile都可能会被重建。为了避免这种情况,我们为每个ListTile指定了一个UniqueKey,这样即使父Widget重建,只要ListTile的内容没有变化,它们就不会被重建。

然而,需要注意的是,UniqueKey应该谨慎使用,因为它会强制Flutter每次都重新构建Widget,即使内容没有变化。在大多数情况下,你应该使用更合适的Key类型(如ValueKeyObjectKey等),这些Key类型能够基于Widget的内容生成唯一的Key。

此外,如果你正在寻找类似React中虚拟DOM的优化技术,你可以考虑使用Flutter的性能分析工具(如Performance Overlay)来识别和优化性能瓶颈。这些工具可以帮助你了解哪些Widget在重建时消耗了最多的资源,并据此进行优化。

最后,虽然Flutter没有直接的“虚拟DOM”插件,但你可以通过合理使用keysState和性能分析工具来优化Widget的重建过程,从而提高应用的性能。

回到顶部