综合编程

incremental-dom简析

微信扫一扫,分享到朋友圈

incremental-dom简析
0

想必对于virtual-dom(以下简称vdom)已经耳熟能详了,react和vue均使用了vdom,在更新与DOM时具有效率高、速度快的特点(相比于直接操作dom)。那么incremental-dom又是什么呢?

incremental-dom
(以下简称idom)的一些特点:

  • idom是用于 表现
    更新
    DOM的库,由google开发。
  • idom与vdom相比最大的区别是,它不会构建作为中间层的虚拟dom树。当数据变化时,diff操作会在真实DOM上 逐节点
    执行,而不是在 虚拟DOM树
    之间,内存占用更少。
  • idom的目标并不是直接使用的,而是为更高层的库或框架所提供。

基础

介绍一下idom的基本使用,主要来源于 官方文档

渲染DOM

所渲染的DOM使用节点函数 elementOpen
elementClose
text
来描述。

function renderPart() {
  elementOpen('div');
    text('Hello world');
  elementClose('div');
}

会被渲染成

<div>
  Hello world
</div>

patch
函数中使用上的 renderPart
函数可以在已存在的元素(Element)或文档(Document,包含ShadowDOM)上更新期望的节点。调用patch函数会在DOM树上根据所需的节点变动、更新属性和创建/移除进行局部更新。

patch(document.getElementById('someId'), renderPart);

特性和属性的设置

除了创建DOM节点外,有时还需要在元素上添加/删除特性(attribute)和属性(property)。它们被指定为可变的参数,通过attribute/property键值对来改变。传入的对象与函数类型会被设为属性,其他类型会被设为特性。

ps: attribute是html标签上的特性,只能是字符串。property是DOM上的属性,是JS对象。

属性设置的一个用法是储存一个事件代理的回调函数。当你可以在DOM节点上分配任何属性时,甚至可以分配一些on*事件的处理器,比如onClick。

elementOpen('div', null, null,
    'class', 'someClass',
    'onclick', someFunction);
  …
elementClose('div');

静态数组

很多时候DOM节点的一些属性是不会改变的,比如 <input type="text">
的type特性。idom提供了一个shortcut来避免已知不变的特性/属性比较。

elementOpen
的第三个参数表示不会改变的特性数组,为了避免每次参数传入都分配一个数组,可以在闭包外声明数组,使其仅执行一次。

在提供静态数组的同时还需要提供key,这确保了idom永远不会重用那些有着相同标签但不同静态数组的元素。

function render() {
  const s1 = [ 'type', 'text', 'placeholder', '…'];

  return function(isDisabled) {
    elementOpen('input', '1', s1,
        'disabled', isDisabled);
    elementClose('input');
  };
}

在上面的代码中, 1
就是key,s1就是静态数组。

赋予样式

使用字符串或对象均可以设置一个元素的样式。当使用对象设置样式时,它的键名格式应为驼峰式。

  1. 作为字符串:

    elementOpen('div', null, null,
        'style', 'color: white; background-color: red;');
          …
    elementClose('div');
  2. 作为对象:

    elementOpen('div', null, null,
        'style', {
          color: 'white',
          backgroundColor: 'red'
        });
      …
    elementClose('div');

条件渲染

  1. if/else

    function renderGreeting(date, name) {
      if (date.getHours() < 12) {
        elementOpen('strong');
        text('Good morning, ');
        elementClose('strong');
      } else {
        text('Hello ');
      }
    
      text(name);
    }
  2. DOM元素更新/复用

    if (condition) {
      elementOpen('div');
        // subtree 'A'
      elementClose('div');
    }
    
    elementOpen('div');
      // subtree 'B'
    elementClose('div');
  3. 逻辑化的特性

    elementOpenStart('div');
      for (const key in obj) {
        attr(key, obj[key]);
      }
    elementOpenEnd('div');

钩子

值的设置

idom提供了用来自定义如何处理传入值的钩子。 attributes
对象允许你提供一个函数来决定:当一个特性传入到elementOpen或其他函数中时要做的事情。下面的例子中使得idom总会将value作为属性(property)来设置。

import {
  attributes,
  applyProp,
  applyAttr
} from 'incremental-dom';

attributes.value = applyProp;

若想更一步的控制设置值的方式,可以设置自定义执行更新的函数:

attributes.value = function(element, name, value) {
  …
};

若未给键名指定函数,那么一个默认函数会被用来执行那些在属性和特性中的值,这个函数可以通过给 symbols.default
指定函数来修改。

import {
  attributes,
  symbols
} from 'incremental-dom';

attributes[symbols.default] = someFunction;

添加/移除节点

通过指定 notifications.nodesCreated
notifications.nodesDeleted
上的函数来让idom在节点被添加和移除时发出通知。若在patch操作的过程中添加或移除节点,则会在patch添加或移除节点的操作完成后调用对应的钩子函数。

import { notifications } from 'incremental-dom';

notifications.nodesCreated = function(nodes) {
  nodes.forEach(function(node) {
    // node may be an Element or a Text
  });
};

优点

idom跟vdom相比有如下两个优点:

  1. 逐个操作(incremental nature)使其在渲染过程中可以有效地减少内存占用,并且具有更加可预测的性能, 更适合移动端场景
  2. 更容易映射到模板上。可以轻松的在控制与循环语句混入元素与特性声明。

idom是一个小巧的(2.6kB min+gzip)、独立并且灵活的库。用它可以渲染出DOM节点并且设置特性/属性,至于如何组织视图等剩余的工就取决于用户了。比如说,一个Backbone应用可以在传统的模板与手动更新的基础上使用idom来渲染与更新DOM。

