Vue100问(第6-17问)

412 阅读9分钟

大家好,我是阿林。

在本系列文章中,将以提问和解答的方式,和大家一起熟练掌握vue开发,共同进步,欢迎留言区讨论~

问题

第6问

为什么 vue 中的 data 必须是个函数?

第7问

v-if 和 v-show 有什么区别?

第8问

v-if 和 v-for 为什么不能一起使用?

第9问

给对象新增或删除属性,Vue 能检测到变化吗?

第10问

不需要响应式的数据应该怎么处理?

第11问

直接给一个数组项赋值,Vue 能检测到变化吗?

第12问

vue触发数组更新的方法有哪些?

第13问

用 computed 返回值和用 method 返回值有什么区别?

第14问

computed 可以传参吗?

第15问

讲一下 computed 和 watch 分别的使用场景?

第16问

watch 可以监听 computed 变化吗?

第17问

watch 的 immediate 和 deep 是干什么用的?

解答

6.为什么 vue 中的 data 必须是个函数?

因为这样才能保证组件数据的独立性和可复用性

vue 文档中其实说明了这个问题,下面的例子摘自 vue 文档。

当我们定义一个 <button-counter> 组件时,你可能会发现它的 data 并不是像这样直接提供一个对象:

data: { 
  count: 0 
}

一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝:

data: function () {
  return {
    count: 0
  }
}

如果 Vue 没有这条规则,点击一个按钮就可能会像如下代码一样影响到其它所有实例

这个问题的本质原因是,data是一个对象,对象是引用类型,引用类型的传值都指向同一内存地址,就会导致组件复用的时候,也共享同一个data。

function MyButton() {}

MyButton.prototype.data = {
  count: 10
}

const buttonA = new MyButton()
const buttonB = new MyButton()

buttonA.data.count = 20

console.log(buttonA.data.count)   // 20
console.log(buttonB.data.count)   // 20

上面的代码展示的就是把data写成对象的情况,更改一处,另一处也跟着被修改。

而如果data是一个函数,返回一个对象,就不一样了。

function MyButton() {
  this.data = this.data()
}

MyButton.prototype.data = function () {
  return {
    count: 10
  }
}

const buttonA = new MyButton()
const buttonB = new MyButton()

buttonA.data.count = 20

console.log(buttonA.data.count)  // 20
console.log(buttonB.data.count)  // 10

这样两个组件的data值分别保存在闭包中,是分隔开的,不会互相影响。

7.v-if 和 v-show 有什么区别?

v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。

v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换(display:none)。

一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。

因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。

8.v-if 和 v-for 为什么不能一起使用?

当 Vue 处理指令时,v-for 比 v-if 具有更高的优先级,如果把 v-for 和 v-if写在同一元素上,例如:

<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>

将会经过如下运算:

this.users.map(function (user) {
  if (user.isActive) {
    return user.name
  }
})

假设 users 数组有 10000 项,但只有一个 user 是 isActive,也会遍历整个 users 数组。

这样对性能是极不友好的。

通过将其更换为在如下的一个计算属性上遍历:

<ul>
  <li
    v-for="user in activeUsers"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>

computed: {
  activeUsers: function () {
    return this.users.filter(function (user) {
      return user.isActive
    })
  }
}

我们将会获得如下好处:

  • 过滤后的列表会在 users 数组发生相关变化时才被重新运算,过滤更高效。
  • 使用 v-for="user in activeUsers" 之后,我们在渲染的时候遍历活跃用户,渲染更高效。
  • 解耦渲染层的逻辑,可维护性 (对逻辑的更改和扩展) 更强。

为了获得同样的好处,我们也可以把:

<ul>
  <li
    v-for="user in users"
    v-if="shouldShowUsers"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>

更新为:

<ul v-if="shouldShowUsers">
  <li
    v-for="user in users"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>

通过将 v-if 移动到容器元素,我们不会再对列表中的每个用户检查 shouldShowUsers。取而代之的是,我们只检查它一次,且不会在 shouldShowUsers 为否的时候运算 v-for

9.给对象新增或删除属性,Vue 能检测到变化吗?

不能。

<p>{{ obj.name }}</p>
<p>{{ obj.age }}</p>
<button @click="handleObjChange">change this.obj</button>

data() {
  return {
    obj: {
      name: 'wang'
    }
  }
}

handleObjChange() {
  delete this.obj.name   // 视图不会更新
  this.obj.age = 25      // 视图不会更新
  this.$set(this.obj, 'age', 25)    // 视图更新
  this.$delete(this.obj, 'name')    // 视图更新
}

image.png

打开 vue 开发者工具发现,实际上对 obj 的操作是生效了的,name 属性被删除,age 属性被赋值为 25。

只是 vue 响应式的核心 API Object.defineProperty 监听不到给对象添加新属性和删除属性,所以视图没更新。

所以 vue 才会有 Vue.setVue.delete 这两个全局 API。

  • 对象无法新增属性,使用 this.$set(obj, key, val) 新增。
  • 对象无法删除属性,使用 this.$delete(obj, key) 删除。

10.不需要响应式的数据应该怎么处理?

组件开发中,总有一些数据不需要做响应式处理,比如一些常量,或者一些静态资源数据,或者一些定死的枚举值,这时可以不直接写进 data 里,节约性能。

// 将数据定义在data之外或使用 Object.freeze

data() {
  this.constA = 'xxx'
  this.listA = []
  this.objA = {}
  return {
    listB: Object.freeze([]),
    objB: Object.freeze({}),
  }
}

Object.freeze()  方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。

⚠️ 最好不要使用 Object.freeze 方法,会抛出错误并阻断程序运行,不然还要写 try catch,麻烦。

image.png

11.直接给一个数组项赋值,Vue 能检测到变化吗?

不能

<div v-for="item in list" :key="item.name">
  {{ item.name }}
</div>
<button @click="handleArrChange">change this.list</button>

data() {
  return {
    list: [
      {
        name: 'lin'
      }
    ]
  }
}

handleArrChange() {
  this.list[0] = { name: 'wang' }            // 视图不会更新
}

image.png

查看 vue 开发者工具,发现数据已经更新了,只是视图不会更新。

可以用以下方法实现更新:

  • 使用 this.$set(arr, index, value)
 this.$set(this.list, 0, { name: 'wang' })
  • 使用数组 splice方法,arr.splice(index, 1, item)
 this.list.splice(0, 1, { name: 'wang' })
  • 使用 this.$forceUpdate(),强制渲染,不推荐。
this.list[0] = { name: 'wang' }
this.$forceUpdate()
  • 使用深拷贝,也不是很推荐
import { clone } from 'xe-utils'
...
this.list = clone(this.list, true)
  • 使用map方法
this.list = this.list.map((item, index) => {
  if (index === 0) {
    item.name = 'wang'
  }
  return item
})

12.vue 触发数组更新的方法有哪些?

  • push 向数组后面添加一个或多个元素
this.list.push({ name: 'lin1' })    
this.list.push({ name: 'lin1' },{ name: 'lin2' })
  • pop 删除数组最后一个元素
this.list.pop()
  • unshift 向数组前面添加一个或多个元素
this.list.unshift({ name: 'lin1' })
this.list.unshift({ name: 'lin1' },{ name: 'lin2' })
  • shift 删除数组最前面一个元素
this.list.shift()
  • splice 删除、插入、替换数组 Array.splice(index, howmany, item1, ....., itemX)
删除  
this.list.splice(0, 1)
插入  
this.list.splice(0, 0, { name: 'lin1' })
替换
this.list.splice(0, 1, { name: 'lin1' })
  • sort 排序
this.list.sort()
  • reverse 反转
this.list.reverse()

为什么这些数组方法可以触发视图更新,因为 Vue 将被侦听的数组的变更方法进行了包裹。

但是像 sliceconcatfilter 等不会改变原数组的方法,不会触发视图更新。

13.用 computed 返回值和用 method 返回值有什么区别?

<p>{{ reversedMessage }}</p>
<p>{{ reversedMessage1() }}</p>
<button @click="forceUpdate">forceUpdate</button>
...
data() {
  return {
    message: 'hello world'
  }
},
computed: {
  reversedMessage() {
    console.log('执行computed :>> ')
    return this.message.split('').reverse().join('')
  }
},
methods: {
  reversedMessage1() {
    console.log('执行methods :>> ')
    return this.message.split('').reverse().join('')
  },
  forceUpdate() {
    this.$forceUpdate()
  }
}

以反转字符串为例子,可以用 computed 实现,也可以用 method 实现。

computed.gif

当执行 this.$forceUpdate() 时,只会重新执行 methods 里的 reversedMessage1 方法,不会执行 computed 里的 reversedMessage,因为 computed 里会缓存,除非里面的响应式数据发生变化,computed 都不会重新计算。

所以,除非希望不用缓存,一般都用计算属性来返回值,这样可以避免重新计算。

14.computed 可以传参吗?

可以

<p>{{ totalNum(3) }}</p>

data() {
  return {
    num: 5
  }
},
computed: {
  totalNum(n) {
    return n * this.num
  }
}

直接传参,会报错

image.png

报错显示 totalNum is not a function,那么我们返回一个函数,用闭包的形式返回,就可以实现 computed 传参。

computed: {
  totalNum() {
    return function (n) {
      return n * this.num
    }
  }
}

传参之后,结果会缓存吗?

在totalNum里加个log,然后执行 this.$forceUpdate() 测试一下:

totalNum() {
  return function (n) {
    console.log('执行computed传参')
    return n * this.num
  }
}

computed传参.gif

每次 forceUpdate 的时候,都重新计算了。

很显然,computed 的缓存特性没了,这样写和在 methods 里返回值没有区别,所以这可能也是 vue 没有设计 computed 可传参功能的原因吧。

15.讲一下 computed 和 watch 分别的使用场景?

计算属性 computed

  • 依赖已有的变量来计算一个目标变量,可以减少模版中计算逻辑的书写。
  • 具有缓存机制,依赖值不变的情况下会直接读取缓存,不用重新计算。
  • 不能进行异步操作

侦听器 watch

  • 用于监听某一变量的变化,并执行回调函数,
  • 回调函数中可以执行任何逻辑,比如:异步操作,函数节流,甚至操作DOM(不推荐)

computed 能做到的功能,watch 也能做,只是用 computed 能让代码更加简洁,如果有大量计算用 computed 性能也会更好。

watch 则是更加灵活、通用,功能强大,但是能用 computed 做到尽量就不要用 watch 了。

16.watch 可以监听 computed 变化吗?

可以。

<p>{{ reversedMessage }}</p>
<button @click="changeMsg">changeMsg</button>

data() {
  return {
    message: 'hello world'
  }
}
computed: {
  reversedMessage() {
    return this.message.split('').reverse().join('')
  }
}
watch: {
  reversedMessage: {
    handler(val) {
      console.log('reversedMessage val :>> ', val)
    }
  }
}
methods: {
  changeMsg() {
    this.message = 'who are you'
  }
}

watch-computed.gif

computed 是依赖已有的响应式变量来计算一个目标变量,也是响应式的,也可以被监听。

17.watch 的 immediate 和 deep 是干什么用的?

immdiate

和 hander 一起使用,不用等到监听的变量值改变,立即执行一次 hander 函数

watch: {
  message: {
    handler(newVal, oldVal) {
      // ...
    },
    immediate: true
  }
}

使用场景:页面首次加载时,是不会执行的。只有值发生改变才会执行,这时可用 immediate 立即执行。

比如created时要请求一次数据,并且当搜索值改变,也要请求数据,我们这么写:

created(){
  this.getList()
},
watch: {
  searchInputValue(){
    this.getList()
  }
}

可以改成这样

watch: {
  searchInputValue:{
    handler: 'getList',
    immediate: true
  }
}

deep

如果是监听的是对象类型,当手动修改对象的某个属性时,watch 不会执行。

这时可用 deep 属性。

data:{
  userInfo:{
    name: 'lin'
  }
},
watch:{
  userInfo: {
    handler(newVal, oldVal){
      // ...
    },
    deep:true 
  }
}

deep 为 true,表示深度监听,这时候就能监测到 userInfo 里的 name 值变化。

它会一层层遍历,给这个对象的所有属性都加上这个监听器。但是这样性能开销会比较大,修改任何一个属性,都会触发这个监听器里的handler。

deep 属性优化

可以使用字符串形式监听。

watch:{
  'userInfo.name': {
    handler(newVal, oldVal){
      // ...
    },
    deep:true 
  }
}

这样 vue 就会一层层解析,知道遇到属性 name,然后才给 name 设置监听函数。

总结

平时等后端联调,经常一等就是一个小时,然后接口依然报错,不如把这些时间利用起来,总结点知识。

问题有点乱,看到啥或者想到啥就去写 demo 试试,然后再总结成文章,后面慢慢整理吧!