之前写了一篇文章《Weex 框架中 JS Framework 的结构》概述了 JS Framework 的整体结构,其中编译过程写的有些简略,这里再详细介绍一下。

一句话概括 JS Framework 的编译过程就是: 将 JS Bundle 转换成 Virtual DOM 发送到原生模块渲染。

这个过程涉及三种数据类型:JS BundleVirtual DOMVm

  • JS Bundle 是由 .we 文件转换过来的,会被视为代码而执行。
  • Virtual DOM 是描述页面结构的 JSON 数据,用于给原生模块发送消息。
  • Vm 是 View Model 的简写,属于 MVVM 结构中的一部分,会执行模板编译、数据绑定等操作。

简化版的 Vm 构造函数如下:

// html5/default/vm/index.js
function Vm() {// ...// 初始化生命周期initEvents(this, externalEvents)this.$emit('hook:init')// ...// 监听 data 中的数据initState(this)this.$emit('hook:created')// ...// 启动模板编译build(this)
}

在 Vm 构造函数最后调用 build 函数启动模板的编译,是一种尾递归,便于 js 引擎优化。

整体编译流程

在调用 build 触发编译后,真正实现编译功能的是 compile 方法(代码位置在 html5/default/vm/compiler.js 中)。build 方法并不递归,它做的只是根据配置项选择合适的参数,然后调用 compile 方法。

compile 方法

compile 接受四个参数:

  1. vm: 待编译的 Vm 对象。
  2. target: 需要编译的节点,是模板中的标签经过 transformer 转换后的结构。
  3. dest: 当前节点父节点的 Virtual DOM。
  4. meta: 元数据,在内部调用时可以用来传递数据。

在 compile 函数的最后,会调用 compileNativeComponent 编译生成原生组件,除了它和 createBlock 两个方法以外,其他都是会触发递归的。

分发编译逻辑

compile 函数内部并没有渲染逻辑,他只是将不同类型的节点交给不同的函数来编译;换句话说说,它是负责逻辑的分发和实现递归的。同样职责的函数还有 compileChildren ,他会对每个子节点(模板中的子标签)调用 compilecompileFragment 方法可以编译数组,其中每个数据项都会调用 compile 方法,但是会共用一个 Block。

创建 Block

在上述的方法中,createBlock 是实际的创建节点的操作,可以视为递归的终止条件。创建的 block 是封装后的 Virtual DOM 节点,结构如下:

{blockId,start,    // 节点的起点位置,Comment 节点,在当前元素的前一个位置element,  // 实际的节点元素,Element 节点end,      // 节点的结束位置,Comment 节点,在当前元素的后一个位置
}

这样做的目的一方面是将 Element 和 Fragment 操作统一化,另一方面是为了 UI 更新时能够快速定位到节点。

编译指令

Weex 的模板标签上支持使用指令,最基本的是 ifrepeat 指令,详细用法参考官方文档。

if 指令

if 指令可以控制节点的显示和隐藏,用法如下:

<text if={{visible}}>Show something here.</text>

这段模板会被 transformer 转换成下面这种结构:

{"type": "text","shown": function () { return this.visible }
}

编译 if 指令时,先会创建一个 Block,然后创建 Watcher 执行数据绑定。当 this.visible 发生变化时,会更新视图,若 shown 函数返回为真,则会调用 compile 方法重新编译节点,否则会调用 removeTarget 将节点从父节点中删除。

repeat 指令

repeat 指令可以根据模板循环渲染数组中的所有数据。假设有如下某个 .we 文件:

<script>module.exports = {data: {images: [{ source: 'somewhere/a.png' },{ source: 'somewhere/b.png' },{ source: 'somewhere/c.png' }]}}
</script>
<template><list><cell repeat={{images}}><image src="{{source}}"></image></cell></list>
</template>

编译生成的结果如下:

{"type": "list","children": [{"type": "cell","append": "tree","repeat": function () { return this.images },"children": [{"type": "image","attr": {"src": function () { return this.source }}}]}]
}

