详细介绍 Weex 的 JS Framework

好久之前,我写过两篇文章(《 Weex 框架中 JS Framework 的结构》,《 详解 Weex JS Framework 的编译过程》)介绍过 JS Framework。可是文章写于 2016 年 8 月份,这都是一年半之前的事了,说是“详解”其实解释得并不详细,并且是基于旧版 .we 框架写的,DSL 和底层框架各部分的功能解耦得的并非很清楚。这一年多以来 JS Framework 已经有了很大的变化,不只支持了 Vue 和 Rax,原生容器和底层接口也作了大量改造,这里再从新介绍一遍。

在 Weex 框架中的位置

Weex 是一个既支持多个前端框架又能跨平台渲染的框架,JS Framework 介于前端框架和原生渲染引擎之间,处于承上启下的位置,也是跨框架跨平台的关键。不管你使用的是 Vue 仍是 Rax,不管是渲染在 Android 仍是 iOS,JS Framework 的代码都会运行到(若是是在浏览器和 WebView 里运行,则不依赖 JS Framework)。html

js framework position

像 Vue 和 Rax 这类前端框架虽然内部的渲染机制、Virtual DOM 的结构都是不一样的,可是都是用来描述页面结构以及开发范式的,对 Weex 而言只属于语法层,或者称之为 DSL (Domain Specific Language)。不管前端框架里数据管理和组件管理的策略是什么样的,它们最终都将调用 JS Framework 提供的接口来调用原生功能而且渲染真实 UI。底层渲染引擎中也没必要关心上层框架中组件化的语法和更新策略是怎样的,只须要处理 JS Framework 中统必定义的节点结构和渲染指令。多了这么一层抽象,有利于标准的统一,也使得跨框架和跨平台成为了可能。前端

图虽然这么画,可是大部分人并不区分得这么细,喜欢把 Vue 和 Rax 以及下边这一层放一块儿称为 JS Framework。

主要功能

若是将 JS Framework 的功能进一步拆解,能够分为以下几个部分:express

  • 适配前端框架
  • 构建渲染指令树
  • JS-Native 通讯
  • JS Service
  • 准备环境接口

适配前端框架

前端框架在 Weex 和浏览器中的执行过程不同,这个应该不难理解。如何让一个前端框架运行在 Weex 平台上,是 JS Framework 的一个关键功能。api

以 Vue.js 为例,在浏览器上运行一个页面大概分这么几个步骤:首先要准备好页面容器,能够是浏览器或者是 WebView,容器里提供了标准的 Web API。而后给页面容器传入一个地址,经过这个地址最终获取到一个 HTML 文件,而后解析这个 HTML 文件,加载并执行其中的脚本。想要正确的渲染,应该首先加载执行 Vue.js 框架的代码,向浏览器环境中添加 Vue 这个变量,而后建立好挂载点的 DOM 元素,最后执行页面代码,从入口组件开始,层层渲染好再挂载到配置的挂载点上去。浏览器

在 Weex 里的执行过程也比较相似,不过 Weex 页面对应的是一个 js 文件,不是 HTML 文件,并且不须要自行引入 Vue.js 框架的代码,也不须要设置挂载点。过程大概是这样的:首先初始化好 Weex 容器,这个过程当中会初始化 JS Framework,Vue.js 的代码也包含在了其中。而后给 Weex 容器传入页面地址,经过这个地址最终获取到一个 js 文件,客户端会调用 createInstance 来建立页面,也提供了刷新页面和销毁页面的接口。大体的渲染行为和浏览器一致,可是和浏览器的调用方式不同,前端框架中至少要适配客户端打开页面、销毁页面(push、pop)的行为才能够在 Weex 中运行。前端框架

js framework apis

在 JS Framework 里提供了如上图所示的接口来实现前端框架的对接。图左侧的四个接口与页面功能有关,分别用于获取页面节点、监听客户端的任务、注册组件、注册模块,目前这些功能都已经转移到 JS Framework 内部,在前端框架里都是可选的,有特殊处理逻辑时才须要实现。图右侧的四个接口与页面的生命周期有关,分别会在页面初始化、建立、刷新、销毁时调用,其中只有 createInstance 是必须提供的,其余也都是可选的(在新的 Sandbox 方案中,createInstance 已经改为了 createInstanceContext)。详细的初始化和渲染过程会在后续章节里展开。weex

构建渲染指令树

不一样的前端框架里 Virtual DOM 的结构、patch 的方式都是不一样的,这也反应了它们开发理念和优化策略的不一样,可是最终,在浏览器上它们都使用一致的 DOM API 把 Virtual DOM 转换成真实的 HTMLElement。在 Weex 里的逻辑也是相似的,只是在最后一步生成真实元素的过程当中,不使用原生 DOM API,而是使用 JS Framework 里定义的一套 Weex DOM API 将操做转化成渲染指令发给客户端。框架

patch virtual dom

JS Framework 提供的 Weex DOM API 和浏览器提供的 DOM API 功能基本一致,在 Vue 和 Rax 内部对这些接口都作了适配,针对 Weex 和浏览器平台调用不一样的接口就能够实现跨平台渲染。dom

此外 DOM 接口的设计至关复杂,背负了大量的历史包袱,也不是全部特性都适合移动端。JS Framework 里将这些接口作了大量简化,借鉴了 W3C 的标准,只保留了其中最经常使用到的一部分。目前的状态是够用、精简高效、和 W3C 标准有不少差别,可是已经成为 Vue 和 Rax 渲染原生 UI 的事实标准,后续还会从新设计这些接口,使其变得更标准一些。JS Framework 里 DOM 结构的关系以下图所示:异步

Weex DOM

前端框架调用这些接口会在 JS Framework 中构建一颗树,这颗树中的节点不包含复杂的状态和绑定信息,可以序列化转换成 JSON 格式的渲染指令发送给客户端。这棵树曾经有过不少名字:Virtual DOM Tree、Native DOM Tree,我觉的其实它应该算是一颗 “Render Directive Tree”,也就是渲染指令树。叫什么无所谓了,反正它就是 JS Framework 内部的一颗与 DOM 很像的树。

这颗树的层次结构和原生 UI 的层次结构是一致的,当前端的节点有更新时,这棵树也会跟着更新,而后把更新结果以渲染指令的形式发送给客户端。这棵树并不计算布局,也没有什么反作用,操做也都是很高效的,基本都是 O(1) 级别,偶尔有些 O(n) 的操做会遍历同层兄弟节点或者上溯找到根节点,不会遍历整棵树。

JS-Native 通讯

在开发页面过程当中,除了节点的渲染之外,还有原生模块的调用、事件绑定、回调等功能,这些功能都依赖于 js 和 native 之间的通讯来实现。

js-native communication

首先,页面的 js 代码是运行在 js 线程上的,然而原生组件的绘制、事件的捕获都发生在 UI 线程。在这两个线程之间的通讯用的是 callNativecallJS 这两个底层接口(如今已经扩展到了不少个),它们默认都是异步的,在 JS Framework 和原生渲染器内部都基于这两个方法作了各类封装。

callNative 是由客户端向 JS 执行环境中注入的接口,提供给 JS Framework 调用,界面的节点(上文提到的渲染指令树)、模块调用的方法和参数都是经过这个接口发送给客户端的。为了减小调用接口时的开销,其实如今已经开了更多更直接的通讯接口,其中有些接口还支持同步调用(支持返回值),它们在原理上都和 callNative 是同样的。

callJS 是由 JS Framework 实现的,而且也注入到了执行环境中,提供给客户端调用。事件的派发、模块的回调函数都是经过这个接口通知到 JS Framework,而后再将其传递给上层前端框架。

JS Service

Weex 是一个多页面的框架,每一个页面的 js bundle 都在一个独立的环境里运行,不一样的 Weex 页面对应到浏览器上就至关于不一样的“标签页”,普通的 js 库没办法实如今多个页面之间实现状态共享,也很难实现跨页通讯。

在 JS Framework 中实现了 JS Service 的功能,主要就是用来解决跨页面复用和状态共享的问题的,例如 BroadcastChannel 就是基于 JS Service 实现的,它能够在多个 Weex 页面之间通讯。

准备环境接口

因为 Weex 运行环境和浏览器环境有很大差别,在 JS Framework 里还对一些环境变量作了封装,主要是为了解决解决原生环境里的兼容问题,底层使用渲染引擎提供的接口。主要的改动点是:

  • console: 原生提供了 nativeLog 接口,将其封装成前端熟悉的 console.xxx 并能够控制日志的输出级别。
  • timer: 原生环境里 timer 接口不全,名称和参数不一致。目前来看有了原生 C/C++ 实现的 timer 后,这一层能够移除。
  • freeze: 冻结当前环境里全局变量的原型链(如 Array.prototype)。

另外还有一些 ployfill:PromiseArary.fromObject.assignObject.setPrototypeOf 等。

这一层里的东西能够说都是用来“填坑”的,也是与环境有关 Bug 的高发地带,若是你只看代码的话会以为莫名奇妙,可是它极可能解决了某些版本某个环境中的某个神奇的问题,也有可能触发了一个更神奇的问题。随着对 JS 引擎自己的优化和定制愈来愈多,这一层代码能够愈来愈少,最终会所有移除掉。

执行过程

上面是用空间角度介绍了 JS Framework 里包含了哪些部分,接下来从时间角度介绍一下某些功能在 JS Framework 里的处理流程。

框架初始化

JS Framework 以及 Vue 和 Rax 的代码都是内置在了 Weex SDK 里的,随着 Weex SDK 一块儿初始化。SDK 的初始化通常在 App 启动时就已经完成了,只会执行一次。初始化过程当中与 JS Framework 有关的是以下这三个操做:

  1. 初始化 JS 引擎,准备好 JS 执行环境,向其中注册一些变量和接口,如 WXEnvironmentcallNative
  2. 执行 JS Framework 的代码
  3. 注册原生组件和原生模块

针对第二步,执行 JS Framework 的代码的过程又能够分红以下几个步骤:

  1. 注册上层 DSL 框架,如 Vue 和 Rax。这个过程只是告诉 JS Framework 有哪些 DSL 可用,适配它们提供的接口,如 initcreateInstance,可是不会执行前端框架里的逻辑。
  2. 初始化环境变量,而且会将原生对象的原型链冻结,此时也会注册内置的 JS Service,如 BroadcastChannel
  3. 若是 DSL 框架里实现了 init 接口,会在此时调用。
  4. 向全局环境中注入可供客户端调用的接口,如 callJScreateInstanceregisterComponents,调用这些接口会同时触发 DSL 中相应的接口。

再回顾看这两个过程,能够发现原生的组件和模块是注册进来的,DSL 也是注册进来的,Weex 作的比较灵活,组件模块是可插拔的,DSL 框架也是可插拔的,有很强的扩展能力。

JS Bundle 的执行过程

在初始化好 Weex SDK 以后,就能够开始渲染页面了。一般 Weex 的一个页面对应了一个 js bundle 文件,页面的渲染过程也是加载并执行 js bundle 的过程,大概的步骤以下图所示:

execute js bundle

首先是调用原生渲染引擎里提供的接口来加载执行 js bundle,在 Android 上是 renderByUrl,在 iOS 上是 renderWithURL。在获得了 js bundle 的代码以后,会继续执行 SDK 里的原生 createInstance 方法,给当前页面生成一个惟一 id,而且把代码和一些配置项传递给 JS Framework 提供的 createInstance 方法。

在 JS Framework 接收到页面代码以后,会判断其中使用的 DSL 的类型(Vue 或者 Rax),而后找到相应的框架,执行 createInstanceContext 建立页面所须要的环境变量。

create instance

在旧的方案中,JS Framework 会调用 runInContex 函数在特定的环境中执行 js 代码,内部基于 new Function 实现。在新的 Sandbox 方案中,js bundle 的代码再也不发给 JS Framework,也再也不使用 new Function,而是由客户端直接执行 js 代码。

页面的渲染

Weex 里页面的渲染过程和浏览器的渲染过程相似,总体能够分为【建立前端组件】-> 【构建 Virtual DOM】->【生成“真实” DOM】->【发送渲染指令】->【绘制原生 UI】这五个步骤。前两个步骤发生在前端框架中,第三和第四个步骤在 JS Framework 中处理,最后一步是由原生渲染引擎实现的。下图描绘了页面渲染的大体流程:

