小程序双线程模型

1. JavaScript 单线程模型

浏览器并不是单线程而是多进程的。
浏览器内部架构很复杂,在处理 GUI 渲染线程和 JavaScript 逻辑脚本线程上使用了互斥、阻塞的管理模式。
可以打开 chrome 浏览器,点击右上角的设置按钮然后进入 “更多工具” - “任务管理器” 查看。

每个进程之间的 资源(CPU、内存等)和 行为 (UI、逻辑等)互不共享。

GUI 渲染线程,负责把 HTML 渲染成可视化的 UI。
JavaScript 引擎线程负责解析和运行 JavaScript 代码逻辑。
定时触发器线程,负责处理 setTimeout/setInternal 定时器等。
注意 setTimeout、setInternal 并不是 JavaScript 的一部分,而是浏览器提供的能力。

GUI 渲染线程和 JavaScript 引擎线程之间的互斥、阻塞的线程管理方式,让一部分前端开发者以为浏览器是单线程的。

2. 为什么 JavaScript 被设计成单线程?

设计之初,只是为了提供简单的脚本逻辑用来处理用户交互、DOM操作等,所以从设计上必须遵循语法简单和运行机制简单这两点。

  1. 语法上,JavaScript 借鉴了 Java,但是去除了很多复杂的设定。比如类型声明,模块体系等。
  2. 运行机制上,JavaScript 并没有像 Java 那样提供多线程能力,主要就是为了避免多线程操作 DOM 造成 UI 冲突。

如果多个线程同时操作 DOM,会造成线程安全问题。在多线程安全领域有很多的解决方案,比如加入锁机制,但是会带来更多的复杂性,违背了 JavaScript 简单易用的设计初衷。这也同时解释了为什么 GUI 渲染线程和 JavaScript 引擎线程是互斥的。

当 JavaScript 代码被执行时,GUI 渲染线程会被挂起,等待 JavaScript 引擎线程空闲时再被执行,以免在渲染期间被 JavaScript 重复地修改 DOM 造成不必要的渲染压力。采用互斥的模式等待 JavaScript 代码执行完毕后,可以保证渲染是最终的执行结果。浏览器的空闲(Idle)时长是衡量网站性能的重要标准之一。空闲时长多,代表 JavaScript 逻辑不密集,以及 DOM 修改频率低。这时,浏览器可以更加快速、顺畅的影响用户的交互行为。

HTML5 引入了 WebWorker,提供了多线程执行 JavaScript 的能力,但是与其他编程语言不同的是,WebWorker 线程与主线程并不是扁平的,而是主从( Master-Slave)多线程模型。Worker 内的 JavaScript 代码不能操作 DOM,可以将其理解为线程安全的。这也是小程序双线程模型的重要基础。

3. 为什么小程序不直接使用浏览器的线程模型?

1. 产品定位

小程序的宿主是微信,但版本迭代是独立的,升级更新不依赖宿主这一点跟 Web 网站是相同的。小程序的定位是小而美,不追求在微信中实现全部的 Web 能力,所以和 Web 相比上能力上要差一些,同时具备以为微信提供的原生能力。

2. 安全方面

小程序与微信的关系与网站与浏览器的关系不同,小程序更接近 CodePen、JsFiddler 这类在线编程平台中每个程序案例与平台的关系。

平台最核心的一个考量点是为案例提供足够能力的前提下保证案例的逻辑不会危及平台的安全。

以 CodePen 为例,CodePen 使用 iframe 来呈现程序的效果,但并不会把 JavaScript 代码完全拷贝到 iframe 内运行,代码会经过一次编译流程后才会被注入到 iframe 内,基于安全的考虑,在编译过程中将一些危险的代码剔除。CodePen 引入额外的 JavaScript 编译器,保证每个案例的 JavaScript 代码是线程安全的,最基本的就是要禁止程序操作 CodePen 网站的 DOM。

如何实现程序操作不影响DOM?

  • WebWorker
    WebWorker 是线程安全的,Worker 内的 JavaScript 代码无法获取 Window 和 Document 对象,也无法操作 DOM。除此之外,由于 Worker 的线程安全特性,Worker 内的代码运行过程中不会阻塞外层的 GUI 渲染线程,它们可以并行。
  • Shadow DOM
    Shadow DOM 是 Web Components 规范中的一部分,将 ShadowRoot 的模式设置为 closed 就可以禁止获取到 ShadowRoot 节点,从而也无法操作其内部的 DOM。

Shadow DOM 的兼容性比 WebWorker 更差,不适合大规模使用,所以 WebWorkder 的方案更现实一点。这样就形成了一个简易的双线程模型。

这种建议的双线程模型存在比较严重的性能问题,Web Worker 非常耗费资源,与主线程的通信过程对性能的损耗非常严重。

4. 小程序双线程模型

小程序并不支持所有的 HTML 标签,只提供有限的几类 UI 组件。

1. 小程序技术需求

  • 限制 UI 组件类型,只允许声明指定的几个组件。
  • 保证逻辑线程安全,不允许直接操作 UI 组件。
  • 能够在线更新,不依赖微信。
  • 性能需要尽量提升,保证用户体验。

小程序更新 UI 的方式和 Vue、React 等 MVVM 框架类似,JavaScript 代码不直接操作DOM,而是通过更新 状态的方式异步更新 UI,这个过程中使用 VDOM 和高效的 DIFF 算法。

如果使用纯原生实现小程序,小程序的版本更新必须依赖微信,与微信的代码一起发版。
如果使用纯 Web 实现,安全和性能就很难得到保证。
小程序需要像 Web 一样,可以将资源托管在云端,更新独立。同时,又要能够保持足够好的安全性和性能。所以,最终小程序采用了 Hybrid-混合 的架构模式,使用 WebView 渲染UI,使用类似 WebWorker 的独立线程运行逻辑,这就是双线程模型。

2. 双线程模型

小程序的双线程并不是使用 WebWorker 子线程,而是一个独立的主线程,这样可以保证较好的性能。

小程序的双线程指的是渲染线程和逻辑线程,这两个线程分别承担 UI 的渲染和执行 JavaScript 代码的工作。

渲染线程使用 Webview 进行 UI 的渲染呈现。Webview 是一个完整的类浏览器运行环境,本身具备运行 JavaScript 的能力,但是小程序并不是将逻辑脚本放到 Webview 中运行,而是将逻辑层独立为一个与 Webview 平行的线程,使用客户端提供的 JavaScript 引擎运行代码,iOS 的 JavaScriptCore、安卓是腾讯 X5 内核提供的 JsCore 环境以及 IDE 工具的 nwjs 。

并且逻辑线程是一个只能够运行 JavaScript 的沙箱环境,不提供 DOM 操作相关的 API,所以不能直接操作 UI,只能够通过 setData 更新数据的方式异步更新 UI。

事件驱动的通信方式

小程序的渲染层与逻辑层之间的通信并不是两者之间直接传递数据或事件,而是由 Native 作为中间媒介进行转发。

整个过程是典型的事件驱动模型。渲染层通过与用户的交互触发特定的事件 Event,Event 被传递到逻辑层,逻辑层通过一系列的逻辑处理、数据请求、接口调用等行为将加工好的数据 data 传递给渲染层。最后渲染层将 data 渲染成可视化的 UI。

这种逻辑与渲染分离的线程分工模式,能够保证运行在逻辑线程沙箱内的 JavaScript 代码中是线程安全的。由于渲染线程的计算量非常小保证了对用户交互行为的快速响应,提高用户体验。

5. 总结

同浏览器的线程模型相比,小程序的双线程模型在规避 WebWorker 性能堪忧的同时,也实现了 WebWorker 相同的线程安全,从性能和安全两个角度实现了提升。

双线程模式是受限于浏览器现有进程和线程管理模式下,在小程序内的一种改进架构方案。

针对性能方面,有以下建议:

  • 在保证功能的前提下尽量使用结构简单的 UI。
  • 尽量降低 JavaScript 逻辑的复杂度。
  • 尽量减少 setData 的调用次数和携带的数据体量。