热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

探索小程序底层架构原理

双线程架构在这之前,我们先来思考一个问题,小程序在架构上为什么会选择双线程?为什么是双线程?加载及渲染性能小程序的设计之初就是要求快速,这里的快指的是加载以及渲染。目前主流的渲

双线程架构

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


为什么是双线程?


加载及渲染性能

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

目前主流的渲染方式有以下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代码。

小程序双线程.png


webview渲染线程

如何找到渲染层?



  1. 我们可以通过调试微信开发者工具:微信开发者工具 ->调试 ->调试微信开发者工具

wx-1.png



  1. 然后我们会再看到一个调试界面,看起来跟我们平时用的浏览器调试界面几乎一摸一样

wx-2.png

但这并不是小程序的渲染层,而是开发者工具的结构。但我们在里面可以发现有一些webview标签,在第一个webview上的src属性看着是不是有点眼熟,没猜错的话它就是我们当前小程序打开页面的路径。所以这个webview才是小程序真正的渲染层。这里你会发现它里面并不只有一个webview,其实里面包含着视图层的webview业务逻辑层webview开发者工具的webview

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



  1. 打开渲染层一探究竟

通过showdevTools方法来打开调试此webview界面的调试器

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

wx-3.png

这里我们看到的才真正是小程序的渲染层,也就是小程序代码编译后的样子,我们会发现这里的标签都与我们开发时写的不一样,都统一加了wx-前缀。了解过webComponent的同学相信一眼就能看出他们非常相似,但小程序并没有直接使用webComponent,而是自行搭建了一套组件系统Exparser

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

为什么不直接使用webComponent,而是选择自行搭建一套组件系统?



点击查看

- 管控与安全:web技术可以通过脚本获取修改页面敏感内容或者随意跳转其它页面

- 能力有限:会限制小程序的表现形式

- 标签众多:增加理解成本


JSCore逻辑线程

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

wx-逻辑线程.png

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


编译原理

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

在开发者工具输入openVendor(),会帮我们打开微信开发者工具的WeappVendor文件夹

wx-4.png

在这里我们我们会看到一些wxvpkg文件,这是小程序的各个版本的基础库文件,还有两个值得我们注意的文件:wccwcsc,这两个文件是小程序的编译器,分别用来编译wxmlwxss文件。


编译wxml

这里我们可以将开发者工具中的wcc编译器拷贝一份出来,尝试去用它编译一下wxml文件,看看最后的产物是什么?

wx-5.png

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

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

然后它会在当前目录下生成一个wxml_output.js文件,文件中有一个非常重要的方法$gwx,该方法会返回一个函数。该函数的具体作用我们可以尝试执行一下看看结果。

我们打开渲染层webview搜索一下该方法(为了方便查看,这里会用个小项目来演示)

wx-6.png

从这里我们可以看到该方法会传入一个小程序页面的路径,返回的依然是一个函数

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

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



wxml编译
{{ name }}

const func = $gwx(decodeURI('index.wxml'))
console.log(func())

wx-7.png

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


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



点击查看

- 双线程,需要动态注入数据


编译wxss

我们同样可以用微信开发者工具中的wcsc来编译一下wxss文件。

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

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

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

wx-8.png

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



  • rpx单位的换算,转换成px

  • 提供setCssToHead方法将转换好的css添加到head中


rpx动态适配

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

wx-9.png

比如:

