Vue3 新特性 + TypeScript 小实战

上次将 Composition API 大致梳理了一遍 ,这次主要是想记录一些 vue3 相较 vue2 新增出来的一些特性和一些方法上使用的变动,话不多说,直接开撸。

Teleport


我们日常开发中经常会遇到这样一个场景,比如我们封装一个弹层 msk 组件,但是它包裹在一个 position: relative 定位的父元素中,此时它将被局限在父元素的大小中,我们很难将弹层铺满整个窗口。而 Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件。举个栗子:

<body>
  <div id="root"></div>
  <div id="center"></div>
</body>
<script>
const app = Vue.createApp({
  template: `
    <div class="mask-wrapper">
      <msk></msk>
    </div>
  `
})
app.component('msk', {
  template: `
    <div class="msk">111</div>
  `
})
app.mount('#root')
</script>

浏览器渲染结果如下:


未使用 teleport

这肯定不是我们想要实现的效果,我们希望蒙层是充满整个窗口的,此时我们可以直接将蒙层组件通过 teleport 渲染到 body 下面或者我们指定的 dom 节点下面,teleport 上面有一个 to 的属性,它接受一个 css query selector 作为参数。如下栗子:

<script>
const app = Vue.createApp({
  template: `
  <div class="mask-wrapper">
    // 使用 to 属性将其挂载到 id = center 的 dom 节点下
    // 我们也可以直接使用 to = body 将其直接挂载到 body 中
    <teleport to="#center">
      <msk></msk>
    </teleport>
  </div>
`
})
</script>

emits


我们知道在 vue2 中父子组件传值会用到 props$emit ,但是在 vue3 中新增了 emits ,它的主要作用是汇总该组件有哪些自定义事件,可以是一个数组写法,也可以是一个对象写法,同时在对象写法中还支持自定义函数,可以在运行时验证参数是否正确。

了解它的基础用法之后我们将 teleport 中写入的小栗子重写,让其组件通信完成最基本的显示和隐藏的动态交互功能。当然我们在子组件通过 $emit 触发的事件要统一写入 emits 数组中进行管理。

const app = Vue.createApp({
  template: `
    <div class="mask-wrapper">
      <button @click="openMsk">打开弹层</button>
      <teleport to="#center">
        <msk :isOpen="isOpen" @closeMsk="closeMsk"></msk>
      </teleport>
    </div>
  `,
  setup() {
    const { ref } = Vue
    const isOpen = ref(false)
    const openMsk = () => {
      isOpen.value = true
    }
    const closeMsk = () => {
      isOpen.value = false
    }
    return { openMsk, isOpen, closeMsk }
  }
})
app.component('msk', {
  props: ['isOpen'],
  // 子组件中我们会向父组件触发 `closeMsk` 事件,所以将其统一写入 `emits` 中方便管理维护
  emits: ['closeMsk'], 
  template: `
    <div class="msk" v-show="isOpen">
      <button @click="closeMsk">关闭弹层</button>
    </div>
  `,
  setup(props, context) {
    const closeMsk = () => {
      context.emit('closeMsk')
    }
    return { closeMsk }
  }
})
app.mount('#root')

当然我们也可以在 emits 中使用对象写法,并且传入验证的自定义函数:

app.component('msk', {
  props: ['isOpen'],
  emits: {
    // 'closeMsk': null, 无需验证
    'closeMsk': (payload) => {
      return payload === 111 // 事件触发时验证传入的值是否为 111
      // 验证失败,因为我传入的是 222
     // 无效的事件参数:事件“closeMsk”的事件验证失败。
    }
  },
  setup(props, context) {
    const closeMsk = () => {
      context.emit('closeMsk', 222)
    }
    return { closeMsk }
  }
}

小伙伴们可以试一试,当然即使我传入的值和验证时的值不匹配但是并不会影响这个事件的正常执行,只是会在浏览器中给出警告提示。

Suspense


teleport 组件一样,这也是 vue3.0 新推出来的一个全新的组件,它的主要作用是和异步组件一起使用,我们可以现在这里回忆一下 vue2.0 中我们是如何使用动态组件和异步组件的。

动态组件

vue 2.0vue3.0 动态组件的使用方式基本差不多,都是根据数据的变化,结合 component 这个标签,来随时动态切换组件的实现。这里简单做个小回顾:

const app = Vue.createApp({
  setup() {
    const { ref, keepAlive } = Vue
    const currentItem = ref('first-item')
    const handleClick = () => {
      if (currentItem.value === 'first-item') {
        currentItem.value = 'second-item'
      } else {
        currentItem.value = 'first-item'
      }
    }
    return { currentItem, handleClick }
  },
  template: `
    <keep-alive>
      <component :is="currentItem"></component>
    </keep-alive>
    <button @click="handleClick">组件切换</button>
  `
})
app.component('first-item', {
  template: `
    <div>hello world</div>
  `
})
app.component('second-item', {
  template: `
    <input type="text" />
  `
})
app.mount('#root')
异步组件

以前,异步组件是通过将组件定义为返回 Promise 的函数来创建的,这里可以直接查看 vue2.0 中如何定义异步组件。但是在 vue3.0 中现在,由于函数式组件被定义为纯函数,因此异步组件的定义需要通过将其包装在新的 defineAsyncComponent 助手方法中来显式地定义,其实也很简单,看栗子就知道了:

const { defineAsyncComponent } = Vue
const app = Vue.createApp({
  template: `
    <div>
      <async-show></async-show>
      <async-common-item></async-common-item>
    </div>
  `
})
app.component('asyncShow', defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      return resolve({
        template: `<div>我将在 1s 之后被渲染出来</div>`
      })
    }, 1000)
  })
}))
app.component('async-common-item', defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      return resolve({
        template: `<div>我将在 3s 之后被渲染出来</div>`
      })
    }, 3000)
  })
}))

defineAsyncComponent 接受返回 Promise 的工厂函数。从服务器检索组件定义后,应调用 Promiseresolve 回调。你也可以调用 reject(reason),来表示加载失败。

接下来就可以引入我们的主角 Suspense 组件,他可以用来接收一个或多个异步组件,它本身支持两个具名插槽,一个承载异步插件返回等待状态的插槽,一个承载异步插件返回成功状态的插槽。举个小栗子:

const { defineAsyncComponent } = Vue
const app = Vue.createApp({
  template: `
  <Suspense >
    <template #default> // 异步组件成功内容包裹在 default 插槽中
      <async-show></async-show>
    </template>
    <template #fallback> // 异步组件未加载时显示 fallback里的内容
      <h1>loading !!!!</h1>
    </template>
  </Suspense>
`
})

当然 Suspense 组件也支持多个异步组件的插入,并且它会等待所有异步组件都返回才将其显示出来,不过此时我们需要在其根上包一层,如下栗子:

const app = Vue.createApp({
  template: `
    <Suspense >
      <template #default>
        <div>
          <async-show></async-show>
          <async-common-item></async-common-item>
        </div>
      </template>
      <template #fallback>
        <h1>loading !!!!</h1>
      </template>
    </Suspense>
  `
})

Provide / Inject


通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。

对于这种情况,我们可以使用一对 provideinject。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。

上面两段话摘自官网,说的很明白,基础的用法其实和 vue2 中差不多,但是我们知道 vue2 中无法实现数据的响应式监听,但是 vue3 中我们使用 composition API 就可以完成对应响应式变化的监听。我们先来回顾一下 vue2 中的基础用法:

父组件像孙子组件传递固定值
const app = Vue.createApp({
  provide: {
    count: 1
  },
  template: `
    <child />
  `
})
app.component('child', {
  template: `
    <child-child></child-child>
  `
})
app.component('child-child', {
  inject: ['count'],
  template: `
    <div>{{count}}</div>
  `
})

如果我们想使用 provide 传递数据 data 中的值时,我们就不能用上面这种写法,我们需要将 provide 转换为返回对象的函数。栗子如下:

const app = Vue.createApp({
  data() {
    return {
      count: 1
    }
  },
  provide() {
    return {
      count: this.count
    }
  }
})
父组件像孙子组件动态传值

如果此时我们新增一个按钮改变父组件中 count 的值,子组件是无法继续监听到改变后的值的。此时如果我们想对祖先组件中的更改做出响应,我们需要为 providecount 分配一个组合式API computed

const app = Vue.createApp({
  data() {
    return {
      count: 1
    }
  },
  methods: {
    handleClick() {
      this.count++
    }
  },
  provide() {
    return {
      count: Vue.computed(() => this.count)
    }
  },
  template: `
    <child />
    <button @click="handleClick">增加</button>
  `
})
app.component('child', {
  template: `
  <child-child></child-child>
`
})
app.component('child-child', {
  inject: ['count'],
  template: `
  <div>{{count.value}}</div>
`
})

当然此时我们还没有用到 Composition API 来实现,我们在使用 Composiiton API 来重构下上面的代码:

const { ref, provide, inject } = Vue
const app = Vue.createApp({
  setup() {
    let count = ref(1)
    const handleClick = () => {
      count.value++
    }
    provide('count', count)
    return { handleClick }
  },
  template: `
    <child />
    <button @click="handleClick">增加</button>
  `
})
app.component('child', {
  template: `
  <child-child></child-child>
`
})
app.component('child-child', {
  setup() {
    let count = inject('count')
    return { count }
  },
  template: `
  <div>{{count}}</div>
`
})

是不是感觉非常简单,那么问题来了,刚刚我们使用了 provide / inject 实现了父组件和子孙组件中的数据传递,如果两个毫无关联的组件,那么我们应该如何建立数据通讯呢?除了 vuex 你最先能想到什么,在 2.x 中,Vue实例可用于触发通过事件触发 API 强制附加的处理程序 ($on$off$once),这用于创建 event hub,以创建在整个应用程序中使用的全局事件侦听器,因为我前面写过相关文章,关于 vue2 的知识就不在这里过多赘述了,详情可以点击 Vue 常见 API 及问题。

但是在 vue3 中废弃了 $on, $off,为什么会废弃呢,可以参考文章解读Vue3中废弃组件事件进行解读。官方推荐我们使用第三方库 mitt 进行全局事件的绑定和监听。大体用法其实和原来差不多,这里就不过多赘述了。

Mixin


Mixin 应该算 vue2 中用的比较多的,用法其实和以前大体相差不大,我在 Vue 常见 API 及问题 中记录过 Mixin 的基础用法,官网写的也挺详细的,当然现在关于组件公共逻辑的抽离其实更推荐使用 组合式 API 。这里还是简单总结下 Mixin 的几个特点:

1、混入过程中,组件 datamethods、优先级高于 mixin datamethods 优先级。
2、生命周期函数,先执行 mixin 里面的,在执行组件里面的。
3、自定义的属性,组件中的属性优先级高于 mixin 属性优先级。

什么叫自定义属性呢?我们来看个小栗子:

const app = Vue.createApp({
  number: 3,
  template: `
    <div>{{number}}</div>
  `
})

我们直接定义了一个属性 number,它既不在 data 中,也不在 setup 中,而是直接挂载在 app 上,那么它就是 app 上的自定义属性。此时我们无法在模板中直接使用 this 访问到这个 number 属性,必须要通过 this.$options 才能访问到它:

const app = Vue.createApp({
  number: 3,
  template: `
    <div>{{this.$options.number}}</div>
  `
})

如果我们此时在 mixin 中也定义一个 number 属性:

const myMixin = {
  number: 1
}
const app = Vue.createApp({
  mixins: [myMixin],
  number: 3,
  template: `
    <div>{{this.$options.number}}</div>
  `
})

前面我们说过,mixin 的优先级低于组件优先级,所以此时肯定输出的是 3,但是如果我们希望 mixin 的优先级高于组件优先级,我们就可以使用 app.config.optionMergeStrategies 自定义选项合并策略:

const myMixin = {
  number: 1
}
const app = Vue.createApp({
  mixins: [myMixin],
  number: 3,
  template: `
  <div>{{this.$options.number}}</div>
`
})
// 接收两个参数,配置优先返回第一个参数,如找不到在返回第二个参数
app.config.optionMergeStrategies.number = (mixinValue, appValue) => {
  return mixinValue || appValue
}

自定义指令


我们先假想一个使用场景,如果我们希望在页面加载的时候自动获取 input 框的焦点事件,我们一般会这样写:

const app = Vue.createApp({
  template: `
    <input ref="input" />
  `,
  mounted() {
    this.$refs.input.focus()
  }
})

假如另一个组件中又有一个 input ,那么我们就又需要在那个组件的 dom 元素节点处定义 ref,然后在组件的生命周期中调用一遍 this.$refs.input.focus() 。如果我们可以定义一个全局的 autofocus 事件,只要遇到 input 我们就通过给定的指令直接触发那应该怎么办呢?此时我们就可以用到自定义指令了:

const app = Vue.createApp({
  template: `
    <input ref="input" v-focus />
  `
})
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

通过过 app.directive 我们定义了一个全局的 focus 指令,指令的使用只需要在前面加上 v- 即可;当然指令也和组件一样,有着生命周期,我们在 mounted 的时候可以拿到使用指令的 dom 元素节点,然后操作这个节点完成对应的功能。当然上面我们使用 app.directive 将指令定义到了全局,日常开发中我们可能更多的是使用局部指令:

// 定义指令集合,因为可以是多个指令,所以是复数
const directives = {
  focus: {
    mounted(el) {
      el.focus()
    }
  }
}
const app = Vue.createApp({
  directives,
  template: `
    <input ref="input" v-focus />
  `
})
动态指令参数

例如我们想通过一个指令实时改变 input 框的位置,那么此时我们写下代码:

const app = Vue.createApp({
  template: `
    <input class="input" ref="input" v-pos />
  `
})
app.directive('pos', {
  mounted(el) {
    el.style.top = '200px'
  }
})

上面这个栗子虽然我们每次使用 v-pos 都会改变输入框的 top 值,但是如果我们希望这个值不是固定的 200 ,而是指令中传给我们的数字,那该如何进行改造呢?

官网的文档资料告诉了我们,指令中的参数可以是动态的,例如,在 v-mydirective:[argument]="value" 中,argument 参数可以根据组件实例数据进行更新!这使得自定义指令可以在应用中被灵活使用。光看文字可能有点糊,我们来使用实际的栗子:

// 场景一:指令接收传值
const app = Vue.createApp({
  template: `
  <input class="input" ref="input" v-pos="100" />
`
})
app.directive('pos', {
  mounted(el, binding) {
    // binding.value 是我们传递给指令的值——在这里是 200
    el.style.top = binding.value + 'px'
  }
})

其实上面的小栗子还有个缺点,就是我们将 v-pos 的值定义在 data 中,但是我们实时改变 data 中的值,页面并不会产生对应的响应式变化。那是因为我们指令注册的过程中 mounted 生命周期只会执行一遍,所以如果我们希望对应变化的产生就可以使用 updated 生命周期:

const app = Vue.createApp({
  data() {
    return {
      top: 100
    }
  },
  template: `
    <input class="input" v-pos="top" />
  `
})
app.directive('pos', {
  mounted(el, binding) {
    el.style.top = binding.value + 'px'
  },
  // 通过 updated 监听指令值的实时变化
  updated(el, binding) {
    el.style.top = binding.value + 'px'
  }
})
const vm = app.mount('#root')

当然如果我们在 mountedupdated 时触发相同行为,而不关心其他的钩子函数。那么你可以通过将这个回调函数传递给指令来实现:

app.directive('pos', (el, binding) => {
  el.style.top = binding.value + 'px'
})

如果应用场景升级,我们不仅希望它只是是在 top 上的偏移,而是通过我们指定传入的方向值进行偏移,那么应该如何实现呢?这时使用动态参数就可以非常方便地根据每个组件实例来进行更新。

// 场景二:动态指令参数
const app = Vue.createApp({
  template: `
  <input class="input" ref="input" v-pos:[direction]="100" />
`,
  data() {
    return {
      direction: 'bottom'
    }
  }
})
app.directive('pos', {
  mounted(el, binding) {
    // binding.arg 是我们传递给指令的参数
    const direction = binding.arg || 'top'
    el.style[direction] = binding.value + 'px'
  }
})

你可以试着使用自定义组件完成一个这样的功能?

插件


我们在 vue 项目中经常会使用别人写好的插件,例如 vue-routervue-touch 等,那么我们如何自己编写一个插件呢?看官网的介绍:插件是自包含的代码,通常向 Vue 添加全局级功能。它可以是公开 install() 方法的 object ,也可以是 function 。光看这句话可能有点懵,其实就传达给了我们两点讯息:

1、编写插件可以是一个对象写法,也可以是一个函数写法
2、插件有一个公开的 install() 默认方法,它接收 vue 实例和你自定义的属性两个形参。

举个栗子:

// 对象写法:
const myPlugin = {
  install(app, options) {
    console.log(app, options) // vue 实例,{name: "cc"}
  }
}
app.use(myPlugin, { name: 'cc' })

//函数写法:
const myPlugin = (app, options) => {
  console.log(app, options)
}
app.use(myPlugin, { name: 'cc' })

插件一般怎么写呢?我们使用插件的时候额外的参数会放到 options 中,而 app 是使用这个插件的时候 vue 对应的实例。我们既然能得到实例,我们就可以对其做很多拓展,例如:

const app = Vue.createApp({
  template: `
    <child></child>
  `
})
// 子组件就可以通过 inject 接收到我们写的插件里的 `name`
app.component('child', {
  inject: ['name'],
  template: `<div>{{name}}</div>`
})
// 自己写插件,在上面通过 `provide` 拓展一个 name 属性
const myPlugin = (app, options) => {
  app.provide('name', 'cc')
}
app.use(myPlugin, { name: 'cc' })

官网栗子中给出了 app.config.globalProperties 这个语法,其实就是对 vue 全局的属性做一些拓展,比如我们想在全局上加入 sayHello 这样一个属性,我们一般会使用 app.config.globalProperties.$sayHello 这样去写,在属性名前加入 $ 符号代表这是我们自己在 vue 全局添加的一个私有属性,更方便我们管理。此时我们就可以在组件中直接访问到这个全局私有属性:

app.config.globalProperties.$sayHello = 'hello cc'
// 子组件直接通过 this 使用 $sayHello 属性
app.component('child', {
  inject: ['name'],
  template: `<div>{{name}}</div>`,
  mounted() {
    console.log(this.$sayHello)
  }
})

结合官网,我们是否可以简单的写一个小插件,例如表单中的 input 框输入检测,对输入的值进行一些基础的校验,如下栗子:

当然,这个简单的小栗子肯定难不倒聪明的我们,其实我们可以使用一个全局 mixin 就可以完成这个功能:

const app = Vue.createApp({
  data() {
    return {
      name: 'cc',
      age: '18'
    }
  },
  template: `
    <div>
      姓名: <input type="text" v-model="name" />
      <span class="hint" v-if="this.$options.rules.name.error">
        {{this.$options.rules.name.message}}
      </span>
    </div>
    <div>
      年龄: <input type="number" v-model="age" />
      <span class="hint" v-if="this.$options.rules.age.error">
        {{this.$options.rules.age.message}}
      </span>
    </div>
  `,
  rules: {
    name: {
      validate: name => name.length > 3,
      error: false,
      message: '用户名最少为4个字符'
    },
    age: {
      validate: age => age > 20,
      error: false,
      message: '年龄不能小于 20 岁'
    }
  }
})
// 校验插件
const validatorPlugin = (app, options) => {
  app.mixin({
    created() {
      const rules = this.$options.rules
      for (let key in rules) {
        let item = rules[key]
        this.$watch(key, (value) => {
          if (!item.validate(value)) {
            item.error = true
          } else {
            item.error = false
          }
        }, {
          immediate: true
        })
      }
    }
  })
}
app.use(validatorPlugin)

