❣️ 智能小程序视图层和逻辑层数据通信,使用 SJS 优化性能。
❣️ 如何实现智能小程序模块化。
❣️ 自定义组件进阶使用指南。
什么是事件
id
、dataset
、touches
。事件绑定的写法类似于组件的属性,如:
<view bind:tap="handleTap">
Click here!
</view>
如果用户点击这个 view
,则页面的 handleTap
会被调用。
事件绑定函数可以是一个数据绑定,如:
<view bind:tap="{{ handlerName }}">
Click here!
</view>
Page({
data: {
handlerName: "handleTap",
},
handleTap() {
console.log("click handleTap");
},
});
在此绑定中,事件同样会被触发。
<!-- 注:绑定的 SJS 函数必须用 {{}} 括起来 -->
<sjs module="sjs" src="./test.sjs"></sjs>
<view id="tapTest" data-hi="WeChat" bind:tap="{{sjs.tapName}}"> Click me! </view>
// test.sjs 文件实现 tapName 函数
function tapName(event, ownerInstance) {
console.log("tap miniApp", JSON.stringify(event));
}
module.exports = {
tapName: tapName,
};
ownerInstance
包含一些方法,可以设置组件的样式和 class
。
除 bind
外,也可以用 catch
来绑定事件。与 bind
不同,catch
会阻止事件向上冒泡。
inner view
,触发 handleTap3
,handleTap2
。middle view
,触发 handleTap2
。outer view
,触发 handleTap1
。<view id="outer" bind:tap="handleTap1">
outer view
<view id="middle" catch:tap="handleTap2">
middle view
<view id="inner" bind:tap="handleTap3">
inner view
</view>
</view>
</view>
更多详情,请查看 事件系统内容。
频繁的用户交互可能使小程序出现卡顿的现象。例如,页面有 2 个元素 A 和 B,用户在 A 上做 touchmove
手势,要求 B 也跟随移动,movable-view
就是一个典型的例子。1 次 touchmove
事件的响应过程为:
touchmove
事件从视图层(Webview
)抛到逻辑层(App Service
)。App Service
)处理 touchmove
事件,再通过 setData
来改变 B 的位置。1 次 touchmove
的响应需要经过 2 次的逻辑层和渲染层的通信以及 1 次渲染,因此,通信的耗时比较大。此外,setData
渲染也会阻塞其它脚本执行,导致整个用户交互的动画过程会有延迟。
SJS 的思路是减少通信的次数,让事件在视图层(Webview
)响应。小程序的框架分为视图层(Webview
)和逻辑层(App
Service
)。这种分层的目的是管控,让开发者的代码只能运行在逻辑层(App Service
)。然而,SJS
思路必须要让开发者的代码运行在视图层(Webview
)。
TYML 定义事件:
<sjs module="test" src="./test.sjs"></sjs>
<view change:prop="{{test.propObserver}}" prop="{{propValue}}" bind:touchmove="{{test.touchmove}}"
class="movable"></view>
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,
};
上面的 change:prop
(属性前面带 change:
前缀)是在 prop
属性被设置的时候触发 SJS 函数,值必须用 {{}}
括起来。类似 Component
定义的 properties
里面的 observer
属性,在
setData({propValue: newValue})
调用之后会触发。
更多详情,请参考 SJS 语法参考。
在开发的过程中,如需查询某个节点的当前位置、属性、样式等信息,您可以使用 ty.createSelectorQuery。
返回值是一个 SelectorQuery 对象实例。
示例代码
const query = ty.createSelectorQuery();
query.select("#the-id").boundingClientRect();
query.selectViewport().scrollOffset();
query.exec(function (res) {
res[0].top; // #the-id 节点的上边界坐标
res[1].scrollTop; // 显示区域的竖直滚动位置
});
ty.createIntersectionObserver API 可用于监听两个或多个组件节点在布局位置上的相交状态。这一组 API 常常可以用于推断某些节点是否展示给用户 以及 有多大比例的区域会被展示。
这一组 API 涉及的主要概念包括:
selectAll
选项时,可以同时监听多个节点)。以下代码表示当 .ball
和 .scroll-view
相交时,即触发回调函数。
onLoad(){
this._observer = ty.createIntersectionObserver()
this._observer
.relativeTo('.scroll-view')
.observe('.ball', (res) => {
this.setData({
appear: res.intersectionRatio > 0
})
})
}
onUnload() {
if (this._observer) this._observer.disconnect()
}
在小程序中,通常可以使用 CSS 渐变 和 CSS 动画 来创建简易的界面动画。 同时,还可以使用 ty.createAnimation 接口来动态创建简易的动画效果。
示例代码
该示例代码实现一个简单的旋转动画效果,实现更多效果。
<view class="animation-element"
animation="{{animation}}"></view>
<button bind:tap="rotate">旋转</button>
Page({
data: {
animation: [],
},
onReady: function () {
this.animation = ty.createAnimation();
},
rotate: function () {
this.animation.rotate(Math.random() * 720 - 360).step();
let anim = this.animation.export();
this.setData({ animation: anim });
},
});
智能小程序 API 主要提供以下应用级事件:
App.onHide
的回调时机一致。App.onShow
的回调参数一致。API
调用报错等。该事件与 App.onError
的回调时机与参数一致。App.onPageNotFound
的回调时机一致。自定义组件所在页面的生命周期声明对象,可用于响应事件处理。
示例代码
Component({
pageLifetimes: {
show: function () {
// 页面被展示
},
hide: function () {
// 页面被隐藏
},
resize: function (size) {
// 页面尺寸变化
},
},
});
this.selectComponent
,获取子组件的实例对象。这样就可以直接访问组件的任意数据和方法。selector
,如:this.selectComponent(".my-component")
。class
为 my-component
的子组件实例对象,即子组件的 this
。// 父组件
Page({
data: {},
getChildComponent: function () {
const child = this.selectComponent(".my-component");
console.log(child);
},
});
behaviors
是用于组件间代码共享的特性,类似于一些编程语言中的 "mixins"
或 "traits"
。
每个 behavior
可以包含一组属性、数据、生命周期函数和方法。组件引用 behavior
时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。每个组件可以引用多个 behavior
,behavior
也可以引用其它
behavior
。
关于参数含义和使用详情,请参考 Behavior 参考文档。
组件引用时,在 behaviors
定义段中将每个 behavior
逐个列出即可。
示例代码
下面示例是组件的一种实现形式:
<custom-ul>
<custom-li> item 1 </custom-li>
<custom-li> item 2 </custom-li>
</custom-ul>
这个例子中,custom-ul
和 custom-li
都是自定义组件。它们有相互间的关系,相互间的通信往往比较复杂。此时,在组件定义时加入
relations
定义段,可以简洁地表示组件间的通信逻辑关系。
// path/to/custom-ul.js
Component({
relations: {
"./custom-li": {
type: "child", // 关联的目标节点应为子节点,(目标组件的相对关系,可选的值为 parent、child、ancestor、descendant)
linked: function (target) {
// 每次有 custom-li 被插入时执行,target 是该节点实例对象,触发在该节点 attached 生命周期之后
},
linkChanged: function (target) {
// 每次有 custom-li 被移动后执行,target 是该节点实例对象,触发在该节点 moved 生命周期之后
},
unlinked: function (target) {
// 每次有 custom-li 被移除时执行,target 是该节点实例对象,触发在该节点 detached 生命周期之后
},
},
},
});
// path/to/custom-li.js
Component({
relations: {
"./custom-ul": {
type: "parent", // 关联的目标节点应为父节点(目标组件的相对关系,可选的值为 parent、child、ancestor、descendant)
linked: function (target) {
// 每次被插入到 custom-ul 时执行,target 是 custom-ul 节点实例对象,触发在 attached 生命周期之后
},
linkChanged: function (target) {
// 每次被移动后执行,target 是 custom-ul 节点实例对象,触发在 moved 生命周期之后
},
unlinked: function (target) {
// 每次被移除时执行,target 是 custom-ul 节点实例对象,触发在 detached 生命周期之后
},
},
},
});
注意:必须在两个组件定义中都加入
relations
定义,否则不会生效。
纯数据字段是一些不用于界面渲染的 data
字段,可以用于提升页面更新性能。
在特定情况下,某些 data
中的字段(包括 setData
设置的字段)既不会展示在界面上,也不会传递给其他组件,仅仅在当前组件内部使用。
此时,可以指定这样的数据字段为"纯数据字段",它们仅仅被记录在 this.data
中,而不参与任何界面渲染过程,这样有助于提升页面更新性能。
指定"纯数据字段"的方法是在 Component
构造器的 options
定义段中指定 pureDataPattern
为一个正则表达式,字段名符合这个正则表达式的字段将成为纯数据字段。
Component({
options: {
pureDataPattern: /^_/, // 指定所有 _ 开头的数据字段为纯数据字段
},
data: {
a: true, // 普通数据字段
_b: true, // 纯数据字段
},
methods: {
myMethod() {
this.data._b; // 纯数据字段可以在 this.data 中获取
this.setData({
c: true, // 普通数据字段
_d: true, // 纯数据字段
});
},
},
});
上述组件中的纯数据字段不会被应用到 TYML
上:
<view ty:if="{{a}}"> 这行会被展示 </view>
<view ty:if="{{_b}}"> 这行不会被展示 </view>
更多特性详情,请参考 纯数据字段。
为了更好地定制自定义组件的功能,可以使用自定义组件扩展机制。
扩展后的效果
// behavior.js
module.exports = Behavior({
definitionFilter(defFields) {
defFields.data.from = "behavior";
},
});
// component.js
const myBehavior = require("behavior.js");
Component({
data: {
from: "component",
},
behaviors: [myBehavior],
ready() {
console.log(this.data.from); // 此处会发现输出 behavior 而不是 component
},
});
可以看出,自定义组件的扩展其实提供了修改自定义组件定义段的能力。上述例子演示了如何修改自定义组件中的 data 定义段里的内容。
关于使用方法的更多详情,请查看 自定义组件扩展。
您可依据 体验优化指南,对现有小程序进行自查,提升性能和用户体验。