incremental-dom简析
栏目: JavaScript · 发布时间: 5年前
内容简介:想必对于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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web开发敏捷之道
Sam Ruby、Dave Thomas、David Heineme Hansson / 慕尼黑Isar工作组、骆古道 / 机械工业出版社 / 2012-3-15 / 59.00元
本书第1版曾荣获Jolt大奖“最佳技术图书”奖。在前3版的内容架构基础上,第4版增加了关于Rails中新特性和最佳实践的内容。本书从逐步创建一个真正的应用程序开始,然后介绍Rails的内置功能。全书分为3部分,第一部分介绍Rails的安装、应用程序验证、Rails框架的体系结构,以及Ruby语言的知识;第二部分用迭代方式创建应用程序,然后依据敏捷开发模式搭建测试案例,最终用Capistrano完成......一起来看看 《Web开发敏捷之道》 这本书的介绍吧!