5
头图

As of now, Composable (composed functions) should be the best way to organize business logic in a Vue 3 application.
It allows us to extract and reuse some small pieces of general logic, making our code easier to write, read and maintain.
Since this way of writing Vue code is relatively new, you might be wondering what are the best practices for writing composable functions? This series of tutorials can serve as a reference guide for you and your team as you work on composable development.
We will cover the following:

  • 1. How to make your composition more configurable with options object parameters; 👈 this topic
  • 2. Use ref and unref to make our parameters more flexible;
  • 3. How to make your return value more useful;
  • 4. Why starting with an interface definition can make your composable functions more powerful;
  • 5. How to use async code without "await" - makes your code easier to understand;

First, though, we need to make sure our understanding of compositional functions is consistent. Let's take a moment to explain what a composition function is.

What is a Composable-composable function?

According to the official documentation, in the concept of a Vue application, a "composed function" is a function that uses the Vue composition API to encapsulate and reuse stateful logic.
This means that any stateful logic that uses reactive processing can be turned into a composable function. This is a little different from the public methods that we usually extract from encapsulation. The public method we encapsulate tends to be stateless: it returns the desired output as soon as it receives some input. Combinatorial functions are often associated with state logic.

Let's take a look at the official useMouse this combined function:

 import { ref, onMounted, onUnmounted } from 'vue'

// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
  // 被组合式函数封装和管理的状态
  const x = ref(0)
  const y = ref(0)

  // 组合式函数可以随时更改其状态。
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  // 一个组合式函数也可以挂靠在所属组件的生命周期上
  // 来启动和卸载副作用
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // 通过返回值暴露所管理的状态
  return { x, y }
}

We define the state as refs , and we update this state when the mouse moves. By returning x and y , we can use them in any component, and we can even nest multiple composite functions.

when we use in component

 <template>
  X: {{ x }} Y: {{ y }}
</template>

<script setup>
  import { useMouse } from './useMouse';
  const { x, y } = useMouse();
</script>

As you can see, by using useMouse we can easily reuse this logic. With just a little bit of code, we can get the mouse coordinate state in the component.

Now that we know the same about composable functions, let's take a look at the first approach that can help us write better composable functions.

options object parameter

Most composite functions will have one or two required parameters, and then a series of optional parameters to help with some additional configuration. When configuring a composite function, we can put a series of optional configurations into a single options object parameter, rather than a long list of parameters.

 // 使用选项对象参数形式
const title = useTitle('A new title', { titleTemplate: '>> %s <<' });
// Title is now ">> A new title <<"

// 使用多参数形式
const title = useTitle('A new title', '>> %s <<');
// Title is now ">> A new title <<"

The form of options object parameters can bring us some conveniences:
First, we don't have to remember the correct order of parameters, especially when there are many parameters. While we can now avoid this type of problem with Typescript and editor hints, there is still a difference this way. With Javascript objects, the order of parameters is less important. (Of course, this also requires that our function definitions need to be clear and clear, we will talk about it later)
Second, the code is more readable because we know what the option does.
Third, the code is more extensible and it is much easier to add new options later. This works both for adding new options to the composed function itself, and for parameter passing when nested.

So it's more friendly to use object parameters, but how do we do that?

Implementation in a composable function

Now let's see how to use the options object parameter in a composed function. Let's make some extensions to the above useMouse :

 export function useMouse(options) {
  const {
    asArray = false,
    throttle = false,
  } = options;

  // ...
};

useMouse itself does not have to pass parameters, so we directly add a options parameter to it for some additional configuration. Through destructuring, we can access all optional parameters, and set default values for each parameter, which avoids the situation that some optional parameters are not passed in calls that do not require additional configuration.

Now let's see how the two composable functions above VueUse use this pattern. VueUse is a common tool set serving Vue3's combined functions. Its original intention is to make all JS APIs that did not support responsiveness support responsive, and save programmers from writing related code themselves.

Let's first look at useTitle , and then look at how useRefHistory is implemented.

Example - useTitle

The function of useTitle is very simple, that is, it is used to update the title of the page.

 const title = useTitle('Initial Page Title');
// Title: "Initial Page Title"

title.value = 'New Page Title';
// Title: "New Page Title"

It also has several selection parameters to facilitate additional flexibility. We can pass titleTemplate as a template, and call it observable by observe (internally implemented through MutationObserver):

 const title = useTitle('Initial Page Title', {
  titleTemplate: '>> %s <<',
  observe: true,
});
// Title: ">> Initial Page Title <<"

title.value = 'New Page Title';
// Title: ">> New Page Title <<"

When we look at its source code , we can see the following processing

 export function useTitle(newTitle, options) {
  const {
    document = defaultDocument,
    observe = false,
    titleTemplate = '%s',
  } = options;
  
  // ...
}

useTitle contains a required parameter and an optional parameter object. As we described above, it is implemented exactly according to this pattern.
Next, let's see how a more complex compositional function uses the options object pattern.

Example - useRefHistory

useRefHistory can help us track all changes to a reactive variable, allowing us to easily perform undo and redo operations.

 // Set up the count ref and track it
const count = ref(0);
const { undo } = useRefHistory(count);

// Increment the count
count.value++;

// Log out the count, undo, and log again
console.log(counter.value); // 1
undo();
console.log(counter.value); // 0

It supports setting many different configuration options

 {
  deep: false,
  flush: 'pre',
  capacity: -1,
  clone: false,
  // ...
}

If you want to know the complete list of these option parameters and their corresponding functions, you can go to the related documents , so I won't repeat them here.

We can pass an options object as the second parameter to further configure the behavior of this combined function, same as our previous example:

 export function useRefHistory(source, options) {
  const {
    deep = false,
    flush = 'pre',
    eventFilter,
  } = options;
 
  // ...
}

We can see that it only deconstructs part of the parameter values internally, this is because useRefHistory internally depends on useManualHistory this combined function, other option parameters will be transparently passed useManualHistory expand and merge.

 // ...

const manualHistory = useManualRefHistory(
  source,
  {
    // Pass along the options object to another composable
    ...options,
    clone: options.clone || deep,
    setSource,
  },
);

// ...

This also matches what we said earlier: composition functions can be nested.

summary

This article is the first part of "Best Practices for Composing Functions". We looked at how the flexibility of composite functions can be improved with options object arguments. There is no need to worry about the problem caused by the wrong order of parameters, and you can flexibly add and expand configuration items. We didn't just study the pattern itself, we also learned how to implement this pattern through useTitle and useRefHistory in VueUse. They are slightly different, but the pattern itself is simple, and there are limits to what we can do with it.

In the next article we will cover how to make our parameters more flexible with ref and unref

 // Works if we give it a ref we already have
const countRef = ref(2);
useCount(countRef);

// Also works if we give it just a number
const countRef = useRef(2);

This adds flexibility and allows us to use our combination in more situations in our application.


前端荣耀
1.6k 声望745 粉丝

一个在程序猿修炼生涯中的修行者