遇到 repeat 指令时,会调用 compileRepeat 方法,它先整理好数据,然后调用 bindRepeat 执行编译。编译过程中会将 repeat 指令所在的节点视为模板,循环展开对应的数据,逐条调用 compileItem 渲染每个节点。期间也会添加数据绑定,当数组中的数据有变化时,会自动更新 List。

append 属性

如果仔细看一下上面 repeat 指令转换出来的代码,会发现 cell 节点上有一个 append 属性,这个属性在官方文档中写的比较详细了,它是用来控制渲染次序的,属于比较底层的属性,在内部指令中用到了,开发者通常不会用到。这里再总结一下:

  • append="tree" 会先编译子节点,再编译自身。编译速度快,但是容易造成较长时间的白屏。
  • append="node" 会先编译自身,再编译子节点。整体编译速度略慢,但是用户体验好一些。
  • 默认的编译方式是 node,先创建容器,再创建内容。

不过 repeat 指令默认的编译方式是 tree;由于内容可变,它是先编译生成所有子节点,然后再编译自身,避免频繁地插入操作,这种编译方式也比较符合列表的特性。

编译组件

除了内置标签以外,Weex 还支持自定义的组件(标签),这是最基本也是最好用特性了。

编译自定义的组件

每个组件(.we 文件)都对应的是一个 Vm 实例,当编译过程中遇到某标签不是内置标签而是一个自定义的组件时,会创建一个新的 Vm 实例,合并父子组件中添加的样式,同时也添加了生命周期的钩子:

  1. init: 设置子组件 id。
  2. created: bindSubVm 合并父子组件中定义的属性。
  3. ready: 调用 compileChildren 开始编译子组件中的节点。

值得注意的是,创建新的 Vm,又会执行本文从开头开始讲的所有步骤:初始化数据、数据绑定、递归编译各种节点…… 所以说整个编译过程包含了大量的递归,函数调用栈比较深,会消耗大量的内存和时间。

编译生成原生组件

compile 函数结尾会调用 compileNativeComponent 绘制原生组件,也就是说,原生组件的绘制是在递归编译过程中进行的,不需要等待完整的 Virtual DOM tree 拼好之后再绘制原生 UI,页面可以实现流式渲染,持续不断的渲染碎片化的 Virtual DOM。

compile 的各种流程产出的是 Virtal DOM,而 compileNativeComponent 实现的是将碎片化的 Virtual DOM 通过 callNative 发送渲染指令给原生模块,通知其绘制 Native UI。原生模块在绘制 UI 的过程中,如果发生了错误,会返回 -1 存放在 app.lastSignal 中,此时 JS Framework 会终止编译。

结语

接前一篇讲 JS Framework 的文章,这篇文章详细介绍了组件的编译流程,涉及很多技术细节,这些细节有可能在以后的版本中有改动。了解 JS Framework 的实现细节,有助于开发者在使用时能避开不恰当的用法,少踩一些坑,希望这篇文章能对大家有所帮助,如果有疑问或者有不同看法,欢迎来找我探讨。

