面试不面试,你都必须得掌握的vue知识

lxf2023-04-05 21:49:02

我报名参加1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

传送门——我的第二篇文章: 我的css世界

前言

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

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

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

a. $watch和watch的使用

$watch的使用: 面试不面试,你都必须得掌握的vue知识

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>
面试不面试,你都必须得掌握的vue知识

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

面试不面试,你都必须得掌握的vue知识

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(()=> { 
        ...
    })

面试不面试,你都必须得掌握的vue知识

面试不面试,你都必须得掌握的vue知识

计算属性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的数据)

   c. 听说过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>

结果截图:

面试不面试,你都必须得掌握的vue知识

  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传递而来的事件。

inherits属性说明

inherits属性-AdminJS文章说明

$props-AdminJS文章说明

$listeners-AdminJS文章说明

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. 修饰符的顺序及理解

  修饰符详解1

  修饰符详解2

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

原创——深度剖析render函数、函数式组件与JSX之间的爱恨情仇

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

  • 递归组件:

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

`利用递归组件实现el-tree的基础功能:https://Admin.net/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>

效果浏览:

面试不面试,你都必须得掌握的vue知识

  • 动态组件:

  任意标签上添加: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抽出来放到混入中
  • 变量的命名要合理,不能出现很奇葩的命名干扰开发

2. vue的冷知识(vue间谍,大部分内容来自Sunshine_Lin的AdminJS博客。部分内容有自己的思考和扩展, 以扩展两字进行标注)

a. 为什么不建议v-for和v-if同时存在?

<div v-for="item in [1, 2, 3, 4, 5, 6, 7]" v-if="item !== 3">
    {{item}}
</div>

`拓展:`
`vue2中的v-for优先级高于v-if, vue3则相反。首先会把7个元素都遍历出来,然后再一个个判断是否为3,并把3的dom给隐藏掉,
这样的坏处就是,渲染了无用的3节点,增加无用的dom操作,建议使用computed来解决这个问题:`

`有一个小技巧就是,给模态框组件加上v-if,当控制模态框的visible值为true时,会重新触发模态框的生命周期。
这点和给组件绑定:key值很像,会刷新组件`

<div v-for="item in list">
    {{item}}
</div>

computed() {
    list() {
        return [1, 2, 3, 4, 5, 6, 7].filter(item => item !== 3)
    }
}

b. 为什么不建议用index做key,为什么不建议用随机数做key?

<div v-for="(item, index) in list" :key="index">{{item.name}}</div>

list: [
    { name: '小明', id: '123' },
    { name: '小红', id: '124' },
    { name: '小花', id: '125' }
]

渲染为
<div key="0">小明</div>
<div key="1">小红</div>
<div key="2">小花</div>

现在我执行 list.unshift({ name: '小林', id: '122' })

渲染为
<div key="0">小林</div>
<div key="1">小明</div>
<div key="2">小红</div>
<div key="3">小花</div>


新旧对比

<div key="0">小明</div>  <div key="0">小林</div>
<div key="1">小红</div>  <div key="1">小明</div>
<div key="2">小花</div>  <div key="2">小红</div>
                         <div key="3">小花</div>

可以看出,如果用index做key的话,其实是更新了原有的三项,并新增了小花,虽然达到了渲染目的,但是损耗性能

现在我们使用id来做key,渲染为

<div key="123">小明</div>
<div key="124">小红</div>
<div key="125">小花</div>

现在我执行 list.unshift({ name: '小林', id: '122' }),渲染为

<div key="122">小林</div>
<div key="123">小明</div>
<div key="124">小红</div>
<div key="125">小花</div>

新旧对比

                           <div key="122">小林</div>
<div key="123">小明</div>  <div key="123">小明</div>
<div key="124">小红</div>  <div key="124">小红</div>
<div key="125">小花</div>  <div key="125">小花</div>

可以看出,原有的三项都不变,只是新增了小林这个人,这才是最理想的结果

  用index和用随机数都是同理,随机数每次都在变,做不到专一性,很渣男,也很消耗性能,所以,拒绝渣男,选择老实人

c. 为什么data是个函数并且返回一个对象呢?

`data`之所以是一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行`data函数`并返回新的数据对象。
这样,可以避免多处调用的`数据污染`

d. vue2的内部指令

面试不面试,你都必须得掌握的vue知识

`拓展:`

1. `v-text`的用法:

本质其实和`插值表达式`一样。不同的是,`v-text`后面必须得`赋值`,它只与`赋予的变量的值`有关。比如:

<div v-text="123"> {{ unsignedCountries }} </div>

此处只`渲染123`, 而不会渲染`计算属性`的值。

2. `v-once`:指令所绑定的`html标签``组件`中的变量只会渲染一次,`不会随着变量的变化而变化`,利用好这一点可以优化

e. vue2的生命周期

面试不面试,你都必须得掌握的vue知识 面试不面试,你都必须得掌握的vue知识

面试不面试,你都必须得掌握的vue知识
`拓展:`
`1. vue生命周期钩子函数的组成:`

`vue的生命周期`是一个`组件/实例``创建到销毁`的过程中自动执行的函数,主要分为:`创建、挂载、更新、销毁`四个模块。

`挂载``$el``$mount`都是为了将`实例化后的vue挂载`到指定的`dom元素`中,但是`$el`的优先级要高于`$mount`。

如果`实例化vue`的时候指定`el`,则`vue`将会渲染到`此el对应的dom`中,
反之,若没有指定`el`,则`vue实例`会处于一种`未挂载`的状态,此时可以通过`$mount`来手动`执行挂载``2. 被keep-alive缓存的组件的生命周期:`

第一次进入,`created` -> `mounted` -> `activated`,
退出时触发`deactivated`。当再次进入时,只触发`activated``3. 创建期间的生命周期函数:`

`beforeCreate``vue实例`刚在内存中被创建出来,此时还没有初始化好`data`、计算属性以及`methods`等。
`虽然在该钩子函数中,可以获取到this,但无法获取到该vue文件中的data数据和methods方法。`

`created`:实例已经在内存中创建完毕,此时的`data``methods`也已经创建好了,但是还没开始编译模板。

`beforeMount`:此时已经完成了模板的`编译`,但是还没有挂载到页面中。

  `换句话说,此时页面中的类似 {{ msg }} 这样的语法还没有被替换成真正的数据。`

`mounted`:此时已经将编译好的模板,挂载到了页面指定的容器中显示,可以获取`DOM`节点, 发起异步请求,
用户已经可以看到渲染好的页面了。


`4. 运行期间的生命周期函数beforeUpdate(需要特别注意,该钩子函数中的data值都是最新的数据)与updated:`

`beforeUpdate钩子`: 数据发生变化,`dom`更新之前执行此函数, 此时`data`中的值是最新的,但是界面上显示的还是旧数据,
因为此时还`没有重新渲染DOM`节点。

`updated钩子`: `dom`更新完毕之后调用此函数,此时`data`中的值也是最新的,而且`dom`已经被重新渲染好了。

`5. 销毁期间的生命周期函数:
`
`beforeDestroy``vue实例销毁`之前调用。在这一步,`实例`仍然完全可用。

`destroyed``vue实例`销毁后调用。调用后,`vue实例`指示的所有东西都会解绑,所有的事件监听器会被移除,
`所有的子实例也会被销毁`

f. 如何设置动态class,动态style?

(1) 动态class(对象形式):

<div :class="{ 'is-active': true, 'red': isRed }"></div>

(2) 动态class(数组形式):

拓展: <div :class="['is-active', isRed && 'red' ]"></div>

(3) 动态style对象(其中的css属性必须使用驼峰命名):

<div :style="{ color: textColor, fontSize: '18px' }"></div>

(4) 动态style数组(使用多个对象包裹,对象之间的css属性使用逗号隔开):

<div :style="[{ color: textColor, fontSize: '18px' }, { fontWeight: '300' }]"></div>

g. 如何处理非响应式数据?

  在我们的Vue开发中,会有一些数据,从始至终都未曾改变过,这种死数据,既然不改变,那也就不需要对他做响应式处理了,不然只会做一些无用功消耗性能,比如一些写死的下拉框,写死的表格数据,这些数据量大的死数据,如果都进行响应式处理,那会消耗大量性能。

// 方法一:将数据定义在data之外
data () {
    `拓展:这种非响应式的数据可以直接定义,包括在methods方法里。
     但是这类数据如果在template中使用,后续也使用$set动态更新,虽然数据会发生变化,但是视图不变,
     因为并未挂载到data函数中的return对象中,无法收集依赖。
    `
    this.list1 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list2 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list3 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list4 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list5 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    return {}
}

// 方法二:Object.freeze()
data () {
    return {
        list1: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list2: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list3: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list4: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list5: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
    }
}

h. 父子组件的生命周期?

  挂载阶段: 父beforeCreate >父created > 父beforeMount > 子beforeCreate >子created> 子beforeMount > 子mounted >父mounted

  扩展:

  子组件更新过程: 父beforeUpdate > 子beforeUpdate > 子updated > 父updated

  父组件更新过程(影响到子组件): 父beforeUpdate > 子beforeUpdate > 子updated > 父updated

  父组件更新过程(不影响子组件): 父beforeUpdate > 父updated

  销毁过程: 父beforeDestroy > 子beforeDestroy > 子destroyed > 父destroyed

h. Vue采用的是异步更新的策略,通俗点说就是,同一事件循环内多次修改,会统一进行一次视图更新,这样才会节省性能。

i. 关于props的自定义校验

props: {
    num: {
        default: 1,
        `// type:Number, 指定props的类型`
        `// required: true, 指定props是否为必填项`
        validator: function (value) {
            // 返回值为false则验证不通过,报错
            return [1, 2, 3, 4, 5].indexOf(value) !== -1
        }
    }
}

`拓展:`

`如果props没有默认值,一般的写法是:`
props: {
    num: Number
}

`如果props有默认值,一般的写法是:`
props: {
    num: {
        type: Number,
        default: 1
    }
}

`特别地,如果需要给对象或者数组指定默认值,一般的写法是:`
props: {
    selectList: {
        type: Array,
        default: () => []
    }
}

默认值使用`函数的形式`返回的原因,其实和`data为什么需要返回一个函数`大同小异。
多个`父组件`引用`同一个子组件`时,引用类型`props``相互隔离``如果需要为props的类型指定多个值,可以使用数组包裹多种类型`
props: {
    selectList: {
        type: [Array, Object]
    }
}

i. 使用this.$options.data()获取vue文件data函数返回的初始对象状态

  vue实例属性$options是一个对象, 可以调用vue各个组件下的方法和数据。即new vue({})大括号内的东西,统称为$options。因此,如果我们需要在模态框关闭时,初始化模态框中的数据, 可以使用如下代码:

`扩展:`

onClose() {
  const data = this.$options.data()
  Object.keys(data).map((prop) => {
    this[prop] = data[prop]
  })
}
面试不面试,你都必须得掌握的vue知识

j. 自定义v-model

  默认情况下,v-model@input 事件侦听器:value 属性上的语法糖。但是,你可以在你的Vue组件中指定一个模型属性来定义使用什么事件value属性——非常棒!

