- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
在这之前,我们先来思考一个问题,小程序在架构上为什么会选择双线程?
小程序的设计之初就是要求快速,这里的快指的是加载以及渲染.
目前主流的渲染方式有以下3种:
从小程序的定位来讲,它就不可能用纯原生技术来进行开发,因为那样它的编译以及发版都得跟随微信,所以需要像Web技术那样,有一份随时可更新的资源包放在远程,通过下载到本地,动态执行后即可渲染出界面.
但如果用纯 web 技术来开发的话,会有一个很致命的缺点那就是在 Web 技术中,UI渲染跟 JavaScript 的脚本执行都在一个单线程中执行,这就容易导致一些逻辑任务抢占UI渲染的资源,这也就跟设计之初要求的 快 相违背了.
因此微信小程序选择了Hybrid 技术,界面主要由成熟的 Web 技术渲染,辅之以大量的接口提供丰富的客户端原生能力。同时,每个小程序页面都是用不同的WebView去渲染,这样可以提供更好的交互体验,更贴近原生体验,也避免了单个WebView的任务过于繁重.
微信小程序是以webview渲染为主,原生渲染为辅的混合渲染方式 。
由于web技术的灵活开放特点,如果基于纯web技术来渲染小程序的话,势必会存在一些不可控因素和安全风险.
为了解决安全管控的问题,小程序从设计上就阻止了开发者去使用一些浏览器提供的开放性api,比如说跳转页面、操作DOM等等。如果把这些东西一个一个地去加入到黑名单,那么势必会陷入一个非常糟糕的循环,因为浏览器的接口也非常丰富,那么就很容易遗漏一些危险的接口,而且就算是禁用掉了所有的接口,也防不住浏览器内核的下次更新.
所以要彻底解决这个问题,必须提供一个沙箱环境来运行开发者的 JavaScript 代码。这个沙箱环境只提供纯 JavaScript 的解释执行环境,没有任何浏览器相关接口。那么像 HTML5 中的 ServiceWorker 、 WebWorker 特性就符合这样的条件,这两者都是启用另一线程来执行 javaScript .
这就是小程序双线程模型的由来:
渲染层: 界面渲染相关的任务全都在 WebView 线程里执行,通过逻辑层代码去控制渲染哪些界面。一个小程序存在多个界面,所以渲染层存在多个 WebView.
逻辑层: 创建一个单独的线程去执行 JavaScript,在这个环境下执行的都是有关小程序业务逻辑的代码.
小程序的架构模型有别与传统web单线程架构,小程序为双线程架构.
微信小程序的渲染层与逻辑层分别由两个线程管理,渲染层的界面使用 webview 进行渲染;逻辑层采用 JSCore 运行 JavaScript 代码.
如何找到渲染层?
微信开发者工具 ->调试 ->调试微信开发者工具
但这并不是小程序的渲染层,而是开发者工具的结构。但我们在里面可以发现有一些 webview 标签,在第一个 webview 上的src属性看着是不是有点眼熟,没猜错的话它就是我们当前小程序打开页面的路径。所以这个 webview 才是小程序真正的渲染层。这里你会发现它里面并不只有一个 webview ,其实里面包含着 视图层的webview , 业务逻辑层webview , 开发者工具的webview 。
开发者工具的逻辑层跑在 webview 中主要是为了模拟真机上的双线程 。
通过 showdevTools 方法来打开调试此webview界面的调试器 。
document.querySelectorAll('webview')[0].showDevTools(true)
这里我们看到的才真正是小程序的渲染层,也就是小程序代码编译后的样子,我们会发现这里的标签都与我们开发时写的不一样,都统一加了 wx- 前缀。了解过 webComponent 的同学相信一眼就能看出他们非常相似,但小程序并没有直接使用 webComponent ,而是自行搭建了一套组件系统 Exparser .
Exparser 的组件模型与 WebComponents 标准中的 Shadow DOM 高度相似。 Exparser 会维护整个页面的节点树相关信息,包括节点的属性、事件绑定等,相当于一个简化版的 Shadow DOM 实现.
为什么不直接使用 webComponent ,而是选择自行搭建一套组件系统?
逻辑层我们直接在小程序开发者工具的调试器中输入 document 就能看到 小程序将所有业务代码置于同一个线程中运行,在小程序开发者工具中逻辑线程同样是跑在一个webview中;webview中的appservice.html除了引入业务代码js之外,还有后台服务内嵌的一些基础功能代码.
了解完小程序的双线程架构,我们再来看一下小程序的代码是如何编译运行的,微信开者工具模拟器运行的代码是经过本地预处理、本地编译,而微信客户端运行的代码是额外经过服务器编译的。这里我们还是以微信开发者工具为例来探索一番.
在开发者工具输入 openVendor() ,会帮我们打开微信开发者工具的 WeappVendor 文件夹 。
在这里我们我们会看到一些 wxvpkg 文件,这是小程序的各个版本的基础库文件,还有两个值得我们注意的文件: wcc 、 wcsc ,这两个文件是小程序的编译器,分别用来编译 wxml 和 wxss 文件.
这里我们可以将开发者工具中的 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
? 我们同样可以用微信开发者工具中的 wcsc 来编译一下 wxss 文件.
(大家认为这里应该是会生成 css 文件还是 js 文件呢?) 。
我们在终端执行一下以下命令来编译wxss文件 。
./wcsc -js index.wxss >> wxss_output.js
相比之前的 wcc 编译 wxml 文件来说,这次的编译相对来说比较简单,它主要完成了以下内容:
setCssToHead
方法将转换好的css添加到head中 小程序提供 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 * 真实设备宽
上面了解完 wxml 与 wxss 的编译过程,我们再来整体了解一下页面的渲染流程.
从上面的渲染层 webview 我们可以找到这两个webview 。
第一个 index/index webview我们上面说了它就是对应我们的小程序的渲染层,也就是真正的小程序页面.
那么下面这个 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已经初始化完成.
了解了上面这个重要过程,我们就可以将整个流程串联起来了 。
webview
,并且会将该web view地址设置为 instanceframe.html
,也就是我们的渲染层模版 /index/index
,等 instanceframe
webview初始化完成,会将页面 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")
}
history.pushState
方法修改webview的src但是webview并不会发送页面请求,并且将调用 $gwx
为生成一个 generateFun
方法,前面我们了解到该方法是用来生成虚拟dom的 document.dispatchEvent
派发发自定义事件 generateFuncReady
将generateFunc当作参数传递给底层渲染库 WAWebview.js
中会监听自定义事件 generateFuncReady
,然后通过 WeixinJSBridge 通知 JS 逻辑层视图已经准备好()
WAWebview.js
在通过 virtual dom
生成真实dom过程中,它会挂载到页面的 document.body
上,至此一个页面的渲染流程就结束了 小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境.
在架构上,WebView 和 JS Core 都是独立的模块,并不具备数据直接共享的通道。所以在更新数据时必须调用 setData 来通知渲染层做更新.
这里第二步由于WebView 和 JS Core 都是独立的模块,数据传输是通过 evaluateJavascript 实现的,还会有额外 JS 脚本解析和执行的耗时因此数据到达渲染层是异步的.
因此切记 。
整体来讲,小程序身上或多或少都有着vue的影子...(模版文件,data,指令,虚拟dom,生命周期等) 。
但在数据更新这里,小程序却与Vue表现的截然不同.
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 内多次赋值,也只会渲染一次.
原文首发地址 点这里 ,欢迎大家关注公众号 「前端南玖」 ,如果你想进前端交流群一起学习, 请点这里 。
我是南玖,我们下期见!!! 。
最后此篇关于探索小程序底层架构原理的文章就讲到这里了,如果你想了解更多关于探索小程序底层架构原理的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我有以下功能: function addChange(result, bill) { for (var i=0;i
这是网站: www.wearethefirehouse.com/phasetest 如果您慢慢滚动,您会注意到一旦菜单栏完全不透明,nav li 元素就会全部从 Enzo 300 跳起来(如在没有导航
美好的一天。对于当前的项目,我需要知道数据类型如何表示为字节。例如,如果我使用: long three = 500;var bytes = BitConverter.GetBytes(three);
请解释 JVM 是如何在底层收集 ThreadDump 的。 我不明白它如何收集脱离 CPU 的线程的堆栈跟踪(等待磁盘 IO、网络、非自愿上下文切换)。 例如,linux perf 仅收集有关 on
开始学习 R,如果能帮助我理解 R 如何决定不同向量的类别,我将不胜感激。我初始化 vec <- c(1:6)当我执行 class(vec)我得到“整数”。为什么它不是“数字”,因为我认为 R 中的整
我有一个透明的 UIView,几乎覆盖了整个屏幕。我在顶部留下了 50 像素。它是 View Controller View 的 subview 。 在UIView下面有一个继承自UIView的MyV
我很好奇对象是如何在 Nodejs 中显示的,在本例中是 Promise。使用 console.log(promiseObject) 时,输出的类型为 {状态:待处理} 这对我来说似乎很奇怪,因为在该
当您在 Windows Azure 中使用表服务 API 时,幕后到底在做什么?我想我在某处读到这没有使用 SQL Server。它是否执行哈希表,然后过滤器真的像映射/减少操作一样运行?我对这些东西
如何查看函数 concat 中的代码?它是如何做的?有没有人有代码的副本或在浏览器控制台中查看它的方法? console.dir 不给我访问权限 console.dir(Array.prototype
我是 C++ 的新手,所以如果这个问题的答案显而易见,我深表歉意。 我一直在编写 STL 样式的自定义数据结构,以此来提高我的技能。 (我实际上也确实需要这种结构,但出于学习目的,我有点过分了。) 此
我正在尝试使用 log4j appender 将日志发送到 GrayLog2 (log4j2-gelf)。所以我将我的依赖项添加到我的 pom.xml 配置 log4j2.xml 来配置我的 appe
我正在使用带有 vector 的 priority_queue 作为底层容器。但是我希望堆的大小非常大。我知道动态 vector 容量调整大小的问题。所以我正在寻找方法来为我的priority_que
我有一个 SqlDataAdapter,它填充了 21 行数据(4 列)。驱动它的 sproc 在几秒钟内在 SQL Mgmt Studio 中返回,但 .Fill() 需要 5 分钟。 Ar
我想实现一个屏幕控制按钮,按下它可以作为 GUI 交互的修饰符。 这对于 MouseArea 是不可能的,因为该 API 只能处理一个鼠标区域中的一个触摸点。 该限制不适用于 MultiPointTo
我试图将图像和 div 层置于包含 div 的中心,但到目前为止我无法让它从列的左侧移动。我尝试了几种不同的方法,但就是无法让它移动。即使 margin auto 技巧也不起作用,我怀疑这是因为 bo
需要明确的是,我不是在询问 HDFS 中的权限设置,而是在 ext3 中或在 HDFS 运行于其上的各个数据节点机器上使用的任何文件系统中。 p> 我知道我们设置了 sudo chown hduser
我在服务器上创建了一个枚举,其中手动设置了整数值,而不是默认从 0 开始递增 public enum UserType { Anonymous = 0, Customer = 10,
如果显示框架图像,我们能否使以下 Google map 具有交互性。 Vie
我有一个顶部有自定义状态栏的布局 [在 Apple 的状态栏下方],然后是 UIScrollview 在中间部分从左到右分页,然后我有一个 UIView 底部有一些自定义按钮。一个简单的三 Pane
事情是这样的。我有一个 MVC 操作,在该操作上,我应用了自定义 ActionFilterAttribute 来使反序列化工作。现在,我想要做的是根据在此 View 中设置的 ViewBag.Titl
我是一名优秀的程序员,十分优秀!