Vue—2和3版本都有哪些好用的骚操作

659 阅读7分钟

一、Vue2

请求数据放在哪个生命周期钩子中比较合适?

答案是:created

解析:

  1. 在created阶段props、data等已经创建,但还没有el选项,因此在这里请求后端接口比较合适,如果接口响应速度快,在执行到beforeMount阶段时创建了el选项并读取data中的数据进行渲染(也就是说此时首次渲染就已经是接口数据),如果放在mounted中请求,此时组件已经渲染完成,等到接口数据请求回来之后又会触发一次渲染,导致二次渲染问题。

  2. 在created阶段请求接口能更快的获取到服务端数据,因为vue diff过程是递归进行的(同步),如果组件树层级过于复杂,等到mounted阶段时已经耗时挺久了,而请求接口是异步的,不会阻断后面代码的执行,因此也适合放在created阶段。

  3. 在created阶段进行请求有助于一致性,因为SSR不支持beforeMount、mounted钩子,后续项目如果改造成SSR可以平滑过渡。

如何在beforeCreate钩子里获取data?

这个问题看起来有点无聊,毕竟工作中不怎么会关注这个钩子,但这是一个很有意思的面试题。按我们正常的想法,在beforeCreate时data等选项还没进行挂载,因此data肯定是拿不到的,这没什么问题,那么这个问题的考点在哪里?

  1. 异步方式获取:$nextTick或者setTimeout都行,vue初始化生命周期并按顺序执行生命周期钩子,这是同步的,在beforeCreate钩子中使用异步方式来获取data,来相当于在初始化前告诉容器,等执行完了再跑里面的代码。借助下一次tick来获取data,这种方式可以拿data,也可以拿更新后的dom

  2. 同步方式获取:需要了解一下框架内部原理,beforeCreate时,所有的options(data、props、methods等)都会先存到vm.$options中,此时data是一个函数,需要调用该data函数来获取内部return的数据,因此可以通过this.$options.data()["属性名"]来获取数据,如果data中的初始值是简单的string,那直接this.$options.data()["属性名"]就好。涉及到复杂的情况,建议看看源码里是怎么处理的,具体在core/instance/state.js中的initData(vm)里。在beforeCreate之后,将$options里的data、props、methods等一个个挂载到vm上,然后再触发created钩子。所以在beforeCreate的时候,通过this.xxx是拿不到值的,在created的时候就能通过this.xxx拿到值了。

所谓的同步获取data数据,面试官的考的就是候选人对event loop的理解和是否了解vue的初始化过程。

computed、watch、method怎么用?

  1. computed:会将计算结果缓存,与watch一样会监听依赖数据的改变,依赖发生变化时才会重新触发计算,是提升性能的好帮手,一般情况下自动触发计算,如果需要手动触发其他操作则需要设置get和set,在set中加入自己要执行的操作。

  2. watch:与computed相同的是都会监听依赖变化,不同的是watch没有缓存能力,并且需要手动设置回调,在回调中执行相应操作,一般用于监听数据变化后执行某些异步操作,如请求数据。

  3. method:普通方法,没有缓存也不能监听依赖变化,调用即返回结果,作为普通函数使用即可。

如何在 Vue 之外创建一个具有响应性的变量?

如果你想创建一个具有响应性的变量又不想将它放在组件的data、props、computed中,可以使用Vue提供的observable方法来创建,例如:

import Vue from 'vue'
 
//  可以完全在 Vue 组件之外完成
const reactiveVariable = Vue.observable('test');

返回的对象可以直接用于渲染函数计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器,用于简单的场景:

const state = Vue.observable({ count: 0 })

const Demo = {
  render(h) {
    return h('button', {
      on: { click: () => { state.count++ }}
    }, `count is: ${state.count}`)
  }
}

v2.cn.vuejs.org/v2/api/#Vue…

怎么“窃取props类型”?

窃取 = 偷

偷 = 走捷径,快速达到目的

为什么要快速达到目的?=> 因为懒

这是在封装组件时容易遇到的场景,假设现在有一个Icon组件,其中有一大堆的props,如下所示:

export default {
  props: {
    iconType: {
      type: String,
      required: true,
    },
    iconSize: {
      type: String,
      default: 'medium',
      validator: size => [
        'small',
        'medium',
        'large',
        'x-large'
      ].includes(size),
    },
    iconColour: {
      type: String,
      default: 'black',
    },
    heading: {
      type: String,
      required: true,
    },
  },
};

现在需要创建一个A组件来复用Icon组件并且加入一些新功能,同时要让A组件支持Icon组件中的props验证和自身的props验证,我们需要手动将Icon组件中的props都复制过来,如下所示:

<template>
  <div>
    <h2>{{ title }}</h2>
    <Icon
      :type="iconType"
      :size="iconSize"
      :colour="iconColour"
    />
  </div>
</template>

<script>
import Icon from './Icon';
export default {
  components: { Icon },
  props: {
    iconType: {
      type: String,
      required: true,
    },
    iconSize: {
      type: String,
      default: 'medium',
      validator: size => [
        'small',
        'medium',
        'large',
        'x-large'
      ].includes(size),
    },
    iconColour: {
      type: String,
      default: 'black',
    },
    title: {
      type: String,
      required: true,
    },
  },
};
</script>

这样做无疑是非常麻烦的且不好维护,当Icon组件中的props被更新,往往会忘记同时更新A组件中的props,最终导致一些莫名其妙的报错。

因此我们需要有一种方式来巧妙的避开这些问题,做法是在A组件中拿到Icon组件实例上的props并添加到自己的props中,代码如下:

import Icon from './Icon';

export default {
  components: { Icon },
  props: {
    ...Icon.props, // 获取Icon组件的props并添加到自己的props上
    title: {
      type: String,
      required: true,
    },
  },
};

现在,如果Icon组件中的props被修改,我们的A组件将保持最新状态。

怎么实现slot透传?

场景举例:业务组件A使用了B、C等组件,B、C组件都有自己的slot,现在D组件使用了A组件,需要将slot传给B组件。

vue为我们提供了$scopedSlots这个API,用来访问作用域插槽。对于包括 默认 slot 在内的每一个插槽,该对象都包含一个返回相应 VNode 数组。

因此,可以在业务组件A中将slot分发给B、C组件,代码如下:

<!-- 实现插槽透传 -->
<!-- 调用子组件的插槽,向子组件传递内容 -->
<template>
    <B>
        <template v-for="(_, name) in $scopedSlots" v-slot:[name]="data">
            <!-- 定义插槽用于接受其父级组件的插槽内容 -->
            <slot :name="name" v-bind="data"></slot>
        </template>
    </B>
    <C>
        <template v-for="(_, name) in $scopedSlots" v-slot:[name]="data">
            <!-- 定义插槽用于接受其父级组件的插槽内容 -->
            <slot :name="name" v-bind="data"></slot>
        </template>
    </C>
</template>

怎么快速重置data中的属性为初始值(以重置表单为例)?

假设现在有如下表单,当我填写完所有的表单项后,想要一键重置除了调用el-form提供的重置初始值方法外,能否借助vue自身来完成呢?

image.png

data() {
  return {
    form: {
      name: '',
      region: '',
      date1: '',
      date2: '',
      delivery: false,
      type: [],
      resource: '',
      desc: ''
    }
  }
}

当然可以,原理为获取data初始值覆盖现有值,代码如下:

onReset() {
    Object.assign(this.$data, this.$options.data())
},

这里借助了几个api

  1. Object.assign()
  2. this.$data
  3. this.$options.data()

生命周期不展开讲了,vm.$options.data() 方法可以获取data的初始状态(没有被Object.defineProperty代理),vm.$data指向当前组件的data(已经被代理),可以验证一下

image.png

this.$data 打印结果为(可以看到打印的属性上有get和set表示已经被代理):

image.png

this.$options.data() 打印结果为:

image.png

Object.assign() 方法下面附一张图

image.png

解释一下也就是说Object.assign()传入target对象和sources对象时,sources对象中的属性将会覆盖target对象中的同名属性,此时target对象(vm.$data)中的属性已经被代理,当被修改为初始值(this.$options.data())时也会触发响应式更新,因此就达到了“一键重置”的效果。

二、Vue3

怎么实现slot透传?

原理跟vue2版本相同,只不过在vue3中所有的插槽都是函数,统一暴露在$slots中,我们可以看做vue2的$scopedSlots

以封装一个el-dialog组件为例,dialog组件代码如下:

<template>
  <el-dialog v-bind="$attrs" class="global-dialog">
    <!-- 默认插槽 -->
    <slot></slot>
    <!-- 具名插槽透传 -->
    <template v-for="(item, key, index) in $slots" :key="index" v-slot:[key]="slotProps">
      <slot :name="key" v-bind="{...slotProps}"></slot>
    </template>
  </el-dialog>
</template>

除了更改了组件名以外其它的全部按照el-dialog文档配置即可,使用示例:

<GlobalDialog v-model="dialogVisible" :title="dialogTitle" width="400">
  <div class="dialog-content">
    content
  </div>
  <!-- 可以通过这种方式来接收slot props -->
  <!-- <template #footer="footerProps"> {{ footerProps }} </template> -->
  <template #footer>
    <div class="dialog-footer">
      <el-button size="default" @click="dialogVisible = false" plain>取消</el-button>
      <el-button size="default" type="primary" @click="dialogVisible = false">确认</el-button>
    </div>
  </template>
</GlobalDialog>

为什么组件有多个根节点时会导致切换页面白屏?

场景:有A、B两个页面,A为正常,B为错误

  • 第一次直接进入错误页B,B页面正常显示,从错误页B切换到正常页A,正常页A空白,再切回错误页B,B依然空白
  • 第一次进入正常页A,不进入错误页B,均正常显示,切换任意非错误页B的页面都显示正常
  • 只要点过错误页面B,都会导致正常页显示异常

这个问题目前我发现两种情况,虽然vue3已经不再限制template只能有一个根节点了,但如果在router-view中给内置component组件传了参数或者指令,就会出现这种问题,例如:

layout组件:

image.png

  • 第一种情况是:组件template中写了多个根节点,vue会在运行时抛出一个警告。

路由页面组件:

image.png

警告:

image.png

此时就会发现切换路由时符合以上场景的情况会出现页面空白。

  • 第二种情况为:在根节点同级添加了注释,导致vue把他当成fragment渲染了,例如:
<template>
    <!-- 在此处写了注释内容 -->
    <div class="page"></div>
</template>

此时也会出现切换路由时页面空白的情况,并且不会有任何报错和警告

vue官网给出的解释是:

image.png

官文链接:cn.vuejs.org/guide/compo…

因此解决方式有以下几种:

  1. 在组件内部显示绑定传递过来的属性。
  2. 保证template只存在一个根节点。
  3. 注释内容写在根节点内或者template之外。

怎么快速重置data中的属性为初始值(以重置表单为例)?

以如下表单为例:

image.png

vue3中不能像在vue2中那样通过调用$options.data()来获取初始值,因此就需要换一种方式来实现。

首先创建一个class,定义好初始值

class FormState {
    risk_level = 0
    app_ids = ''
    strategy_name = ''
    domain = ''
    client_ip = ''
    code = 0
    date = ''
}

通过reactive或者ref创建form,初始值为FormState类的实例

const form = reactive(new FormState())

// or

const form = ref(new FormState())

reactive方式:

const onReset = () => {
    console.log(form)
    Object.assign(form, new FormState()) // Object.assign原理可参考上面vue2的案例
    console.log(form)
}

ref方式:

const onReset = () => {
    console.log(form.value)
    form.value = new FormState()
    console.log(form.value)
}

随便填写一些数据点击重置打印结果:

image.png

这种方式下,new FormState() 是一个新实例,不需要担心引用问题,如果你使用了ts,class不仅可以通过new来作为值用,也可以使用interface作为类型用,算是进可攻退可守,一举两得。