我们在组件中定义了 rules 属性,所以我们可以通过 this.$options.rules 直接访问到这个属性,然后我们通过 watch 监听 nameage 的变化,通过回调函数来校验值是否满足条件,当然判断的过程中我们知道 watch 是有惰性的,所以我们在 watch 的配置中要加上 immediate: true ,这样就可以在页面加载完成时立即执行。这样我们就完成了一个迷你版的 input 校验功能。

自定义 v-model

vue2 中自定义 v-model 的实现及双向绑定的原理我已经写过对应的文章了,Vue 中如何自定义 v-model 及双向绑定的实现原理 ,老版本的 v-model 有几个痛点:

1、比较繁琐,要添加一个 model 属性
2、组件上只能有一个 v-model,如果组件上出现多个 v-model,实现就比较困难
3、对初学者比较不友好,看的云里雾里

所以在 vue3 中对 v-model 也是进行了大刀阔斧的改革,在 vue3 中实现 v-model 不需要再给组件添加一个 model 属性,只需要:

1、在组件的 props 中添加一个 modelValue 的属性
2、更新值的时候组件中的 emit 时有一个 update:modelValue 的方法

我们直接通过一个栗子来认识 vue3 中的自定义 v-model

const app = Vue.createApp({
  data() {
    return {
      count: 1
    }
  },
  template: `
    <div>{{count}}</div>
    <child v-model="count"></child>
  `
})
app.component('child', {
  // props 中默认的 modelValue 接收父组件中 count 的值
  props: {
    modelValue: String
  },
  template: `
    <div @click="handleClick">{{modelValue}}</div>
  `,
  methods: {
    handleClick() {
      // 组件更新值的时候使用规定的 `update: modelValue` 方法
      this.$emit('update:modelValue', this.modelValue + 3)
    }
  }
})

前面说过,vue3 中可以使用多个 v-model,那么我们在来看看多个 v-model 的应用:

const app = Vue.createApp({
  data() {
    return {
      count: 1,
      age: 18
    }
  },
  template: `
    <div>父组件的值</div>
    <div>{{count}}</div>
    <div>{{age}}</div>
    <div>子组件 v-model 绑定的值</div>
    <child v-model="count" v-model:age="age"></child>
  `
})

app.component('child', {
  props: {
    modelValue: Number,
    age: Number
  },
  template: `
    <div @click="handleClick">{{modelValue}}</div>
    <input :value="age" @input="handleInput" type="number" />
  `,
  methods: {
    handleClick() {
      this.$emit('update:modelValue', this.modelValue + 3)
    },
    handleInput(event) {
      this.$emit('update:age', +(event.target.value))
    }
  }
})

当我们在 v-model 后面不接入任何参数时,就可以直接在子组件中使用默认的 modelValue 与父组件中 v-model 的值进行绑定,而当我们在 v-model:age 传入 age 参数之后,对应的子组件的 props 中也需要改成 age,而更新值的时候组件中的 emit 中的方法也要改成对应的 update: age 。其实新版本中的 v-model 使用更简单更方便,同时可以绑定多个互不干扰。

非 Prop 的 Attribute


官网给出的定义为一个非 propattribute 是指传向一个组件,但是该组件并没有相应 propsemits 定义的 attribute。常见的示例包括 classstyleid 属性。咋看这段解释可能有点懵,其实我们可以通过一些栗子来看问题

Attribute 继承

当组件返回单个根节点时,非 prop attribute 将自动添加到根节点的 attribute 中。例如下列栗子:

const app = Vue.createApp({
  template: `
    <child type="number" class="parent"></child>
  `
})

