当前位置: 首页 > Web前端 > HTML

探究小程序底层架构原理

时间:2023-03-28 01:45:22 HTML

双线程架构在此之前,我们先思考一个问题,为什么小程序会选择双线程架构?为什么是双线程?加载和渲染性能小程序一开始就被设计成快,这里的快指的是加载和渲染。目前主流的渲染方式有3种:Web技术渲染Native技术渲染混合技术渲染(同时使用webview和native渲染)就小程序的定位来说,不可能使用纯原生技术进行开发,因为那它的编译和发布一定要跟着微信,所以它需要有一个可以随时更新的资源包,像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标签。第一个webview的src属性是不是很眼熟?如果没猜错的话,就是我们当前小程序打开页面的路径。所以这个webview才是小程序真正的渲染层。到这里你会发现里面不止一个webview。其实包含了视图层的webview,业务逻辑层的webview,开发者工具的webview。开发者工具的逻辑层运行在webview中主要是为了模拟真机上的双线程。打开渲染层一探究竟。使用showdevTools方法打开用于调试webview界面的调试器document.querySelectorAll('webview')[0].showDevTools(true)这里我们看到的是小程序的渲染层,也就是小程序的代码之后编译好了,我们会发现这里的标签和我们开发时写的不一样,都是以wx-为前缀。了解过webComponent的同学相信一看就很相似,但是小程序并没有直接使用webComponent,而是自己搭建了一套组件系统Exparser。Exparser的组件模型与WebComponents标准中的ShadowDOM高度相似。Exparser会维护整个页面的节点树的信息,包括节点属性、事件绑定等,相当于ShadowDOM实现的简化版。为什么不直接使用webComponent,而是选择自己搭建一个组件系统呢?

