浅析weex之vdom渲染 #51

Open
wgyixin opened this Issue Jul 21, 2016 · 7 comments

Projects

None yet

8 participants

@wgyixin
wgyixin commented Jul 21, 2016

前言

前段时间进行了weex页面尝试, 页面滚动加载渲染得非常流畅, 让H5页面拥有了native般的体验。
如此之利器,让人非常想探一究竟,因此最近进行了js-framwork源码学习(weex开源地址:https://github.com/alibaba/weex),希望能进一步了解其dom渲染机制。

一. 文件结构

weex代码结构如下,重点关注其js-framework实现。

    ├── weex-dev
      ├── android
      ├── ios
      ├── bin
      ├── doc
      ├── examples
      ├── src
      │  ├── components
      │  ├── h5-render
      │  ├── js-framework
      │  ├── README.md
      ├── test
      ├── website
      ├── index.html
      ├── package.json
      ├── webpack.config.js
      ├── README.md
      └── ...

阅读js-framework代码,我整理了一份思维导图。

文件结构图

framework.js是Instance创建的入口,可以从这个文件开始自顶向下地阅读代码,了解其工作原理。可以重点理解它的DOM结构,初始化过程,数据更新过程,下面我也将从这几个方面进行描述。

二. 主要类分析

1. DOM、Listener与EventManager

DOM结构、Listener与EventManager类图

Weex的DOM结构由DocumentElementComment三类组成。Element创建普通节点,Comment用于创建frag block节点。每个节点都有一个唯一的ref值,可以很方便地在文档中被查询到,同时记录其父节点parentRef,通过这种’双向链表‘的操作可以方便进行节点拼接和获取。文档树节点Document记录整个DOM的结构,同时在Document上绑定EventManager事件处理器和Listener监听操作处理器。EventManager记录每个绑定了事件的节点和它对应的事件处理函数,提供事件的添加、删除和触发。Listener提供了dom操作转化为callNative的能力,通过将每一个操作转化为对应类型的actions,如createBodyaddElement,并将每一个actions记录updates数组。

2. compiler、directive、watcher

compiler、directive、watcher类
Weex复用了 Vue.js 的数据监听和依赖收集的代码实现。通过observer、directive、watcher之间的协作,建立数据(Model)和视图(View)的关联关系:

  • observer 对 data 进行了监听,并且提供订阅某个数据项的变化的能力
  • compiler解析template,并解析其中的 directive,得到每一个 directive 所依赖的数据项及其更新方法
  • watcher 把上述两部分结合起来,即把 directive 中的数据依赖订阅在对应数据的 observer 上,这样当数据变化的时候,就会触发 observer,进而触发相关依赖对应的视图更新方法。

三. 初始化过程

当我们在浏览器中输入我们的bundle地址,其解析渲染为HTML过程大致可以分解为createInstance->initInstace->run bundle->define->boostrap->create Vm->生命周期函数。可细化为下面这些步骤:
初始化过程

initInstance: 根据webpack打包后的js代码来定义实例。
define: 解析代码中的__weex_define__("@weex-component/bottom-bar")定义的component,包含依赖的子组件。并将component记录到customComponentMap[name] = exports数组中,维护组件与组件代码的对应关系。由于会依赖子组件,因此会被多次调用。

  • define执行后的APPInstance实例结构:
    define执行后的APPInstance实例结构

bootstrap:解析代码中的__weex_bootstrap__("@weex-component/30d1c553f95b5f87afb6a1cff70a7bbd")执行当前页面,提取customComponentMap中记录的组件代码进行初始化。只会执行一次。
downgrade: 检测页面降级配置进行页面降级。
initEvents: 绑定events和lifecycle(init、create、ready)执行的钩子。
initScope: 执行initData()、initComputed、initMethods。初始化data、computed属性和methods,并进行data的observer监听。
build: 根据预留选项opt.replace进行编译,目前该选项还未被实质使用。编译完成后执行ready的钩子命令,执行ready。
compile: 编译视图。
updateActions: 检测是否有数据更新需要执行。
createFinish: 表明dom结构创建完成,想callQueue队列中添加一个'createFinish'的actions。
processCallQueue: 依次执行队列中的actions,进行节点渲染到页面的过程,为了性能考虑,通过requestAnimationFrame进行分帧渲染。

  • callQueue队列
    callQueue

通过初始化过程我们可以得到init -> 数据监听 -> created -> 视图生成 -> ready,为了避免重复的视图操作,可在init进行数据的获取,created阶段进行数据的赋值和修改。

部分过程细化

1. compile():

compile
首先根据tagert的type类型选择不同的编译方式:数组类型、content类型(占位,可参考special-element)、repeat元素、if元素、function类型元素、子组件元素、内置定义的元素。内置元素类型可在config.js中查看,目前是text、image、container、slider、cell。以内置元素的编译为例,进行body和element节点的编译。在编译的时候会解析节点的attr、class、style、events指令,并进行监听。
从上图可知,weex提供了两种append方式:tree、node。

  • tree:先渲染子节点树,最后渲染父节点
  • node:先渲染父节点,然后子节点一个个append
  • 进行了不同的节点数量,VM创建耗时采样对比,从图中可以看出当节点个数较多的时候tree模式比node模式渲染的快
1个文本节点 第1次 第2次 第3次 第4次 第5次 平均
tree 8ms 10ms 12ms 9ms 10ms 9.8ms
node 10ms 9ms 9ms 9ms 9ms 9.2ms

20个文本节点 第1次 第2次 第3次 第4次 第5次 平均
tree 17ms 18ms 18ms 16ms 18ms 17.4ms
node 18ms 17ms 21ms 17ms 18ms 18.2ms

50个文本节点 第1次 第2次 第3次 第4次 第5次 平均
tree 32ms 28ms 26ms 27ms 27ms 28ms
node 30ms 29ms 34ms 33ms 31ms 31.4ms

100个文本节点 第1次 第2次 第3次 第4次 第5次 平均
tree 44ms 41ms 37ms 37ms 44ms 40.6ms
node 46ms 44ms 41ms 44ms 43ms 43.6ms

2. attachTarget()、updateActions()、callTasks():


attachTarget: 进行节点渲染的时候,将每个append动作细化为具体的actions,置入callQueue队列中。
updateActions: 检测是否有diff,如果有,则执行diff中记录的task
callTasks: 调用callNative,根据执行状态判断是否执行callQueue列表中的人物或者置入callQueue队列中。

四. 数据更新过程

执行click事件,其中修改了data数据值,执行顺序如下:

![](https://img.alicdn.com/tps/TB1sN8fKVXXXXbJXXXXXXXXXXXX-286-834.png) CallJS响应事件、接受事件,通过eventManager获得事件目标响应函数并fire执行,通过Watcher监听数据修改,如果数据前后不等则将修改更新操作记入diff中,同时通知订阅它的依赖继续收集更新操作。最终执行updateActions完成数据更新操作。 # 总结

通过上文分析,可以认为:

  1. tree模式比node模式渲染的快。
  2. 每个节点的创建都对应一个callQueue任务,节点逐个逐个的append到页面中。
  3. 依赖发布订阅模式收集依赖,监听每一个属性的变化,可直接获取更新操作,映射到dom结构中。
  4. 与native交互通过 CallNative()方法;响应JS调用采用CallJS()方法。

以上是个人拙见,本文中描述不正确的地方欢迎指正~

@barretlee

点个赞。

@bluemsn
bluemsn commented Jul 23, 2016

膜拜~

@lecer
lecer commented Sep 5, 2016

weex代码目录 变更了吗?js-framework 目录在哪儿,未发现你说的这些文件

@infinnie
infinnie commented Oct 7, 2016

Weex 本身就是 Native 吧 (虽然有同构 web 版)

@loganjing

很赞

@richardyuq

对双向绑定的最牛逼诠释,现在不太明,但是绝对觉厉!👍,支持tree模式渲染。

@cuanhanshansi

看不懂,但感觉很厉害的样子。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment