Vue.js 挑战练习

最近做了一下这个Vue.js 挑战,其中的题目大多出自Vue3 文档,都不是很难,但涉及到的知识点 比较琐碎,用来复习挺好的。

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

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

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

有时组件模板的一部分逻辑上属于该组件,而从技术角度来看,最好将模板的这一部分移动到 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

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

函数式组件

这题我不是很懂,翻了一下大家的解决方案,感觉这个比较能看懂:21 - functional component · Issue #322 · webfansplz/vuejs-challenges · GitHub

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>

相关知识点:

渲染函数[h()]

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

js
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>

参考:

Composable Function

本节相关知识点:组合式函数 | Vue.js

切换器

尝试编写可组合函数

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>

实现本地存储函数

封装一个localStorageAPI

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>

相关知识点:

鼠标坐标

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

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>

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>

相关知识点:

响应性丢失

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

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

可写的计算属性

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>

相关知识点:可写的计算属性

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

浅层 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()

依赖注入

child.vue

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

相关知识点:组合式 API:依赖注入 | Vue.js

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

自定义 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

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 修饰符

优化性能的指令

见上面。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

防抖点击指令

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

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>

相关知识点:指令钩子

激活的样式-指令

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>

相关知识点:事件修饰符

按键修饰符

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>

相关知识点:按键修饰符

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()

Lifecycle

生命周期钩子

同上:生命周期钩子

Reactivity:Advanced

浅层 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>

相关知识点:

Effect 作用域 API

同上:Effect 作用域 API

自定义 ref

同上:自定义 ref

Reactivity:Core

ref 全家桶

同上:ref 全家桶

可写的计算属性

同上:可写的计算属性

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