一文掌握Vue3基础&高阶用法

题记

通过一个开源项目Vue Challenge, 由浅入深的掌握Vue3 的用法,包括自定义指令,自定义Ref,依赖注入(Dependency Injection),Hooks封装等一些有趣的用法,VueUse中全是这种高阶的用法。本文通过记录这些挑战的答案,温故而知新,篇幅较长,适合慢慢看。
容易题:平时用到最多的基础语法和概念
中等题:平时用的较少的,着重优化的一些封装,vue-use上大部分都是此
困难题:高级的封装组件这些,自定义组件

热身题(warm-up)1

1. Hello World

考察渲染语法 {{ msg }},这题不会就告辞吧

容易题(easy)13

2. ref 全家桶

考察对 isRef, unref, toRef 的理解和使用

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

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

// 挑战 1: 更新 ref
function update(value) {
  // 实现...
  count.value = value
}

/**
 * 挑战 2: 检查`count`是否为一个 ref 对象
 * 确保以下输出为1
*/
console.log(
  // impl ? 1 : 0
  isRef(count) ? 1 : 0
)

/**
 * 挑战 3: 如果参数是一个 ref,则返回内部值,否则返回参数本身
 * 确保以下输出为true
*/
function initialCount(value: number | Ref<number>) {
  // 确保以下输出为true
  console.log(unref(value) === 10)
}

initialCount(initial)

/**
 * 挑战 4:
 * 为源响应式对象上的某个 `property` 新创建一个 `ref`。
 * 然后,`ref` 可以被传递,它会保持对其源`property`的响应式连接。
 * 确保以下输出为true
*/
const state = reactive({
  foo: 1,
  bar: 2,
})
const fooRef = toRef(state,'foo') // 修改这里的实现...

// 修改引用将更新原引用
fooRef.value++
console.log(state.foo === 2)

// 修改原引用也会更新`ref`
state.foo++
console.log(fooRef.value === 3)

</script>

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

3. 响应性丟失

考察对 toRefs 的理解和使用

<script setup lang="ts">
import { reactive } from "vue"

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

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

  return {
     // 此处转为响应式
    state: toRefs(state),
    update,
  }
}

// 确保解构不丢失响应性
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>

4. 可写的计算属性

考察对 computed 的 set 的理解和使用

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

const count = ref(1)
// const plusOne = computed(() => count.value + 1)
// 定义一个带有 get 和 set 的 computed 属性
const plusOne = computed({
  get: () => count.value + 1,
  set: (val: number) => {
    count.value = val - 1
  }
})

/**
 * 确保 `plusOne` 可以被写入。
 * 最终我们得到的结果应该是 `plusOne` 等于 3 和 `count` 等于 2。
*/

plusOne.value++

</script>

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

5. watch 全家桶

考察对 watch 的理解和使用

// 你的答案
<template>
  <div>
    <p>
      {{ count }}
    </p>
    <p ref="eleRef">
      {{ age }}
    </p>
  </div>
</template>
<script setup >
import { ref, watch } from "vue"
const count = ref(0)
/**
 * 挑战 1: Watch 一次
 * 确保副作用函数只执行一次
*/
//watch(count, () => {
//  console.log("Only triggered once")
//})
const unWatch = watch(count, () => {
  console.log("Only triggered once")
  unWatch()
})
count.value = 1
setTimeout(() => count.value = 2)
/**
 * 挑战 2: Watch 对象
 * 确保副作用函数被正确触发
*/
const state = ref({
  count: 0,
})
//watch(state, () => {
//  console.log("The state.count updated")
//})
watch(state, () => {
  console.log("The state.count updated")
}, {
  deep: true
})
state.value.count = 2
/**
 * 挑战 3: 副作用函数刷新时机
 * 确保正确访问到更新后的`eleRef`值
*/
const eleRef = ref()
const age = ref(2)
//watch(age, () => {
// console.log(eleRef.value)
//})
watch(age, () => {
  console.log(eleRef.value)
}, {
  flush: 'post'
})
age.value = 18 
</script> 

在 Vue 3 中,watch API 的 deep 选项用于深度监听对象的变化。默认情况下,watch 只会监听对象的顶层属性的变化,而不会监听嵌套属性的变化。通过设置 deep: true,你可以让 watch 监听对象及其所有嵌套属性的变化。

在 Vue 3 中,watch API 的 flush 选项可以用来控制监听回调函数的调用时机。具体来说,flush 选项有三个可能的值:'pre'、'post' 和 'sync'。
'pre':这是默认值,回调函数会在组件更新之前调用。
'post':回调函数会在组件更新之后调用。
'sync':回调函数会在数据变化时同步调用。

设置 flush: 'post' 的意义在于,确保 watch 的回调函数在 DOM 更新之后执行。这样,你可以确保在回调函数中访问到的是更新后的 DOM 状态。对于需要在 DOM 更新后执行某些操作的场景,比如操作 DOM 元素或读取更新后的 DOM 状态,这个选项非常有用。

6. 浅层 ref

考察对 shallowRef 的理解和使用

shallowRef 创建的是一个浅层响应的引用,这意味着它只会对顶层属性的变化作出反应,而不会对嵌套属性的变化作出反应。因此,使用 shallowRef 时,即使设置了 deep: true,当嵌套属性变化时也不会触发 watch 回调。

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

const state = shallowRef({ count: 1 })

// 回调没被触发
watch(state, () => {
  console.log("State.count Updated")
}, { deep: true })

/**
 * 修改以下代码使watch回调被触发
 *
*/
// state.value.count = 2
state.value = { ...state.value, count: 2 };

</script>

9. 依赖注入(Dependency Injection)

考察对 Composition API: Dependency Injection provide 和 inject API 的理解和使用

provide 和 inject API 提供了灵活且强大的工具来管理 Vue 组件之间的状态和依赖关系,简化了组件通信、减少了 prop 传递的复杂性、实现了依赖注入模式,并增强了代码的灵活性和可测试性。这使得开发和维护大型 Vue 应用变得更加容易和高效。

// App.vue
<script setup lang="ts">
import { ref, provide } from "vue"
import Child from "./Child.vue"
const count = ref(1)
provide("count", count)
setInterval(() => {
  count.value++
}, 1000)
</script>

<template>
  <Child />
</template>

// Child.vue 

<script setup lang="ts">
// 添加代码,使`count`值注入子组件
import { inject } from 'vue';

// Add a piece of code to make the `count` value get injected into the child component.
const count = inject('count')
</script>

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

10.生命周期钩子(Lifecycle Hooks)

在 Vue 组件中使用定时器时,如果组件被卸载而定时器未被清除,就会导致定时器继续运行。这会造成如下问题:
未清除的定时器继续运行:当组件被卸载时,如果定时器未被清除,它仍然会继续运行,并尝试更新已经销毁的组件中的数据。这会导致不必要的资源消耗和潜在的内存泄漏。
访问已销毁的组件状态:未清除的定时器可能会尝试访问和修改已被销毁的组件状态,这可能会导致错误和不稳定的行为,因为定时器回调中的代码仍在执行,而组件的状态和 DOM 可能已经被清除。
故采用 onUnmounted 来保证定时器清理

// Child.vue

<script setup lang="ts">
import { onMounted, onUnmounted, inject } 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>

11.下一次DOM更新(Next DOM update flush)

考察对 nextTick 的理解和使用

nextTick 是 Vue 提供的一个全局 API,用于在下一个 DOM 更新循环结束后执行回调函数。它通常用于在数据更新后获取更新后的 DOM 状态。由于 Vue 的 DOM 更新是异步的,所以在更新数据后,立即访问 DOM 可能会得到旧的状态。nextTick 允许你在数据更新并且 DOM 完成重新渲染之后执行某些操作。

<script setup>
import { ref } from "vue"

const count = ref(0)

function increment() {
  count.value++
 /**
   * DOM还未更新,如何确保DOM已经更新 ?
   * 请保证以下输出为true
  */
 nextTick(()=>{
   console.log(+document.getElementById("counter").textContent === 1)
 })
}
</script>

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

13. DOM传送门(DOM Portal)

考察对 teleport 的理解和使用

使用 Vue 3 的 teleport 组件,你可以将组件内容渲染到 DOM 的任何位置,包括 body 元素。这对于需要在 Vue 组件之外的 DOM 节点中渲染内容的场景特别有用,例如模态框、弹出层等。

<script setup>

const msg = "Hello World"

