incremental-dom简析
栏目: JavaScript · 发布时间: 6年前
内容简介:想必对于virtual-dom(以下简称vdom)已经耳熟能详了,react和vue均使用了vdom,在更新与DOM时具有效率高、速度快的特点(相比于直接操作dom)。那么incremental-dom又是什么呢?介绍一下idom的基本使用,主要来源于
想必对于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就是静态数组。
赋予样式
使用字符串或对象均可以设置一个元素的样式。当使用对象设置样式时,它的键名格式应为驼峰式。
-
作为字符串:
elementOpen('div', null, null, 'style', 'color: white; background-color: red;'); … elementClose('div'); -
作为对象:
elementOpen('div', null, null, 'style', { color: 'white', backgroundColor: 'red' }); … elementClose('div');
条件渲染
-
if/else
function renderGreeting(date, name) { if (date.getHours() < 12) { elementOpen('strong'); text('Good morning, '); elementClose('strong'); } else { text('Hello '); } text(name); } -
DOM元素更新/复用
if (condition) { elementOpen('div'); // subtree 'A' elementClose('div'); } elementOpen('div'); // subtree 'B' elementClose('div'); -
逻辑化的特性
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相比有如下两个优点:
- 逐个操作(incremental nature)使其在渲染过程中可以有效地减少内存占用,并且具有更加可预测的性能, 更适合移动端场景 。
- 更容易映射到模板上。可以轻松的在控制与循环语句混入元素与特性声明。
idom是一个小巧的(2.6kB min+gzip)、独立并且灵活的库。用它可以渲染出DOM节点并且设置特性/属性,至于如何组织视图等剩余的工就取决于用户了。比如说,一个Backbone应用可以在传统的模板与手动更新的基础上使用idom来渲染与更新DOM。
例子
这里 是一个简单的使用idom和markdown的例子
原理
API
idom所提供的API主要可以分为对 元素
和对 指针
的操作。
对元素的操作
-
使用
elementOpen、elementClose和elementClose等函数指定所操作的元素(渲染&更新),自动移动指针到该元素内 -
使用
attr、text和key等修改元素的特性、属性或内容 -
使用
patch函数在指定元素上执行传入的更新函数
对指针的操作
-
使用
currentElement获取当前打开的元素,使用currentPointer获取当前idom指向的位置 -
使用
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;
});
...
简析它的过程:
-
首先,使用了
patchFactory这个工厂函数进行构建,在这个函数中主要进行了一些数据初始化,如上下文、文档、元素路径、父元素等等。 -
使用
enterNode修改currentParent为currentNode,将currentNode置为null。 - 使用传入的函数及数据进行更新操作。
-
使用
exitNode清空当前范围的未访问节点,重置currentNode和currentParent属性。
在第三步的更新函数中,一般会使用 text
、 elementOpen
等会在真实DOM上进行修改操作的函数。
模板
可以参考官方的 ecosystem
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
PHP 5完全攻略
杜江 / 2010-5 / 79.00元
《PHP 5完全攻略(畅销书升级版)》是目前第一本真正介绍PHP 5及MySQL 5新增语法与功能的中文版本权威宝典!《PHP 5完全攻略(畅销书升级版)》本着精、全、要三宗旨,从理论中延伸,从实践中深入,翔实并完善地描述了PHP 5的开发特性与MySQL 5数据库。《PHP 5完全攻略(畅销书升级版)》分为两大部分,第1部分主要阐述PHP开发的基础知识,如PHP数组与表单处理、PHP 5面向对象......一起来看看 《PHP 5完全攻略》 这本书的介绍吧!