最近做了一下这个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创建liprops.list.map((item: { name: string }, index: number) =>h('li',{// 点击时处罚toggle。并将当前index作为参数传入toggleonClick: () => 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: stringtitle: stringchildren: 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
- Creating a Recursive Tree Component in Vue.js | DigitalOcean
相关知识点:单文件组件
<script setup>
| Vue.js
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?: numbermax?: 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>
相关知识点:
鼠标坐标
这个没通过单元测试,不知道什么原因,试了下其他人的也没能通过……
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.pageXy.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 reffunction 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 trueconsole.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 originalfooRef.value++console.log(state.foo === 2)// mutating the original also updates the refstate.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 reactivityconst {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 = 1setTimeout(() => (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 triggerwatch(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 onceconst scope = effectScope()scope.run(() => {watch(doubled, () => console.log(doubled.value))watchEffect(() => console.log(`Count: ${doubled.value}`))counter.value = 2})setTimeout(() => {counter.value = 4scope.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 timeoutreturn customRef((track, trigger) => {return {get() {track()return value},set(newValue) {clearTimeout(timeout)timeout = setTimeout(() => {value = newValuetrigger()}, 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.valueif (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 timeoutlet count = 0return (...args) => {if (count === 0) {count++fn(...args)}clearTimeout(timeout)timeout = setTimeout(() => {fn(...args)}, delay)}}const VDebounceClick = {mounted: (el, binding) => {const { value, arg } = bindingel.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.valuewatchEffect(() => {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><liv-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.valueel.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
原始值 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
自定义 ref
Reactivity:Core
ref 全家桶
可写的计算属性
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 = valueresolve(initial.value)})}return {toBe}}async function increase() {count.value = 0setInterval(() => {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