Weex 中的 virtual-DOM 介绍

概述

Weex 在 JS 端有一层 virtual-DOM 的设计,这一层设计一方面使得 Weex 能够通过 JS 控制 native 的视图层,另外也提供了一个相对中立的规范,供上层 JS 框架调用。

传统的 DOM 大概是这个样子的

// 构造函数
HTMLElement
HTMLInputElement
Text
Comment
// 创建元素
var text = document.createTextNode('User Name:')
var el = document.createElement('input')
var note = document.createComment(someNoteTextHere)
// 特性
el.setAttribute('placeholder', 'Hello')
// 样式
el.style.width = '200px'
// 属性和方法
el.value = username
// DOM 事件
el.addEventListener('focus', eventHandler)
// DOM 0 级事件
el.onchange = changeHandler
// DOM 数管理
// document.body 作为现成的页面根元素
document.body.appendChild(text)
document.body.appendChild(el)
document.body.insertBefore(note, el)

Weex 对 DOM 设计的简化和取舍

我们对 virtual-DOM 的设计很大程度上借鉴了 HTML DOM 的设计,不论从 API 还是 class,但做了一定的简化和取舍,主要包括以下几点:

  1. 传统的 HTML DOM 分了很多种 nodeType,比如 ElementTextNodeCommentCDATAEntityAttributeFragment … 等, Weex 只保留了 ElementComment ,一个 Element 对应着 native 的一个 View,而 Comment 通常对 native 来说是无意义的,但是它可以帮助 JS 上层的框架用作一些特殊处理时的 placeholder。
  2. 传统的 HTML DOM 是既有 attribute 又有 property 的,property 里还包括 style、方法调用这样的特殊 property, Weex 只保留了 attributes,没有 properties ,但支持一个特殊的维度,就是样式 style。
  3. 传统的 HTML DOM 是支持同一个 Element 绑定多个事件的,从 JS 和 native 通信的角度,这样做是没有必要的, 所以 Weex 只提供了 DOM Level 0 的事件模型,也就是 onxxx="fn" 如果同一个 Element 需要在业务层绑定多个事件,可以在 virtual-DOM 上层再进行封装
  4. 传统的 HTML DOM 事件是存在捕获和冒泡阶段的, Weex 做了精简,没有支持冒泡或捕获事件 ,只有在 native 层的当前元素触发该事件才会 fireEvent 给 JS。
  5. 传统的 HTML DOM 针对每个页面有唯一且现成的 documentdocument.documentElementdocument.body,但是在 Weex 中,由于每个页面需要的初始化 body 类型是有选择的,基本上分 scrollerdivlist 这三种,根据页面不同的展示特征而定, 所以 Weex 页面的 document.body 是需要手动创建的,并且有机会制定其类型为 scrollerdivlist 其中的一种。
  6. Weex 不支持 XML 的 namespace 语法

所以综上所述,这是一个非常精简版的 virtual-DOM 设计。我们在 Weex 中所能感受到的各种视觉效果和交互效果,实际上都是通过这样的 virtual-DOM 结构进行分解和执行的。

示例说明

接下来结合几个例子介绍一下

创建元素

document.createElement('div')
new Element('div') // just the same as `document.createElement`
new Element('text') // no `TextNode` but `<text>` element
new Element('text', {
  attr: {
    value: username
  },
  style: {
    fontSize: 14 // no `px` unit
  }
})
new Comment(someNoteTextHere)

// especially `<body>` need to be created with a certain type
// between `div`, `scroller` and `list`
// once the `<body>` created, you can access `document.body`
document.createBody('div')
// just the same as:
var body = document.createBody('div')
document.documentElement.appendChild(body)

创建的方式和传统的 DOM 是很接近的,只不过我们这里没有 TextNode 这种类型,另外 body 是需要自行创建的

比如展示一张图片

document.createBody('div')
var el = document.createElement('image')
el.setAttr('src', imageUrl)
el.setStyle('width', 200)
el.setStyle('height', 200)
document.body.appendChild(el)