</script>

<template>
  <!-- 将以下元素渲染成`body`的子元素 -->
  <teleport to="body">
    <span>{{ msg }}</span>
  </teleport>
</template>

14. 动态CSS(Dynamic css values)

考察对 v-bind 的理解和使用, 或者直接在标签里写style, 不过建议 v-bind 这样子可读性比较高

<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>
/* 修改以下代码绑定动态颜色 */
/* 利用v-bind实现动态样式 */
p {
  color: v-bind(theme)
}
</style>

234. 阻止事件冒泡(prevent event propagation)

第一种,通过原生阻止冒泡,第二种通过 stop 修饰符来处理

<script setup lang="ts">

const click1 = () => {
  console.log('click1')
}

const click2 = (event) => {
  console.log('click2')
  // 原生冒泡事件阻止
  // event.stopPropagation()
}

</script>

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

305. 大写(Capitalize)

考察对 vModelText 自定义修饰符的使用,提高了可读性

<script setup>
// 请创建一个自定义的修饰符 capitalize,它会自动将 v-model 绑定输入的字符串值首字母转为大写
import { ref, vModelText } from 'vue';
const value = ref('');
vModelText.beforeUpdate = (el, binding) => {
  if (el.value && binding.modifiers.capitalize) {
    el.value = el.value.charAt(0).toUpperCase() + el.value.slice(1);
  }
};
</script>

<template>
  <input type="text" v-model.capitalize="" />
</template>

323. Prop验证(Prop Validation)

考察对 Prop 声明的用法

<script setup>
// 请验证Button组件的Prop类型 ,使它只接收: primary | ghost | dashed | link | text | default ,且默认值为default。
defineProps({
  type: {
    type: String,
    required: true,
    default: 'default',
    validator: (value: string) => {
      return ['primary', 'ghost', 'dashed', 'link', 'text', 'default'].includes(
        value,
      )
    },
  },
})
</script>

<template>
  <button>Button</button>
</template>

中等题(medium)14

7. Raw API

考察对 toRaw, markRaw 的理解和使用

toRaw 是 Vue 3 提供的一个方法,用于获取一个响应式对象的原始非响应式版本。这在需要直接操作原始数据或避免某些场景下响应式行为干扰时非常有用。
markRaw 是 Vue 3 提供的另一个方法,用于将一个对象标记为不可被 Vue 的响应式系统转换的原始对象。这对于某些不需要响应式处理的复杂对象(例如第三方库对象)非常有用。

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

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

/**
 * 修改以下代码使输出为true
*/
// console.log(reactiveState === state)
console.log(toRaw(reactiveState) === state)
/**
 * 修改以下代码使输出为false
*/
// const info = { count: 1 }
const info = markRaw({ count: 1 })
const reactiveInfo = reactive(info)

console.log(isReactive(reactiveInfo))

</script>

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

8. Effect作用域 API

考察对 API: effectScope 的理解和使用

effectScope 是 Vue 3 提供的一个功能,用于管理和控制一组响应式副作用(如 watch、watchEffect、computed 等)的生命周期。通过使用 effectScope,你可以在一个作用域内收集和管理这些副作用,并在需要时统一停止它们。这样可以提高代码的可维护性,特别是在需要管理复杂的响应式逻辑或需要在组件销毁时清理资源的情况下。

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

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

// 使用 `effectScope` API 使这些Effect效果在触发一次后停止
// watch(doubled, () => console.log(doubled.value))
// watchEffect(() => console.log("Count: ", doubled.value))

const scope = effectScope();
scope.run(() => {
    watch(doubled, () => console.log(doubled.value))
    watchEffect(() => console.log("Count: ", doubled.value))
})

counter.value = 2

setTimeout(() => {
  scope.stop();
  counter.value = 4
})

</script>

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

12.优化性能的指令(Optimize performance directive)

考察对 v-once 的理解和使用

v-once 是 Vue 的一个指令,它可以将元素和组件的内容渲染一次后缓存起来,以后即使数据变化,内容也不会再更新。适用于你希望在渲染后内容不再改变的场景。

