小程序操作dom
小程序不能使用各种浏览器暴露出来的 DOM API,进行 DOM 选中和操作
原因:在小程序中,渲染层和逻辑层是分开的,分别运行在不同的线程中,逻辑层运行在 JSCore 中,并没有一个完整浏览器对象,因而缺少相关的DOM API和BOM API。
为什么要这样设计?
因为JavaScript是可操纵DOM的,如果JavaScript线程和UI线程同时运行,即在修改这些元素属性同时渲染界面,那么渲染线程前后获得的元素数据就可能不一致,导致传统web开发渲染线程和脚本线程是互斥的。于是当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。 因此长时间的脚本运行可能会导致页面失去响应。
小程序的通信过程:
小程序的逻辑层和渲染层是分开的两个线程, 小程序的运行环境分成渲染层和逻辑层,其中 WXML 模板和 WXSS 样式工作在渲染层,JS脚本工作在逻辑层。
在渲染层,宿主环境会把WXML转化成对应的JS对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的setData方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的UI界面。
小程序的渲染层和逻辑层分别由2个线程管理:渲染层的界面使用了WebView 进行渲染;逻辑层采用JsCore线程运行JS脚本。一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由客户端做中转,逻辑层发送网络请求也经由Native转发,小程序的通信模型下图所示。
双线程的优劣:
- 小程序是基于双线程模型,那就意味着任何数据传递都是线层间的通信,也就是统一都会有一定的延迟,不像传统web,当界面需要更新时,通过调用更新接口UI就会同步地渲染出来。在小程序的架构里,一切都是异步。
- 除了逻辑层与渲染层之间的通信有延时,各层与客户端原生交互同样是有延时的。
- 异步带来的好处,页面也js并行执行,不会出现传统web端js阻塞页面渲染的情况。
小程序DOM操作API —— SelectQuery
首先我们来看dom操作在浏览器和小程序中用法的区别:
浏览器中:
const el = document.querySelector('#the-id')
console.log(el.boundingClientRect().top)
小程序中:
const query = wx.createSelectorQuery()
// 组件中:const query = wx.createSelectorQuery().in('组件id')
query.select('#the-id').boundingClientRect()
query.exec(function (res) {
console.log(res[0].top)
})
可以发现有三点明显的区别:
- 通过createSelectorQuery创建了一个query对象而不是document.querySelector
- 在query对象上执行查询操作并非马上执行,而是进入等待队列,直至query对象上exec被调用才触发查询行为
- 查询结果是异步返回的,在callback中按查询顺序依次从参数中读取
第一点区别产生的原因是首先小程序双线程模型决定了业务代码中不能拿到document对象, 也就无法在上面调用相关查询方法。那为什么要通过调用createSelectorQuery()返回查询对象而不是把查询对象定义为全局的呢?这是因为小程序每个页面的视图层都对应一个webview,而所有的页面都共用一个逻辑线程,这种一对多的关系在通讯时需要有id来进行区分,所以每次createSelectorQuery时返回的query对象是绑定了当前视图层webview对应id的。
第二、三点的查询非立即执行,而是在exec被调用时真正触发比较好理解:业务代码在逻辑线程,真实dom在另一个webview线程,线程间的通讯需要借助宿主能力完成,query对象的exec方法被触发时小程序把callbak存储起来,然后调用native宿主暴露的方法去通讯,拿到webview线程查询回来的结果之后进行反序列化处理,传递给先前保存的callbak并开始执行,所以这里是异步的。