特性 (attr) 和样式 (style)

我们可以通过 setAttrsetStyle 来对特性和样式进行设置,同时,我们也可以直接访问:

el.ref // 每个被创建的元素的唯一标识,通常用来传递给 native 端做结点识别
el.attr.src // imageUrl
el.style.width // 200
el.style.height // 200

来获取当前元素的值

处理文本结点

由于我们对 DOM 模型做了简化,所以 TextNode 在 Weex 的 virtual-DOM 里是没有的,我们将其简化成了父元素的 value 特性值,并引入了 <text> 类型的元素,这样文本结点就可以通过这样的方式和其它元素的结构统一,比如:

<!-- 传统 DOM 的结构 -->
<div>
  Hello
  <span style="font-weight: bold;">World</span>
</div>

<!-- Weex 中对应的 DOM 结构 -->
<div>
  <text>Hello</text>
  <text style="font-weight: bold;">World</text>
</div>

<!-- Weex 中等价的 DOM 结构 -->
<div>
  <text value="Hello"></text>
  <text style="font-weight: bold;" value="World"></text>
</div>

所以我们可以把代码写成:

var text = document.createElement('text')
text.setAttr('value', username)

事件管理

绑定事件和解绑事件分别是 addEvent(type, handler)removeEvent(type) 两个非常简单的 API

text.addEvent('click', function (e) {
  e.target // text
  e.target.attr.value // username
  e.timestamp
  e.type // 'click'
})

DOM 树管理

主要用到的几个操作和传统 DOM 的一样:

  • element.parentNode
  • element.children[]
  • element.nextSibling
  • element.previousSibling
  • parent.appendChild(child)
  • parent.insertBefore(child, before)
  • parent.removeChild(child)

额外的,我们还提供了

  • parent.insertAfter(child, after):和 insertBefore 相反
  • parent.clear():删除所有子元素

用注释处理占位符

我们可以创建一些 Comment 类型的结点,作为某些特殊处理时用到的占位符,比如:

document.createComment('start') // <!--start-->
document.createComment('end') // <!--end-->

假设我想划分一个固定的范围将来收集一个或多个可改变的元素,比如:

<div>
  <something-before></something-before>
  <something-before></something-before>
  <something-before></something-before>
  <foo></foo>
  <bar></bar>
  ...
  <something-after></something-after>
  <something-after></something-after>
  <something-after></something-after>
</div>

那么你完全可以提前在这段连续的 DOM 结点两侧设置好两个注释结点

<div>
  <something-before></something-before>
  <something-before></something-before>
  <something-before></something-before>
  <!--start-->
  <foo></foo>
  <bar></bar>
  ...
  <!--end-->
  <something-after></something-after>
  <something-after></something-after>
  <something-after></something-after>
</div>

这两个结点不会对 native 端真正的渲染造成困扰

而为了方便开发者很好的区分 el.children[] 中哪些元素会真正被渲染到 native 端,我们提供了另外一个字段,叫 el.pureChildren[]。比如上述的例子中,根元素的 children 大致内容是:

<something-before>x3, <!--start-->, <foo>, <bar>, ..., <!--end-->, <something-after>x3

而根元素的 pureChildren 则大致内容是

<something-before>x3, <foo>, <bar>, ..., <something-after>x3

这样就同时方便了上层框架的结构化管理以及 native 端接受指令的纯净度

DOM 操作对应的背后的 native 指令

我们把所有的 DOM 操作归纳成了下面几种指令,每当我们用 JS 操作 virtual-DOM 的时候,实际上背后是这些命令在驱动 native 渲染层进行渲染的:

  • document.createBody(type) -> nativeDomModule.createBody(type)
  • el.appendChild(child) -> nativeDomModule.addElement(el.ref, child.toJSON(), -1)
  • el.insertBefore(child, before) -> nativeDomModule.addElement(el.ref, child.toJSON(), el.pureChildren.indexOf(before))
  • el.removeChild(child) -> nativeDomModule.removeElement(child.ref)
  • el.setAttr(k, v) -> nativeDomModule.updateAttr(el.ref, {[k]: v})
  • el.setStyle(k, v) -> nativeDomModule.updateStyle(el.ref, {[k]: v})
  • el.addEvent(type, handler) -> nativeDomModule.addEvent(el.ref, type)
  • el.removeEvent(type) -> nativeDomModule.removeEvent(el.ref, type)