虽然 Vue 3 没有直接类似于 React 的 memo 函数,但你可以通过以下方式实现类似的性能优化:
computed:用于计算属性的缓存,只在依赖项变化时重新计算。
watch:用于监视数据变化并在满足条件时执行特定逻辑。
defineComponent 和 props:通过优化子组件的 props 和使用计算属性来减少不必要的更新。
这些方法可以帮助你控制组件的更新逻辑,从而提高应用性能。

<script setup>
import { ref } from "vue"

const count = ref(0)

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

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

15. 切换器(useToggle)

考察对 组合函数的理解和使用

组合函数 是一种用于封装逻辑的函数,通常用于处理 Vue 3 中的响应式状态或行为。组合函数有助于将状态管理和业务逻辑从组件中分离出来,使代码更具可复用性和可维护性。

<script setup lang='ts'>
import { Ref, ref } from 'vue';

/**
 * 实现一个切换状态的可组合函数
 * 确保该功能正常工作
*/
function useToggle(val: boolean): [state: Ref, toggle: () => void] {
  const state = ref(val) 
  function 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>

16. until

综合知识考察 promise & watch

使用了 Vue 的响应式系统和 watch API,有效地监控了响应式数据的变化,并在满足条件时解决了 Promise。它是处理异步条件等待的一个合适方法,特别适用于 Vue 组件的响应式数据。

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

const count = ref(0)

/**
 * 实现`until`函数
*/

function until(initial) {
  function toBe(value) {
    return new Promise((resolve, reject) => {
      const unWatch = watch(initial, (newVal) => {
        if (newVal === value) {
          unWatch()
          resolve(true)
        }
      })
    })
  }

  return {
    toBe,
  }
}

async function increase() {
  count.value = 0
  setInterval(() => {
    count.value++
  }, 1000)
  await until(count).toBe(3)
  console.log(count.value === 3) // 确保输出为true
}

</script>

17. 计数器(useCounter)

Hooks封装考察,可用于复用

<script setup lang='ts'>

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

/**
 * 实现计数器函数,确保功能正常工作
 * 1. 加 (+)
 * 2. 减 (-)
 * 3. 重置 
 * 4. 最小值 & 最大值 选项支持
*/
function useCounter(initialValue = 0, options: UseCounterOptions = {}) {
  let count = ref(initialValue)

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

  const dec = () => {
    if (options && (options.min || options.min === 0) && options.min < count.value) count.value--
  }

  const reset = () => {
    count.value = initialValue
  }

  return { count, inc, dec, reset }
}

const { count, inc, dec, reset } = useCounter(0, { min: 0, max: 10 })

</script>

18. 实现本地存储函数(useLocalStorage)

Hooks封装考察,可用于复用, vue-use 中有很多类似这种封装,可学习参考

<script setup lang='ts'>

import { customRef } from "vue"

/**
 * 实现`useLocalStorage`函数
*/
function useLocalStorage(key: string, initialValue: any) {
  const value = customRef((track, trigger) =>{
    return {
      get(){
        track();
        return localStorage.getItem(key) ?? initialValue
      },
      set(nval){
        trigger();
        localStorage.setItem(key, nval)
      }
    }
  })
  return value
}

const counter = useLocalStorage("counter", 0)

// 我们可以通过触发`counter`的`getter`来获取本地存储的值
console.log(counter.value)

// 同样地,我们也可以通过触发`counter`的`setter`来设置本地存储的值

counter.value = 1

</script>

19. 切换焦点指令(v-focus)

考察对 自定义指令实现 的理解和使用

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

const state = ref(false)

/**
 * 实现一个自定义指令,让元素获取焦点
 * 确保当切换`state`时,元素随着状态值获取/失去焦点
 *
*/
// 定义自定义指令
const VFocus: ObjectDirective = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    if (binding.value) {
      el.focus()
    } else {
      el.blur()
    }
  },
  updated(el: HTMLElement, binding: DirectiveBinding) {
    if (binding.value) {
      el.focus()
    } else {
      el.blur()
    }
  }
}

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

</script>

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

20. 防抖点击指令(v-debounce-click)

考察对 自定义指令实现 的理解和使用

<script setup lang='ts'>

/**
 * 实现以下自定义指令
 * 确保在一定时间内当快速点击按钮多次时只触发一次点击事件
 * 你需要支持防抖延迟时间选项, 用法如 `v-debounce-click:ms`
 *
*/

