关于前端:探索小程序底层架构原理

7次阅读

共计 6941 个字符,预计需要花费 18 分钟才能阅读完成。

双线程架构

在这之前,咱们先来思考一个问题,小程序在架构上为什么会抉择双线程?

为什么是双线程?

加载及渲染性能

小程序的设计之初就是要求疾速,这里的快指的是加载以及渲染。

目前支流的渲染形式有以下 3 种:

  • Web 技术渲染
  • Native 技术渲染
  • Hybrid 技术渲染(同时应用了 webview 和原生来渲染)

从小程序的定位来讲,它就不可能用纯原生技术来进行开发,因为那样它的编译以及发版都得追随微信,所以须要像 Web 技术那样,有一份随时可更新的资源包放在近程,通过下载到本地,动静执行后即可渲染出界面。

但如果用纯 web 技术来开发的话,会有一个很致命的毛病那就是在 Web 技术中,UI 渲染跟 JavaScript 的脚本执行都在一个单线程中执行,这就容易导致一些逻辑工作抢占 UI 渲染的资源,这也就跟设计之初要求的 相违反了。

因而微信小程序抉择了 Hybrid 技术,界面次要由成熟的 Web 技术渲染,辅之以大量的接口提供丰盛的客户端原生能力。同时,每个小程序页面都是用不同的 WebView 去渲染,这样能够提供更好的交互体验,更贴近原生体验,也防止了单个 WebView 的工作过于沉重。

微信小程序是以 webview 渲染为主,原生渲染为辅的混合渲染形式

管控平安

因为 web 技术的灵便凋谢特点,如果基于纯 web 技术来渲染小程序的话,势必会存在一些不可控因素和平安危险。

为了解决平安管控的问题,小程序从设计上就阻止了开发者去应用一些浏览器提供的开放性 api,比如说跳转页面、操作 DOM 等等。如果把这些货色一个一个地去退出到黑名单,那么势必会陷入一个十分蹩脚的循环,因为浏览器的接口也十分丰盛,那么就很容易脱漏一些危险的接口,而且就算是禁用掉了所有的接口,也防不住浏览器内核的下次更新。

所以要彻底解决这个问题,必须提供一个沙箱环境来运行开发者的 JavaScript 代码。这个沙箱环境只提供纯 JavaScript 的解释执行环境,没有任何浏览器相干接口。那么像HTML5 中的 ServiceWorkerWebWorker 个性就合乎这样的条件,这两者都是启用另一线程来执行 javaScript

这就是小程序双线程模型的由来:

  • 渲染层: 界面渲染相干的工作全都在 WebView 线程里执行,通过逻辑层代码去管制渲染哪些界面。一个小程序存在多个界面,所以渲染层存在多个 WebView。
  • 逻辑层: 创立一个独自的线程去执行 JavaScript,在这个环境下执行的都是无关小程序业务逻辑的代码。

双线程模型

小程序的架构模型有别与传统 web 单线程架构,小程序为双线程架构。

微信小程序的渲染层与逻辑层别离由两个线程治理,渲染层的界面应用 webview 进行渲染;逻辑层采纳 JSCore运行 JavaScript 代码。

webview 渲染线程

如何找到渲染层?

  1. 咱们能够通过调试微信开发者工具:微信开发者工具 -> 调试 -> 调试微信开发者工具
  1. 而后咱们会再看到一个调试界面,看起来跟咱们平时用的浏览器调试界面简直一摸一样

但这并不是小程序的渲染层,而是开发者工具的构造。但咱们在外面能够发现有一些 webview 标签,在第一个 webview 上的 src 属性看着是不是有点眼生,没猜错的话它就是咱们以后小程序关上页面的门路。所以这个 webview 才是小程序真正的渲染层。这里你会发现它外面并不只有一个 webview,其实外面蕴含着 视图层的 webview业务逻辑层 webview开发者工具的 webview

开发者工具的逻辑层跑在 webview 中次要是为了模仿真机上的双线程

  1. 关上渲染层一探到底

通过 showdevTools 办法来关上调试此 webview 界面的调试器

document.querySelectorAll('webview')[0].showDevTools(true)

这里咱们看到的才真正是小程序的渲染层,也就是小程序代码编译后的样子,咱们会发现这里的标签都与咱们开发时写的不一样,都对立加了 wx- 前缀。理解过 webComponent 的同学置信一眼就能看出他们十分类似,但小程序并没有间接应用webComponent,而是自行搭建了一套组件零碎Exparser

Exparser的组件模型与 WebComponents 规范中的 Shadow DOM 高度类似。Exparser会保护整个页面的节点树相干信息,包含节点的属性、事件绑定等,相当于一个简化版的 Shadow DOM 实现。

为什么不间接应用webComponent,而是抉择自行搭建一套组件零碎?

<details>
<summary> 点击查看 </summary>

  • 管控与平安:web 技术能够通过脚本获取批改页面敏感内容或者随便跳转其它页面 <br/>
  • 能力无限:会限度小程序的表现形式 <br/>
  • 标签泛滥:减少了解老本 </details>

