---
title: Vue.js 挑战练习
created: 2022-08-12
summary: 我的答案以及相关知识点
tags:
  - Vue
---

最近做了一下这个[Vue.js 挑战](https://cn-vuejs-challenges.netlify.app/questions/14-dynamic-css-values/README.zh-CN.html)，其中的题目大多出自[Vue3 文档](https://staging-cn.vuejs.org/)，都不是很难，但涉及到的知识点
比较琐碎，用来复习挺好的。

然后这是我的答案和题目涉及到的知识点，除了[鼠标指针](###鼠标指针)这个部分没通过单元测试之外，其他都都通过了，然后这个鼠标指针为什么没通过单元测试我也没弄明白，试了下其他人的也通过不了，好奇怪……

这里省去部分题目，主要写答案。

## Built-ins

### DOM 传送门

Vue.js 提供了一个内置组件，将其插槽内容渲染到另一个 DOM，成为该 DOM 的一部分。

```vue
<script setup>
const msg = 'Hello World'
</script>
<template>
  <teleport to="body">
    <span>{{ msg }}</span>
  </teleport>
</template>
```

相关知识点 ：[Teleport | Vue.js](https://v3.cn.vuejs.org/guide/teleport.html#teleport)

> 有时组件模板的一部分逻辑上属于该组件，而从技术角度来看，最好将模板的这一部分移动到 DOM 中 Vue app 之外的其他位置[^1]。

- 有点像传送门，将相应元素渲染到制定位置
- to 后面写 css selector

### 优化性能的指令

Vue.js 提供了一个指令，以便只渲染一次元素和组件，并且跳过以后的更新。

```vue
<script setup>
import { ref } from 'vue'

const count = ref(0)

setInterval(() => {
  count.value++
}, 1000)
</script>

<template>
  <span v-once>使它从不更新: {{ count }}</span>
</template>
```

相关知识点：`Vue-事件修饰符`

## CSS Features

### 动态 CSS

Vue 单文件组件 `<style>` 模块支持给 CSS 绑定动态值。

```vue
<script setup>
import { ref } from 'vue'
const theme = ref('red')

const colors = ['blue', 'yellow', 'red', 'green']

setInterval(() => {
  theme.value = colors[Math.floor(Math.random() * 4)]
}, 1000)
</script>

<template>
  <p>hello</p>
</template>

<style scoped>
/* Modify the code to bind the dynamic color */
p {
  color: v-bind(theme);
}
</style>
```

相关知识点：`v-bind` `Dynamic Styling动态绑定样式`

### 全局 CSS

给具有 CSS 作用域的 Vue 单文件组件设置全局 CSS 样式

```vue
<template>
  <p>Hello Vue.js</p>
</template>

<style scoped>
p {
  font-size: 20px;
  color: red;
  text-align: center;
  line-height: 50px;
}

/* Make it work */
:global(body) {
  width: 100vw;
  height: 100vh;
  background-color: burlywood;
}
</style>
```

或者

```vue
<template>
  <p>Hello Vue.js</p>
</template>

<style scoped>
p {
  font-size: 20px;
  color: red;
  text-align: center;
  line-height: 50px;
}
</style>
<style>
/* Make it work */
body {
  width: 100vw;
  height: 100vh;
  background-color: burlywood;
}
</style>
```

相关知识点：[单文件组件 CSS 功能 | Vue.js](https://staging-cn.vuejs.org/api/sfc-css-features.html)

## Components

### DOM 传送门

见上面

### Props 验证

验证 Button 组件的 Prop 类型 ，使它只接收: primary | ghost | dashed | link | text | default ，且默认值为 default

```vue
<script setup>
import Button from './Button.vue'
defineProps({
  type: {
    type: String,
    default: 'default',
    validator: value => {
      ;['primary', 'ghost', 'dashed', 'link', 'text', 'default'].includes(value)
    }
  }
})
</script>

<template>
  <Button type="dashed" />
</template>
```

相关知识点：[Props | Vue.js](https://staging-cn.vuejs.org/guide/components/props.html#prop-validation)

### 函数式组件

这题我不是很懂，翻了一下大家的解决方案，感觉这个比较能看懂：[21 - functional component · Issue #322 · webfansplz/vuejs-challenges · GitHub](https://github.com/webfansplz/vuejs-challenges/issues/322)

```vue
<script setup lang="ts">
import { ref, h } from 'vue'

/**
 * Implement a functional component :
 * 1. Render the list elements (ul/li) with the list data
 * 2. Change the list item text color to red when clicked.
 */
const ListComponent = (props, { emit }) =>
  h(
    // 创建 ul
    'ul',
    // 根据传入的props创建li
    props.list.map((item: { name: string }, index: number) =>
      h(
        'li',
        {
          // 点击时处罚toggle。并将当前index作为参数传入toggle
          onClick: () => emit('toggle', index),
          // 将当前点击的li颜色设置为红色
          style: index === props.activeIndex ? { color: 'red' } : null
        },
        // li 默认值
        item.name
      )
    )
  )
ListComponent.props = ['list', 'activeIndex']
ListComponent.emits = ['toggle']

const list = [
  {
    name: 'John'
  },
  {
    name: 'Doe'
  },
  {
    name: 'Smith'
  }
]

const activeIndex = ref(0)

function toggle(index: number) {
  activeIndex.value = index
}
</script>

<template>
  <list-component :list="list" :active-index="activeIndex" @toggle="toggle" />
</template>
```

相关知识点：

- [渲染函数 & JSX | Vue.js](https://staging-cn.vuejs.org/guide/extras/render-function.html#functional-components)
- [渲染机制 | Vue.js](https://staging-cn.vuejs.org/guide/extras/rendering-mechanism.html)

### 渲染函数[h()]

> 使用 h 渲染函数来实现一个组件。

```vue
import { defineComponent, h } from 'vue'; export default defineComponent({ name: 'MyButton', props: { disabled: { type: Boolean,
default: false, }, }, emits: ['custom-click'], setup(props, { emit, slots }) { return () => h( 'button', { disabled:
props.disabled, onClick: () => emit('custom-click'), }, slots.default?.() ); }, });
```

### 树组件

实现一个树组件

```vue
<script setup lang="ts">
import { defineComponent } from 'vue'
interface TreeData {
  key: string
  title: string
  children: TreeData[]
}
defineProps<{ data: TreeData[] }>()
</script>

<template>
  <ul v-for="node in data">
    <li>{{ node.title }}</li>
    <template v-if="node.children && node.children.length">
      // 用递归的方法来实现
      <TreeComponent :data="node.children" />
    </template>
  </ul>
</template>
```

参考：

- [208 - Tree Component · Issue #659 · webfansplz/vuejs-challenges · GitHub](https://github.com/webfansplz/vuejs-challenges/issues/659)
- [Creating a Recursive Tree Component in Vue.js | DigitalOcean](https://www.digitalocean.com/community/tutorials/vuejs-recursive-components)
  相关知识点：[单文件组件 `<script setup>` | Vue.js](https://staging-cn.vuejs.org/api/sfc-script-setup.html#recursive-components)

## Composable Function

本节相关知识点：[组合式函数 | Vue.js](https://staging-cn.vuejs.org/guide/reusability/composables.html)

### 切换器

尝试编写可组合函数

```vue
<script setup lang="ts">
import { ref } from 'vue'
/**
 * Implement a composable function that toggles the state
 * Make the function work correctly
 */
function useToggle(init: boolean) {
  const state = ref(init)
  const toggle = () => (state.value = !state.value)
  return [state, toggle]
}

const [state, toggle] = useToggle(false)
</script>

<template>
  <p>State: {{ state ? 'ON' : 'OFF' }}</p>
  <p @click="toggle">Toggle state</p>
</template>
```

### 计数器

实现一个计数器

```vue
<script setup lang="ts">
import { ref } from 'vue'

interface UseCounterOptions {
  min?: number
  max?: number
}

/**
 * Implement the composable function
 * Make sure the function works correctly
 */
function useCounter(initialValue = 0, options: UseCounterOptions = {}) {
  const count = ref(initialValue)

  const inc = () => {
    if (count.value < options.max) count.value++
  }

  const dec = () => {
    if (count.value > options.min) count.value--
  }

  const reset = () => (count.value = initialValue)
  return { count, inc, dec, reset }
}

const { count, inc, dec, reset } = useCounter(0, { min: 0, max: 10 })
</script>
```

### 实现本地存储函数

封装一个`localStorage`API

```vue
<script setup lang="ts">
import { ref, watchEffect } from 'vue'

/**
 * Implement the composable function
 * Make sure the function works correctly
 */
function useLocalStorage(key: string, initialValue: any) {
  const value = ref(localStorage.getItem(key) || initialValue)
  watchEffect(() => {
    localStorage.setItem(key, value.value)
  })
  return value
}

const counter = useLocalStorage('counter', 0)

// We can get localStorage by triggering the getter:
console.log(counter.value)

// And we can also set localStorage by triggering the setter:

const update = () => counter.value++
</script>

<template>
  <p>Counter: {{ counter }}</p>
  <button @click="update">Update</button>
</template>
```

相关知识点：

- [watchEffect()](https://staging-cn.vuejs.org/api/reactivity-core.html#watcheffect)
- [Window.localStorage - Web API 接口参考 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/localStorage)

### 鼠标坐标

这个没通过单元测试，不知道什么原因，试了下其他人的也没能通过……

```vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
// Implement ...
function useEventListener(target, event, callback) {
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))
}

// Implement ...
function useMouse() {
  const x = ref(0)
  const y = ref(0)
  useEventListener(window, 'mousemove', e => {
    x.value = e.pageX
    y.value = e.pageY
  })
  return { x, y }
}
const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>
```

## Composition API

### 生命周期钩子

```vue
<script setup lang="ts">
import { onMounted, inject, onUnmounted } from 'vue'

const timer = inject('timer')
const count = inject('count')

onMounted(() => {
  timer.value = window.setInterval(() => {
    count.value++
  }, 1000)
})
// 计时器要清除
onUnmounted(() => {
  window.clearInterval(timer.value)
})
</script>

<template>
  <div>
    <p>Child Component: {{ count }}</p>
  </div>
</template>
```

- 相关知识点：
  - [组合式 API：依赖注入 | Vue.js](https://staging-cn.vuejs.org/api/composition-api-dependency-injection.html)
  - [组合式 API：生命周期钩子 | Vue.js](https://staging-cn.vuejs.org/api/composition-api-lifecycle.html#onunmounted)

### ref 全家桶

```vue
<script setup lang="ts">
import { ref, Ref, reactive, isRef, unref, toRef } from 'vue'

const initial = ref(10)
const count = ref(0)

// Challenge 1: Update ref
function update(value) {
  count.value = value
}

/**
 * Challenge 2: Check if the `count` is a ref object.
 * Make the output be 1
 */
console.log(isRef(count) ? 1 : 0)

/**
 * Challenge 3: Unwrap ref
 * Make the output be true
 */
function initialCount(value: number | Ref<number>) {
  // Make the output be true
  console.log(unref(value) === 10)
}

initialCount(initial)

/**
 * Challenge 4:
 * create a ref for a property on a source reactive object.
 * The created ref is synced with its source property:
 * mutating the source property will update the ref, and vice-versa.
 * Make the output be true
 */
const state = reactive({
  foo: 1,
  bar: 2
})
const fooRef = toRef(state, 'foo') // change the impl...

// mutating the ref updates the original
fooRef.value++
console.log(state.foo === 2)

// mutating the original also updates the ref
state.foo++
console.log(fooRef.value === 3)
</script>

<template>
  <div>
    <p>
      <span @click="update(count - 1)">-</span>
      {{ count }}
      <span @click="update(count + 1)">+</span>
    </p>
  </div>
</template>
```

相关知识点：

- [isRef()](https://staging-cn.vuejs.org/api/reactivity-utilities.html#isref)
- [unref()](https://staging-cn.vuejs.org/api/reactivity-utilities.html#unref)
- [toRef](https://staging-cn.vuejs.org/api/reactivity-utilities.html#toref)

### 响应性丢失

保证解构/扩展不丢失响应性

```vue
<script setup lang="ts">
import { reactive, toRefs } from 'vue'

function useCount() {
  const state = reactive({
    count: 0
  })

  function update(value: number) {
    state.count = value
  }

  return {
    state: toRefs(state),
    update
  }
}

// Ensure the destructured properties don't lose their reactivity
const {
  state: { count },
  update
} = useCount()
</script>

<template>
  <div>
    <p>
      <span @click="update(count - 1)">-</span>
      {{ count }}
      <span @click="update(count + 1)">+</span>
    </p>
  </div>
</template>
```

相关知识点：[toRefs](https://staging-cn.vuejs.org/api/reactivity-utilities.html#torefs)

### 可写的计算属性

```vue
<script setup lang="ts">
import { ref, computed } from 'vue'

const count = ref(1)
const plusOne = computed({
  get() {
    return count.value + 1
  },
  set(newValue) {
    count.value = newValue - 1
  }
})

/**
 * Make the `plusOne` writable.
 * So that we can get the result `plusOne` to be 3, and `count` to be 2.
 */

plusOne.value++
</script>

<template>
  <div>
    <p>{{ count }}</p>
    <p>{{ plusOne }}</p>
  </div>
</template>
```

相关知识点：[可写的计算属性 ](https://staging-cn.vuejs.org/guide/essentials/computed.html#writable-computed)

### watch 全家桶

```vue
<script setup lang="ts">
import { ref, watch } from 'vue'

const count = ref(0)

/**
 * Challenge 1: Watch once
 * Make sure the watch callback only triggers once
 */
const watchOnce = watch(count, () => {
  console.log('Only triggered once')
  watchOnce()
})

count.value = 1
setTimeout(() => (count.value = 2))

/**
 * Challenge 2: Watch object
 * Make sure the watch callback is triggered
 */
const state = ref({
  count: 0
})

watch(
  state,
  () => {
    console.log('The state.count updated')
  },
  { deep: true }
)

state.value.count = 2

/**
 * Challenge 3: Callback Flush Timing
 * Make sure visited the updated eleRef
 */

const eleRef = ref()
const age = ref(2)
watch(
  age,
  () => {
    console.log(eleRef.value)
  },
  {
    flush: 'post'
  }
)
age.value = 18
</script>

<template>
  <div>
    <p>
      {{ count }}
    </p>
    <p ref="eleRef">
      {{ age }}
    </p>
  </div>
</template>
```

相关知识点：[侦听器 | Vue.js](https://staging-cn.vuejs.org/guide/essentials/watchers.html)

### 浅层 ref

响应式 API: shallowRef

```vue
<script setup lang="ts">
import { shallowRef, watch } from 'vue'

const state = shallowRef({ count: 1 })

// Does NOT trigger
watch(
  state,
  () => {
    console.log('State.count Updated')
  },
  { deep: true }
)

/**
 * Modify the code so that we can make the watch callback trigger.
 */
state.value = { count: 2 }
</script>

<template>
  <div>
    <p>
      {{ state.count }}
    </p>
  </div>
</template>
```

相关知识点：[shallowRef()](https://staging-cn.vuejs.org/api/reactivity-advanced.html#shallowref)

### 依赖注入

child.vue

```vue
<script setup lang="ts">
import { inject } from 'vue'
const count = inject('count')
</script>

<template>
  {{ count }}
</template>
```

相关知识点：[组合式 API：依赖注入 | Vue.js](https://staging-cn.vuejs.org/api/composition-api-dependency-injection.html)

### Effect 作用域 API

```vue
<script setup lang="ts">
import { ref, computed, watch, watchEffect, effectScope } from 'vue'

const counter = ref(1)
const doubled = computed(() => counter.value * 2)

// use the `effectScope` API to make these effects stop together after being triggered once

const scope = effectScope()
scope.run(() => {
  watch(doubled, () => console.log(doubled.value))
  watchEffect(() => console.log(`Count: ${doubled.value}`))
  counter.value = 2
})

setTimeout(() => {
  counter.value = 4
  scope.stop()
})
</script>

<template>
  <div>
    <p>
      {{ doubled }}
    </p>
  </div>
</template>
```

相关知识点：[effectScope](https://staging-cn.vuejs.org/api/reactivity-advanced.html#effectscope)

### 自定义 ref

```vue
<script setup>
import { watch, customRef } from 'vue'

/**
 * Implement the function
 */
function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger()
        }, delay)
      }
    }
  })
}
const text = useDebouncedRef('hello')

/**
 * Make sure the callback only gets triggered once when entered multiple times in a certain timeout
 */
watch(text, value => {
  console.log(value)
})
</script>

<template>
  <input v-model="text" />
</template>
```

相关知识点：[customRef](https://staging-cn.vuejs.org/api/reactivity-advanced.html#customref)

## Directives

### 大写

创建一个自定义的修饰符  `capitalize`，它会自动将  `v-model`  绑定输入的字符串值首字母转为大写：
App.vue

```vue
<script setup>
import { ref } from 'vue'
import Input from './Input.vue'

const value = ref('')
</script>

<template>
  <Input type="text" v-model.capitalize="value" />
</template>
```

Input.vue

```vue
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
  modelValue: String,
  modelModifiers: {
    default: () => ({})
  }
})

const emit = defineEmits(['update:modelValue'])
function emitValue(e) {
  let value = e.target.value
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:modelValue', value)
}
</script>

<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>
```

相关知识点：[处理 v-model 修饰符](https://staging-cn.vuejs.org/guide/components/events.html#usage-with-v-model)

### 优化性能的指令

见上面。v-once

### 切换焦点指令

```vue
<script setup lang="ts">
import { ref } from 'vue'

const state = ref(false)

/**
 * Implement the custom directive
 * Make sure the input element focuses/blurs when the 'state' is toggled
 *
 */

const VFocus = {
  updated: (el, state) => (state.value ? el.focus() : el.blur())
}

setInterval(() => {
  state.value = !state.value
}, 2000)
</script>

<template>
  <input v-focus="state" type="text" />
</template>
```

相关知识点：[自定义指令 | Vue.js](https://staging-cn.vuejs.org/guide/reusability/custom-directives.html)

### 防抖点击指令

尝试实现一个防抖点击指令

```vue
<script setup lang="ts">
/**
 * Implement the custom directive
 * Make sure the `onClick` method only gets triggered once when clicked many times quickly
 * And you also need to support the debounce delay time option. e.g `v-debounce-click:ms`
 *
 */

function debounce(fn, delay) {
  let timeout
  let count = 0
  return (...args) => {
    if (count === 0) {
      count++
      fn(...args)
    }
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      fn(...args)
    }, delay)
  }
}

const VDebounceClick = {
  mounted: (el, binding) => {
    const { value, arg } = binding
    el.addEventListener('click', debounce(value, arg))
  }
}

function onClick() {
  console.log('Only triggered once when clicked many times quickly')
}
</script>

<template>
  <button v-debounce-click:200="onClick">Click on it many times quickly</button>
</template>
```

相关知识点：[指令钩子](https://staging-cn.vuejs.org/guide/reusability/custom-directives.html#introduce)

### 激活的样式-指令

```vue
<script setup lang="ts">
import { ref, watchEffect } from 'vue'

/**
 * Implement the custom directive
 * Make sure the list item text color changes to red when the `toggleTab` is toggled
 *
 */
const VActiveStyle = {
  mounted: (el, binding) => {
    const [styles, fn] = binding.value
    watchEffect(() => {
      Object.keys(styles).map(key => (el.style[key] = fn() ? styles[key] : ''))
    })
  }
}

const list = [1, 2, 3, 4, 5, 6, 7, 8]
const activeTab = ref(0)
function toggleTab(index: number) {
  activeTab.value = index
}
</script>

<template>
  <ul>
    <li
      v-for="(item, index) in list"
      :key="index"
      v-active-style="[{ color: 'red' }, () => activeTab === index]"
      @click="toggleTab(index)">
      {{ item }}
    </li>
  </ul>
</template>
```

### 实现简易版`v-model`指令

```vue
<script setup lang="ts">
import { ref } from 'vue'

/**
 * Implement a custom directive
 * Create a two-way binding on a form input element
 *
 */
const VOhModel = {
  mounted: (el, binding) => {
    el.value = binding.value
    el.addEventListener('input', () => {
      value.value = el.value
    })
  }
}

const value = ref('Hello Vue.js')
</script>

<template>
  <input v-oh-model="value" type="text" />
  <p>{{ value }}</p>
</template>
```

## Event Handling

### 阻止事件冒泡

```vue
<script setup lang="ts">
const click1 = () => {
  console.log('click1')
}

const click2 = e => {
  console.log('click2')
}
</script>

<template>
  <div @click="click1()">
    <div @click.stop="click2()">click me</div>
  </div>
</template>
```

相关知识点：[事件修饰符](https://staging-cn.vuejs.org/guide/essentials/event-handling.html#event-modifiers)

### 按键修饰符

```vue
<template>
  <!-- Add key modifiers made this will fire even if Alt or Shift is also pressed -->
  <button @click.alt="onClick1" @click.shift="onClick1">A</button>

  <!-- Add key modifiers made this will only fire when Shift and no other keys are pressed -->
  <button @click.shift.exact="onCtrlClick">A</button>

  <!-- Add key modifiers made this will only fire when no system modifiers are pressed -->
  <button @click.exact="onClick2">A</button>
</template>

<script setup>
function onClick1() {
  console.log('onClick1')
}
function onCtrlClick() {
  console.log('onCtrlClick')
}
function onClick2() {
  console.log('onClick2')
}
</script>
```

相关知识点：[按键修饰符](https://staging-cn.vuejs.org/guide/essentials/event-handling.html#key-modifiers)

## Global API:General

### 下一次 DOM 更新

在`Vue.js`中改变响应式状态时，DOM 不会同步更新。 `Vue.js`  提供了一个用于等待下一次 DOM 更新的方法

```vue
<script setup>
import { ref, nextTick } from 'vue'

const count = ref(0)
const counter = ref(null)

async function increment() {
  count.value++

  /**
   * DOM is not yet updated, how can we make sure that the DOM gets updated
   * Make the output be true
   */
  await nextTick()
  console.log(+counter.value.textContent === 1)
}
</script>

<template>
  <button ref="counter" @click="increment">
    {{ count }}
  </button>
</template>
```

相关知识点：[nextTick()](https://staging-cn.vuejs.org/api/general.html#nexttick)

## Lifecycle

### 生命周期钩子

[同上：生命周期钩子](#生命周期钩子)

## Reactivity:Advanced

### 浅层 ref

[同上：浅层 ref](#浅层-ref)

### 原始值 API

```vue
<script setup lang="ts">
import { reactive, isReactive, toRaw, markRaw } from 'vue'

const state = { count: 1 }
const reactiveState = toRaw(reactive(state))

/**
 * Modify the code so that we can make the output be true.
 */
console.log(reactiveState === state)

/**
 * Modify the code so that we can make the output be false.
 */
const info = markRaw({ count: 1 })
const reactiveInfo = reactive(info)

console.log(isReactive(reactiveInfo))
</script>

<template>
  <div>
    <p>
      {{ reactiveState.count }}
    </p>
  </div>
</template>
```

相关知识点：

- [toRaw](https://staging-cn.vuejs.org/api/reactivity-advanced.html#toraw)
- [markRaw](https://staging-cn.vuejs.org/api/reactivity-advanced.html#markraw)

### Effect 作用域 API

[同上：Effect 作用域 API](#effect-作用域-api)

### 自定义 ref

[同上：自定义 ref](#自定义-ref)

## Reactivity:Core

### ref 全家桶

[同上：ref 全家桶](#ref-全家桶)

### 可写的计算属性

[同上：可写的计算属性](#可写的计算属性)

### watch 全家桶

[同上：watch 全家桶](#watch-全家桶)

## Reactivity:Utilities

### 响应性丟失

[同上：响应性丟失](#响应性丟失)

## Utility Function

### until

```vue
<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)

/**
 * Implement the until function
 */

function until(initial) {
  function toBe(value) {
    return new Promise(resolve => {
      initial.value = value
      resolve(initial.value)
    })
  }

  return {
    toBe
  }
}

async function increase() {
  count.value = 0
  setInterval(() => {
    count.value++
  }, 1000)
  await until(count).toBe(3)
  console.log(count.value === 3) // Make sure the output is true
}
</script>

<template>
  <p @click="increase">Increase</p>
</template>
```

## Web Components

### 自定义元素

```vue
<script setup lang="ts">
import { onMounted, defineCustomElement } from 'vue'

/**
 * Implement the code to create a custom element.
 * Make the output of page show "Hello Vue.js".
 */
const VueJs = defineCustomElement({
  props: { message: String },
  template: '<span>{{message}}</span>'
})

customElements.define('vue-js', VueJs)
onMounted(() => {
  document.getElementById('app')!.innerHTML = '<vue-js message="Hello Vue.js"></vue-js>'
})
</script>

<template>
  <div id="app"></div>
</template>
```

并且 vite.config.js 里要做相关设置
相关知识点：[Vue 与 Web Components | Vue.js](https://staging-cn.vuejs.org/guide/extras/web-components.html)