export default: {
  model: {
    event: 'change',
    prop: 'checked'  
  }
}

k. 给组件绑定动态key值,当key值变化时,可以刷新组件,重新走组件的生命周期

l. 动态指令和动态参数的使用

`拓展: 可以理解为,在template中使用[]传递动态参数,
       用 @[响应式变量名] 实现动态自定义事件, 使用 :[响应式变量名] 实现动态props
`

<template>
    ...
    <aButton @[someEvent]="handleSomeEvent()" :[someProps]="1000" />...
</template>
<script>
  ...
  data(){
    return{
      ...
      someEvent: someCondition ? "click" : "dbclick",
      someProps: someCondition ? "num" : "price"
    }
  },
  methods: {
    handleSomeEvent(){
      // handle some event
    }
  }  
</script>

m. (拓展) hook和$once的使用

`1. $once的介绍:

(1) $once是一个函数,可以为Vue组件实例绑定一个自定义事件,但该事件只能被触发一次,触发之后随即被移除。
(2) $once有两个参数,第一个参数为字符串类型,用来指定绑定的事件名称,第二个参数设置事件的回调函数。
(3) $once可以多次为同一个事件绑定多个回调,触发时,回调函数按照绑定顺序依次执行。
(4) once可以作为修饰符,.once只会触发一次 `

<template>
  <div>
    <button @click="$emit('clickHander')">按钮</button>
  </div>
</template>
 
<script>
export default {
  mounted() {
    `在按钮第一次点击时,会先后调用两次回调函数`
    `此后再点击按钮,不会触发回调函数`
    this.$once('clickHander', () => {
      console.log('第一次:该事件只能够被触发一次,触发后立刻被移除');
    });
    this.$once('clickHander', () => {
      console.log('第二次:该事件只能够被触发一次,触发后立刻被移除');
    });
  }
}
</script>

`2. 使用$once清除定时器:`

通常的代码:`使用这种方式会多定义一个响应式变量timer,而且需要分别在两个生命周期里定义定时器以及清除定时器。`

export default{
  data(){
    timer: null  
  },
  mounted(){
      this.timer = setInterval(() => {
      //具体执行内容
      console.log('1')
    },1000)
  }
  beforeDestory(){
    clearInterval(this.timer)
    this.timer = null
  }
}

`使用$once的方法解决问题会更加优雅:`
export default{
  mounted(){
    let timer = setInterval(() => {
      //具体执行内容
      console.log('1')
    },1000);
    this.$once('hook:beforeDestroy',() => { 
      clearInterval(timer) 
      timer = null
    })
  }
}

`3. 使用监听生命周期钩子的hook进行父子组件之间的事件传递:`

通常的代码:

//父组件
<rl-child @childMounted="childMountedHandle"
/>
method () {
  childMountedHandle() {
  // do something...
  }
},

// 子组件
mounted () {
  this.$emit('childMounted')
},

`使用hook写出的优雅代码:`

//父组件
<rl-child @hook:mounted="childMountedHandle"
/>
method () {
  childMountedHandle() {
  // do something...
  }
}

n. (拓展) v-for的使用 ( 非必须,指定key值方便diff算法新旧虚拟dom进行比较,提升效率)

1. `v-for在数组中的遍历(item 是数组的每一项,index 对应数组的索引,同时支持解构)`
<div v-for="({name, id}, index) in cityArr" :key="id"> </div>

2. `v-for在对象中的遍历(value, key, index分别对应值、键、索引)`
<div v-for="(value, key, index) in form" :key="key"> </div>

3. `v-for迭代数字(从1开始打印,1 2 3 4)`
<div v-for="count in 4" :key="count"> </div>

4. `v-for迭代数字(输出每一个字符)`
<div v-for="str in 'hello'"> {{ str }} </li>

o. (拓展) vue2组件为什么只能有一个根节点?

`结论`: `vue2`组件只能有一个根节点,但是在`vue3组件`中,可以有`多个根节点``原因:` 

(1) `vue2``虚拟dom`是一颗`单根树形结构``patch方法`在遍历的时候`从根节点开始遍历和比较`,
它要求组件只有一个`根节点`,组件会转换为一个`虚拟dom`。

(2) `vue3`引入了`Fragment`的概念。这是一个抽象的节点,如果发现组件有多个根,就创建一个`Fragment节点`,
把`多个根节点`作为它的`children`

p. (拓展) 使用vue.config全局配置,在开发阶段获取组件的错误信息

`vue.config是一个对象,包含vue的很多全局配置,这里不一一展开介绍,有兴趣的朋友请移步vue官网`
`不过vue已经对组件名称进行处理,我们并不清楚具体指代,所以这个扩展我们看看就好,知道就行,最好的是sentry配置`

.在`main.js`中进行`全局配置`:

Vue.config.errorHandler = function(err, vm, info) {
  console.log(`组件${vm.$vnode.tag}发生错误:${err.message},${info}`)
}

      面试不面试,你都必须得掌握的vue知识

`sentry上注册一个账号,并创建一个vue项目`

1.`在main.js中,引入配置sentry的index文件, 获取对应的dsn地址`
import '@/sentry'

2.`index.js配置(参照官网)`

import Vue from 'vue'
import router from '../router'
import * as Sentry from '@sentry/vue'
import { BrowserTracing } from '@sentry/tracing'

const needSentry = ['pro', 'localdev', 'pet', 'test', 'sit'].includes(process.env.VUE_APP_ENV_STAGE)
const isProduction = process.env.NODE_ENV === 'production'
if (needSentry && isProduction) {
  Sentry.init({
    Vue,
    dsn: '请填写sentry对应的dsn地址',
    integrations: [
      new BrowserTracing({
        routingInstrumentation: Sentry.vueRouterInstrumentation(router),
        tracingOrigins: [process.env.VUE_APP_BASE_URL]
      })
    ],
    beforeSend(event, hint) {
      const error = hint.originalException
      Sentry.configureScope(function (scope) {
        scope.setLevel('error')
      })
      if (
        error &&
        error.message &&
        error.detailMessage
      ) {
        event.exception.values[0].value = `message: ${error.message}\n detailMessage: ${error.detailMessage}`
      }
      return event
    },
    tracesSampleRate: 1.0
  })
}

3.vue2.x的底层原理(vue刺客)

a. vue2.x在初始化的过程中做了什么事?

`原文传送门:https://blog.csdn.net/weixin_37517329/article/details/121861153`

`总结:`
1. `选项合并`,处理组件的配置内容,将`传入的options``构造函数本身的options`进行合并
(`用户选项和系统默认的选项进行合并`,与本文第一点:`vue常用知识点总结`中的`第k点`中的`merge`类似)
(初始化做了一些`性能优化`,将组件配置对象上的一些深层次属性放到`vm.$options`选项中,以提高代码的执行效率)
2. `初始化组件实例的关系属性`,如`$parent``$children``$root``$refs`3.`初始化自定义组件事件的监听`, 若存在父监听事件, 则添加到该实例上
4. 初始化`render渲染`所需的`slots``渲染函数`等。
(其实就两件事:`插槽的处理``$createElm的声明`,也就是`render函数中的h的声明`)
5. `调用beforeCreate钩子函数`,在这里就能看出一个组件在创建前和后分别做了哪些初始化
6. `初始化注入数据`,隔代传参时先`inject`。
(作为一个组件,在要给后辈组件提供数据之前,需要先把`祖辈传下来的数据注入`进来)
7.`props`,`methods`,`data`,`computed`,`watch`进行初始化,包括`响应式的处理`
8.`把祖辈传下来的数据注入`进来以后, 再`初始化provide`
9. 调用`created`钩子函数,初始化完成后,就可以执行挂载,进入挂载阶段

b. vue2.x在挂载的过程中做了什么?

c. vue2.x的响应式原理?

  (1) vue2.x重写了数组的部分原生方法:

    响应式方法(会改变原数组,简略复习下js)

  • unshift:数组的头部增加一条记录数据, 返回值是增加的数据,会改变原数组
var a = [1, 2, 3]
var b = a.unshift(4)
a  `结果是 [4, 1, 2, 3]`
b  `结果是4`
  • push:数组的末尾增加一条记录数据, 返回值是增加的数据,会改变原数组
var a = [1, 2, 3]
var b = a.push(4)
a   `结果是[1, 2, 3, 4]`
b   `结果是4`
  • shift: 删除数组中的第一条记录, 返回值是删除的记录,会改变原数组
var a = [1, 2, 3]
var b = a.shift()
a   `结果是[2, 3]`
b   `结果是1`
  • pop: 删除数组中的最后一条记录, 返回值是删除的记录,会改变原数组
var a = [1, 2, 3]
var b = a.pop()
a   `结果是[1, 2]`
b   `结果是3`
  • sort: 排序方法,会改变原数组
`a. 如果未指定函数,则默认根据元素按照转换为的字符串的各个字符的Unicode位点进行排序:`

var arr = [12, 13, 24, 46, 49, 32, 34]
arr.sort()  // (7) [12, 13, 24, 32, 34, 46, 49]
['Javascript','Vue','React','Node','Webpack'].sort()  // 

`b. 指定函数的情况下:`

如果想按照其他标准进行排序,就需提供比较函数compareFunction(a,b),数组会按照调用该函数的返回值排序,
即a和b是两个将要比较的元素:
1.如果compareFunction(a,b)小于0,则a排列到b之前;
2.如果 compareFunction(a, b)等于0,a和b的相对位置不变(并不保证);
3.如果 compareFunction(a, b)大于0,b排列到a之前;

let Users = [
  {name:'鸣人', age:16},
  {name:'卡卡西', age:28},
  {name:'自来也', age:50},
  {name:'佐助', age:17}
];
Users.sort((a, b) => {
   return a.age - b.age
})

// => 鸣人、佐助、卡卡西、自来也的对象数组
  • reverse:原数组进行翻转,会改变原数组。因为原数组的引用地址未发生变化,所以原数组翻转后的数组结果一样。
var a = [1,2,3,4]
var b = a.reverse()
a   `结果是[4, 3, 2, 1]`
a === b   `true, 因为翻转后的原数组和原数组都指向用一个内存地址`
  • splice:数组添加或者删除元素, 返回删除的元素,会改变原数组
`参数说明:
   (1) 第一个参数为插入元素或者删除元素的位置(从0开始)
   (2) 第二个参数为要删除的元素数量
   (3) (从删除的位置开始)后面的参数都会依次添加到数组中
`

const arr1 = [1, 2, 3, 4, 5]
const arr2 = [1, 2, 3, 4, 5]
const arr3 = [1, 2, 3, 4, 5]


const a = arr1.splice(0, 2)   `输出: [1, 2]; 原数组: [3, 4, 5]` 
const b = arr2.splice(3)     `输出: [4, 5]; 原数组: [1, 2, 3]`
const c = arr3.splice(3, 1, "a", "b", "c")   `输出: [4]; 原数组: [1, 2, 3, "a", "b", "c", 5]`
import { def } from './utils'

const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)
const methodsNeedChange = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
`重写数组原型链上的方法,先将数组转换为响应式数组,触发watcher监听,并返回数组方法的执行结果`
methodsNeedChange.forEach(methodName => {
  const original = arrayPrototype[methodName]
  def(
    arrayMethods,
    methodName,
    function () {
      const result = original.apply(this, arguments)
      const arr = [...arguments]
      const ob = this['__ob__']
      let inserted = []
      switch (methodName) {
        `对可能会增加数组索引的操作,找到新增的数据,并重新手动Observer`
        case 'push':
        case 'unshift':
          inserted = arr
          break
        `splice插入的数据是第三个参数,找到新增的数据, 并重新手动Observer`
        case 'splice':
          inserted = arr.slice(2)
          break
      }
      `判断是因为inserted可能为空数组,只有数组有值才手动Observer`
      if (inserted.length) {
        ob.observeArray(inserted)
      }
      `重点:每次使用数组的响应式方法,都会触发watcher监听,更新数组的值,并在vue的下一个周期渲染dom。`
      ob.dep.notify()
      return result
    },
    false
  )
})

    非响应式方法举例(不会改变原数组,简略复习下js)

  • slice: 用于字符串或者数组截取
`参数说明:
   (1) 第一个参数为截取的起始位置(从0开始,包含)
   (2) 第二个参数为截取的结束位置(不包含,如果第二个参数不取值,则从起始位置截取到数组末尾)
`

const array = [1, 2, 3, 4, 5]

const array1 = array.slice(0, 2)   `输出: [1, 2]`
const array2 = array.slice(2, 3)   `输出: [3]`
const array3 = array.slice(3) 	   `输出: [4, 5]`
const array4 = array.slice(-4, -3) `输出: [2], 从倒数第四个取到倒数第三个,但不包含倒数第三个`
const array4 = array.slice(-3, -4) `输出: [], 只能从前往后取,不能从后往前取,故为空数组`

`特别说明: 
   因为slice是非响应式方法,切记不能直接对响应式数组变量使用slice方法,页面无法获取最新的数据
`

`举个栗子,对于定义在data中的响应式变量arr:`
   this.arr.slice(0, 2)  
   
`这种做法是不可取的,无法达到响应式的效果,但是可以通过定义一个中间引用类型变量去接收,并将地址重新赋值给响应式变量`
  const tempArr = this.arr.slice(0, 2) 
  this.arr = tempArr
  
`这样就可以达到响应式效果了`

  (2)非响应式操作情况:

  • 对于数组:为空数组新增索引并赋值,比如为第一项新增name字段,并赋值为cxk(可以使用this.$set(this.list[0], 'name', 'cxk')实现响应式)。
  • 对于对象:直接给对象新增key并赋值(可以使用类似this.$set(this.person, 'name', 'cxk')的写法实现响应式)或者直接使用delete删除已有的key, 都无法达到响应式的效果(结果是key对应的value还是显示在网页上,更不会因为后续value的变化而变化。 可以使用vue内置api属性$delete就可以在页面上删除已有的value,如this.$delete(this.person, 'name'))。

Tips:当然,对于第一种情况,我们也可以使用重写的splice方法实现响应式:this.idList.splice(this.currentIndex, 1, this.modifiedId)

给对象新增key并赋值的vue案例说明:

  1. 假定一个vue文件的结构:
<template>
  <div class="app-container">
    {{ obj.name }}
    {{ obj.age }}
    <el-button type="primary" size="mini" @click="clickHandler">change</el-button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      obj: {
        name: 'cxk'
      }
    }
  },
  
  methods: {
    clickHandler() {
      this.obj.age = 20
    }
  }
}
</script>
  1. 如果我们使用created钩子函数给obj添加age字段
created() {
  `由于在created生命周期中,页面dom还未渲染完毕,新增字段后,在mounted生命周期渲染dom,所以初始化能显示age的值`
  `此时如果点击按钮,虽然obj中的age发生了变化,但由于这种方法不是响应式,所以能获取但无法渲染最新的数据`
  this.obj.age = 18
  `用vue内置的api方法$set可以达到响应式的效果,在点击按钮后,能获取并渲染最新的数据`
  this.$set(this.obj, 'age', 18)
}
  1. 如果我们使用mounted钩子函数给obj添加age字段
`注意与created钩子的区别`

mounted() {
  `页面初始化就能获取但无法渲染age的值,这是因为mounted生命周期已经完成dom的渲染,此时再新增字段,由于不是响应式`
  `所以无法在页面初始化时,通知页面重新实时渲染age的值,同样由于非响应式的特点,在点击按钮时也无法渲染最新的age值`
  this.obj.age = 18
  `而使用$set则可以达到响应式的效果`
  this.$set(this.obj, 'age', 18)
}

给对象重新分配地址,也是响应式的:

`页面初始化时,name和age字段并没有显示。而在点击按钮,给对象重新分配地址后,此时页面就能正常显示name和age字段`

<template>
  <div class="app-container">
    {{ obj.name }}
    {{ obj.age }}
    <el-button type="primary" @click="clickHandler">change</el-button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      obj: {}
    }
  },

  methods: {
    clickHandler() {
      this.obj = {
        name: 'test',
        age: 20
      }
    }
  }
}
</script>

关于$set的补充:$set无法对对象上已经存在的key做响应式处理

<template>
  <div class="app-container">
    <div class="mb10">{{ obj.sex }}</div>
    <el-button size="small" type="primary" @click="changeHandler">change</el-button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      obj: {
        name: 'cxk'
      }
    }
  },

  mounted() {
    `直接给对象增加sex属性是非响应式的`
    this.obj.sex = 'male'
  },

  methods: {
    changeHandler() {
      `$set无法对对象上已经存在的key做响应式处理`
      this.$set(this.obj, 'sex', 'female')
      `此时obj虽然发生了变化,但无法在dom上响应式变化`
      `但是,如果我们在mounted阶段,去掉给对象增加sex属性的操作,再用$set增加字段,是可以达到响应式效果的`
      `为什么vue要做判断对象上是否已存在key值的判断呢?这是为了提高vue性能所作出的举措`
      `只要这个key已经存在在对象上,则默认vue已经收集过了,已经双向绑定过了,无需再次收集`
      console.log("this.obj", this.obj)
    }
  }
}
</script>

  (3) 个人理解vue2.x的响应式原理:

  在datareturn的变量都是响应式变量。一个组件a在template模板中(在js代码中使用到的data中return的变量,也是响应式变量,只是变化后不会更新dom)使用到这个定义好的响应式变量(简而言之,data中return的变量如果有在template模板中使用,就会收集依赖。特别注意,在js中对未定义在template中的变量进行修改,是不会触发依赖收集的),就会收集依赖。那什么是依赖呢?打印data中定义的变量,会展示subssubs下对应的watcher,这个watcher就是依赖。一个vue组件a如果在template中使用到data中定义的变量val,那么val就会收集到一个本组件的依赖watcher。而在a组件的子组件及孙子组件中,如果有n个组件在template模板中,有使用到a组件定义好的响应式变量val,就会收集到nwatcher对象。换句话说,watcher是和组件挂钩的,每个组件有且只有一个对应的全局渲染watcher去监听。

  • 当组件a中在template中使用的val发生变化,就会触发set方法,刷新a组件。同时通知a组件的后代组件的template模板中有使用到val的watcher对象,包含template、computed以及watch里面的watcher, 执行update方法来更新组件的dom
  • 当在a的后代组件b中,template方法有使用到的val发生改变时,就会触发set方法,刷新a组件及b组件。

  (4) vue2vue3响应式原理的对比:

  vue2响应式底层原理使用的是Object.defineProperty

面试不面试,你都必须得掌握的vue知识

  • vue2无法实现响应式的情况和原因分析:

  1. 为对象新增属性不能实现响应式的原因其实很简单:因为vue2的响应式是针对属性进行劫持的,这一操作在vue2的初始化阶段就已经完成。如果需要给对象新增的属性也实现响应式效果,需要手动observer

  2. 使用delete删除对象属性不能实现响应式的原因同样也很简单:因为这一操作并没有触发definePropertyset方法,所以同样不会实现响应式。

  3. 使用Object.defineProperty可以监控到数组中存在的key对应value的变化,因为对于数组来说,key就是索引,value就是数组对应索引的值。而对于为数组新增的索引值,该方法是无法监测到的,除非再次手动observe

Tips: 重新为data中定义的响应式对象分配新的内存地址,也会达到响应式的效果,这是因为触发了响应式对象的setter方法。

  • vue2响应式的缺点分析:

  1. vue2初始化时的递归遍历会造成性能损失;

  2. 新增或删除属性需要使用$set或者$delete这些vue内置的api才能达到响应式效果;

  3. 响应式效果不适用于es6新增的MapSet这些数据结构;

`简单实现下vue2的响应式原理:`

`1. 依赖收集方法:`

function defineReactive(obj, key, value) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`获取了${key}属性`)
      return value
    },
    
    set(val) {
      if (value === val) return
      console.log(`${key}属性的值被设置为${val}`)
      value = val
    }
  })
}

`递归响应式方法:`

function observer(obj) { 
  for (const [key, value] of Object.entries(obj)) {
    defineReactive(obj, key, value)
    if (typeof value === 'object' && value !== null) observer(value)
  }
}
  
`2. 对于对象:`

const obj = { name: 'cxk', age: 18, info: { sex: 'male' } }

observer(obj)

obj.name  // 获取了name属性 'cxk'
obj.name = 'cxk1'  // name属性的值被设置为cxk1 'cxk1'
obj.sex = 'male'  // male,此时并没有触发set方法

`如果我们对更新后的obj, 手动observe。此时再获取sex的值,是会触发set方法的`

`此时只会提示: 获取了name属性 获取了age属性。这是因为:我们开始支队obj的name和age属性挂载了get和set方法`
observer(obj) // 手动observer

obj.sex  // 获取了sex属性 'male'
obj.sex = 'female' // sex属性的值被设置为female 'female'