JSCore 逻辑线程

逻辑层咱们间接在小程序开发者工具的调试器中输出 document 就能看到

小程序将所有业务代码置于同一个线程中运行,在小程序开发者工具中逻辑线程同样是跑在一个 webview 中;webview 中的 appservice.html 除了引入业务代码 js 之外,还有后盾服务内嵌的一些根底性能代码。

编译原理

理解完小程序的双线程架构,咱们再来看一下小程序的代码是如何编译运行的,微信开者工具模拟器运行的代码是通过本地预处理、本地编译,而微信客户端运行的代码是额定通过服务器编译的。这里咱们还是以微信开发者工具为例来摸索一番。

在开发者工具输出 openVendor(),会帮咱们关上微信开发者工具的WeappVendor 文件夹

在这里咱们咱们会看到一些 wxvpkg 文件,这是小程序的各个版本的根底库文件,还有两个值得咱们留神的文件:wccwcsc,这两个文件是小程序的编译器,别离用来编译 wxmlwxss文件。

编译 wxml

这里咱们能够将开发者工具中的 wcc 编译器拷贝一份进去,尝试去用它编译一下 wxml 文件,看看最初的产物是什么?

咱们在终端执行一下以下命令

./wcc -b index.wxml >> wxml_output.js

而后它会在当前目录下生成一个 wxml_output.js 文件,文件中有一个十分重要的办法$gwx,该办法会返回一个函数。该函数的具体作用咱们能够尝试执行一下看看后果。

咱们关上渲染层 webview 搜寻一下该办法(为了不便查看,这里会用个小我的项目来演示)

从这里咱们能够看到该办法会传入一个小程序页面的门路,返回的仍然是一个函数

var decodeName = decodeURI("./index/index.wxml")
var generateFunc = $gwx(decodeName)

咱们尝试按这里流程执行一下 $gwx 返回的函数,看看返回的内容是什么?

<!--compiler-test/index.wxml-->
<view class="qd_container" >
  <text name="title">wxml 编译 </text>
  <view >{{name}}</view>
</view>
const func = $gwx(decodeURI('index.wxml'))
console.log(func())

没错,这个函数正是用来生成Virtual DOM

思考:为什么 $gwx 不间接生成Virtual DOM

<details>
<summary> 点击查看 </summary>

  • 双线程,须要动静注入数据
    </details>

编译 wxss

咱们同样能够用微信开发者工具中的 wcsc 来编译一下 wxss 文件。

(大家认为这里应该是会生成 css 文件还是 js 文件呢?)

咱们在终端执行一下以下命令来编译 wxss 文件

./wcsc -js index.wxss >> wxss_output.js

相比之前的 wcc 编译 wxml 文件来说,这次的编译相对来说比较简单,它次要实现了以下内容:

  • rpx 单位的换算,转换成 px
  • 提供 setCssToHead 办法将转换好的 css 增加到 head 中

rpx 动静适配

小程序提供 rpx 单位来适配各种尺寸的设施

比方:

/*index.wxss */
.qd_container {
  width: 100rpx;
  background: skyblue;
  border: 1rpx solid salmon;
}
.qd_reader {
  font-size: 20rpx;
  color: #191919;
  font-weight: 400;
}

通过编译之后会生成 setCssToHead 办法并执行

setCssToHead([".",[1],"qd_container {width:",[0,100],"; background: skyblue; border:",[0,1],"solid salmon; }\n.",[1],"qd_reader {font-size:",[0,20],"; color: #191919; font-weight: 400; }\n",])(typeof __wxAppSuffixCode__ == "undefined"? undefined : __wxAppSuffixCode__);

外面会调用 transformRPX 办法将 rpx 转成px

var transformRPX = window.__transformRpx__ || function(number, newDeviceWidth) {if ( number === 0) return 0;
number = number / BASE_DEVICE_WIDTH * (newDeviceWidth || deviceWidth);
number = Math.floor(number + eps);
if (number === 0) {if (deviceDPR === 1 || !isIOS) {return 1;} else {return 0.5;}
}
return number;
}
// 次要公式
number = number / BASE_DEVICE_WIDTH * (newDeviceWidth || deviceWidth);
number = Math.floor(number + eps);  // 为了准确
// rpx 值 / 根底设施宽 750 * 实在设施宽

渲染流程

下面理解完 wxmlwxss的编译过程,咱们再来整体理解一下页面的渲染流程。

先来理解渲染层模版

从下面的渲染层 webview 咱们能够找到这两个 webview

第一个index/indexwebview 咱们下面说了它就是对应咱们的小程序的渲染层,也就是真正的小程序页面。

那么上面这个 instanceframe.html 是什么呢?

这个 webview 其实是小程序渲染模版,关上查看一番

它其实就是提前注入了一些页面所须要的公共文件,以及红框内的一些页面独立的文件占位符,这些占位符会等小程序对应页面文件编译实现后注入进来。

如何保障代码的注入是在渲染层 webview 的初始化之后执行?

