微信小程序底层原理

6,925 阅读8分钟

本文大部分摘抄自微信官方文档,这里仅做归纳整理。

小程序的由来

首先,简单介绍一下微信小程序的发展历史。当微信中的 WebView 逐渐成为移动 Web 的一个重要入口时,微信就有相关的 JS API。早期,腾讯内部一些业务使用 WeixinJSBridge 调用了一些微信原生能力,但是没有对外开放。2015年初,微信发布了一整套网页开发工具包,称之为 JS-SDK,开放了更多微信原生能力。为了提供更原生的体验、更快的加载、更强大的能力,微信开始设计小程序应用,并于 2016 年 9 月开始内测,2017 年 1 月正式上线。

双线程架构:渲染层和逻辑层

首先,我们来简单了解下小程序的运行环境。小程序是基于双线程模型的,在这个模型中,小程序的逻辑层与渲染层分开在不同的线程运行,这跟传统的 Web 单线程模型有很大的不同,使得小程序架构上多了一些复杂度,也多了一些限制。至于为何选择基于双线程模型来搭建小程序,以及因此而产生的问题和解决方案,接下来我们将一一介绍。

小程序的渲染层和逻辑层分别由2个线程管理:渲染层的界面使用了 WebView 进行渲染;逻辑层采用 JsCore 线程运行JS脚本。一个小程序存在多个界面,所以渲染层存在多个 WebView 线程,这两个线程的通信会经由微信客户端(下文中也会采用 Native 来代指微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发,小程序的通信模型下图所示。

逻辑层和视图层必须通过 Native 层通信。逻辑层和视图层之间的工作方式为:数据变更通过 setData 驱动视图更新;视图层交互触发事件,然后触发逻辑层的事件响应函数,函数中修改数据再次触发视图更新,如下图所示。

image.png

不同的操作系统,微信小程序的运行环境稍有不同,如下图所示。但是,总的来说,都是由逻辑层和渲染层构成。开发者可以不必在意这点差异。

为什么采用双线程架构

为什么采用双线程架构?可能是出于以下原因:

  1. 安全可控,沙箱隔离,限制 DOM 和 BOM 能力
    • 不允许跳转至其他网页;
    • 不允许直接操作 DOM;
    • 不允许随意使用 window 上的某些未知的可能有危险的 API;
  2. 性能
    • 在浏览器网页中,虽然 JS 执行和 UI 渲染也是处于两个线程,但是 JS 线程和 UI 线程是互斥的(之所以设计为互斥的主要是 JS 可以操作 DOM,所以必须设计为互斥的才能避免二者的不同步问题);
    • 在小程序中,逻辑层和渲染层是独立的,二者不会互相阻塞,因此性能更优(小程序限制了 JS 操作 DOM 的能力,因此不用担心二者的不同步问题);
  3. 提供原生渲染能力和原生 API 能力;

以上只是个人理解,其实微信官方在这里详细介绍了采用双线程模型的原因,主要目的就是为了安全和管控。

数据驱动 虚拟DOM

小程序采用类似 Vue.js 的数据驱动机制进行页面的渲染与更新。

在开发 UI 界面过程中,程序需要维护很多变量状态,同时要操作对应的UI元素。随着界面越来越复杂,我们需要维护很多变量状态,同时要处理很多界面上的交互事件,整个程序变得越来越复杂。通常界面视图和变量状态是相关联的,如果有某种“方法”可以让状态和视图绑定在一起(状态变更时,视图也能自动变更),那我们就可以省去手动修改视图的工作。这个方法就是“数据驱动”。

小程序的逻辑层和渲染层是分开的两个线程。在渲染层,宿主环境会把 WXML 转化成相应的 JS 对象,在逻辑层发生数据变更的时候,通过宿主环境提供的 setData 方法将数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的 DOM 树上,进而渲染出正确的 UI 界面,如下图所示。其实就是采用了虚拟 DOM 的思想。

image.png

组件系统

小程序的视图是在 WebView 里渲染的,所以底层还是 HTML。原生 HTML 太开放了,直接采用 HTML 作为渲染层语言,会有一些安全问题。因此,微信设计一套组件框架——Exparser。基于这个框架,内置了一套组件,以涵盖小程序的基础功能。

Exparser 是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由 Exparser 组织管理。

Exparser 的组件模型与 WebComponents 标准中的 ShadowDOM 高度相似。Exparser 会维护整个页面的节点树相关信息,包括节点的属性、事件绑定等,相当于一个简化版的 Shadow DOM实现。Exparser 的主要特点包括以下几点:

  1. 基于 Shadow DOM 模型:模型上与 WebComponents 的 ShadowDOM 高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程。
  2. 可在纯 JS 环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
  3. 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。

小程序中所有节点树相关的操作都依赖于 Exparser,包括 WXML 到页面节点树的构建、createSelectorQuery 调用和自定义组件特性等。 ​

原生组件

在内置组件中,有一些组件较为特殊,它们并不完全在 Exparser 的渲染体系下,而是由客户端原生参与组件的渲染,这类组件我们称为“原生组件”,这也是小程序 Hybrid 技术的一个应用。 ​

要介绍原生组件的运行机制,我们需要从一行代码看起。

<map latitude="39.92" longtitude="116.46"></map>

在原生组件内部,其节点树非常简单,基本上可以认为只有一个div元素。上面这行代码在渲染层开始运行时,会经历以下几个步聚:

  1. 组件被创建,包括组件属性会依次赋值。
  2. 组件被插入到DOM树里,浏览器内核会立即计算布局,此时我们可以读取出组件相对页面的位置(x, y坐标)、宽高。
  3. 组件通知客户端,客户端在相同的位置上,根据宽高插入一块原生区域,之后客户端就在这块区域渲染界面
  4. 当位置或宽高发生变化时,组件会通知客户端做相应的调整

我们可以看出,原生组件在WebView这一层的渲染任务是很简单,只需要渲染一个占位元素,之后客户端在这块占位元素之上叠了一层原生界面。因此,原生组件的层级会比所有在WebView层渲染的普通组件要高。 ​

引入原生组件主要有3个好处:

  1. 扩展Web的能力。比如像输入框组件(input, textarea)有更好地控制键盘的能力。
  2. 体验更好,同时也减轻WebView的渲染工作。比如像地图组件(map)这类较复杂的组件,其渲染工作不占用 WebView 线程,而交给更高效的客户端原生处理。
  3. 绕过 setData、数据通信和重渲染流程,使渲染性能更好。比如像画布组件(canvas)可直接用一套丰富的绘图接口进行绘制。

原生组件脱离在 WebView 渲染流程外,这带来了一些限制。最主要的限制是一些 CSS 样式无法应用于原生组件,例如,不能在父级节点使用 overflow:hidden 来裁剪原生组件的显示区域;不能使用 transformrotate 让原生组件产生旋转等。

参考