const VDebounceClick: ObjectDirective = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    let timeout: number | null = null;
    const delay = binding.arg ? parseInt(binding.arg, 10) : 300; // 默认防抖延迟时间为300ms

    el.addEventListener('click', () => {
      if (timeout !== null) {
        clearTimeout(timeout);
      }
      timeout = window.setTimeout(() => {
        binding.value();
        timeout = null;
      }, delay);
    });
  },
  unmounted(el: HTMLElement) {
    el.removeEventListener('click', () => {});
  }
}

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>

21. 函数式组件(functional component)

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

/**
 * 实现该函数式组件 :
 * 1. 使用`list`数据渲染列表元素 (ul/li)
 * 2. 当点击列表子元素时,将其文本颜色更改为红色
*/

// 定义 ListComponent 组件
const ListComponent = defineComponent({
  name: 'ListComponent',
  props: {
    list: {
      type: Array as PropType<{ name: string }[]>,
      required: true
    },
    activeIndex: {
      type: Number,
      required: true
    }
  },
  emits: ['toggle'],
  setup(props, { emit }) {
    const handleClick = (index: number) => {
      emit('toggle', index)
    }

    return () => {
      return h(
        'ul',
        props.list.map((item, index) =>
          h(
            'li',
            {
              style: {
                color: props.activeIndex === index ? 'red' : 'black'
              },
              onClick: () => handleClick(index)
            },
            item.name
          )
        )
      )
    }
  }
})

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>

25. 鼠标坐标(useMouse)

useEventListener 函数:
使用 onMounted 和 onUnmounted 生命周期钩子来添加和移除事件监听器。
target 可以是 Window 对象或 HTMLElement,event 是事件名称,callback 是事件回调函数。
useMouse 函数:
定义响应式数据 x 和 y 来存储鼠标位置。
使用 useEventListener 监听 window 上的 mousemove 事件,并在回调函数中更新 x 和 y 的值。
返回 x 和 y 以供模板使用。
模板: 显示鼠标位置 x 和 y。

<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";

function useEventListener(target: Window, event: string, callback: any) {
  onMounted(() => target.addEventListener(event, callback));
  onUnmounted(() => target.removeEventListener(event, callback));
}

function useMouse() {
  const x = ref(0);
  const y = ref(0);
  const handleMouseMove = (e: MouseEvent) => {
    x.value = e.clientX;
    y.value = e.clientY;
  };
  useEventListener(window, "mousemove", handleMouseMove);
  return { x, y };
}
const { x, y } = useMouse();
</script>

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

27. 全局CSS(Global CSS)

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

利用 :global 选择器来处理, 获取去除scoped, 使得样式作用全局

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

<style scoped>

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

/* 使其工作 */
:global(body){
  width: 100vw;
  height: 100vh;
  background-color: burlywood;
}
</style>

218.渲染函数[h()](render function[h()])

<script setup lang="ts">
import MyButton from "./MyButton.ts"
const onClick = () => {
  console.log('onClick')
}
</script>
<template>
  <MyButton :disabled="false" @custom-click="onClick">
    my button
  </MyButton>
</template>

// MyButton.ts
import { defineComponent, h } from 'vue';

export default defineComponent({
  name: 'MyButton',
  props: {
    disabled: {
      type: Boolean,
      default: false
    }
  },
  emits: ['custom-click'],
  setup(props, { emit, slots }) {
    const handleClick = (event: Event) => {
      if (!props.disabled) {
        emit('custom-click', event);
      }
    };

    return () => 
      h('button', {
        disabled: props.disabled,
        onClick: handleClick
      }, slots.default ? slots.default() : 'Button');
  }
});

232. 按键修饰符(Key Modifiers)

在监听键盘事件时,我们经常需要检查特定的按键。Vue 允许为 v-on 或者 @ 在监听键盘事件时添加按键修饰符

只有在 `key` 是 `Enter` 时调用 `vm.submit()` 
<input @keyup.enter="submit" />
<template>
添加按键修饰符让即使 Alt 或 Shift 被一同按下时也会触发
<button @click="onClick1">A</button>
添加按键修饰符让有且只有 Shift 被按下的时候才触发
<button @click="onCtrlClick">A</button>
添加按键修饰符让没有任何系统修饰符被按下的时候才触发
<button @click="onClick2">A</button>
</template>
// 你的答案
<template>
  <!-- Add key modifiers made this will fire even if Alt or Shift is also pressed -->