其中的一个细节是,在 el.addEvent(type, handler) 的时候,函数 handler 实际上并没有传递给 native 端,因为 native 端不需要知道这个函数是什么,handler 是在 JS 这边自己记录下来的,当有事件触发时,JS 会收到来自 native 的 fireEvent 事件,然后再在 JS 端匹配需要被触发的 handler

更细节的 API 设计详见文档:https://github.com/alibaba/weex/blob/dev/doc/specs/virtual-dom-apis.md

可改进的空间

目前随着 Weex 的技术形态不断发展,之前精简掉的一部分内容可能还是会随时拿出来进行讨论,这里分享几个自己最近想到的

1 Element 的 property

没有 property 对于某些元素来说确实会显得有些不方便,比如 <web> 元素在处理前进后退刷新的时候实际上是和另外一个完全解耦的 native webview module 配合使用的,大概写法:

var el = document.createElement('web')
web.setAttr('src', pageUrl)
...
// 目前的写法
var webviewModule = require('@weex-module/webview')
webviewModule.goBack(el)

// 希望未来可以直接在 元素 上调用自己的方法
el.goBack()

但是 JS 中判断一个元素具备哪些可调用的方法,在 ES 的 Proxy 特性普及之前是很难直接支持的,一个看上去可行的办法是每个组件可以在 JS 引擎初始化的时候注册自己可被调用的方法名称,这样我们在创建元素的时候有机会通过这些知识,把该类型元素支持的成员方法对应的绑定在元素上。

2 性能

目前 DOM 操作的性能还有待提高,尤其是 JS 和 native 之间的通信的时间代价是一个不可忽视的成本,如何尽量回避这部分的开销一直是团队通过各种努力改进的地方

3 全局事件

在传统的网页中,我们通常习惯把网页全局或具体元素无关的事件绑定在 window 上 (由于一些历史原因,部分全局事件在 documentdocument.body 上一样可以绑得上),但是:

  1. Weex 没有 window 这样的 host
  2. 这种中心化的设计容易使本来各自独立的模块相互“打架”,目前这种实践在 HTML5 最新的规范设计原则中已经不被推荐了

所以我们倾向于各子功能模块且具体元素无关的事件绑定在各自的功能模块上,另外有些事件特别简单,做成模块会显得有点笨重,或许在未来一个合适的时机我们还会把全局事件引入,但不会推荐大家滥用

上层 JS Framework 对 virtual-DOM 的使用

Weex JS Framework

Weex 的 JS Framework 在基础的 DOM 设计之上主要封装了一个叫做 Block 的东西

block

Block 是用头尾两个 Comment 结点来标识一块 DOM 区间的,Block 的数据结构其实就是:

  1. 一个绑定的目标,通常情况下是一个根元素,但也可以是另一个 Block,这样可以形成 Block 嵌套
  2. 一个开头的注释结点
  3. 一个结尾的注释结点

它主要用在模板编译的时候,比如 if, repeat, 动态组件类型等场景

如果一个元素具有 if 指令,比如 <foo if="{{expr}}">,那么我会在当前位置创建两个 start 和 end 的 Comment 结点,将来 {{expr}} 的值发生改变的时候,我们可以这样判断:

  • 如果为真,那么就生成 <foo> 元素,放在 start 和 end 中间
  • 如果为假,那么把 start 和 end 中间的元素删掉

同理,如果我们面对 <foo repeat="{{list}}">,则每次 list 发生变化的时候,我们可以这样判断:

  • 把 start 和 end 之间的所有元素找出来形成一个列表
  • 和新的 list 应该生成的元素列表进行比对
  • 在 start 和 end 之间的范围内完成元素的更新

其它场景道理接近,这里不再赘述

然后我们又基于 Block 封装了几个常用操作:

  • createBlock(vm, target)
  • attachTarget(vm, target, dest)
  • moveTarget(vm, target, after)
  • removeTarget(vm, target)

这其中 targetdestafter 都既可以是 Element 也可以是 Block,所以抽象层面可以同等对待,实际渲染层面,只会渲染 Element。我们可以以此进行更高集成度的 virtual-DOM 操作。

代码在 https://github.com/alibaba/weex/blob/dev/html5/default/vm/dom-helper.js

Vue 2.0 for Weex

对于 Vue 2.0 在 Weex 上的应用,也简单介绍一下,其实 Vue 2.0 本身已经有一个 virtual-DOM 层了,并抽象了这么几个 functional 的操作:

  • createElement(tagName)
  • createElementNS(namespace, tagName)
  • createTextNode(text)
  • insertBefore(node, target, before)
  • removeChild(node, child)
  • appendChild(node, child)
  • parentNode(node)
  • nextSibling(node)
  • tagName(node)
  • setTextContent(node, text)
  • childNodes(node)

其和 Weex 本身的 virtual-DOM 设计几乎是对应的,只有几个小的地方,我们做了特殊处理:

  1. 我们没有 namespace,所以第二个 API 我们就没有支持
  2. 我们没有 TextNode,所以我们做了巧妙的转换,将其转换成 parentNodevalue attribute

其它基本都是“无脑”转换和适配就搞定了

代码在 https://github.com/weexteam/weex-vue-framework/blob/weex-port/src/platforms/weex/runtime/node-ops.js

额外提一下,就是 Vue 2.0 里有 functional component 这个概念,上层语法上是一个标签,但实际上真正的 DOM 结构里是没有这个标签的,他只表示一个抽象的功能,比如 <transition>,这个地方脑洞很大,虽然不是我们这里想介绍的 virtual-DOM,但是一个如虎添翼配合使用的很赞的点。

Rx for Weex

这部分内容就有待 @元彦 为我们补充了,时间精力关系,这里不再展开:)

总结

以上介绍了 Weex 的 virtual-DOM 设计,以及我们基于 native 的实际情况和传统 DOM 在设计上的平衡和取舍,也举了一些例子和细节,最后介绍了 Weex JS Framework 和 Vue 2.0 在 Weex 上的 virtual-DOM 上层实践。希望对大家更好的理解 Weex 的工作原理,更好的实践 Weex 有所帮助

谢谢

时间: 2024-08-03 14:45:10

Weex 中的 virtual-DOM 介绍的相关文章

weex中使用数据流工具Vuex实践

背景 weex刚开源不久,作为一名前端,当然是抑制不住自己的好奇心想要尝尝鲜.虽然weex的最大亮点在于对于电商类应用场景能够提供快速动态部署的功能,但是用js就能写跑在native端的页面更加吸引我.于是在空余时间就开始捣腾着weex,想做一个native app看看weex有什么"能耐". 在开发过程中,在体会到weex周边工具带来的效率提升的同时,也发现了不少问题.除了weex本身刚开源肯定会存在各种问题之外,还有一些开发体验的问题.weex相关的问题都在GitHub上提了iss

学Silverlight 2系列(20):如何在Silverlight中与HTML DOM交互(下)

Silverlight中内置了对于HTML.客户端脚本等的支持,本文为如何在 Silverlight 2中与HTML DOM交互第二部分.在第一部分中主要介绍了如何访问 和修改已有的HTML DOM,我们还可以完全创建一个新的DOM元素或者移除一个已 有的DOM元素,除此之外,我们还可以为DOM元素添加事件处理. 创建DOM元素 首先我们来看如何创建一个新的DOM元素,最终的效果如下,当我们在文本框 中输入文字后,单击创建,将在上面的区域中创建一个li元素. 先来定义一下HTML页面,甚至Sil

如何修改虚拟机中(Microsoft Virtual PC或VMware)的bios