/*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

wx-10.png

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

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

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

wx-11.png

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

如何保证代码的注入是在渲染层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 逻辑层视图已经准备好()

wx-12.png



  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)
})
}

nexttick5.png

这里推荐阅读这篇了解更多:Vue异步更新机制以及$nextTick原理

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

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


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


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

作者:前端南玖

出处:https://www.cnblogs.com/songyao666/



每日面试题:Github

-------------------------------------------

个性签名:智者创造机会,强者把握机会,弱者坐等机会。做一个灵魂有趣的人!

如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~

欢迎加入前端技术交流群:928029210(QQ)

扫描下方二维码关注公众号,回复进群,拉你进前端学习交流群(WX),这里有一群志同道合的前端小伙伴,交流技术、生活、内推、面经、摸鱼,这里都有哈,快来加入我们吧~ 回复资料,获取前端大量精选前端电子书及学习视频~

逐梦wx



推荐阅读
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • flowable工作流 流程变量_信也科技工作流平台的技术实践
    1背景随着公司业务发展及内部业务流程诉求的增长,目前信息化系统不能够很好满足期望,主要体现如下:目前OA流程引擎无法满足企业特定业务流程需求,且移动端体 ... [详细]
  • 2018年人工智能大数据的爆发,学Java还是Python?
    本文介绍了2018年人工智能大数据的爆发以及学习Java和Python的相关知识。在人工智能和大数据时代,Java和Python这两门编程语言都很优秀且火爆。选择学习哪门语言要根据个人兴趣爱好来决定。Python是一门拥有简洁语法的高级编程语言,容易上手。其特色之一是强制使用空白符作为语句缩进,使得新手可以快速上手。目前,Python在人工智能领域有着广泛的应用。如果对Java、Python或大数据感兴趣,欢迎加入qq群458345782。 ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • CentOS 7部署KVM虚拟化环境之一架构介绍
    本文介绍了CentOS 7部署KVM虚拟化环境的架构,详细解释了虚拟化技术的概念和原理,包括全虚拟化和半虚拟化。同时介绍了虚拟机的概念和虚拟化软件的作用。 ... [详细]
  • 数字账号安全与数据资产问题的研究及解决方案
    本文研究了数字账号安全与数据资产问题,并提出了解决方案。近期,大量QQ账号被盗事件引起了广泛关注。欺诈者对数字账号的价值认识超过了账号主人,因此他们不断攻击和盗用账号。然而,平台和账号主人对账号安全问题的态度不正确,只有用户自身意识到问题的严重性并采取行动,才能推动平台优先解决这些问题。本文旨在提醒用户关注账号安全,并呼吁平台承担起更多的责任。令牌云团队对此进行了长期深入的研究,并提出了相应的解决方案。 ... [详细]
  • 单点登录原理及实现方案详解
    本文详细介绍了单点登录的原理及实现方案,其中包括共享Session的方式,以及基于Redis的Session共享方案。同时,还分享了作者在应用环境中所遇到的问题和经验,希望对读者有所帮助。 ... [详细]
  • 众筹商城与传统商城的区别及php众筹网站的程序源码
    本文介绍了众筹商城与传统商城的区别,包括所售产品和玩法不同以及运营方式不同。同时还提到了php众筹网站的程序源码和方维众筹的安装和环境问题。 ... [详细]
  • 企业数据应用挑战及元数据管理的重要性
    本文主要介绍了企业在日常经营管理过程中面临的数据应用挑战,包括数据找不到、数据读不懂、数据不可信等问题。针对这些挑战,通过元数据管理可以实现数据的可见、可懂、可用,帮助业务快速获取所需数据。文章提出了“灵魂”三问——元数据是什么、有什么用、又该怎么管,强调了元数据管理在企业数据治理中的基础和前提作用。 ... [详细]
  • 本文介绍了前端人员必须知道的三个问题,即前端都做哪些事、前端都需要哪些技术,以及前端的发展阶段。初级阶段包括HTML、CSS、JavaScript和jQuery的基础知识。进阶阶段涵盖了面向对象编程、响应式设计、Ajax、HTML5等新兴技术。高级阶段包括架构基础、模块化开发、预编译和前沿规范等内容。此外,还介绍了一些后端服务,如Node.js。 ... [详细]
  • 本文介绍了OpenStack的逻辑概念以及其构成简介,包括了软件开源项目、基础设施资源管理平台、三大核心组件等内容。同时还介绍了Horizon(UI模块)等相关信息。 ... [详细]
  • 如何提高PHP编程技能及推荐高级教程
    本文介绍了如何提高PHP编程技能的方法,推荐了一些高级教程。学习任何一种编程语言都需要长期的坚持和不懈的努力,本文提醒读者要有足够的耐心和时间投入。通过实践操作学习,可以更好地理解和掌握PHP语言的特异性,特别是单引号和双引号的用法。同时,本文也指出了只走马观花看整体而不深入学习的学习方式无法真正掌握这门语言,建议读者要从整体来考虑局部,培养大局观。最后,本文提醒读者完成一个像模像样的网站需要付出更多的努力和实践。 ... [详细]
  • 从高级程序员到CTO的4次能力跃迁!如何选择适合的技术负责人?
    本文讲解了从高级程序员到CTO的4次能力跃迁,以及如何选择适合的技术负责人。在初创期、发展期、成熟期的每个阶段,创业公司需要不同级别的技术负责人来实现复杂功能、解决技术难题、提高交付效率和质量。高级程序员的职责是实现复杂功能、编写核心代码、处理线上bug、解决技术难题。而技术经理则需要提高交付效率和质量。 ... [详细]
author-avatar
风之伤ASH
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有