render process

建立前端组件

以 Vue.js 为例,页面都是以组件化的形式开发的,整个页面能够划分红多个层层嵌套和平铺的组件。Vue 框架在执行渲染前,会先根据开发时编写的模板建立相应的组件实例,能够称为 Vue Component,它包含了组件的内部数据、生命周期以及 render 函数等。

若是给同一个模板传入多条数据,就会生成多个组件实例,这能够算是组件的复用。如上图所示,假若有一个组件模板和两条数据,渲染时会建立两个 Vue Component 的实例,每一个组件实例的内部状态是不同的。

构建 Virtual DOM

Vue Component 的渲染过程,能够简单理解为组件实例执行 render 函数生成 VNode 节点树的过程,也就是构建 Virtual DOM 的生成过程。自定义的组件在这个过程当中被展开成了平台支持的节点,例如图中的 VNode 节点都是和平台提供的原生节点一一对应的,它的类型必须在 Weex 支持的原生组件范围内。

生成“真实” DOM

以上过程在 Weex 和浏览器里都是彻底同样的,从生成真实 DOM 这一步开始,Weex 使用了不一样的渲染方式。前面提到过 JS Framework 中提供了和 DOM 接口相似的 Weex DOM API,在 Vue 里会使用这些接口将 VNode 渲染生成适用于 Weex 平台的 Element 对象,和 DOM 很像,但并非“真实”的 DOM。

发送渲染指令

在 JS Framework 内部和客户端渲染引擎约定了一系列的指令接口,对应了一个原子的 DOM 操做,如 addElement removeElement updateAttrs updateStyle 等。JS Framework 使用这些接口将本身内部构建的 Element 节点树以渲染指令的形式发给客户端。

绘制原生 UI

客户端接收 JS Framework 发送的渲染指令,建立相应的原生组件,最终调用系统提供的接口绘制原生 UI。具体细节这里就不展开了。

事件的响应过程

不管是在浏览器仍是 Weex 里,事件都是由原生 UI 捕获的,然而事件处理函数都是写在前端里的,因此会有一个传递的过程。

fire event

如上图所示,若是在 Vue.js 里某个标签上绑定了事件,会在内部执行 addEventListener 给节点绑定事件,这个接口在 Weex 平台下调用的是 JS Framework 提供的 addEvent 方法向元素上添加事件,传递了事件类型和处理函数。JS Framework 不会当即向客户端发送添加事件的指令,而是把事件类型和处理函数记录下来,节点构建好之后再一块儿发给客户端,发送的节点中只包含了事件类型,不含事件处理函数。客户端在渲染节点时,若是发现节点上包含事件,就监听原生 UI 上的指定事件。

当原生 UI 监听到用户触发的事件之后,会派发 fireEvent 命令把节点的 ref、事件类型以及事件对象发给 JS Framework。JS Framework 根据 ref 和事件类型找到相应的事件处理函数,而且以事件对象 event 为参数执行事件处理函数。目前 Weex 里的事件模型相对比较简单,并不区分捕获阶段和冒泡阶段,而是只派发给触发了事件的节点,并不向上冒泡,相似 DOM 模型里 level 0 级别的事件。

上述过程里,事件只会绑定一次,可是极可能会触发屡次,例如 touchmove 事件,在手指移动过程当中,每秒可能会派发几十次,每次事件都对应了一次 fireEvent -> invokeHandler 的处理过程,很容易损伤性能,浏览器也是如此。针对这种状况,可使用用 expression binding 来将事件处理函数转成表达式,在绑定事件时一块儿发给客户端,这样客户端在监听到原生事件之后能够直接解析并执行绑定的表达式,而不须要把事件再派发给前端。

写在最后

Weex 是一个跨端的技术,涉及的技术面比较多,只从前端或者客户端的某个角度去理解都是不全面的,本文只是之前端开发者的角度介绍了 Weex 其中一部分的功能。若是你对 Weex 的 JS Framework 有什么新的想法和建议,欢迎赐教;对 Weex 有使用心得或者踩坑经历,也欢迎分享。

相关文章
相关标签/搜索