<button @click.alt.shift="onClick1">A</button>

<!-- Add key modifiers made this will only fire when Shift and no other keys are pressed -->
<button @click.shift="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>

困难题(hard)5

22. 自定义元素(custom element)

<script setup lang='ts'>

import { onMounted, defineCustomElement, h } from "vue"

/**
 * 实现以下代码创建一个自定义元素.
 * 确保页面输出 "Hello Vue.js".
*/
const VueJs = defineCustomElement({
  props: {message:String},
  render(){
    return h('div',null,this.message)
  }
})
// 注册自定义元素
customElements.define('vue-js', VueJs)

// 定义自定义元素类
class VueJsElement extends HTMLElement {
  connectedCallback() {
    const message = this.getAttribute('message') || 'Hello'
    this.innerHTML = `<p>${message}</p>`
  }
}
// 注册自定义元素
customElements.define('vue-js', VueJsElement)


onMounted(() => {
  document.getElementById("app")!.innerHTML = "<vue-js message=\"Hello Vue.js\"></vue-js>"
})

</script>

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

23. 自定义ref(custom ref)

<script setup>
import { watch, ref, customRef } from "vue"

/**
 * 补全以下函数来实现防抖ref :
*/
function useDebouncedRef(value: any, delay = 200) {
  let timeout: number
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = window.setTimeout(() => {
          value = newValue
          trigger()
        }, delay)
      }
    }
  })
}
const text = useDebouncedRef("hello")

/**
 * 确保在输入框快速输入时, 只触发一次回调。
*/
watch(text, (value) => {
  console.log(value)
})
</script>

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

24.激活的样式-指令(v-active-style)

<script setup lang='ts'>

import { ref } from "vue"

/**
 * 实现该指令 :
 * 当切换该选项卡时,列表项文本颜色变为红色
 * 
*/
const VActiveStyle = {
  mounted(el: HTMLElement, binding: any) {
    const [styles, isActive] = binding.value
    if (isActive()) {
      Object.keys(styles).forEach(key => {
        el.style[key] = styles[key]
      })
    }
  },
  updated(el: HTMLElement, binding: any) {
    const [styles, isActive] = binding.value
    if (isActive()) {
      Object.keys(styles).forEach(key => {
        el.style[key] = styles[key]
      })
    } else {
      Object.keys(styles).forEach(key => {
        el.style[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>

26. 实现简易版v-model指令(v-model)

在这个挑战中,我们将尝试实现一个简单的v-model指令,让我们开始吧 👇

<script setup lang='ts'>

import { ref, Binding, unref } from "vue"

/**
 * Implement a custom directive
 * Create a two-way binding on a form input element
 *
*/
const VOhModel = {
  mounted(el: HTMLInputElement, binding: Binding) {
    const { value } = binding
    const elValue = el.value
    if (value && elValue !== value) {
      el.value = unref(value)
    }
    el.addEventListener('input', (e: HTMLInputElement) => {
      const curValue = e.target.value;
      if (values.value !== curValue) {
       values.value = curValue
      }
    }, false)
  },
  unmounted(el: HTMLInputElement ) {
    el.removeEventListener('input', (e: HTMLInputElement) => {
      const curValue = e.target.value;
      if (values.value !== curValue) {
       values.value = curValue
      }
    }, false)
  }
}

const value = ref("Hello Vue.js")

</script>

<template>
  <input v-oh-model="value" type="text" />
</template>

208. 树组件(Tree Component)

在这个挑战中,你需要实现一个树组件,让我们开始吧。
树组件采用递归的方式来实现

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

export interface TreeData {
  key: string;
  title: string;
  children?: TreeData[];
}

const TreeComponent: FunctionalComponent<{ data: TreeData[] }> = (props) => {
  const { data } = props;

  if (!data || !data.length) return h('ul');

  const item = data.map(v => {
    const { key, children, title } = v;
    const node = [h('label', null, title)];
    if (children && children.length) {
      node.push(h(TreeComponent, { data: children }));
    }
    return h('li', { key }, node);
  });

  return h('ul', null, item);
};
</script>
<template>
  <!-- do something.... -->
</template>
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容