例子

这里
是一个简单的使用idom和markdown的例子

原理

API

idom所提供的API主要可以分为对 元素
和对 指针
的操作。

对元素的操作

  1. 使用 elementOpen
    elementClose
    elementClose
    等函数指定所操作的元素(渲染&更新),自动移动指针到该元素内
  2. 使用 attr
    text
    key
    等修改元素的特性、属性或内容
  3. 使用 patch
    函数在指定元素上执行传入的更新函数

对指针的操作

  1. 使用 currentElement
    获取当前打开的元素,使用 currentPointer
    获取当前idom指向的位置
  2. 使用 skip
    将指针移动到当前打开元素的末尾,使用 skipNode
    向后跳过一个节点

diff方法

idom所提供的diff方法比较的是键值对数组。在下面的代码中可以看到:

prev
next
表示更新前与更新后的 键值对
,注意是字符串数组类型。 {key1: value1, key2: value2}
对象对应的是形如: ['key1', 'value1', 'key2', 'value2']
的数组,偶数索引为键,奇数索引为值。

src/diff.ts

...
function calculateDiff<T>(
  // diff时传入的参数,其中更新的上下文为泛型T,在外部指定类型
    prev: string[], next: string[], updateCtx: T,
    updateFn: (ctx: T, x: string, y: {}|undefined) => undefined) {
  
  // 1.首先判断是否为新添加的数据
  const isNew = !prev.length;
  let i = 0;

  // 2. 遍历更新后的键值对
  for (; i < next.length; i += 2) {
    // 2.1 比较是否有不同的键名
    const name = next[i];
    if (isNew) {
      // 更新prev
      prev[i] = name;
    } else if (prev[i] !== name) {
      // 一旦遇到不同的键名,则终止循环
      break;
    }
    // 2.2 比较值
    const value = next[i + 1];
    if (isNew || prev[i + 1] !== value) {
      // 若为新数据或对应索引的值不同,则更新prev并执行更新函数
      prev[i + 1] = value;
      updateFn(updateCtx, name, value);
    }
  }

  // 当更新前与更新后的键名及顺序完全相同或更新前数据为空,则不会进行下面这步

  // 3. 键值对中项的排列顺序可能与之前的并不完全一样,需要确保旧的项被移除,这种情况比较少见,比如
  // pre:  ['key1', 'value1', 'key2', 'value2', 'key4', 'value4', 'key3', 'value3']
  // next: ['key1', 'value1', 'key3', 'value3', 'key2', 'value2']
  if (i < next.length || i < prev.length) {
    const startIndex = i;
    // 3.1 暂存剩余的prev键值对
    for (i = startIndex; i < prev.length; i += 2) {
      prevValuesMap[prev[i]] = prev[i + 1];
    }
    // 3.2 遍历next键值对
    for (i = startIndex; i < next.length; i += 2) {
      const name = (next[i]) as string;
      const value = next[i + 1];
      // 若对应prev键名的值与next值不同,则执行更新函数
      if (prevValuesMap[name] !== value) {
        updateFn(updateCtx, name, value);
      }
      // 更新prev
      prev[i] = name;
      prev[i + 1] = value;
      // 删除prevValuesMap对象中已比对的键值对
      delete prevValuesMap[name];
    }

    // 4. 进行去尾操作,删除超过next长度的项,即已不存在的项
    truncateArray(prev, next.length);

    // 5. 若prev中存在的值在next中已不存在,则传入undefined执行更新函数
    for (const name in prevValuesMap) {
      updateFn(updateCtx, name, undefined);
      delete prevValuesMap[name];
    }
  }
}
...

该方法的变体同样用在了 elementOpen
函数中的attribute diff操作:

src/virtual_elements

...
const elementOpen = function(tag, key, statics, var_args) {
  ...
  for (; i < arguments.length; i += 2, j += 2) {
    const attr = arguments[i];
    if (isNew) {
      attrsArr[j] = attr;
      newAttrs[attr] = undefined;
    } else if (attrsArr[j] !== attr) {
      break;
    }

    const value = arguments[i + 1];
    if (isNew || attrsArr[j + 1] !== value) {
      attrsArr[j + 1] = value;
      updateAttribute(node, attr, value);
    }
  }

  if (i < arguments.length || j < attrsArr.length) {
    ...
  }

  return node;
};

patch方法

在自定义更新指定元素时,最关键的无疑是 patch
函数,那么idom的 patch
函数做了什么呢,如下所示:

src/core.ts

...
const patchInner = patchFactory((node, fn, data) => {
  currentNode = node;

  enterNode();
  fn(data);
  exitNode();
  ...
  return node;
});
...

简析它的过程:

  1. 首先,使用了 patchFactory
    这个工厂函数进行构建,在这个函数中主要进行了一些数据初始化,如上下文、文档、元素路径、父元素等等。
  2. 使用 enterNode
    修改 currentParent
    currentNode
    ,将 currentNode
    置为null。
  3. 使用传入的函数及数据进行更新操作。
  4. 使用 exitNode
    清空当前范围的未访问节点,重置 currentNode
    currentParent
    属性。

在第三步的更新函数中,一般会使用 text
elementOpen
等会在真实DOM上进行修改操作的函数。

模板

可以参考官方的 ecosystem

阅读原文...

Avatar

Tencent-Backed Meituan's Losses Balloon in Duel With Alibaba

上一篇

What is machine learning? A psychedelic graphical journey

下一篇

您也可能喜欢

评论已经被关闭。

插入图片
incremental-dom简析

长按储存图像,分享给朋友