修改Virtual PC虚拟机BIOS Microsoft Virtual PC,它的优势是内存占用小,与操作系统的兼容性强.通过修改虚拟机BIOS信息中的OEM字符,这样就可以安装OEM版本的Windows XP实现免激活了. 1. 需要准备的工具软件 为了修改虚拟机的BIOS信息,我们需要准备好如下工具软件: (1)Microsoft Virtual PC 2004:安装SP1补丁包后版本号为5.3.582.27. (2)ResScope:这是一个类似于eXeScope的软件资源分析和编辑工

Extjs中常用表单介绍与应用_extjs

目标: 知道表单面板如何创建 了解表单面板中xtype的类型的应用 知道表单面板如何验证,绑定,取值 综合应用表单面板(玩转它) 内容: 首先我们要理解的是FormPanel也是继承panel组件的.所以它有着panel的属性 要创建一个表单面板其实很简单 var MyformPanel=new Ext.form.formpanel(); 表单面板和面板一样只是作为一个容器出现的,需要我们使用items加入各控件元素来丰富我们的表单面板, defaults:{},此属性提取了items中各组件项

MathType标签栏中的一些符号介绍

  MathType标签栏中的一些符号介绍          MathType标签栏是下图中红框中的部分: MathType标签栏示例 MathType标签栏对常用的公式或符号做了一个简单的分类,第一个就是代数类(Algebra),在这个类别下就是我们常用的代数公式,根号,差别公式,求根公式,极限等等,如果你要保存一些其它的代数公数,也可以将保存的公式放在代数类别下,这样在使用时便于查找. 第二个是微分类(Darivs),这个类别下保存的常用的公式就是我们经常会用到的一些微分公式或者符号,如果我

[推荐系统]Mahout中相似度计算方法介绍

Mahout中相似度计算方法介绍      在现实中广泛使用的推荐系统一般都是基于协同过滤算法的,这类算法通常都需要计算用户与用户或者项目与项目之间的相似度,对于数据量以及数据类型不同的数据源,需要不同的相似度计算方法来提高推荐性能,在mahout提供了大量用于计算相似度的组件,这些组件分别实现了不同的相似度计算方法.下图用于实现相似度计算的组件之间的关系: 图1.项目相似度计算组件 图2.用户相似度计算组件 下面就几个重点相似度计算方法做介绍: 皮尔森相关度 类名:PearsonCorrela

IE中的XML DOM

dom|xml 当微软在IE 5.0中第一次加入对XML支持时,他们只是在MSXML ActiveX库(最初是为了在IE 4.0中解析Active Channels的组件)中实现XML的功能.最初的版本并没有打算公开使用,然而随着开发人员逐渐了解这个组件并尝试使用时,微软才意识到这个库的重要性,很快就在IE 4.01中发布了MSXML完全升级版本.MSXML最初还只是IE的一个组件.直到2001年,微软发布了MSXML 3.0,这是一个通过其公司网站独立发布的产品.在2001年晚些时候,微软又发

在Silverlight 2应用程序中集成Virtual Earth

概述 Virtual Earth是什么,我想不用多做解释了.微软在推出自己的Virtual Earth之后,开放了大量的 APIs,使得我们可以方便集成到自己的应用程序中. 在HTML中集成 在开始之前,我们先来简单看一下如何在HTML中集成Virtual Earth,大家可以去这里查询相关APIs, 我们来看看如何加载默认地图,如下代码所示: <html> <head> <title></title> <meta http-equiv="C

《Effective C++》读书笔记09:绝不在构造和析构过程中调用virtual函数

首先明确一下,对于一个继承体系,构造函数是从基类开始调用了,而析构函数则正 好相反,从最外层的类开始. 对于在构造函数中调用virtual函数,先举个例子: 1 class Transaction //所有交易的基类 2 { 3 public: 4 Transaction(); 5 virtual void logTransaction() const = 0;//日志记 录,因交易类型的不同而有不同的记录 6 } 7 8 Transaction::Transaction()//构造函数实现 9