# worklet 動畫
小程序采用雙線程架構(gòu),渲染線程(UI 線程)和邏輯線程(JS 線程)分離。JS 線程不會影響 UI 線程的動畫表現(xiàn),如滾動效果。但引入的問題是,UI 線程的事件發(fā)生后,需跨線程傳遞到 JS 線程,進(jìn)而觸發(fā)開發(fā)者回調(diào),當(dāng)做交互動畫(如拖動元素)時(shí),這種異步性會帶來較大的延遲和不穩(wěn)定。
worklet 動畫正是為解決這類問題而誕生的,使得小程序可以做到類原生動畫般的體驗(yàn)。
# 立即體驗(yàn)
使用 worklet 動畫能力時(shí)確保以下兩項(xiàng):
- 確保開發(fā)者工具右上角 > 詳情 > 本地設(shè)置里的
將 JS 編譯成 ES5選項(xiàng)被勾選上 (代碼包體積會少量增加) - worklet 動畫相關(guān)接口僅在
Skyline渲染模式下才能使用

首先,我們需要了解一些相關(guān)概念。
# 概念一:worklet 函數(shù)
一種聲明在開發(fā)者代碼中,可運(yùn)行在 JS 線程或 UI 線程的函數(shù),函數(shù)體頂部有 'worklet' 指令聲明。
# worklet 函數(shù)定義
function someWorklet(greeting) {
'worklet';
console.log(greeting);
}
// 運(yùn)行在 JS 線程
someWorklet('hello') // print: hello
// 運(yùn)行在 UI 線程
wx.worklet.runOnUI(someWorklet)('hello') // print: [ui] hello
# worklet 函數(shù)間相互調(diào)用
const name = 'skyline'
function anotherWorklet() {
'worklet';
return 'hello ' + name;
}
// worklet 函數(shù)間可互相調(diào)用
function someWorklet() {
'worklet';
const greeting = anotherWorklet();
console.log('another worklet says ', greeting);
}
wx.worklet.runOnUI(someWorklet)() // print: [ui] another worklet says hello skyline
# 從 UI 線程調(diào)回到 JS 線程
function someFunc(greeting) {
console.log('hello', greeting);
}
function someWorklet() {
'worklet'
// 訪問非 worklet 函數(shù)時(shí),需使用 runOnJS
// someFunc 運(yùn)行在 JS 線程
runOnJS(someFunc)('skyline')
}
wx.worklet.runOnUI(someWorklet)() // print: hello skyline
# 概念二:共享變量
在 JS 線程創(chuàng)建,可在兩個(gè)線程間同步的變量。
const { shared, runOnUI } = wx.worklet
const offset = shared(0)
function someWorklet() {
'worklet'
console.log(offset.value) // print: 1
// 在 UI 線程修改
offset.value = 2
console.log(offset.value) // print: 2
}
// 在 JS 線程修改
offset.value = 1
runOnUI(someWorklet)()
由 shared 函數(shù)創(chuàng)建的變量,我們稱為 sharedValue 共享變量。用法上可類比 vue3 中的 ref,對它的讀寫都需要通過 .value 屬性,但需注意的是它們并不是一個(gè)概念。sharedValue 的用途主要如下。
# 跨線程共享數(shù)據(jù)
由 worklet 函數(shù)捕獲的外部變量,實(shí)際上會被序列化后生成在 UI 線程的拷貝,如下代碼中, someWorklet 捕獲了 obj 變量,盡管我們修改了 obj 的 name 屬性,但在 someWorklet 聲明的位置,obj 已經(jīng)被序列化發(fā)送到了 UI 線程,因此后續(xù)的修改是無法同步的。
const obj = { name: 'skyline'}
function someWorklet() {
'worklet'
console.log(obj.name) // 輸出的仍舊是 skyline
}
obj.name = 'change name'
wx.worklet.runOnUI(someWorklet)()
sharedValue 就是用來在線程間同步狀態(tài)變化的變量。
const { shared, runOnUI } = wx.worklet
const offset = shared(0)
function someWorklet() {
'worklet'
console.log(offset.value) // 輸出的是新值 1
}
offset.value = 1
runOnUI(someWorklet)()
# 驅(qū)動動畫
worklet 函數(shù)和共享變量就是用來解決交互動畫問題的。相關(guān)接口 applyAnimatedStyle 可通過頁面/組件實(shí)例訪問,接口文檔參考。
<view id="moved-box"></view>
<view id="btn" bind:tap="tap">點(diǎn)擊驅(qū)動小球移動</view>
Page({
onLoad() {
const offset = wx.worklet.shared(0)
this.applyAnimatedStyle('#moved-box', () => {
'worklet';
return {
transform: `translateX(${offset.value}px)`
}
})
this._offset = offset
},
tap() {
// 點(diǎn)擊時(shí)修改 sharedValue 值,驅(qū)動小球移動
this._offset.value = Math.random()
}
})
當(dāng)點(diǎn)擊按鈕 #btn 時(shí),我們用隨機(jī)數(shù)給 offset 進(jìn)行賦值,小球會隨之移動。
applyAnimatedStyle 接口的第二個(gè)參數(shù) updater 為一個(gè) worklet 函數(shù),其捕獲了共享變量 offset,當(dāng) offset 的值變化時(shí),updater 會重新執(zhí)行,并將返回的新 styleObject 應(yīng)用到選中節(jié)點(diǎn)上。
當(dāng)然,光看這個(gè)例子,跟用 setData 看好像沒有什么區(qū)別。但當(dāng) worklet 動畫和手勢結(jié)合時(shí),就產(chǎn)生了質(zhì)變。
# 示例用法
# 手勢處理
<pan-gesture-handler onGestureEvent="handlepan">
<view class="circle"></view>
</pan-gesture-handler>
Page({
onLoad() {
const offset = wx.worklet.shared(0);
this.applyAnimatedStyle('.circle', () => {
'worklet';
return {
transform: `translateX(${offset.value}px)`
};
});
this._offset = offset;
},
handlepan(evt) {
'worklet';
if (evt.state === GestureState.ACTIVE) {
this._offset.value += evt.deltaX;
}
}
});
當(dāng)手指在 circle 節(jié)點(diǎn)上移動時(shí),會產(chǎn)生平滑的拖動效果。handlepan 回調(diào)觸發(fā)在 UI 線程,同時(shí)我們修改了 offset 的值,會在 UI 線程產(chǎn)生動畫,不必再繞回到 JS 線程。
# 自定義動畫曲線
<view id="moved-box"></view>
<view id="btn" bind:tap="tap">點(diǎn)擊驅(qū)動小球移動</view>
const { shared, Easing, timing } = wx.worklet
Page({
onLoad() {
const offset = shared(0)
this.applyAnimatedStyle('#moved-box', () => {
'worklet';
return {
transform: `translateX(${offset.value}px)`
}
})
this._offset = offset
},
tap() {
/**
* 目標(biāo)值 300
* 動畫時(shí)長 200ms
* 動畫曲線 Easing.ease
*/
this._offset.value = timing(300, {
duration: 200,
easing: Easing.ease
})
}
})
內(nèi)置如 timing、spring 等常見動畫方式的封裝方法,開發(fā)者可自定義動畫曲線,同時(shí)可對不同的動畫類型進(jìn)行組合、重復(fù),形成交織動畫。
# 相關(guān)接口
基礎(chǔ)類型 shared、 derived、cancelAnimation
緩動函數(shù) Easing
頁面實(shí)例方法 applyAnimatedStyle、clearAniamtedStyle
# 注意事項(xiàng)
worklet 函數(shù)內(nèi)部有一些調(diào)用上的限制需要留意
- 頁面/組件實(shí)例中定義的
worklet類型回調(diào)函數(shù),內(nèi)部訪問wx上的接口,可按如下方式,通過runOnJS調(diào)回到JS線程。 worklet函數(shù)引用的外部變量,對象類型將被Object.freeze凍結(jié),使用時(shí)需直接訪問對象上具體的屬性。
<tap-gesture-handler onGestureEvent="handleTap">
<view class="circle" >showModal</view>
</tap-gesture-handler>
const { runOnJS, timing } = wx.worklet
Page({
data: {
msg: 'Skyline'
},
onLoad() {
const toValue = 100
const showModal = this.showModal.bind(this)
timing(toValue, { duration: 300 }, () => {
'worklet'
runOnJS(showModal)(msg)
})
},
handleTap() {
'worklet'
// 非常重要!?。?/span>
// const { msg } = this.data
// 通過解構(gòu) this.data 訪問 msg,此時(shí) this.data 將被 Object.freeze 凍結(jié),會導(dǎo)致 setData 無法生效
// 而通過 this.data.msg 則不會凍結(jié) this.data
const msg = `hello ${this.data.msg}`
// 非常重要!?。?/span>
// Page method 必須通過 this.methodName.bind(this) 訪問
const showModal = this.showModal.bind(this)
// 場景一:返回 JS 線程
runOnJS(showModal)(msg)
// 場景二:動畫完成回調(diào)里返回 JS 線程
const toValue = 100
timing(toValue, { duration: 300 }, () => {
'worklet'
runOnJS(showModal)(msg)
})
// 場景三:調(diào)用其它 worklet 函數(shù)
this.doSomething()
},
doSomething() {
'worklet'
},
showModal(msg) {
wx.showModal({
title: msg // title: hello skyline
})
},
})