背景信息
如有频繁用户交互,在小程序上表现是比较卡顿的。例如,页面有 2 个元素 A 和 B,用户在 A 上做touchmove
手势,要求 B 也跟随移动,movable-view
就是一个典型的例子。一次touchmove
事件的响应过程为:
touchmove
事件从视图层(Webview
)抛到逻辑层(App Service)。逻辑层(App Service)处理
touchmove
事件,再通过setData
来改变 B 的位置。
一次touchmove
的响应需要经过 2 次逻辑层和渲染层的通信以及一次渲染,通信的耗时比较大。此外,setData
渲染也会阻塞其它脚本执行,导致整个用户交互的动画过程出现延迟。
实现方案
本方案基本的思路是减少通信次数,让事件在视图层(Webview
)响应。小程序的框架分为视图层(Webview
)和逻辑层(App Service)。这样分层的目的是管控,开发者的代码只能运行在逻辑层(App Service),而这个思路就必须要让开发者的代码运行在视图层(Webview
),如下图所示的流程:
使用 SJS 函数用来响应小程序事件,目前只能响应内置组件的事件,不支持自定义组件事件。SJS 函数除了纯逻辑的运算,还可以通过封装好的ComponentDescriptor
实例来访问以及设置组件的class
和样式。对于交互动画,设置style
和class
足够了。SJS 函数的例子如下:
const sjsFunction = function (event, ownerInstance) {const instance = ownerInstance.selectComponent('.classSelector'); // 返回组件的实例instance.setStyle({'font-size': '14px',});instance.getDataset();instance.setClass(className);// ...return false; // 不往上冒泡,相当于同时调用了 stopPropagation 和 preventDefault};
其中,入参event
是小程序事件对象基础上多了event.instance
,来表示触发事件的组件的ComponentDescriptor
实例。ownerInstance
表示的是触发事件的组件所在组件的ComponentDescriptor
实例。如果触发事件的组件是在页面内的,ownerInstance
表示的是页面实例。
ComponentDescriptor
的定义如下:
方法 | 参数 | 描述 |
---|---|---|
selectComponent | selector 对象 | 返回组件的ComponentDescriptor 实例。 |
selectAllComponents | selector 对象数组 | 返回组件的ComponentDescriptor 实例数组。 |
setStyle | Object/string | 设置组件样式。设置的样式优先级高于组件tyml 里面定义的样式。不能设置最顶层页面的样式。 |
addClass/removeClass /hasClass | string | 设置组件的class 。设置的class 优先级高于组件tyml 里面定义的class 。不能设置最顶层页面的class 。 |
getDataset | 无 | 返回当前组件或者页面的dataset 对象。 |
callMethod | (funcName:string, args:object) | 调用当前组件或者页面在逻辑层(App Service)定义的函数。funcName 表示函数名称,args 表示函数的参数。 |
requestAnimationFrame | Function | 和原生requestAnimationFrame 一样。用于设置动画。 |
getState | 无 | 返回一个object 对象。当局部变量需要存储起来,以便后续使用的时候,可以用这个方法。 |
triggerEvent | (eventName, detail) | 和组件的triggerEvent 一致。 |
getComputedStyle | Array. | 参数与 SelectorQuery 的computedStyle 一致。 |
setTimeout | (Function, Number) | 与原生setTimeout 一致。用于创建定时器。 |
clearTimeout | Number | 与原生clearTimeout 一致。用于清除定时器。 |
getBoundingClientRect | 无 | 返回值与 SelectorQuery 的boundingClientRect 一致。 |
SJS 运行在视图层(Webview
),里面的逻辑毕竟能做的事件比较少,需要有一个机制和逻辑层(App Service)开发者的代码通信。上面的callMethod
是 SJS 里面调用逻辑层(App Service)开发者的代码的方法,而SjsPropObserver
是逻辑层(App Service)开发者的代码调用 SJS 逻辑的机制。
使用方法
tyml
定义事件:
<view change:prop="{{test.propObserver}}" prop="{{propValue}}" bind:touchmove="{{test.touchmove}}" class="movable">
上面的change:prop
(属性前面带change:
前缀)是在prop
属性被设置的时候触发 SJS 函数,值必须用{{}}
括起来。类似Component
定义的properties
里面的observer
属性,在setData({propValue: newValue})
调用之后会触发。
注意:SJS 函数必须用{{}}
括起来。当prop
的值被设置时,SJS 函数就会触发,而不只是值发生改变。所以在页面初始化时,会调用一次SjsPropObserver
的函数。
SJS 文件test.sjs
里面定义并导出事件处理函数和属性改变触发的函数:
// event:事件对象。// ownerInstance:表示的是触发事件的组件所在组件的 ComponentDescriptor 实例。如果触发事件的组件是在页面内的,ownerInstance 表示的是页面实例。const touchmove = function (event, ownerInstance) {console.log('log event', JSON.stringify(event));}; // newValue:新值。// oldValue:旧值。// ownerInstance:表示的是触发事件的组件所在组件的 ComponentDescriptor 实例。如果触发事件的组件是在页面内的,ownerInstance 表示的是页面实例。// instance:表示触发事件的组件的 ComponentDescriptor 实例。const propObserver = function (newValue, oldValue, ownerInstance, instance) {console.log('prop observer', newValue, oldValue);}; export default {touchmove: touchmove,propObserver: propObserver,};
立即开发。