mirror of
https://github.com/Sevichecc/Urara-Blog.git
synced 2025-04-30 22:29:29 +08:00
1237 lines
25 KiB
Markdown
1237 lines
25 KiB
Markdown
---
|
||
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)
|