有过微信小程序开发经验的朋友应该都知道“双线程模型”这个概念,本文简单梳理一下双线程模型的一些科普知识,学识浅薄,若有错误欢迎指正。
我以前就职于「小程序·云开发」团队,在对外的一些培训和技术分享里经常被人问到这样一个问题:“微信小程序与 Web 网站在技术层面的主要区别是什么?”,在编程语言和范式上,小程序开发与 Web 前端开发非常相似(比如都用 JavaScript 语言、与 HTML/CSS 非常相似的 WXML/WXSS 等),可它却没有直接用原生的前端技术。
与 Web 网站相比,以微信为宿主的小程序更需要考虑安全、性能等因素,保障小程序不会对微信App本身产生安全隐患,同时要尽量达到接近原生应用的性能和用户体验。这是为什么小程序不直接用浏览器的线程模型,非要自己弄一套双线程模型最主要的两个原因。
那什么是小程序的双线程模型呢?
理解一个新概念或技术的最好的方法就是给它一个参照物,所以要搞清楚小程序的线程模型,首先要对浏览器的线程模型有一定的了解。
浏览器是多进程的可能每个前端工程师在刚入行的时候都不止一次地被面试官问到“怎么理解前端的单线程?”,因为前端核心技能之一的 JavaScript 语言是单线程的,充分理解并掌握JS单线程的运作方式对一个前端工程师来说是最基本的要求。但是很多初学者容易走入的一个误区:错误地把 “JavaScript 单线程”理解为“浏览器单线程”。
事实上,浏览器内部架构很复杂,只不过在处理 GUI 渲染线程和 JavaScript 逻辑脚本线程上用了互斥、阻塞的管理模式,让一些开发者产生了误解。
以 Chrome 浏览器为例,点击右上角的设置按钮然后进入“更多工具”->“任务管理器”会看到这样的弹窗:
能看到Chrome 开启了多个进程,包括浏览器进程、网络进程、GPU 进程等,这些都是通用的进程。请注意,上图里有两个标签页进程,Chrome 为每个标签页开启了一个独立的渲染进程( Renderer Process ),每个进程之间的资源( CPU、内存等)和行为( UI、逻辑等)互不共享,所以即便某个标签页崩溃了也不会影响其他标签页。
而在每个标签页进程中,浏览器会把不同的工作交给对应的线程,比如 GUI 渲染线程负责把 HTML 渲染成可视化的 UI;JavaScript 引擎线程负责解析和运行 JavaScript 代码逻辑;定时触发器线程负责处理 setTimeout/setInterval 定时器等。
多说一句,这里有一个很容易搞混的地方,其实setTimeout/setInterval 并不是 JavaScript 语言的一部分,而是运行时(最初是浏览器,后来 Node.js 也支持)提供的能力。
GUI 渲染线程和 JavaScript 引擎线程是互斥的,JavaScript 在执行期间会阻塞 UI 的渲染,甚至如果脚本执行时间太长会由于页面长时间无响应然后崩溃,正是 GUI 渲染线程和 JavaScript 引擎线程之间的这种互斥、阻塞的线程管理方式,让一部分前端开发者以为浏览器是单线程的。
那为什么 JavaScript 被设计成单线程的呢?
JavaScript 祖师爷只用了 10 天就创造了这门语言,最初他的想法只是在浏览器中提供一些简单的脚本逻辑用来处理用户交互、DOM 操作等,所以从设计上必须遵循两点:
语法简单;
运行机制简单。
在语法上,JavaScript 借鉴了 Java,但是去除了很多复杂的设定,比如类型声明、模块体系(后来加入)等。
在运行机制上,JavaScript 并没有像 Java 那样提供多线程能力,最主要就是为了避免多线程操作 DOM 造成 UI 冲突。比如存在多个线程同时操作同一个 DOM,浏览器该如何判断最终的 UI 效果是采用哪个线程的结果?这是经典的线程安全(也称为线程同步)问题,在多线程编程领域有很多解决方案,比如加入锁机制,但这样却又带来了更多的复杂性,与 JavaScript 简单易用的设计初衷相违背。
这同时也解释了为什么 GUI 渲染线程与 JavaScript 引擎线程是互斥的:JavaScript 代码有修改DOM 的权限。
当 JavaScript 代码被执行时,GUI 渲染线程会被挂起,等待 JavaScript 引擎线程空闲时再被执行,以免在渲染期间被 JavaScript 重复地修改 DOM 造成不必要的渲染压力。采用互斥的模式等待 JavaScript 代码执行完毕后,可以保证渲染是最终的执行结果。所以浏览器的空闲(Idle)时长也成了衡量网站性能的重要指标之一,空闲时长多代表 JavaScript 逻辑不密集以及 DOM改动频率低,这种情况下浏览器可以更快速顺畅地响应用户的交互行为,如下图: