# worklet 动画

小程序采用双线程架构,渲染线程(UI 线程)和逻辑线程(JS 线程)分离。JS 线程不会影响 UI 线程的动画表现,如滚动效果。但引入的问题是,UI 线程的事件发生后,需跨线程传递到 JS 线程,进而触发开发者回调,当做交互动画(如拖动元素)时,这种异步性会带来较大的延迟和不稳定。

worklet 动画正是为解决这类问题而诞生的,使得小程序可以做到类原生动画般的体验。

# 立即体验

首先,我们需要了解一些相关概念。

worklet 动画相关接口仅在 Skyline 渲染模式下才能使用

# 概念一:worklet 函数

一种声明在开发者代码中,可运行在 JS 线程或 UI 线程的函数。

# 示例一:运行在 UI 线程

function someWorklet(greeting) {
  'worklet';
  console.log(greeting, 'On the UI thread');
}

// 运行在 JS 线程
someWorklet('hello') 

// 运行在 UI 线程
runOnUI(someWorklet)('hello skyline') 

# 示例二:worklet 函数间相互调用

const name = 'skyline'

// 可捕获外部变量
function anotherWorklet() {
  'worklet';
  return 'hello ' + name;
}

// worklet 函数间可互相调用
function someWorklet() {
  'worklet';
  const greeting = anotherWorklet();
  console.log('On the UI thread, another worklet says', greeting);
}

# 示例三:从 UI 线程调回到 JS 线程

function someFunc(greeting) {
  console.log(greeting, 'On the JS thread');
}

function someWorklet() {
  'worklet'
  // 访问非 worklet 函数时,需使用 runOnJS
  // someFunc 运行在 JS 线程
  runOnJS(someFunc)('hello')
}

runOnUI(someWorklet)()

# 概念二:共享变量

JS 线程创建,可在两个线程间同步的变量。

# 示例用法

const offset = shared(0)

function someWorklet() {
  'worklet'
  console.log(offset.value)

  // 也可在 UI 线程修改
  offset.value = 2
}

// 在 JS 线程修改
offset.value = 1

// 自动同步到 UI 线程
runOnUI(someWorklet)()

shared 函数创建的变量,我们称为 sharedValue。用法上可类比 vue3 中的 ref,对它的读写都需要通过 .value 属性,但需注意的是它们并不是一个概念。

sharedValue 的用途主要有以下三点:

# 跨线程共享数据

worklet 函数捕获的外部变量,实际上会被序列化后生成在 UI 线程的拷贝,如下代码中, someWorklet 捕获了 obj 变量,尽管我们修改了 objname 属性,但在 someWorklet 声明的位置,obj 已经被序列化发送到了 UI 线程,因此后续的修改是无法同步的。

sharedValue 就是用来在线程间同步状态变化的变量。

const obj = { name: 'skyline'}
function someWorklet() {
  'worklet'
  console.log(obj.name)
}
obj.name = 'change name'

// 输出的仍旧是 skyline
runOnUI(someWorklet)() 

# 驱动动画

worklet 函数和共享变量就是用来解决交互动画问题的。

# 示例用法一:

<!-- 页面中的某个元素 -->
<view id="moved-box"></view>
<view id="btn" bind:tap="tap">点击驱动小球移动</view>
Page({
  onLoad() {
    const offset = shared(0)
    this.applyAnimatedStyle('#moved-box', () => {
      'worklet';
      return {
        translate: `translateX(${offset.value}px)`
      }
    })
    this._offset = offset
  }
  // 点击事件无需声明为 worklet 函数
  tap() {
    this._offset.value = Math.random()
  }
})

当点击按钮 #btn 时,我们用随机数给 offset 进行赋值,小球会随之移动。

applyAnimatedStyle 函数的第二个参数,为一个 worklet 函数,其捕获了共享变量 offset,当 offset 的值变化,返回的样式会应用到对应的选择器节点上。

当然,光看这个例子,跟用 setData 看好像没有什么区别。但当 worklet 动画和手势结合时,就产生了质变。

# 示例用法二

完整示例参考小程序示例 gesture 页面

<pan-gesture-handler onGestureEvent="handlepan">
  <view class="circle"></view>
</pan-gesture-handler>
Page({
  onLoad() {
    const offset = shared(0);
    this.applyAnimatedStyle('.circle', () => {
      'worklet';
      return {
        transform: `translateX${offset.value}px`
      };
    });
    this._offset = offset;
  },
  handlepan(gestureEvent) {
    'worklet';
    if (gestureEvent.state === GestureState.ACTIVE) {
      this._offset.value += gestureEvent.deltaX;
    }
  }
});

当在 circle 节点上移动时,会产生平滑的拖动效果。handlepan 回调触发在 UI 线程,同时我们修改了 offset 的值,会在 UI 线程产生动画,不必再绕回到 JS 线程。

# 示例用法三:

完整示例参考小程序示例 animation 页面

const offset = shared(0)

// timing 动画,指定目标值和动画时长、曲线
offset.value = timing(300, {
  duration: 200
  easing: Easing.bezier(0.25, 0.1, 0.25, 1),
})

// sprint 动画,指定目标值和阻尼、质量等
offset.value = spring(300, {
  damping: 10,
  mass: 1
})

// 使用 repeat 重复动画过程
offset.value = repeat(timing(10), 4)

// 使用 sequence 对动画进行组合
offset.value = sequence(
  timing(10, { duration: 100 }),
  repeat(timing(50), 4),
  spring(100)
)

我们提供如 timingspring 等常见动画方式的封装方法,开发者可自定义动画曲线,同时可对不同的动画类型进行组合、重复,形成交织动画。

# 相关接口

# 基础类型

# 工具函数

# 动画相关

# 组合动画