详解 Weex JS Framework 的编译过程相关推荐

  1. php滚动窗口多条动态,详解原生JS是实现控制多个滚动条同步跟随滚动

    本文主要和大家详解详解原生JS是实现控制多个滚动条同步跟随滚动,当这两个容器元素的内容都超出了容器高度,即都出现了滚动框的时候,如何在其中一个容器元素滚动时,让另外一个元素也随之滚动. 在一些支持用 ...

  2. Spotify敏捷模式详解三部曲第二篇:研发过程

    本文转自:Scrum 中文网 引言 在本系列文章的第一篇,我们介绍了Spotify的敏捷研发团队,以及它独特的组织架构.Spotify的研发团队采用的是一种非常独特的组织架构,如下图所示: 整个研发组 ...

  3. layui日期与vue_详解Vue.js和layui日期控件冲突问题解决办法

    详解Vue.js和layui日期控件冲突问题解决办法 发布于 2020-8-10| 复制链接 摘记: 事故还原: 今天在用layui的日期控件的时候发现一个问题,就是form表单中的日期选择之后,如果 ...

  4. CMake使用详解二(多文件编译)

    文章目录 1 同一目录,多个源文件 1.1 创建2个源文件 1.2 编写CMakeLists.txt 1.3 编译项目 2 多个目录,多个源文件 2.1 在不同的目录下新建源代码 2.2 编写CMak ...

  5. CMake使用详解一(单文件编译)

    文章目录 1 什么是CMake 2 单个源文件编译 2.1 编写单个cpp文件 2.2 编写CMakeLists.txt 2.3 使用`cmake .`和`make`编译项目 2.3.1 执行`cma ...

  6. java源码系列:HashMap底层存储原理详解——4、技术本质-原理过程-算法-取模具体解决什么问题

    目录 简介 取模具体解决什么问题? 通过数组特性,推导ascii码计算出来的下标值,创建数组非常占用空间 取模,可保证下标,在HashMap默认创建下标之内 简介 上一篇文章,我们讲到 哈希算法.哈希 ...

  7. vue 加载页面时触发时间_详解Vue.js在页面加载时执行某个方法

    详解Vue.js在页面加载时执行某个方法 jQuery中可以这样写 vue中,如果要达到相同效果,可以使用vue的生命周期函数,如create或者mounted 附上vue.js的生命周期函数执行流程 ...

  8. vue调用手机相机相册_详解Vue.js调用手机相机和相册以及上传

    详解Vue.js调用手机相机和相册以及上传 发布于 2020-7-7| 复制链接 摘记: 组件 ```xhtml .. 组件 ```xhtml --> 选中{{imgList.length}}张 ...

  9. DDR3内存详解,存储器结构+时序+初始化过程

    转载 DDR3内存详解,存储器结构+时序+初始化过程 2017-06-17 16:10:33 a_chinese_man 阅读数 23423更多 分类专栏: 硬件开发基础 转自:http://www. ...

最新文章

  1. C语言-程序运行效率总结及注意事项
  2. 闭包函数python_Python--函数对象闭包函数
  3. 新手对于iPhone开发环境等入门问题解答汇总
  4. Linux安装Oracle11G
  5. VTK:点定位器可视化用法实战
  6. yandexbot ip列表整理做俄罗斯市场的站长可以关注一下
  7. torch.backends.cudnn.benchmark--提升卷积神经网络的运行速度
  8. 注册Nocos配置中心失败:Could not resolve placeholder ‘config.info‘ in value “${config.info}
  9. 【论文写作】毕业论文写作的基本流程
  10. ENVI的seamless mosaic工具详解
  11. 文件上传—DiskFileItemFactory核心类
  12. python应聘要求_python爬取招聘要求等信息实例
  13. 外星人m15键盘灯光设置_Alienware Command Center灯光软件高级设置
  14. 2020-10-20 Java基础_定义和语法
  15. 8/3 MATLAB绘制正态分布概率密度函数(normpdf)图形
  16. 【数据挖掘】《数据分析与数据挖掘》--天津大学公开课
  17. Python-import
  18. oracle utl file grant,oracle中设置UTL_FILE_DIR参数
  19. 利用手机相机扫描文件和身份证件
  20. 游戏动画引擎 -(Crapell Game Engine Design - animation)

热门文章

  1. python常用标准库有哪些-Python 常用的标准库以及第三方库有哪些?
  2. 学python可以做什么产品-学完Python可以做什么?主要用途有哪些?
  3. python对文件的读操作方法有哪些-用python实现读写文件常见操作方式
  4. python自然语言处理书籍推荐-python自然语言处理
  5. LeetCode Max Points on a Line
  6. 如何查看Linux上程序或进程用到的库
  7. LVS/NAT 配置
  8. Java集合及concurrent并发包总结(转)
  9. [转]JAVA实现文件压缩
  10. 图的数组(邻接矩阵)存储结构