在刚刚渲染模版 webview 的下方有这样一段脚本:

if (document.readyState === 'complete') {alert("DOCUMENT_READY")
  } else {const fn = () => {alert("DOCUMENT_READY")
      window.removeEventListener('load', fn)
    }
    window.addEventListener('load', fn)
  }

很显著,这里在页面初始化实现后,通过 alert 来进行告诉。此时的 native/nw.js 会拦挡这个alert,从而晓得此时的 webview 曾经初始化实现。

整体渲染流程

理解了下面这个重要过程,咱们就能够将整个流程串联起来了

  1. 关上小程序,创立视图层页的 webview 时,此时会初始化渲染层webview,并且会将该 web view 地址设置为instanceframe.html,也就是咱们的渲染层模版
  2. 而后进入页面 /index/index,等instanceframewebview 初始化实现,会将页面index/index 编译好的代码注入进来并执行
// 将 webview src 门路批改为页面门路
history.pushState('','', 'http://127.0.0.1:26444/__pageframe__/index/index')

/*
... 
这里还有一些 wx config 及 wxss 编译后的代码
*/

// 这里是
var decodeName = decodeURI("./index/index.wxml")
var generateFunc = $gwx(decodeName)
if (decodeName === './__wx__/functional-page.wxml') {generateFunc = function () {
    return {
      tag: 'wx-page',
      children: [],}
  }
}
if (generateFunc) {var CE = (typeof __global === 'object') ? (window.CustomEvent || __global.CustomEvent) : window.CustomEvent;
  document.dispatchEvent(new CE("generateFuncReady", {
    detail: {generateFunc: generateFunc}
  }))
  __global.timing.addPoint('PAGEFRAME_GENERATE_FUNC_READY', Date.now())
} else {
  document.body.innerText = decodeName + "not found"
  console.error(decodeName + "not found")
}
  1. 此时通过 history.pushState 办法批改 webview 的 src 然而 webview 并不会发送页面申请,并且将调用 $gwx 为生成一个 generateFun 办法,后面咱们理解到该办法是用来生成虚构 dom 的
  2. 而后会判断该办法存在时,通过document.dispatchEvent 派发发自定义事件generateFuncReady 将 generateFunc 当作参数传递给底层渲染库
  3. 而后在底层渲染库 WAWebview.js 中会监听自定义事件generateFuncReady,而后通过 WeixinJSBridge 告诉 JS 逻辑层视图曾经筹备好()
  1. 最初 JS 逻辑层将数据给 Webview 渲染层,WAWebview.js在通过 virtual dom 生成实在 dom 过程中,它会挂载到页面的 document.body 上,至此一个页面的渲染流程就完结了

数据更新

小程序的视图层目前应用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。

在架构上,WebView 和 JS Core 都是独立的模块,并不具备数据间接共享的通道。所以在更新数据时必须调用 setData 来告诉渲染层做更新。

setData

  • 逻辑层虚构 DOM 树的遍历和更新,触发组件生命周期和 observer 等;
  • 将 data 从逻辑层传输到视图层;
  • 视图层虚构 DOM 树的更新、实在 DOM 元素的更新并触发页面渲染更新。

这里第二步因为 WebView 和 JS Core 都是独立的模块,数据传输是通过 evaluateJavascript 实现的,还会有额定 JS 脚本解析和执行的耗时因而数据达到渲染层是异步的。

因而切记

  • 不要频繁的去 setData
  • 不要每次 setData 都传递大量新数据(单次 stringify 后不超过 256kb)
  • 不要对后盾态页面进行 setData,会抢占正在执行的前台页面的资源

与 Vue 比照(再来看看 Vue)

整体来讲,小程序身上或多或少都有着 vue 的影子 …(模版文件,data,指令,虚构 dom,生命周期等)

但在数据更新这里,小程序却与 Vue 体现的截然不同。

1. 页面更新 DOM 是同步的还是异步的?

2. 既然更新 DOM 是个同步的过程,为什么 Vue 中还会有 nextTick 钩子?

mounted() {
  this.name = '前端南玖'
  console.log('sync',this.$refs.title.innerText) // 旧文案
  // 新文案
  Promise.resolve().then(() => {console.log('微工作',this.$refs.title.innerText)
  })
  setTimeout(() => {console.log('宏工作',this.$refs.title.innerText)
  }, 0)
  this.$nextTick(() => {console.log('nextTick',this.$refs.title.innerText)
  })
}

这里举荐浏览这篇理解更多:Vue 异步更新机制以及 $nextTick 原理

然而小程序却没有这个队列概念,频繁调用,视图会始终更新,阻塞用户交互,引发性能问题。

而 Vue 每次赋值操作并不会间接更新视图,而是缓存到一个数据更新队列中,异步更新,再触发渲染,在同一个 tick 内屡次赋值,也只会渲染一次。

原文首发地址点这里,欢送大家关注公众号 「前端南玖」,如果你想进前端交换群一起学习,请点这里

我是南玖,咱们下期见!!!

正文完
 0