In this tutorial, we'll implement a swiper component to teach you how to write high-performance interactive code. You'll learn:
Let's have a look at what we're building! To try it out, download and install the Lynx Explorer App, then scan the QR code below.
Here's a product detail page example that includes a swiper and some product details. The <Swiper>
accepts images and displays them in a row. Currently, it can't scroll - let's make it interactive.
To achieve this, we need to complete two tasks:
Let's start by listening to touch events to calculate the current scroll progress.
When a touch starts, we record the initial touch coordinates. This allows us to calculate the distance moved (represented by delta
) when the finger moves.
Next, we use currentOffsetRef
to track the swiper component's offset, adding it to delta
to get the final offset.
Once we get the offset, we can update the scroll position. Add an updateSwiperOffset
function and call it when the finger moves.
Next, we use Node Manipulation to get the swiper-container
node and use setNativeProps
to update the transform
property, thereby updating the scroll position.
Now the <Swiper>
component can scroll with finger movements!
You might think of using state
to update the progress, like this:
However, in scenarios requiring frequent updates, this approach would cause constant component re-rendering, affecting performance. A better approach is to directly manipulate nodes, as shown in the example.
You can refer to Direct Node Manipulation to learn more.
The <Swiper>
component's code is getting complex. We can use hooks to encapsulate the logic into two parts, simplifying the component code and improving maintainability:
useOffset
, centralizing all scroll-related logic in this hookuseUpdateSwiperStyle
, centralizing all <Swiper>
component style update logic in this hookFinally, the code is more concise.
You may have noticed that sometimes the scrolling doesn't feel smooth. This is because touch events occur in the main thread, while event listener code runs in the background thread, causing delayed touch event responses. This phenomenon is particularly noticeable on low-end devices.
We can use Main Thread Script to optimize this issue. After converting to main thread script, the scrolling becomes much smoother!
To achieve that, we need to migrate frequently triggered code to main thread script, including:
Let's modify both useOffset
and useUpdateSwiperStyle
.
useOffset
Add the main thread
identifier to handleTouchStart
and handleTouchMove
to convert them into main thread functions.
Convert bindtouchstart
and bindtouchmove
to main-thread:bindtouchstart
and main-thread:bindtouchmove
to listen to events in main thread script.
useUpdateSwiperStyle
Convert useRef
to useMainThreadRef
.
Pass swiperContainerRef
to <view>
through the main-thread:ref
attribute to access the node in the main thread.
The main thread node provides many capabilities, as shown in MainThread.Element
. Here we call the setStyleProperties
method to modify the transform
property, updating the <Swiper>
component's position.
With this, we've completed the main thread script conversion. Now high-frequency functions run in the main thread, making the interaction smoother.
Only use main thread script when encountering response delay issues with frequently triggered events!
Introducing main thread script increases code complexity because main thread script and background thread script run in isolated environments and need "special bridges" to communicate.
Main thread script run high-frequency code in the main thread, increasing its burden. Overuse may cause main thread lag.
Here's a progress indicator example that shows which page you're on when scrolling.
Currently, it only has styling but lacks progress update logic. We'll use this example to demonstrate how to enable communication between main thread and background thread:
The core of the progress indicator is the <Indicator>
component, which accepts a current
prop indicating the current page.
Now we just need to update current
when scrolling.
We add an onIndexChange
callback to useOffset
to update current
during scrolling.
And pass setCurrent
as the onIndexUpdate
callback to useOffset
.
This way, when scrolling past a page, useOffset
will call onIndexUpdate
to update current
, thereby updating the progress indicator.
But wait, why is there an error?!
Main thread script and background thread script run in separate runtimes. Functions in one runtime cannot directly call functions in another runtime. They need "special bridges" to communicate:
runOnMainThread
runOnBackground
onIndexUpdate
is a background thread function. When called in a main thread function, we need to use runOnBackground
Now the progress indicator updates automatically as you scroll!
A useful progress indicator should also support clicking to jump to the corresponding page. Let's add click-to-jump functionality to the <Indicator>
component.
Add an updateIndex
method in useOffset
that uses runOnMainThread
to call updateOffset
to update the component position.
Here's the complete code:
Great! Now the progress indicator supports click-to-jump functionality.
In the following example, we've added a snap effect animation to the <Swiper>
component. Currently, the snap effect animation isn't ideal, we can add some props to customize it.
We'll use this example to demonstrate value passing between main thread and background thread script.
First, we add a duration
prop to the <Swiper>
component to control the snap animation duration.
Let's see how it works internally. When touch ends, useOffset
calls the animate
function to update the component position with animation effects. animate
is a main thread function that accepts initial and target values and updates the component position according to the animation curve over the duration
time.
Here, both animate
and handleTouchEnd
are main thread functions, and they can access the background thread value duration
.
Main thread script and background thread script run in separate runtimes and are isolated from each other.
However, to simplify main thread script development, Lynx automatically passes background thread values that main thread functions depend on to those functions, though this process has some limitations:
Promise
s, and other non-serializable values cannot be passed.render
. If background thread values change after render
, main thread functions won't be aware of these updates.Next, we add a main-thread:easing
prop to the <Swiper>
component to allow users to customize the animation curve.
Inside the component, the main thread function easeInOut
is passed to the background thread hook useOffset
And in useOffset
, it's passed to the main thread function animate
.
Main thread values, such as MainThreadRef
and main thread functions, cannot be directly used by the background thread.
However, they can be passed by the background thread, such as being passed as props
to components or as function parameters to other hooks or functions, and ultimately used in the main thread.
You may have noticed that when passing main thread functions or MainThreadRef
as attributes, they need the main-thread:
prefix, like main-thread:ref
and main-thread:bindtouchstart
.
By convention, when a prop expects a main thread function, it should have the main-thread:
prefix, like main-thread:easing
. We recommend following this convention for custom components too. This helps component users understand that the property requires a main thread function.
However, because variable names containing colons :
are illegal in JavaScript, you need to rename these props when using them inside components.
Finally, we have a swiper with customizable animation curves.
In this tutorial, we started with a simple <Swiper>
component, gradually optimized its performance, and finally implemented a swiper with customizable animation curves.
We learned about: