【面试题】你都必须得掌握的vue知识

 大厂面试题分享 面试题库

前后端面试题库 (面试必备) 推荐:★★★★★

地址:前端面试题库  web前端面试题库 VS java后端面试题库大全

前言

  大家好,我是前端贰货道士。最近抽空整理了下我对vue2.x的理解和认知,涵盖了vue2.x常用知识冷知识以及一些底层原理,算是我的vue2世界观。由于文章比较长,大家可以酌情根据自身需求选读,相信各位耐心看完定会有所收获

  因为是自己的理解,所以难免会出现错误。如果大家发现了错误,或者有些问题需要交流,欢迎在评论区下留言。由于最近项目加急,还有很多事情需要处理,剩下的vue2.x底层原理会在后续抽空更完,在此向大家说声抱歉。有兴趣继续读下去的朋友们可以先收藏吃灰,哈哈哈。如果本篇文章对您有帮助,烦请大家一键三连哦, 蟹蟹大家~

1. vue常用知识点总结(vue群演)

a. $watch和watch的使用

$watch的使用: 

Tips:

  • $watch的第二个参数是一个对象,定义handler方法以及上图四个属性的值。
  • $watch的第二个参数也可以为一个函数, 此时第三个参数则是一个对象,用于定义上图四个属性的值
  • watch中监听多个相同属性或者对象,后面会覆盖前面的。因为在里面定义的是对象的keyvalue,最后vue会遍历这些key并初始化各个计算属性的watch监听。
  • $watchwatch虽然都是监听方法,但是$watch中可以定义watch中定义好的属性或者对象,这两者相互独立。而且,也可以存在多个监听相同对象的$watch方法,它们之间也是互相独立的。
  • $watchwatch的主要区别是:$watch更加灵活,可以监听任何数据上的变化,也可以写在vue实例各位置(比如生命周期钩子函数、方法中等位置中)。而且比较重要的一点是:$watch可以取消对于自身的监听
`1. 点击添加元素,提示添加成功。再次点击,还是会显示添加成功的提示,因为此时已经取消了对于自身的监听。`
`a. 这个时候的dataArr之所以能监听到,是因为vue重写了数组的push方法,在调用这个响应式方法后,会通知watch进行监听。`
`b. 而当我们将dataArr的值改为[{ a: 100 }],并修改dataArr的值,点击时,this.dataArr[0].a = 1000。`
   `这个时候发现并没有触发监听,这是因为在$watch中deep的默认值也是false,同时也没有触发vue的响应式方法。`

`2. vue会监听数据的两种情况:`
`a. 使用$set(包括给对象重新分配地址)或$delete更新数据,触发响应式方法,从而触发监听`
`b. 使用7种重写的响应式数组方法,调用后便会通知watch进行监听`

<template>
  <div class="about">
    <div class="box">我是watch组件</div>
    <el-button class="box" @click="dataArrAdd">向数组中添加元素</el-button>
    <div class="box" v-if="showBtn">添加成功</div>
  </div>
</template>

<script>
export default {
  name: "watch",
  data() {
    return {
      dataArr: [],
      showBtn: false,
    }
  },

  created() {
    let unDataArr = this.$watch(
      () => {
        return this.dataArr
      },

      (newVal, oldVal) => {
        this.showBtn = !this.showBtn
        unDataArr()
      }
    )
  },

  methods: {
    dataArrAdd() {
      this.dataArr.push("1")
    }
  }
}
</script>
复制代码

Tips: $watch第一个参数如果为函数形式,一般要return出需要监听的变量。观察源码,value其实是$watch的第一个参数(如果是函数,就是返回值,如果没返回,就是undefined)。$watch方法触发的前提是监听的对象发生变化,而需要满足以下三种情况之一: 监听变量的值发生变化、是深度监听或者value是对象。

扫描二维码关注公众号,回复: 14954712 查看本文章

Tips:上图的deep如果为true,且收集的依赖value是对象且有值,就可以进行深度监听。所以在$watch中的deep有两个作用:

  • a. 为收集的对象依赖进行深度监听
  • b. 当$watch中监听的对象发生变化时,作为是否触发handler函数的依据之一存在

总结:

  • 会存在一种特殊情况:比如return a + b + c, 如果abc单个发生变化,但是整体没变。此时如果不写deep: true是无法触发监听handler方法的,所以一般情况下,$watch中最好写上deep: true
  • $watch的触发是存在前提的,只有收集的依赖发生变化时,才执行update方法。
  • 如果$watch中第一个参数是一个函数,且获取的是定义在data中的对象,但是没有return出去。此时就算写了deep: true,改变对象中某个属性的值,也不会触发$wacth方法。这是因为,收集的依赖是undefined,因此不会触发$watch方法。
```传送门(vue3):https://staging-cn.vuejs.org/api/component-instance.html#attrs```
```传送门(vue2):https://cn.vuejs.org/v2/api/#vm-watch```

总结:

```1. 监听一个属性名```
this.$watch('a', (newVal, oldVal) => {})

```2. 监听对象的属性```
this.$watch('a.b', (newVal, oldVal) => {})

```3. 监听getter函数的值```
this.$watch(
  // 每一次这个 `this.a + this.b` 表达式生成一个
  // 不同的结果,处理函数都会被调用
  // 这就好像我们在侦听一个计算属性
  // 而不定义计算属性本身。
  () => this.a + this.b,
  (newVal, oldVal) => {}
)

```4. 停止该侦听器```
const unwatch = this.$watch('a', cb)
unwatch()

```5. 排除对象某些属性的监听```
mounted() {
    Object.keys(this.params)
      .filter((_) => !["c", "d"].includes(_)) // 排除对c,d属性的监听
      .forEach((_) => {
        this.$watch((vm) => vm.params[_], handler, {
          deep: true,
        });
      });
},
data() {
    return {
      params: {
        a: 1,
        b: 2,
        c: 3,
        d: 4
      },
    };
},
watch: {
    params: {
      deep: true,
      handler() {
        this.getList;
      },
    }
}
复制代码

watch的使用:

`1. 监听:`

watch: { 
    `不需要深度监听和开始就要立即监听一次的情况下,以需要监听的变量名定义函数就好`
    pageSize(newVal, oldVal) {
      `newVal表示新的pageSize值,oldVal表示旧的oldVal值`
      console.log(newVal, oldVal) 
    }
}

`2. 深度监听(对引用类型进行深度递归监听):`

watch: { 
  `需要深度监听或开始就要立即监听一次的情况下,以需要监听监听的变量名作对象`
  `分别传入handler方法、immediate属性和deep属性`
  person: { 
      handler(newVal, oldVal) {
        console.log(newVal, oldVal)
      },
      immediate: true,
      `是否立即监听一次`
      deep: true
  }
}

`3. (小技巧)监听的值也可以为对象中的某个属性:`

watch: {
  '$route.query.id'() {
     ...
  },
   //或者:
  '$route.query.id': {
      handler(new, old) {
          
      },
      immediate: true
     ...
  },
  $route(to, from) {
      // 监听路由变化
      console.log("watch", to.query.id)
  }
}
复制代码

b. 计算属性的用法

`1.不能直接修改计算属性的值,除非为计算属性添加get和set方法:`

sizeCheckAll: {
  get() {
    return this.selectSizeList?.length == this.allSizes?.length
  },
  
  set(bool) {
    if(bool) this.selectSizeList = this.allSizes
    else this.selectSizeList = []
  }
}

`2.计算属性中可以解构引用data中定义的变量, 当这些变量发生变化时,会重新执行计算属性:`
text({ loading }) {
  return !loading ? this.$t('page.folder.upload') : this.$t('page.folder.uploading')
}

`3.在计算属性中使用闭包的思想传递参数:`
(1) 需要传递参数的情况不推荐使用计算属性,因为无法回收变量,会造成内存泄漏,需要改成方法;
(2) 除这种情况外,建议使用计算属性,因为有缓存(除非依赖的变量变化才会重新执行)。
    当其他变量改变视图更新时, 方法会重新执行,而计算属性不会;

getSku() {
  return (type, row) => {
    return row.customProductList[row.$sizeIndex].customSpecificProductList?.map((item) => item[type])
  }
}

`4.计算属性中的mapState(存储在vuex中的state数据)和mapGetters(对state中的数据进行包装的数据)`
import { mapState,mapGetters } from 'vuex'

computed: {
   ...mapState(['categoryList','productList']),
   ...mapGetters(['finalData'])
}

对于解构出来的`mapMutations和mapActions`需要定义在`methods`中,并在`methods`中进行调用
methods: {
    ...mapMutations(['add','addN']),
    ...mapActions(['awaitAdd'])
}

`5.计算属性中不能执行异步操作`

`6. 重新执行计算属性的方法:`

(1) 计算属性中可以`解构非关联的属性`,当`非关联属性属性变化`时,会`重新获取`计算属性的值(`小技巧`,但不推荐)

(2) 利用计算属性的原理(`_computedWatchers.计算属性名称.dirty`,不推荐)
    a. 计算属性是通过`dirty`来判断是否要进行`重新计算`,默认为`true`,是懒加载,`lazy`为`true`, 而`watch`监听
       的`lazy`为`false`,它们走的是不同的逻辑;
    b. 在获取当前计算属性的值后,重置为`false`;
    c. 当计算属性依赖的值发生变化且对应的计算属性在模板中使用到时,会触发`计算属性watcher`的`update`方法,
       将对应计算属性的`dirty`值变为`true`,重新得到计算属性的值,并刷新页面。
   
    1. `dirty`为`false`时, 会`缓存`计算结果,
    2. `dirty`为`true`时, 会`重新获取计算属性的值`。

    this._computedWatchers.计算属性名称.dirty = true
    this.$forceUpdate()
    this.$nextTick(()=> { 
        ...
    })
复制代码

计算属性computedwatch的区别:

  计算属性监听属性, 本质上都是一个watcher实例, 它们都通过响应式系统与数据、页面建立通信。但它们之间也存在一些差异:

  • 执行时机不同:watch是在数据变化时立即执行回调函数,而computed是在属性值被访问时才会执行计算函数。
  • 是否具有缓存功能:watch没有缓存功能,只要监听的数据发生变化,它就会触发相应的操作,而computed具有缓存功能,只有属性值被访问且依赖的其它变量发生变化时,才会执行计算函数。
  • 是否支持异步: computed不支持异步操作,需要返回值,否则计算属性不会生效。而watch支持异步操作,不需要返回值。

c. 关于路由跳转

 1. 使用`$router.push`进行`vue页面`间的`跳转`(会向`vue的history`中添加记录):
(1) 使用`name`进行跳转(推荐,因为`path`可能会移动和变化,但我们一般不会改变路由名称`name`的值)
    `在路由上显示传参,可以通过this.$route.query.id获取参数:`
    this.$router.push({ name: "detail", query: { id } })
    
    `不会在路由上显示传参,可以通过this.$route.params.id获取参数`
    this.$router.push({ name: "detail", params: { id } })
     
(2) 使用`path`进行跳转(不推荐):

    `在路由上显示传参,可以通过this.$route.query.id获取参数:`
    this.$router.push(`/product/detail?id=${id}`)
    this.$router.push({ path: "/product/detail", query: { id } })
    
    `不会在路由上显示传参,可以通过this.$route.params.id获取参数`
    this.$router.push({ path: "/product/detail", params: { id } })
    
 2. 使用`router.replace`跳转路由(`不会记录路由的history`)
 this.$router.replace(`/product/detail?id=${id}`)

 this.$router.replace({ name: "detail", query: { id } })
 this.$router.replace({ name: "detail", params: { id } })

 this.$router.replace({ path: "/product/detail", query: { id } })
 this.$router.replace({ path: "/product/detail", params: { id } })

 3. 使用`router-link`跳转路由:
 <router-link :to="{ name: 'detail', params: { id } }">
 <router-link :to="{ name: 'detail', query: { id } }">
 <router-link :to="{ path: '/product/detail', params: { id } }">
 <router-link :to="{ path: '/product/detail', query: { id } }">
 
 4. 新开页面跳转路由:
  let routeUrl = this.$router.resolve({
    name: 'exportRecords'
  })
  window.open(routeUrl.href, '_blank')
复制代码

d. 关于vue中的通讯方式

  1. 全局通讯:

   a. vueX

   b. $root(获取根组件,即App.vue的数据)

   听说过eventBus吗

`为避免内存泄漏,需要销毁监听的自定义事件,有两种解决方案:`

(1) 在组件的`beforeDestroy`钩子函数中销毁监听的自定义事件:

beforeDestroy() {
  //销毁监听事件
  this.$bus.off("addProduct");
}

(2) 每次$on之前$off需要销毁的事件名称;
复制代码

   d. localStorage详解

   e. 页面的路由传参

  2. 父子组件之间通讯:

   a.父组件使用props向子组件传值, 子组件定义props接收父组件传递过来的值,prop在父子组件之间是双向绑定的。特别注意:如果prop基本数据类型, 那么在子组件中,不能直接修改父组件传递过来的prop。但是我们可以通过语法糖: 即给父组件挂载的prop加上.sync,子组件通过$emit("update:prop的名称", 需要修改的值)达到间接修改父组件传递过来的prop的效果(传递事件给父组件,修改父组件的prop值,因为prop是双向绑定的,从而导致子组件的prop发生变化);

   b.子组件使用$emit向父组件传递事件和值,父组件使用@事件名接收事件和参数

1. 父子组件使用`$emit`和`@自定义事件`传递方法:

(1) `父组件中的方法按序接收传递过来的参数` 

子组件传递多个参数的情况:
this.$emit('test', 1, 2, 3)

父组件接收子组件的自定义事件:

@test="test"

test(a, b, c) {
  console.log('我是接收过来的参数', a, b, c)
}

`或`

test(...params) {
  console.log('我是接收过来的参数', params[0], params[1], params[2])
}

(2) `父组件使用函数中内置的arguments伪数组(且必须为这个内置参数),接收传递过来的参数` 

this.$emit('test', 1, 2, 3)

@test="test(arguments)"

test(params) {
  console.log('我是接收过来的参数', params[0], params[1], params[2])
}

(3) `使用对象的方式组装数据`

this.$emit('test', { age, sex, city })

@test="test"

test(params) {
  console.log('我是接收过来的参数', params.age, params.sex, params.city)
}

(4) `自定义事件传递一个参数,自定义事件需要使用子组件的参数和父组件的参数:`

this.$emit('updateProductExternalSkuCode', this.selectData)

<template #productInfo="{ row }">
  <productInfo
    :isDetail="true"
    :data="row"
    :canRelation="canEdit"
    @updateProductExternalSkuCode="updateProductExternalSkuCode($event, row)"
  />
</template>

(5) `自定义事件传递多个参数,自定义事件需要使用子组件的参数和父组件的参数:`

`使用arguments伪数组接收自定义事件传递过来的多个参数`

this.$emit('updateProductExternalSkuCode', this.selectData, this.data)

<template #productInfo="{ row }">
  <productInfo
    :isDetail="true"
    :data="row"
    :canRelation="canEdit"
    @updateProductExternalSkuCode="updateProductExternalSkuCode(arguments, row)"
  />
</template>

2. `$emit`的扩展: 使用`$on监听本组件的自定义事件`, 后文会讲到可以`使用$once只监听一次本组件的自定义事件`

mounted() {
    `因为不确定事件监听的触发时机,一般会在mounted或created钩子中来监听`
    `// 在钩子函数中定义了一个方法,用于closeModal调用时再去执行`
    `// 至于$on调用的方法和父组件从子组件接收来的自定义方法执行的快慢就看它们的执行机制
    (同步状态下,处理相同条件,父组件更快,一方异步一方同步的状态下,同步的那方先执行,都是异步看谁先执行完)`
    this.$on('closeModal',res=>{
        console.log(res);
    })
},

destoryed() {
    `// 使用$off移除事件监听
    1)如果没有提供参数,则移除所有的事件监听器;
    2)如果只提供了事件,则移除该事件所有的监听器;
    3)如果同时提供了事件与回调,则只移除这个回调的监听器。
    `
    this.$off("closeModal");
},

closeModal(){ 
    this.$emit('closeModal')
}

3. `$emit`的扩展: 
   `this.$emit('update:visible',false)`, 使用双向绑定的`语法糖`,在父组件中使用`.sync`对传入的`props`进行
    双向绑定,更新父组`visible`的`prop`值;
复制代码

   c. 父组件使用this.$refs.子组件的ref名称获取子组件的vue实例$el是针对组件的dom元素的,this.$refs.子组件的ref名称.$el是获取组件dom元素。如果this.$refs.名称不是一个组件,则不用加.$el,也识别不了。

   d. 父组件使用this.$children(包含所有子组件(不包含孙子组件)的 VueComponent 对象数组) 获取子组件的数据,例如this.$children[0].someMethod()执行子组件的方法。对于子组件,则直接使用this.$parent获取父组件的值。

   e. 插槽

  3. 祖先跨级通讯:

   a. 祖先组件使用provide返回需要传递的参数,后代组件使用inject接收参数

// 祖先组件
provide() {
    return {
        `// keyName: this, // 把整个vue实例的this对象传过去,因为是同一地址,里面的name变化,值也会响应式变化`
         `通过函数的方式也可以[注意,这里是把函数作为value,而不是this.changeValue()]`
          keyName: this.changeValue
        `// keyName: 'test' value 如果是基本类型,就无法实现响应式`
    }
},

data() {
    return {
        name:'张三'
    }
},

methods: {
    changeValue(){
        this.name = '改变后的名字-李四'
    }
}  
  