obj.info `打印:获取了info属性,并显示对应的value`
obj.info.sex `打印:获取了info属性 获取了sex属性 并显示对应的value` // 此处触发了两次get方法
obj.info.hobby = 'rap'   `此处只打印获取了info属性,触发了一次set方法,因为info是响应式而非新增的hobby属性`
obj.info.hobby  `此处同样只打印获取了info属性,触发一次get方法,因为info是响应式而非新增的hobby属性`

`3. 对于数组:`

const arr = [1, 2, 3]

observe(arr)

arr[0]  // 获取了0属性 1
arr[2]  // 获取了2属性 3
arr[0] = 4 // 0属性的值被设置为4 4
arr[0] // 获取了0属性 4
arr[3] = 5 // 5 同样可以发现未触发set方法,用push方法也同样不会触发,因为新增了属性
delete arr[0] // true, 删除成功返回true,此时同样未触发set方法
arr[0] // undefined, arr[0]此时已不再是响应式

`
defineProperty监测数组下标变化的情况总结:
  a. 对于存在的索引,通过索引访问或者设置对应元素的值时,可以触发getter和setter方法;
  b. 通过原生数组的push或unshift方法会为数组增加索引。对于新增的索引,需要手动observe才能触发getter和setter方法;
  c. 通过原生数组的pop或shift删除元素,会删除并更新索引,对于存在的索引,也可以触发getter和setter方法;
`

  vue3是直接通过Proxy代理目标对象,且代理的是最外层的对象,可以监听到新增的属性,性能自然会更好。

面试不面试,你都必须得掌握的vue知识

`使用递归方法封装简单实现下vue3的响应式原理:`

`考虑到对象中可能嵌套对象属性的情况,获取值的时候,如果值是对象类型,则继续获取,直到它不再为对象为止`
`所以递归的出口为不是对象类型,且不为null,并返回对应的值`

function observer(target) {
  `递归的出口`
  if (typeof target !== 'object' || target == null) return target 
  
  `配置代理`
  const proxy = {
    get(target, key, receiver) {
      `Reflect.get中的第一个参数为源对象,第二个参数为源对象的key,第三个参数可以省略,为当前this的指向`
      `在这个方法中,receiver参数代表代理后的对象,其实源对象和代理后的对象指向是一样的,那为什么不省略呢`
      `这是因为,源对象可能也是另一个代理的代理对象,为了避免污染,就将this指向到代理后的对象,及receiver上`
      const result = Reflect.get(target, key, receiver)
      console.log(`获取了${key}属性`)
      `深度代理,递归循环,直到value不再为对象类型且不为null就返回`
      return observer(result)
    },

    set(target, key, val, receiver) {
      if (val === target[key]) return
      console.log(`设置了${key}属性,值为${val}`)
      return Reflect.set(target, key, val, receiver)
    },

    deleteProperty(target, key) {
      console.log('delete property', key)
      return Reflect.deleteProperty(target, key)
    }
  }

  // 生成代理对象
  return new Proxy(target, proxy)
}

面试不面试,你都必须得掌握的vue知识

  可以发现,使用proxy作为响应式原理的实现方法,可以监测到新增的属性,这是defineProperty所不具备的特性。与此同时,我们还可以在代理中对传入的数据做一系列譬如删除之类的拦截操作

  (5) vue2.x底层响应式原理解析(从控制台打印中get响应式原理):

  • vue框架MVVM模式的理解:

面试不面试,你都必须得掌握的vue知识

面试不面试,你都必须得掌握的vue知识

面试不面试,你都必须得掌握的vue知识

  总结:MVVM框架下视图和模型是不能直接通信的,但是它们可以通过ViewModel来通信, 而MVVM中的ViewViewModel却可以互相通信。ViewModel要实现一个Observer观察者,当数据发生变化,ViewModel能够监听到数据的这种变化,然后通知到对应的视图做自动更新;而当用户操作视图,ViewModel也能监听到视图的变化,然后通知数据做改动,这实际上就实现了数据的双向绑定。

  • 视图变化,对应数据会随之变化的效果是通过事件监听的方式实现的。在此重点讨论数据变化是怎么引起视图的变化:
  1. 实现一个监听器 Observer ,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者;
  2. 实现一个订阅器 Dep,用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理;
  3. 实现一个订阅者 Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图;
  4. 实现一个解析器 Compile,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化。

面试不面试,你都必须得掌握的vue知识

  • 从打印截图上的整体概览:
`index.vue 父vue文件`

<template>
  <div class="app-container">
    {{ obj.name }}
    {{ obj1.name }} 
    {{ obj.hobby }} 
    {{ finalName }}
    <test :data="obj1" />
  </div>
</template>

<script>
import test from './module/test'

export default {
  components: { test },
  mounted() {
    console.dir(this)
  },

  data() {
    return {
      name: 'cxk',
      obj: {
        name: 'cxk',
        age: 18,
        hobby: {
          hobbyName: 'rap'
        }
      },
      obj1: {
        name: '1',
        sex: 'male'
      }
    }
  },

  watch: {
    'obj.name': {
      handler(newVal, oldVal) {},
      immediate: true,
      deep: true
    }
  },

  computed: {
    finalName({ obj1, obj }) {
      return  obj.name + obj1.name
    }
  }
}
</script>
`test.vue 子vue文件`

<template>
  <div>{{ data }}</div>
</template>

<script>
export default {
  props: {
    data: Object
  }
}
</script>

<style lang="scss" scoped></style>

面试不面试,你都必须得掌握的vue知识

  在上述截图中,忘解释了全局渲染watcher为什么会有9个deps,在此给出文字说明:template模板上挂载了obj.name(2 + 1 = 3)、obj1.name(2 + 1 = 3)以及obj.hobby(obj之前已经收集过了,不会再次收集。所以,会收集obj下的hobby对象。hobby对象占2个,hobbyName占1个,2 + 1 = 3)。所以,最终会收集到3 + 3 + 3 = 9个依赖。

扩展tips:

1. 只要watch方法有监听data中定义的响应式变量(哪怕这个变量未挂载到template模板),也同样会在全局watchers和监听响应式变量的Observer中观察到监听watcher这类watcher

2. 如果定义的计算属性未挂载到template模板上,虽然在全局watchers上还是会存在这类计算属性watcher,但是与之依赖关联的变量是不会挂载这一类计算属性的。

3. 单独一个计算属性或者单独一个watch方法对应单独的一个watcher,并挂载到全局watchers中。

4. 如何理解watcherdep互相收集?

a. 从截图可以看出:Observes下的dep.subs存放的其实是这个属性的所有watcher

b. 从截图可以看出:dep.subs中的每个watcher都存放了这个watcher下收集到的所有dep实例

5. vue设计思想用到了那些设计模式?

发布订阅模式又叫观察者模式,它定义对象之间一种一对多的依赖关系。当一个对象的状态改变时,所有依赖于它的对象都将得到通知。在vue中,将所有依赖收集起来作为订阅者。数据变化就通知所有依赖者,更新dom渲染页面,而数据的变化这个过程就称为发布者

6. 为什么一个对象收集两次依赖?

这是因为对象本身收集一次,对象上的__ob__(即对象上的Observer也收集一次)

面试不面试,你都必须得掌握的vue知识

  • 监听器Observer

  Observer,是通过在Object.defineProperty方法上定义getset方法,循环遍历响应式数据的每一个属性,使响应式数据对象变得可观测。简单来说,就是我们之前封装的Observer方法:

function defineReactive(obj, key, value) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`获取了${key}属性`)
      return value
    },
    
    set(val) {
      if (value === val) return
      console.log(`${key}属性的值被设置为${val}`)
      value = val
    }
  })
}

`递归响应式方法:`

function observer(obj) { 
  for (const [key, value] of Object.entries(obj)) {
    defineReactive(obj, key, value)
    if (typeof value === 'object' && value !== null) observer(value)
  }
}
  • 订阅器Dep

  我们需要创建一个依赖收集容器,也就是消息订阅器Dep,用来容纳所有的订阅者。订阅器Dep主要负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数。

function Dep () {
    this.subs = [];
}

Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        })
    }
};
Dep.target = null;  // 全局变量,防止重复push相同的watcher

  和Observer监听器结合到一起,重构下来就是:

function defineReactive(obj, key, value) {
  var dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      console.log(`获取了${key}属性`)
      `dep.subs收集watcher`
      if(Dep.target) dep.addSub(Dep.target)
      return value
    },
    
    set(val) {
      if (value === val) return
      console.log(`${key}属性的值被设置为${val}`)
      value = val
      `通知watcher执行notify方法`
      dep.notify()
    }
  })
}

`递归响应式方法:`

function observer(obj) { 
  for (const [key, value] of Object.entries(obj)) {
    defineReactive(obj, key, value)
    if (typeof value === 'object' && value !== null) observer(value)
  }
}
  • 订阅者Watcher
function Watcher(vm, exp, cb) {
    ```
    vm:Vue的实例对象;
    exp:是node节点v-model等指令的属性值 或者插值符号中的属性。如:v-model="name", exp 就是name;
    cb:是 Watcher 绑定的更新函数;
    ```
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    this.value = this.get();  // 将自己添加到订阅器中
}

Watcher.prototype = {
    update: function() {
        this.run();
    },
    
    run: function() {
        `获取挂载到data上的响应式变量的值`
        var value = this.vm.data[this.exp];
        var oldVal = this.value;
        `如果值发生变化,就执行方法,更新dom`
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);  // 调用更新函数重新渲染dom
        }
    },
    
    get: function() {
        Dep.target = this; // 将watcher收集到dep.subs中
        let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数,触发get方法
        Dep.target = null; // watcher收集完使用到的响应式变量后,释放Dep.target,防止重复push
        return value;
    }
}
  • 解析器Compile

  解析模板指令,替换模板数据,初始化视图。将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器Dep

compileText: function(node, exp) {
    var self = this;
    `获取data中响应式变量的值:`
    var initText = this.vm[exp];  
    `更新dom节点中文本的值:`
    this.updateText(node, initText);  
    `将这个指令初始化为一个订阅者,后续变量改变时,就会触发这个更新回调,从而更新视图`
    new Watcher(this.vm, exp, function (value) { 
        self.updateText(node, value);
    });
}

初始化:

1.`Observer`对数据进行响应式绑定;
2.`Compiler`编译解析模块指令,初始化渲染页面,将每个指令的节点绑上更新函数,并实例化监听监听数据的订阅者`Watcher`;
3.数据`getter`时,执行对应数据的`dep`收集所有`watcher`依赖;

更新:

1.更新时触发`dep.notify()`,派发通知所有订阅者`watcher`;
2.订阅者`watcher`执行`update()`回调函数;
3.调用对应`Compiler`编译解析模块,重新更新视图;

d. vue2.x的diff算法?

结语

  就这样吧~ 以上内容是对学习vue2.x框架的一个小结。最近比较忙,后续如果有空会学习下react