vue3挑战-中等篇[2]

83 阅读6分钟

我正在参加「掘金·启航计划」

前言

Vue挑战是一个非常适合Vue3入门的项目,里面的题目基本涵盖了Vue3的所有基础用法(特别是Vue3新引入的组合式写法),并且还在持续更新中,关于这个项目的具体介绍和原理,可以看一下这篇文章。并且在看这篇文章之前,你最好自己先去做一遍,这个文章里的写法只是我自己的方式(当然基础用法大家应该都大同小异)。

这篇文章里的题目将会用到一些比较偏门但是对特殊场景大有裨益的API,能够熟悉并掌握它们,能够大幅提升开发效率。

自定义指令

点击跳转到题目

这个挑战开始,我们将尝试编写自定义指令,让我们从v-focus开始 👇:

关于代码的复用,vue常用的方式有三种,组件、组合式函数和自定义指令,三者的不同点是:

  • 组件用于代码的模块化复用
  • 组合式函数用于复用有状态的逻辑
  • 自定义指令则复用与DOM相关的代码

在组合式写法并且使用了setup后,任何以v开头的驼峰式命名的变量都可以被识别为一个自定义指令:

<script setup>
// 在模板中启用 v-focus
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

<template>
  <input v-focus />
</template>

这个例子中,自定义指令就操作了input元素,使之自动获得焦点。自定义指令的写法就类似于生命周期钩子,在钩子的参数中获取dom来进行相应的操作,可用的钩子如下:

const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode, prevVnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode, prevVnode) {}
}

其中参数el为指令绑定的元素

通过binding可以获取到指令的传值,比如v-test.directive.capitalize:param="state"中,通过binding.value可以获取新传入的state的值,binding.oldValue可以获取旧的state的值(仅在beforeUpdate updated中可用),使用binding.modifiers可以获取到{directive: true, capitalize: true},使用binding.arg可以获取到["param"]

vnode为元素底层的VNode

prevNode为之前的VNode,仅在beforeUpdate updated中可用

更多详情可以查看vue文档的自定义指令

<script setup lang='ts'>
import { ref, unref } 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, binding, vnode, prevVnode) {
    if (unref(state)) {
      el.focus()
    }
    else {
      el.blur()
    }
  }
}

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

</script>

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

自定义指令练习-防抖

点击跳转到题目

利用闭包来做防抖应该没啥可说的吧,js的常用技巧:

function debounce(fn) {
  let timeout = null;
  return function() {
    if (timeout != null) clearTimeout(timeout)
    timeout = setTimeout(fn, binding.arg[0])
  }
}
el.addEventListener("click", debounce(binding.value()))

这道题主要就是练习自定义指令中从bindings中取值的,用bindings.arg获取防抖时间,用bindings.value获取点击后要触发的方法:

<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`
 *
*/

const VDebounceClick = {
  mounted(el,binding) {
    function debounce(fn) {
      let timeout = null;
      return function() {
        if (timeout != null) clearTimeout(timeout)
        timeout = setTimeout(fn, binding.arg[0])
      }
    }
    el.addEventListener("click", debounce(binding.value()))
  }
}

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>

渲染函数h()

点击跳转到题目

在这个挑战中,你需要使用h渲染函数来实现一个组件。 请注意: 你应该确保参数被正确传递、事件被正常触发和插槽内容正常渲染。让我们开始吧。

除了提供模板之外,也可以使用渲染函数h()来渲染模板,h()的参数分别是标签名、属性、文本或子元素,可以进行嵌套:

import { h } from 'vue'

// 除了 type 外,其他参数都是可选的
h('div')
h('div', { id: 'foo' })

// attribute 和 property 都可以用于 prop
// Vue 会自动选择正确的方式来分配它
h('div', { class: 'bar', innerHTML: 'hello' })

// class 与 style 可以像在模板中一样
// 用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })

// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })

// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')

// 没有 prop 时可以省略不写
h('div', 'hello')
h('div', [h('span', 'hello')])

// children 数组可以同时包含 vnode 和字符串
h('div', ['hello', h('span', 'hello')])

【注意】渲染函数的事件监听器最好使用胖箭头函数进行声明,而不要直接使用具名函数或function匿名函数,此二者会改变this的作用域可能会导致部分API无法使用:

render() {
    return h('button',{
      disabled: this.disabled,
      onClick: () => {
        this.$emit("customClick") // 可以使用
      },
      onClick() {
        this.$emit("customClick") // Error: $emit is not a funtion, 改变了this指向导致找不到$emit
      },
    }, this.$slots)
}

关于渲染函数的用法,参考渲染函数 & JSX这篇文档。

import { defineComponent,h } from "vue"

export default defineComponent({
  name: 'MyButton',
  props: {
    disabled: {
      type: Boolean,
      default: false
    }
  },
  render() {
    return h('button',{
      disabled: this.disabled,
      onClick: () => {
        this.$emit("customClick")
      },
    }, this.$slots)
  }
})

函数式组件

点击跳转到题目

在这个挑战中,我们将尝试实现一个函数式组件,让我们开始吧 👇:

函数式组件,顾名思义,是用函数来声明的一个组件,它没有状态,没有生命周期钩子,所有跟组件相关的API它只能使用porpsemits,优点是逻辑简单方便复用,减少你多谢一个class多引用一次的心智负担,在渲染的性能上也会快一些。

函数式组件的核心是声明时返回上一道题提过的h渲染函数,以此来代替SFC单文件中的模板:

const DynamicHeading = (props, context) => {
    return h(`h${props.level}`, context.attrs, context.slots)
}

然后声明函数式组件所要接受的props和需要触发的emits:

ListComponent.props = ["color"]
ListComponent.emits = ["toggle"]

这二者在函数式组件的声明中可以通过参数来接收:

const ListComponent = (props, { slots, emit, attrs }) => {
    return h("div", {
        style: {color: props.color},
        onClick() {
            emit("toggle")
        }
    }, slots)
})

所以这道题的解法是:

<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, { slots, emit, attrs }) => {
  return h("ul", props.list.map((item, index) => {
    return h(
      "li", 
      {
        style: props.activeIndex == index ? {color: 'red'} : undefined,
        onClick() {
          emit("toggle", index) 
        }
      }
    , item.name) 
  }))
}

ListComponent.props = ["list", "active-index"]
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>

全局CSS

点击跳转到题目

有些时候,我们想在具有CSS作用域的Vue单文件组件设置全局CSS样式, 该怎么设置呢 ? 让我们开始吧 👇:

一般来说,在SFC单文件中,我们需要让一个CSS在全局生效的时候,一般会采用删去<style>标签的scope属性的写法,也就会是两个<style>标签的混用:

<style>
/* 全局样式 */
</style>

<style scoped>
/* 局部样式 */
</style>

而实际上我们可以通过:global伪类来包裹选择器以达到使CSS全局生效的目的:

<style scoped>
:global(.red) {
  color: red;
}
</style>

类似的,还有深度选择器:deep,插槽选择器:slot来处理子组件和插槽的样式作用域问题。

更多详情可以查看vue文档的组件作用域CSS

<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 允许为 v-on 或者 @ 在监听键盘事件时添加按键修饰符,例如:

<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
<input @keyup.enter="submit" />

在这个挑战中,我们将尝试它,让我们开始吧:

没什么特别的,在事件结尾加上对应按键的修饰符即可,按键名需要使用kebab-case短横线的形式:

<input @keyup.page-down="onPageDown" />

此外,如果要精确到某个按键或者按键组合的话,需要在结尾再加上.exact修饰符:

<!-- 当按下 Ctrl 时,即使同时按下 Alt 或 Shift 也会触发 -->
<button @click.ctrl="onClick">A</button>

<!-- 仅当按下 Ctrl 且未按任何其他键时才会触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 仅当没有按下任何系统按键时触发 -->
<button @click.exact="onClick">A</button>

这道题的解法:

<template>
  <!-- Add key modifiers made this will fire even if Alt or Shift is also pressed -->
  <button @click.alt.exact="onClick1" @click.shift.exact="onClick1" @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.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>

其中第一个要求单独按alt或shift以及组合按下时都会触发,因此分别注册了三个click事件,并且给单独按键事件加上了.exact修饰符,以避免组合键按下时多次触发。

结尾

中等篇的题目就到这里了,如果能够熟练掌握这些方法,大部分偏门的vue3难题就可以迎刃而解了。接下来最后一篇文章我们来解决最难的几个题目。