// 后代组件
inject:['keyName']
create() {
    `因为是函数,所以得执行才能获取响应式的数据,改变后的名字-李四`
    const keyName = this.keyName() 
    `或者使用const keyName = this.keyName.name`
}
复制代码

   b. 使用$attrs$listeners实现祖先的跨级通讯(详见本文第一点)

   c. 使用$dispatch$broadcast实现祖先跨级通讯的事件传递( Vue.js 1.x中的语法,有点类似provideinject。在现在的vue版本中,已不再支持)

  • $dispatch: 主要用于向祖先组件传递事件。而它的祖先级组件,可以在组件内通过$on监听到,从后代组件中传递过来的自定义事件
  • $broadcast: 主要用于向后代组件广播事件。而它的后代组件,可以在组件内通过$on监听到,从祖先组件中传递过来的自定义事件
  • 虽然在高版本的vue框架中,这两个api已经废弃。但是,我们也可以在项目中,为每个组件,创建一个独一无二的名字name。之后,我们可以通过递归的方式逐级向上或逐级向下,找到需要传递的后代组件或者祖先组件。在它们内部调用$emit方法,并在组件内部调用$on监听本组件的$emit事件即可。而递归的方法,我们可以写在混入里进行封装,方便复用。
`混入文件:`

function broadcast(componentName, eventName, params) {
  this.$children.forEach((child) => {
    const name = child.$options.name
    if (name === componentName) child.$emit(eventName, params)
    else broadcast.call(child, componentName, eventName, params)
  })
}

export default {
  methods: {
    dispatch(componentName, eventName, params) {
      let parent = this.$parent || this.$root
      let name = parent.$options.name

      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent
        if (parent) name = parent.$options.name
      }

      if (parent) {
        parent.$emit(eventName, params)
      }
    },

    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params)
    }
  }
}
复制代码
`子组件:`

<template>
  <el-button type="primary" size="mini" @click="clickHandler">子组件</el-button>
</template>

<script>
import emitMixin from '@/mixins/emitter'

export default {
  name: 'children',
  mixins: [emitMixin],
  
  mounted() {
    this.$on('handleClick', (data) => {
      console.log("我是来自父组件的数据", data)
    })
  },

  methods: {
    clickHandler() {
      this.dispatch('home', 'clickHandler', '我是cxk,我今年18啦,我的爱好是sing, dance and rap')
    }
  }
}
</script>
复制代码
`父组件:`

<template>
  <div class="app-container">
    <el-button type="primary" size="mini" @click="handleClick">父组件</el-button>
    <children />
  </div>
</template>

<script>
import children from './module/children'
import emitMixin from '@/mixins/emitter'

export default {
  name: 'home',
  mixins: [emitMixin],
  components: { children },
  
  mounted() {
    this.$on('clickHandler', (data) => {
      console.log('我是来自子组件的数据', data)
    })
  },

  methods: {
    handleClick() {
      this.broadcast('children', 'handleClick', '我是来自父组件的数据')
    }
  }
}
</script>
复制代码

  4. 两个页面之间进行通讯 ( 使用postMessage或者实时通讯websocket等):

`前端实时通信的方式: https://www.jb51.net/article/246674.htm`
`postMessage通信方式详解: https://blog.csdn.net/huangpb123/article/details/83692019`
`阮一峰websocket详解:http://www.ruanyifeng.com/blog/2017/05/websocket.html`
`websocket插件:https://github.com/joewalnes/reconnecting-websocket`
`黑马websocket:https://www.bilibili.com/video/BV14K411T7cd?p=6&spm_id_from=pageDriver&vd_source=a540d41ff453db4580db0168b87afe38`

1. 对外mes系统首页index文件:

<template>
  <div class="app-container">
    <div>欢迎来到对外系统</div>
    <el-button class="mt20" type="primary" @click="clickHandler">点击前往对内系统</el-button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      data: {
        name: 'cxk',
        age: 18,
        hobby: 'Sing, dance and rap',
        descroption: 'Data provided by emes'
      }
    }
  },

  methods: {
    clickHandler() {
      `8084为对内mes系统的端口`
      const currentUrl = window.open('http://localhost:8084')
      `设置延迟是为了让对外mes系统的数据传输比对内mes设置监听要慢`
      `只有对内mes页面先监听数据,对外mes系统才发送数据,好比打电话一样,只有接收方先接电话,才开始交流`
      setTimeout(() => {
        currentUrl.postMessage(this.data, '*')
      }, 1000)
    }
  }
}
</script>

2. 对内mes系统首页index文件:
`当从对外mes首页点击按钮前往对内mes系统后,对内mes首页就会显示出对外mes传输过来的数据`

<template>
  <div class="app-container">
    <div>欢迎来到对内系统</div>
    <div class="mt20">{
   
   { receivedData }}</div>
  </div>
</template>

<script>
export default {
  mounted() {
    window.addEventListener('message', (e) => {
      this.receivedData = e.data
    })
  },

  data() {
    return {
      receivedData: {}
    }
  }
}
</script>
复制代码

e. 插槽

vue插槽官方文档

1. 在子组件A中使用`<slot></slot>`进行`占位`,父组件引入子组件A,在A下`添加的内容`会`自动转移`到`插槽占位的地方`;
2. 如果`<slot></slot>`中有内容,如果在父组件不给子组件添加内容,那么就会展示插槽的默认内容;
   `需要特别注意的一点是,v-slot是无法挂载在slot上面的,有两种方式给默认插槽加上作用域:`
   `a. <slot :row="personObj"></slot>`
   `b. <slot v-bind="personObj"></slot>`
   `区别在于,a方式传递给父组件的数据,外层由row包裹,而b方式传递给父组件的数据没有`
   
