---
title: SJS 响应事件
summary: "介绍使用 SJS 在视图层直接响应事件的方案，通过 ComponentDescriptor 操作组件样式以减少通信提升交互性能。"
questions:
  - 为什么需要使用 SJS 响应事件而不是在逻辑层处理？
  - SJS 函数中的 ownerInstance 参数表示什么？
  - ComponentDescriptor 实例提供了哪些方法来操作组件样式和类？
  - change:prop 前缀在 tyml 中的作用是什么？
  - SJS 中如何通过 callMethod 与逻辑层进行通信？
  - SjsPropObserver 函数的触发条件是什么？
  - SJS 函数返回 false 时会产生什么效果？
  - 如何在 SJS 文件中定义并导出事件处理函数？
---

## 背景信息

如有频繁用户交互，在小程序上表现是比较卡顿的。例如，页面有 2 个元素 A 和 B，用户在 A 上做 `touchmove` 手势，要求 B 也跟随移动，`movable-view` 就是一个典型的例子。一次 `touchmove` 事件的响应过程为：

1. `touchmove` 事件从视图层（`Webview`）抛到逻辑层（App Service）。

2. 逻辑层（App Service）处理 `touchmove` 事件，再通过 `setData` 来改变 B 的位置。

一次 `touchmove` 的响应需要经过 2 次逻辑层和渲染层的通信以及一次渲染，通信的耗时比较大。此外，`setData` 渲染也会阻塞其它脚本执行，导致整个用户交互的动画过程出现延迟。

## 实现方案

本方案基本的思路是减少通信次数，让事件在视图层（`Webview`）响应。小程序的框架分为视图层（`Webview`）和逻辑层（App Service）。这样分层的目的是管控，开发者的代码只能运行在逻辑层（App Service），而这个思路就必须要让开发者的代码运行在视图层（`Webview`），如下图所示的流程：

<Image src="https://images.tuyacn.com/content-platform/hestia/163583716864d573c523f.png" />

使用 SJS 函数用来响应小程序事件，目前只能响应内置组件的事件，不支持自定义组件事件。SJS 函数除了纯逻辑的运算，还可以通过封装好的 `ComponentDescriptor` 实例来访问以及设置组件的` class` 和样式。对于交互动画，设置 `style` 和 `class` 足够了。SJS 函数的例子如下：

```js
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.<string>`               | 参数与 [SelectorQuery](/cn/miniapp/develop/miniapp/api/tyml/NodesRef/NodesRef) 的 `computedStyle` 一致。                  |
| setTimeout                    | (Function, Number)             | 与原生 `setTimeout` 一致。用于创建定时器。                                                                                |
| clearTimeout                  | Number                         | 与原生 `clearTimeout` 一致。用于清除定时器。                                                                              |
| getBoundingClientRect         | 无                             | 返回值与 [SelectorQuery](/cn/miniapp/develop/miniapp/api/tyml/NodesRef/boundingClientRect) 的 `boundingClientRect` 一致。 |
| eventChannel                  | 无                             | `EventChannel` 对象，两个`EventChannel`对象之间可以使用`emit`和`on`方法相互发送、监听事件。要求基础库版本大于2.18.0       |

**SJS 运行在视图层（`Webview`），里面的逻辑毕竟能做的事件比较少，需要有一个机制和逻辑层（App Service）开发者的代码通信。上面的 `callMethod` 是 SJS 里面调用逻辑层（App Service）开发者的代码的方法，而 `SjsPropObserver` 是逻辑层（App Service）开发者的代码调用 SJS 逻辑的机制。**

## 使用方法

### `tyml` 定义事件：

```tyml
<sjs module="test" src="./test.sjs"></sjs>

<view change:prop="{{test.propObserver}}" prop="{{propValue}}" bind:touchmove="{{test.touchmove}}" class="movable"></view>
```

上面的 `change:prop`（属性前面带 `change:` 前缀）是在 `prop` 属性被设置的时候触发 SJS 函数，值必须用 `{{}}` 括起来。类似 `Component` 定义的 `properties` 里面的 `observer` 属性，在 `setData({propValue: newValue})` 调用之后会触发。

**注意**：SJS 函数必须用 `{{}}` 括起来。当 `prop` 的值被设置时，SJS 函数就会触发，而不只是值发生改变。所以在页面初始化时，会调用一次 `SjsPropObserver` 的函数。

SJS 文件 `test.sjs` 里面定义并导出事件处理函数和属性改变触发的函数：

```js
// 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,
};
```