点击查看控制与安全:web技术可以通过脚本获取和修改页面的敏感内容或随意跳转到其他页面
能力有限:会限制小的表达程序
标签多:增加理解成本
JSCorelogic线程逻辑层我们可以在小程序开发者工具的调试器中直接进入document可以看到小程序把所有业务代码放在同一个线程中运行,在小程序开发者工具中的逻辑线程也是运行在一个webview中;webview中的appservice.html不仅引入了业务代码js,还嵌入了一些后台服务的基本功能代码。编译原理了解了小程序的双线程架构之后,我们来看看小程序的代码是如何编译运行的。微信开发者工具模拟器运行的代码是在本地预处理和编译的,而微信客户端运行的代码是附加服务端编译的。这里我们仍然以微信开发者工具为例进行探讨。在开发者工具中输入openVendor(),会帮我们打开微信开发者工具的WeappVendor文件夹。这里我们会看到一些wxvpkg文件,这些文件是小程序各个版本的基础库文件。另外还有两个值得我们注意的文件:wcc、wcsc,这两个文件是小程序的编译器,分别用来编译wxml和wxss文件。编译wxml这里我们可以复制开发者工具中的wcc编译器,尝试用它来编译wxml文件,看看最终的成品是什么?我们在终端执行下面的命令/wcc-bindex.wxml>>wxml_output.js然后会在当前目录下生成一个wxml_output.js文件。文件中有一个很重要的方法$gwx,会返回一个函数。对于这个函数的具体作用,我们可以尝试执行一下看看结果。我们打开渲染层webview搜索这个方法(为了方便查看,这里用一个小工程来演示)。从这里我们可以看出,这个方法会传入一个小程序页面的路径,返回的还是一个函数vardecodeName=decodeURI("./index/index.wxml")vargenerateFunc=$gwx(decodeName)让我们尝试按照这个过程执行$gwx返回的函数,看看返回了什么?wxml编译{{name}}constfunc=$gwx(decodeURI('index.wxml'))console.log(func())没错,这个函数是用来生成VirtualDOM的思考:$gwx为什么不直接生成VirtualDOM呢?
点击查看双线程,需要动态注入数据
编译wxss,我们也可以使用微信开发者工具中的wcsc来编译wxss文件。(你觉得这里应该生成css文件还是js文件?)我们在终端执行如下命令编译wxss文件/wcsc-jsindex.wxss>>wxss_output.js与之前wcc编译的wxml文件比较,这次编译比较简单,主要完成以下内容:rpx单位的转换,转换为px提供setCssToHead方法将转换后的css添加到头部rpx动态适配小程序提供rpx单位适配各种尺寸的设备如:/*index.wxss*/.qd_container{宽度:100rpx;背景:天蓝色;边框:1rpx实心鲑鱼;}.qd_reader{字体大小:20rpx;颜色:#191919;font-weight:400;}编译后会生成并执行setCssToHead方法。setCssToHead([".",[1],"qd_container{width:",[0,100],";background:skyblue;border:",[0,1],"solidsalmon;}\n.",[1],"qd_reader{font-size:",[0,20],";color:#191919;font-weight:400;}\n",])(typeof__wxAppSuffixCode__=="undefined"?undefined:__wxAppSuffixCode__);这将调用transformRPX方法将rpx转换为pxvartransformRPX=window.__transformRpx__||function(number,newDeviceWidth){if(number===0)return0;number=number/BASE_DEVICE_WIDTH*(newDeviceWidth||deviceWidth);number=Math.floor(number+eps);if(number===0){if(deviceDPR===1||!isIOS){return1;}else{return0.5;}}returnnumber;}//主公式number=number/BASE_DEVICE_WIDTH*(newDeviceWidth||deviceWidth);number=Math.floor(number+eps);//对于精度//rpx值/基础设备宽度750*真实设备宽度渲染过程了解了wxml和wxss的编译过程后,我们整体来看一下页面的渲染过程。我们先了解一下渲染层模板。从上面的渲染层webview中,我们可以找到两个webview的第一个index/index。上面我们说了对应我们的小程序。渲染层,也就是真正的小程序页面。那么下面的instanceframe.html是什么呢?这个webview其实就是一个小程序渲染模板。打开它并检查它。其实就是预先注入一些页面需要的公共文件,以及红框中的一些页面无关的文件占位符。这些占位符会等待小程序对应。在编译页面文件后注入。如何保证渲染层webview初始化后执行代码注入?在刚刚渲染模板的webview下面有这样一个脚本:窗户。removeEventListener('load',fn)}window.addEventListener('load',fn)}显然,页面初始化完成后,会使用alert来通知。这个时候native/nw.js会拦截这个alert,从而知道这个时候webview已经初始化好了。整体渲染流程了解了以上重要流程后,我们就可以将整个流程串联起来,打开小程序了。在创建视图层页面的webview时,此时会初始化渲染层webview,并将web视图地址设置为instanceframe。html,也就是我们的渲染层模板然后进入页面/index/index,等待instanceframewebview初始化完成,它会注入页面index/index的编译代码并执行//将webviewsrc路径改为页面路径history.pushState('','','http://127.0.0.1:26444/__pageframe__/index/index')/*...下面是一些wxconfig和wxss的编译代码*///这是vardecodeName=decodeURI("./index/index.wxml")vargenerateFunc=$gwx(decodeName)if(decodeName==='./__wx__/functional-page.wxml'){generateFunc=function(){return{tag:'wx-page',children:[],}}}if(generateFunc){varCE=(typeof__global==='object')?(window.CustomEvent||__global.CustomEvent):window.CustomEvent;document.dispatchEvent(newCE("generateFuncReady",{detail:{generateFunc:generateFunc}}))__global.timing.addPoint('PAGEFRAME_GENERATE_FUNC_READY',Date.now())}else{document.body.innerText=decodeName+"没找到&qu哦;console.error(decodeName+"notfound")}此时通过history.pushState方法修改了webview的src,但是webview不会发送页面请求,会调用$gwx生成一个generateFun方法,我们前面了解到方法是用来生成virtualdom然后当判断该方法存在时,通过document.dispatchEvent派发自定义事件generateFuncReady,并将generateFunc作为参数传给底层渲染库,然后自定义事件会在底层渲染库WAWebview.js中监听generateFuncReady,然后通过WeixinJSBridge()通知JS逻辑层视图准备好了,最后JS逻辑层将数据发送给Webview渲染层,WAWebview.在通过虚拟dom生成真实dom的过程中,js会挂载到页面的document.body上,至此,一个页面的渲染过程就结束了。数据更新小程序的视图层目前使用WebView作为渲染载体,而逻辑层则使用独立的JavascriptCore作为运行环境。在架构上,WebView和JSCore是独立的模块,没有直接数据共享的通道。所以在更新数据时,必须调用setData通知渲染层更新。setData在逻辑层遍历更新虚拟DOM树,触发组件生命周期和观察者等;将数据从逻辑层传输到视图层;更新视图层中的虚拟DOM树和真实DOM元素,并触发页面渲染更新。这里的第二步,由于WebView和JSCore是独立的模块,数据传输是通过evaluateJavascript实现的,会额外有耗时的JS脚本解析和执行,所以数据是异步到达渲染层的。所以,切记不要频繁setData,不要每次setData都传递大量的新数据(单次stringify后不超过256kb),也不要在后台页面setData,会抢占前台资源正在执行。)整体来说,小程序或多或少有vue的影子。。。(模板文件、数据、指令、虚拟dom、生命周期等)但是在数据更新上,小程序和vue完全不同。1、页面更新DOM是同步的还是异步的?2、既然更新DOM是一个同步过程,为什么Vue中会有nextTick钩子?mounted(){this.name='FrontendNanjiu'console.log('sync',this.$refs.title.innerText)//旧文本//新文本Promise.resolve().then(()=>{console.log('microtask',this.$refs.title.innerText)})setTimeout(()=>{console.log('macrotask',this.$refs.title.innerText)},0)this.$nextTick(()=>{console.log('nextTick',this.$refs.title.innerText)})}建议阅读这篇文章了解更多:Vue异步更新机制和$nextTick的原理但是,小程序却没有队列这个概念,频繁调用,视图会一直更新,阻塞用户交互,造成性能问题。Vue不会为每个赋值操作直接更新视图,而是缓存在一个数据更新队列中,异步更新,然后触发渲染。同一个tick中的多个分配只会呈现一次。原帖地址点此,欢迎大家关注公众号“前端南久”,想加入前端交流群一起学习的请点这里我是南久,下期见!!!