3. 如果未给`<slot></slot>`提供名称,那么该插槽的默认名称是`default`;
4. 如果需要给`插槽`指定名称,直接对子组件使用`name`命名即可;
   `<slot name="footer"></slot>`
   
   如果在父组件中,需要给对应`插槽`添加内容,则可以使用如下三种写法:
   
   `此处如果不加:footer,则表示默认插槽,即vue会将父组件中未命名插槽的html代码隐式加上默认插槽。`
   <template v-slot:footer> 
       <!-- footer 插槽的内容放这里 --> 
   </template>
   
  <template #footer> 
       <!-- footer 插槽的内容放这里 --> 
   </template>
   
   <template slot="footer"> 
       <!-- footer 插槽的内容放这里 --> 
   </template>
   
   
 5. `插槽`是有`作用域`的,父组件中的`插槽`内容无法访问子组件的内容,除非通过`作用域插槽`的方式进行传递:
 
    子组件:
    `<slot name="footer" :row="row"></slot>`
   
    父组件:
    <template v-slot:footer="{ row }"> 
       <!-- footer 插槽的内容放这里 --> 
    </template>
    
    <template #footer="{ row }"> 
       <!-- footer 插槽的内容放这里 --> 
    </template>
     
    <template slot="footer" slot-scope="{ row }">
        <!-- footer 插槽的内容放这里 --> 
    </template>
    
    在这里`扩展`一个代码的优化点,`v-if`的代码可以使用`template`包裹,`语义`会更加清晰。
    
  6. 如果组件只有一个`插槽`,则在父组件上,可以直接使用`插槽`语法,而不需要`template`标签嵌套。
  7. 自定义组件内部的`$scopedSlots`记载了组件的`作用域插槽信息`,以`key(插槽名)-value(对应函数。指定key值,
  执行得到Vnode数组,对应$slots,一般更推荐使用$scopedSlots)`的形式出现。因此,根据这个特性,
  当`有多个具有插槽的组件`定义在`一个自定义组件中`时,可以通过`遍历的方式动态添加插槽`
  
 `(1) 使用$scopedSlots封装组件,动态遍历插槽(以多个具有插槽的组件为例):`
 
<template>
  <div class="el-tree-select_component">
    <el-select
      ref="select"
      :class="[defaultText && 'has-default-text']"
      v-bind="selectProps"
      :value="valueTitle"
      :popperClass="concatPopperClass"
    >
      <template v-for="(val, key) in allSlots.inputScopedSlots" #[key]>
        <slot :name="key"></slot>
      </template>
      <el-option :label="valueTitle" :value="valueId">
        <el-tree
          ref="selectTree"
          :data="options || []"
          :node-key="props.value"
          :default-expanded-keys="defaultExpandedKey"
          v-bind="treeProps"
          @node-click="handleNodeClick"
        >
          <template v-for="(val, key) in allSlots.treeScopedSlots" #[key]="scope">
            <slot :name="key" v-bind="scope"></slot>
          </template>
        </el-tree>
      </el-option>
    </el-select>
    <span class="default-text" v-if="defaultText"> {
   
   { defaultText }} </span>
  </div>
</template>

const INPUT_SLOT_LIST = ['prefix', 'empty']

computed: {
    // 获取select组件和tree组件的插槽
    allSlots({ $scopedSlots }) {
      const inputScopedSlots = {}
      const treeScopedSlots = {}
      for (let key in $scopedSlots) {
        const val = $scopedSlots[key]
        if (INPUT_SLOT_LIST.includes(key)) inputScopedSlots[key] = val
        else treeScopedSlots[key] = val
      }
      return {
        inputScopedSlots,
        treeScopedSlots
      }
    }
}

 `(2) 使用$slots封装组件,动态遍历插槽(以el-input组件的二次封装为例):`
 
      `a. 动态插槽:`
      
      <el-input v-bind='$attrs' v-on="$listeners">
        <template #[slotName] v-for="(slot, slotName) in $slots">
          <slot :name="slotName" /> 
        </template>
      </el-input>
      
      `使用el-input中定义好的插槽:`
      
      <customInput placeholder="请输入内容" v-model="value">
        <el-button slot="append" icon="el-icon-search"> </el-button> 
      </customInput>
      
      `如果需要给slot插槽上的点击事件传递本组件的方法,直接绑定点击事件是行不通的,现有两种方法:
      
       方法一:在slot插槽上添加一个div父级容器,并绑定点击事件@click="需要传入的本组件中的方法" 
       方法二:直接在slot上传递一个:onClick="定义的函数clickHandler"。父组件引入后,在插槽中解构出
       clickHandler,然后再绑定点击事件@click="点击事件方法(定义的函数clickHandler)"
       `
      
       `children.vue
       
        <template>
          <div>
            <h3>插槽$slots的用法</h3>
            <slot name="header"></slot>
            <slot name="main"></slot>
            <slot name="footer"></slot>
          </div>
        </template>
       `
      
       `parent.vue
       
       <template>
         <children>
           <template #[slotName] v-for="(slot, slotName) in $scopedSlots">
             <slot :name="slotName" :clickHandler="clickHandler" />
           </template>
         </children>
       </template>

       <script>
       import children from './children'

       export default {
         components: { children },
         
         methods: {
           clickHandler() {
             console.log('插槽被调用了呢')
           }
         }
       }
       </script>
       `
      
       `index.vue
       <template>
         <parent>
           <template #main>主体区域</template>
           <template #footer="{ clickHandler }">
             <div @click="click('footer', clickHandler)">尾部区域</div>
           </template>
         </parent>
       </template>

        <script>
        import parent from './parent.vue'

        export default {
          components: { parent },

          methods: {
            click(type, clickHandler) {
              console.log(`我是${type}插槽`)
              clickHandler()
            }
          }
        }
        </script>
       `
      
      `b. 动态作用域插槽:(特别注意) $slots无法获取具名作用域插槽, 作用域插槽只能用$scopedSlots获取`
      
      <el-input v-bind='$attrs' v-on="$listeners">
        <template #[slotName]="slotProps" v-for="(slot, slotName) in $scopedSlots">
          <slot :name="slotName"  v-bind="slotProps"/> 
        </template>
      </el-input>
复制代码

f. vueX

  vueX黑马笔记

`在平时的项目中,为了代码看上去不是那么臃肿,一般会使用多个store文件来维护vueX,比如product.js, order.js...`
`并可以通过函数的方式拿到vueX中存储的数据`

computed: {
    ...mapState({
        has_image_gallery: (state) => state.customizer.has_image_gallery,
        library: (state) => state.myImage.library.list,
        meta: (state) => state.myImage.library.pagination,
        last_page: (state) => state.myImage.library.pagination.last_page
    })
}
复制代码

g. 指令(以回到顶部组件说明)

  指令的官方介绍

`自定义指令中的第三个参数vnode的context记载了组件的一些信息,这个是我们比较需要关注的`

`1. 使用自定义指令,实现回到顶部的效果:`

`添加全局公共样式:`
.scroll-top-class {
  position: fixed;
  bottom: 120px;
  right: 30px;
  opacity: 0;
  height: 40px;
  width: 40px;
  line-height: 40px;
  font-size: 30px;
  text-align: center;
  color: #ddd;
  opacity: 0;
  z-index: 2021;
  cursor: pointer;
  border-radius: 50%;
  box-shadow: 0px 0px 8px 1px #ccc;
  background-color: rgba($color: #666, $alpha: 0.5);
  transition: all 1s;
}

`指令挂载方法:在有滚动条的容器上,添加v-scrollTop指令, 并提供相应的值即可。如果不提供,则使用默认值`
<div class="topic-page" v-scrollTop> </div>

`指令注册方法: 同第k点, 先install, 在本文件暴露出去。然后在main.js文件中引入,并使用vue.use(引入的名称)全局注册`

`第一种方法:直接使用binding.value判断回到顶部图标出现的位置(相对推荐)`

Vue.directive('scrollTop', {
  inserted(el, binding) {
    `如果未设置binding.value的值,则默认为200`
    `滚动条移动超过200px的距离就显示,反之则隐藏`
    if (!binding.value) binding.value = 200

    el.style.scrollBehavior = 'smooth'

    const backEl = document.createElement('div')
    backEl.className = 'scroll-top-class el-icon-top'
    el.appendChild(backEl)
    backEl.addEventListener('click', () => (el.scrollTop = 0))

    el.addEventListener('scroll', () => {
      if (el.scrollTop >= binding.value) backEl.style.opacity = 1
      else backEl.style.opacity = 0
    })
  }
})

`第二种方法:使用binding.value,根据滚动条的总高度所占比例,间接判断回到顶部图标出现的位置`
`(不推荐,因为在产品列表无限滚动情况下,滚动条高度是动态变化的,无法适用,而且倍数不好控制)`

// 滚动条指令
Vue.directive('scrollTop', {
  inserted(el, binding) {
    if (binding.value >= 1 || binding.value <= 0) return new Error('v-scrollTop的绑定值需要介于0到1之间')

    `获取元素的整体高度`
    const elH = el.offsetHeight
    
    `也可以给visibilityHeight定值(不推荐,无法兼容所有需要滚动条的页面)`
    let visibilityHeight = 0
    if (binding.value) visibilityHeight = binding.value * elH
    `阈值默认为滚动区域整体高度的0.2倍`
    else visibilityHeight = 0.2 * elH

    `为滚动条返回顶部添加平滑的过渡效果`
    el.style.scrollBehavior = 'smooth'

    const backEl = document.createElement('div')
    backEl.className = 'scroll-top-class el-icon-top'
    `将创建的回到顶部图标作为孩子插入到el中`
    el.appendChild(backEl)
    
    backEl.addEventListener('click', () => (el.scrollTop = 0))

    el.addEventListener('scroll', () => {
      if (el.scrollTop >= visibilityHeight) backEl.style.opacity = 1
      else backEl.style.opacity = 0
    })
  }
})

`2. 自定义组件,实现回到顶部的效果:`
`使用这种方式,需要在每个有回到顶部需求的文件中引入该自定义组件,并指定高度阈值及滚动条dom容器对应的字符串`

<template>
  <el-button v-show="visible" class="back" @click="backTop">top</el-button>
</template>

<script>
export default {
  props: {
    height: {
      required: false,
      type: Number,
      default: 200
    },

    target: {
      required: false,
      type: String,
      default: '.topic-page'
    }
  },

  data() {
    return {
      container: false,
      visible: false
    }
  },

  mounted() {
    this.container = document.querySelector(this.target)
    if (!this.container) throw new Error('target is not existed: ' + this.target)
    this.container.style.scrollBehavior = 'smooth'
    `最保险的做法是,使用this.$nextTick包裹下面的代码,因为vue是异步更新机制,dom可能还未更新`
    this.container.addEventListener('scroll', this.scrollToTop)
    this.$once('hook:beforeDestory', () => {
      this.container.removeEventListener('scroll', this.scrollToTop)
    })
  },

  methods: {
    backTop() {
      this.container.scrollTo({
        top: 0,
        behavior: 'smooth'
      })
    },

    scrollToTop() {
      this.visible = this.container.scrollTop > this.height ? true : false
    }
  }
}
</script>

<style lang="scss" scoped>
.back {
  position: fixed;
  bottom: 100px;
  right: 100px;
}
</style>
复制代码

h. 使用install和use进行全局注册: 使用vue.use(xx),就会调用xx里面的install方法

`lodopPrintPdf.js`

import Vue from 'vue'
import PrintBtn from './printBtn'
import merge from 'element-ui/src/utils/merge'

`手动将PrintBtn这个js对象转换为vue实例,这也是vue内部将对象文件转换为vue实例的过程`
export default async function lodopPrintPdf(option) {
  `使用vue构造器,创建一个vue的子类,及子类构造器`
  const ExtendPrintBtn = Vue.extend(PrintBtn)
  `继承打印组件并初始化vue实例`
  const vm = new ExtendPrintBtn({})
  `合并option,等价于Object.assign(vm, option)`
  `相当于遍历添加传入vm的prop参数`
  merge(vm, option)
  `调用实例的方法,js动态加载完成`
  return vm.printHandler()
}
复制代码
`globalConst.js`

import lodopPrintPdf from '@/components/lodopPrint/lodopPrintPdf.js'

export default {
    install(Vue) {
        `在vue的原型对象上挂载$lodopPrintPdf,并暴露出去`
        Vue.prototype.$lodopPrintPdf = lodopPrintPdf //lodop打印pdf
    }
}
复制代码
`main.js`

`Vue.use的用法: 安装Vue插件。
如果插件是一个对象,必须提供 install 方法。
如果插件是一个函数,它会被作为 install 方法。`

import globalConst from '@/commons/globalConst'

Vue.use(globalConst)
复制代码
`$lodopPrintPdf方法的使用`

`传入的五个参数就是前面定义的函数所接收的option值,相当于调用打印组件,传入对应的五个props`
this.$lodopPrintPdf({
    type: 'html',
    printable: this.$refs.label.$el,
    paperSize: [841.89, 595.28],
    onSuccess: this.resetLoading,
    onError: this.resetLoading
})
复制代码
`需要在main.js中引入该js,并使用vue.use(xx), 就可以将该组件注册为全局组件`

import CrudInput from '../crud/src/crud-input'

/* istanbul ignore next */
CrudInput.install = function (Vue) {
  Vue.component(CrudInput.name, CrudInput)
}

export default CrudInput
复制代码

i. 混入 && 继承

  混入: 对于具有相同逻辑的vue文件,其实可以抽取成一个混入,存放公共的js代码。在使用混入vue文件中,可以定义相同的变量或者方法来覆盖混入中的变量或者方法。

  类: 在混入中定义的变量和方法,很容易与vue文件中定义的变量和方法冲突,从而被vue文件中定义的变量和方法覆盖掉。而相比混入,类中定义的变量和方法不容易被污染,因此开发过程中,尽量多使用类来代替混入。

  继承: 相比混入继承更加霸道,可以继承整个vue文件。同时在继承文件中,可以添加一些额外的js代码。如果在被继承的组件中存在这些js变量和方法,那么继承组件就会覆盖这些变量和方法,如果不存在则为添加。如果在继承组件中添加htmlcss代码,不管这些代码之前是否和被继承组件的htmlcss代码冲突,继承组件的htmlcss代码都会以自身代码为主,不会继承被继承组件的htmlcss代码。

<script>
import dialog from '@/extend/components/dialog/index'

export default {
  extends: dialog
 
}
</script>
复制代码

j. $props三兄弟和inherits属性

  $props:当前组件接收到的 props 对象。Vue 实例代理了对其 props 对象属性的访问。

  $attrs: 包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。

  $listeners:包含了父作用域中的(不含.native 修饰器的)v-on事件监听器。

  inherits属性的作用是禁止传入的属性添加到组件的根元素上。默认为true,即将传入的属性添加到组件的根元素上。

   应用v-bind="$attrs"v-on="$listeners"一般用于组件封装上,必须得绑定在组件上。

  • v-bind="$attrs"相当于一个展开运算符,将从父组件传来的props且未被当前组件props接收的prop挂载到组件上,使得组件具有可扩展性。如果未绑定,孙子组件可以通过this.$attrs拿到子组件的props,但是无法拿到父组件的props。如果要拿到父组件的props,则需要在子组件上绑定v-bind="$attrs",这样孙子组件中的this.$attrs就指向父组件的props(孙子组件和子组件中定义的props会排除在外)。

  • v-on="$listeners"是把父组件的事件绑定在子组件上。因此,会有一种减少代码的小技巧。假定有这么一种情形:有祖先组件A, 父组件B和子组件C,在组件c中某个元素被点击时,需要将事件逐级向上传递到组件组件A中。常规解决思路是,逐级向上$emit事件。但有一种简便的思路是,我们可以利用v-on="$listeners",将它绑定在父组件B上,这样就可以不用在父组件B上再监听子组件C传递而来的事件。

k. v-model语法糖

1. 在未加`.sync`的情形下,`:props`是单向绑定。在不自定义`v-model`的前提下,`v-model`其实是`v-model:value`的简写

2. <input v-model="searchText" />
    
   `等价于`
   
   <input :value="searchText" @input="searchText = $event.target.value" />

3. `二次封装组件`时,如果需要对双向绑定的值做处理,可以将v-model拆开:
`比如有这么一种需求,需要el-input-number组件,在给null默认值或空字符串时,会显示为0`

<template>
  <el-input-number 
    v-bind="$attrs" 
    v-on="$listeners"
    :value='num' 
    @input="$emit('input', $event)">
  </el-input-number>
</template>

<script>
  export default {
    props: {
      `value就是父组件v-model绑定的值`
      value: [String, Number]
    },
    
    computed: {
      num() {
        return typeof this.value === 'number' ? this.value : undefined
      }
    }
  }
</script>

`使用:num和子组件的是value双向绑定的`
<cz-input-number placeholder='请输入数量' @change="change" v-model="num"></cz-input-number>
复制代码

l. 修饰符的顺序及理解

m. render函数 && 函数式组件

n. 递归组件 && 动态组件

  • 递归组件:

  何为递归组件递归组件就是通过调用组件自身来实现递归。因此递归组件需要提供name属性和递归条件(比如是否为数组),方便自己调用。这种组件主要用于处理一些需要递归的数据,最普遍的比如树状结构

`利用递归组件实现el-tree的基础功能:https://juejin.cn/post/7056922161788747789`

`1. 子组件:`

<template>
  <div class="tree-item">
    <div v-for="item in treeData" :key="item.id">
      <div class="item-title" @click="nodeClick(item)">
        <span>{
   
   { item.name }}</span>
        <i
          v-if="isArray(item.children)"
          :class="['ml5', isOpen(item.id) ? 'el-icon-arrow-up' : 'el-icon-arrow-down']"
        >
        </i>
      </div>
      <div v-if="isArray(item.children) && isOpen(item.id)" class="item-childen">
        <my-tree :treeData="item.children" @node-click="$emit('node-click', $event)"></my-tree>
      </div>
    </div>
  </div>
</template>

<script>
import {isArray} from 'lodash'

export default {
  name: 'myTree',
  props: {
    treeData: {
      type: Array,
      default: () => []
    }
  },
  
  data() {
    return {
      expandedKeys: [] // 当前展开的节点id组成的数组
    }
  },
  
  methods: {
    isArray,
    isOpen(id) {
      return this.expandedKeys.includes(id)
    },
    
    nodeClick(item) {
      `判断展开节点id组成的数组是否包含当前id。如果包含,此时点击,就是取消展开;如果不包含,此时点击,就是展开`
      this.$emit('node-click', item)
      `之所以要做这层判断,是为了减少不必要的逻辑,提高代码执行效率`
      if(!this.isArray(item.children)) return
      let index = this.expandedKeys.indexOf(item.id)
      if (index > -1) this.expandedKeys.splice(index, 1)
      else this.expandedKeys.push(item.id)
    }
  }
}
</script>

<style lang="scss" scoped>
.tree-item {
  cursor: pointer;
  .item-title {
    padding: 4px 8px;
    &:hover {
      background: #eee;
    }
    .ml5 {
      margin-left: 5px;
    }
  }
  .item-childen {
    padding-left: 20px;
  }
}
</style>

`2. 父组件:`
<template>
  <my-tree :tree-data="treeData" @node-click="nodeClick"></my-tree>
</template>

<script>
`静态变量,直接定义在data外就好`
const treeData = [
  { id: 1, name: '一级1' },
  {
    id: 2,
    name: '一级2',
    children: [
      { id: 3, name: '二级2-1' },
      { id: 4, name: '二级2-2' }
    ]
  },
  {
    id: 5,
    name: '一级3',
    children: [
      {
        id: 6,
        name: '二级3-1',
        children: [
          { id: 7, name: '三级3-1-1' },
          { id: 8, name: '三级3-1-2' }
        ]
      },
      { id: 9, name: '二级3-2' },
      { id: 10, name: '二级3-3' }
    ]
  }
]

import myTree from './module/myTree.vue'
export default {
  components: {
    myTree
  },

  data() {
    return {
      treeData: treeData
    }
  },

  methods: {
    nodeClick(data) {
      console.log('data', data)
    }
  }
}
</script>
复制代码

效果浏览:

  • 动态组件:

  任意标签上添加:is属性,就会成为一个动态组件。此时,给is属性添加上引入的组件名称,就会根据is的当前值来动态切换组件。但为了语义化,我们最好将这个标签命名为component值得注意的是,组件切换的过程中会销毁上一个组件,每次进入新组件,都会触发新组件的生命周期。为了解决这一问题,我们可以使用keep-alive对动态组件进行缓存。

<keep-alive>
  <component :is="component"></component>
</keep-alive>
复制代码

o. 路由守卫

vue官网——路由守卫

  在此,只对vue组件内的路由守卫进行讨论:

  • beforeRouteEnter:

   (1) 进入组件前调用,此时组件还未渲染,所以该路由守卫中不存在this;

   (2) 但是如果我们一定要获取vue的实例,我们可以给该路由守卫中的参数next,添加一个函数回调,这个函数回调的参数就是当前vue的实例,也就是this

  beforeRouteEnter(to, from, next) {
    next((vm)=>{
      console.log(vm);
    })
  }
复制代码
  • beforeRouteLeave: 离开当前组件后触发,此时存在this
beforeRouteLeave(to, from, next) { 
  // console.log(this); 
  next(); 
}
复制代码
  • beforeRouteUpdate: 同一组件路由传参发生变化时才触发, 也存在this
beforeRouteUpdate(to, from, next) { 
  // console.log(this); 
  next(); 
}
复制代码

p. 关于vue中this的基础知识(特别基础)

  • vue中的this来源:

  vue scrip脚本export default区域以外的this指向undefined, 我们也可以在这个区域中定义一些变量,将这些变量挂载到data中并使用。但是在export default区域中,是不支持定义变量的。我们可以将它理解成一个对象,在这个对象上挂载了很多属性。而有些属性,比如created生命周期钩子函数之所以能使用指向vue实例的this,是因为vue在处理created生命周期时,通过apply方法,将该钩子函数指向了本文件的vue实例,所以就可以通过this拿到本文件的vue实例了。

  • vue中的this理解:

  一个vue文件分为html模板js文件以及css文件,这些文件是相互独立的。如果script下有引入方法,且该方法没有挂载methods中。因为vue中的this指向的是该组件的vue实例本身,所以就无法通过this.someMethods()调用方法this.someMethods()会得到undefined。但是由于它们位于同一个script文件中,因此可以直接通过someMethods()直接调用方法。html模板中使用到的变量方法其实省略了this, 它们分别来源于挂载到data中的响应式变量和methods中的方法。倘若未挂载就使用,会报错。

q. 关于引入的component组件的理解

   我们在vue文件中引入的component组件都是以对象的形式存在,只不过vue帮助我们进行了处理,将这个对象转换成了vue实例,这点也可以从引入的函数式组件或者render函数中体现出来。

r. 如何写出可维护的vue代码?

  • vue的核心思想是数据驱动视图。非必要情况下忌讳父组件直接使用$refs去操作子组件的dom,这种方式可能会在子组件中添加一些本组件未使用到而在父组件中使用到的方法。一种比较好的解决方法是,通过在子组件中定义props,监听到props的变化后再去子组件中执行对应的逻辑,然后在父组件中绑定props
  • 需要判断很多种条件的情况下,其实可以使用js对象配置的方式去操作。对于不同特殊情况,可以通过配置,调用不同的接口,定义不同的方法,传不同的参数去解决问题
  • 忌讳使用很多变量去定义某些特殊状态。这些变化其实和代码中定义的某些变量挂钩,此时使用计算属性去解决即可
  • 不要在代码中出现类似this.变量 == 1的代码,需要定义常量、注释常量并引入常量去解决,这样做的好处是有助于后续的开发和维护
  • 一个方法忌讳写很大一串不同逻辑的代码,不同逻辑的代码需要抽成不同方法,然后定义在这个方法中,即一个方法处理一个逻辑
  • 使用项目中封装好的全局样式、颜色常量以及字体常量等公共资源,方便维护
  • 使用组件化的思想封装vue文件
  • 公共js抽出来放到混入中
  • 变量的命名要合理,不能出现很奇葩的命名干扰开发

 大厂面试题分享 面试题库

前后端面试题库 (面试必备) 推荐:★★★★★

地址:前端面试题库  web前端面试题库 VS java后端面试题库大全

猜你喜欢

转载自blog.csdn.net/weixin_42981560/article/details/130413398
今日推荐