app.component('child', {
  template: `
    <div class="child">
      <input />
    </div>
  `
})

被渲染的 child 组件实际代码结构如下:

// class 和 type 都被渲染到根节点上去了
<div class="child parent" type="number">
  <input>
</div>
禁用 Attribute 继承

如果你不希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false。例如:禁用 attribute 继承的常见情况是需要将 attribute 应用于根节点之外的其他元素。

通过将 inheritAttrs 选项设置为 false,你可以访问组件的 $attrs property,该 property 包括组件 propsemits property 中未包含的所有属性 (例如,classstylev-on 监听器等)。还是上面的栗子,我们需要 child 组件中的 input 去渲染对应的 classtype ,我们就可以将代码改写一下:

app.component('child', {
  inheritAttrs: false,
  template: `
    <div class="child">
      <input v-bind="$attrs" />
    </div>
  `
})

此时我们再从浏览器中查看 DOM 元素节点就可以看到如下结构:

<div class="child">
  <input type="number" class="parent">
</div>
多个根节点上的 Attribute 继承

如果我们的子组件存在多个根节点怎么办,例如:

const app = Vue.createApp({
  template: `
    <child class="child"></child>
  `
})
app.component('child', {
  template: `
    <div class="header" >
      <input />
    </div>
    <div class="main" v-bind="$attrs">main</div>
    <div class="footer">footer</div>
  `
})

如果我们不在其中一个根组件使用 v-bind = "$attrs" 控制台就会给我们报错,我们在其中一个根节点上使用之后父组件上对应的 attribute 就会被继承到这个根组件上。

当然我们也可以禁止掉根节点上的继承,直接在 header 结构下的 input 框加入 v-bind = "$attrs" 即可。如下栗子:

app.component('child', {
  inheritAttrs: false,
  template: `
    <div class="header" >
      <input v-bind="$attrs" />
    </div>
    <div class="main">main</div>
    <div class="footer">footer</div>
  `
})

查漏补缺


我们知道在 vue2 模板中可以通过在 DOM 结构中指定 ref 属性,然后在逻辑代码中通过 this.$refs. 去操作 DOM,那么在 vue3 中我们应该如何操作 DOM 元素呢?

我们先来一个简单的场景,判断点击的 dom 元素是否在 id = 'index'dom 结构中,场景代码如下:

// 判断点击的 dom 元素节点是不是在 index 中
const app = Vue.createApp({
    template: `
      <div id="index">
        <div id="index-list">555</div>
      </div>
      <div id="demo">666</div>
    `,
  })

此时我们就要进行 DOM 元素节点判断,结合 setup 我们应该如何去使用 ref 来获取 dom 元素节点呢?代码比较简单就直接上结果了:

const app = Vue.createApp({
  template: `
    <div id="index" ref="index">
      <div id="index-list">555</div>
    </div>
    <div id="demo">666</div>
  `,
  setup() {
    const { ref, onMounted } = Vue
    const index = ref(null)
    onMounted(() => {
      document.addEventListener('click', function (e) {
        if (index.value.contains(e.target)) {
          console.log('点击的是 index 里面的元素')
        } else {
          console.log('点击的不是 index 里面的元素')
        }
      })
    })
    return { index }
  }
})

因为 setup 的执行是在 beforeCreatecreated 之间,所以我们如果想拿到对应的 dom 元素节点,最好在其内部的生命周期中进行获取。

vue3 中相较 vue2 大体的改动和日常开发中经常会遇到的问题基本都已经整理的差不多了,由于 vue3 代码基本都是 ts 写的,所以学习 ts 其实已经迫在眉睫的。结尾综合做个小栗子吧:

其实很简单,就是一个 form 表单提交验证,不过封装基本用的是 vue3 + ts ,小伙伴可以自己独立实现一个类似 element-ui 中的表单效验插件,其实组件开发更多的是学习思路以及代码的扩展性,优雅性。最近 github 经常打不开,源代码就放在 gitee 上了。本文多为自己学习笔记记录,如有错误,欢迎指正!!!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容