MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue侦听器 监听数组与对象变化的正确姿势

2022-09-121.4k 阅读

Vue 侦听器基础

在 Vue 开发中,侦听器(Watcher)是一个非常强大的工具,它允许我们监听数据的变化,并在数据变化时执行相应的操作。Vue 的响应式系统会自动追踪依赖,当依赖的数据发生变化时,与之相关的视图会自动更新。而侦听器则提供了一种更灵活的方式来处理数据变化,不仅仅局限于视图更新。

基本语法

在 Vue 实例中,可以通过 watch 选项来定义侦听器。例如:

new Vue({
  data() {
    return {
      message: 'Hello Vue'
    };
  },
  watch: {
    message(newValue, oldValue) {
      console.log(`新值: ${newValue}, 旧值: ${oldValue}`);
    }
  }
});

在上述代码中,我们定义了一个 message 数据属性,并为其设置了一个侦听器。当 message 的值发生变化时,会执行侦听器函数,并传入新值和旧值。

深度监听

对于对象类型的数据,如果我们希望监听对象内部属性的变化,就需要使用深度监听。在 Vue 中,默认情况下,侦听器只会监听对象的引用变化,而不会监听对象内部属性的变化。例如:

new Vue({
  data() {
    return {
      user: {
        name: 'John',
        age: 30
      }
    };
  },
  watch: {
    user(newValue, oldValue) {
      console.log(`新的用户对象:`, newValue);
      console.log(`旧的用户对象:`, oldValue);
    }
  }
});

如果我们只是修改 user.name,上述侦听器函数并不会被触发,因为 user 的引用并没有改变。要实现深度监听,可以使用 deep 选项:

new Vue({
  data() {
    return {
      user: {
        name: 'John',
        age: 30
      }
    };
  },
  watch: {
    user: {
      handler(newValue, oldValue) {
        console.log(`新的用户对象:`, newValue);
        console.log(`旧的用户对象:`, oldValue);
      },
      deep: true
    }
  }
});

现在,当 user.nameuser.age 发生变化时,侦听器函数都会被触发。需要注意的是,深度监听会递归遍历对象的所有属性,性能开销较大,因此在不必要的情况下应避免使用。

监听数组变化

数组变化检测原理

Vue 通过包裹数组的变异方法(如 pushpopshiftunshiftsplicesortreverse)来检测数组的变化。当调用这些方法时,Vue 会触发视图更新。例如:

<template>
  <div>
    <ul>
      <li v-for="(item, index) in list" :key="index">{{ item }}</li>
    </ul>
    <button @click="addItem">添加项目</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      list: ['apple', 'banana']
    };
  },
  methods: {
    addItem() {
      this.list.push('cherry');
    }
  }
};
</script>

在上述代码中,当点击按钮调用 addItem 方法时,list 数组会通过 push 方法添加一个新元素,视图会自动更新显示新的列表项。

侦听数组变化

直接对数组进行侦听,与普通数据属性的侦听类似,但需要注意一些细节。例如:

new Vue({
  data() {
    return {
      fruits: ['apple', 'banana']
    };
  },
  watch: {
    fruits(newValue, oldValue) {
      console.log(`新的水果列表:`, newValue);
      console.log(`旧的水果列表:`, oldValue);
    }
  }
});

当通过变异方法修改 fruits 数组时,侦听器函数会被触发。然而,如果我们通过索引直接修改数组元素,例如 this.fruits[0] = 'orange',这种方式不会触发视图更新,也不会触发侦听器。这是因为 Vue 无法检测到这种变化。要解决这个问题,可以使用 Vue.set 方法或数组的 splice 方法。

使用 Vue.set 监听数组元素变化

Vue.set 方法可以向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。对于数组,我们可以使用它来修改指定索引的元素,同时触发侦听器。例如:

new Vue({
  data() {
    return {
      fruits: ['apple', 'banana']
    };
  },
  watch: {
    fruits(newValue, oldValue) {
      console.log(`新的水果列表:`, newValue);
      console.log(`旧的水果列表:`, oldValue);
    }
  },
  methods: {
    changeFruit() {
      Vue.set(this.fruits, 0, 'orange');
    }
  }
});

在上述代码中,changeFruit 方法使用 Vue.set 修改了 fruits 数组的第一个元素,此时侦听器函数会被触发,并且视图也会更新。

使用 splice 监听数组元素变化

除了 Vue.set,我们还可以使用数组的 splice 方法来修改数组元素,同样可以触发视图更新和侦听器。例如:

new Vue({
  data() {
    return {
      fruits: ['apple', 'banana']
    };
  },
  watch: {
    fruits(newValue, oldValue) {
      console.log(`新的水果列表:`, newValue);
      console.log(`旧的水果列表:`, oldValue);
    }
  },
  methods: {
    changeFruit() {
      this.fruits.splice(0, 1, 'orange');
    }
  }
});

changeFruit 方法中,splice(0, 1, 'orange') 表示从索引 0 开始删除 1 个元素,并插入 'orange'。这种方式也能达到修改数组元素并触发侦听器和视图更新的效果。

监听对象变化

对象变化检测原理

Vue 在初始化数据时,会使用 Object.defineProperty 方法将数据属性转换为 getter 和 setter,从而实现数据的响应式。当访问或修改这些属性时,会触发对应的 getter 或 setter 方法,Vue 借此追踪依赖并更新视图。例如:

let data = {
  name: 'John'
};
Object.keys(data).forEach(key => {
  let value = data[key];
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log(`访问属性 ${key}`);
      return value;
    },
    set(newValue) {
      console.log(`设置属性 ${key} 为 ${newValue}`);
      value = newValue;
    }
  });
});

上述代码模拟了 Vue 将普通对象转换为响应式对象的过程。通过 Object.defineProperty,我们为 data 对象的 name 属性设置了自定义的 getter 和 setter 方法。

基本对象监听

对于对象的监听,我们可以直接在 watch 选项中定义。例如:

new Vue({
  data() {
    return {
      user: {
        name: 'John',
        age: 30
      }
    };
  },
  watch: {
    user(newValue, oldValue) {
      console.log(`新的用户对象:`, newValue);
      console.log(`旧的用户对象:`, oldValue);
    }
  }
});

如前文所述,这种方式只能监听 user 对象引用的变化。如果想要监听对象内部属性的变化,就需要使用深度监听。

深度监听对象

使用深度监听可以监听对象内部任意属性的变化。例如:

new Vue({
  data() {
    return {
      user: {
        name: 'John',
        age: 30
      }
    };
  },
  watch: {
    user: {
      handler(newValue, oldValue) {
        console.log(`新的用户对象:`, newValue);
        console.log(`旧的用户对象:`, oldValue);
      },
      deep: true
    }
  }
});

此时,当 user.nameuser.age 发生变化时,侦听器函数都会被触发。然而,深度监听存在性能问题,因为它需要递归遍历对象的所有属性。

监听对象新增属性

在 Vue 中,直接为对象添加新属性不会触发视图更新和侦听器,因为 Vue 在初始化时已经遍历了对象的属性并将其转换为响应式。例如:

new Vue({
  data() {
    return {
      user: {
        name: 'John'
      }
    };
  },
  watch: {
    user: {
      handler(newValue, oldValue) {
        console.log(`新的用户对象:`, newValue);
        console.log(`旧的用户对象:`, oldValue);
      },
      deep: true
    }
  },
  methods: {
    addProperty() {
      this.user.age = 30;
    }
  }
});

在上述代码中,addProperty 方法为 user 对象添加了 age 属性,但由于该属性在初始化时未被转换为响应式,所以不会触发侦听器和视图更新。要解决这个问题,同样可以使用 Vue.set 方法。

使用 Vue.set 监听对象新增属性

Vue.set 方法可以为对象添加响应式属性,并触发视图更新和侦听器。例如:

new Vue({
  data() {
    return {
      user: {
        name: 'John'
      }
    };
  },
  watch: {
    user: {
      handler(newValue, oldValue) {
        console.log(`新的用户对象:`, newValue);
        console.log(`旧的用户对象:`, oldValue);
      },
      deep: true
    }
  },
  methods: {
    addProperty() {
      Vue.set(this.user, 'age', 30);
    }
  }
});

addProperty 方法中,使用 Vue.setuser 对象添加了 age 属性,此时侦听器函数会被触发,视图也会更新。

计算属性与侦听器的对比

计算属性

计算属性是基于它们的依赖进行缓存的,只有在依赖的数据发生变化时,才会重新计算。计算属性适用于一些需要根据其他数据派生出来的值。例如:

<template>
  <div>
    <p>第一个数字: <input v-model="num1"></p>
    <p>第二个数字: <input v-model="num2"></p>
    <p>两数之和: {{ sum }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      num1: 0,
      num2: 0
    };
  },
  computed: {
    sum() {
      return this.num1 + this.num2;
    }
  }
};
</script>

在上述代码中,sum 是一个计算属性,它依赖于 num1num2。只有当 num1num2 发生变化时,sum 才会重新计算。

侦听器

侦听器更侧重于监听数据的变化,并在变化时执行一些副作用操作,如异步请求、数据持久化等。例如:

<template>
  <div>
    <p>搜索关键词: <input v-model="searchKeyword"></p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchKeyword: ''
    };
  },
  watch: {
    searchKeyword(newValue) {
      // 执行搜索请求
      this.fetchSearchResults(newValue);
    }
  },
  methods: {
    fetchSearchResults(keyword) {
      // 模拟异步请求
      console.log(`正在搜索 ${keyword}`);
    }
  }
};
</script>

在上述代码中,当 searchKeyword 发生变化时,会触发侦听器函数,并执行 fetchSearchResults 方法进行搜索操作。

选择使用计算属性还是侦听器

  • 如果只是根据现有数据派生新的数据,并且不需要副作用操作,优先使用计算属性,因为它具有缓存机制,性能更好。
  • 如果需要在数据变化时执行异步操作、数据持久化等副作用操作,或者需要深度监听对象内部属性的变化,应使用侦听器。

实际应用场景

表单验证

在表单开发中,我们经常需要根据用户输入的值进行实时验证。例如,验证邮箱格式是否正确。可以使用侦听器来监听输入框的值变化,并进行验证。

<template>
  <div>
    <p>邮箱: <input v-model="email"></p>
    <p v-if="!isValidEmail">请输入正确的邮箱格式</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      email: '',
      isValidEmail: true
    };
  },
  watch: {
    email(newValue) {
      const re = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/;
      this.isValidEmail = re.test(newValue);
    }
  }
};
</script>

在上述代码中,当 email 的值发生变化时,侦听器会验证其格式,并更新 isValidEmail 的值,从而控制错误提示信息的显示。

数据缓存

在一些应用中,我们可能需要对某些数据进行缓存,以减少重复请求。可以使用侦听器来监听数据的变化,当数据变化时更新缓存。例如:

let cache = {};

new Vue({
  data() {
    return {
      userData: null
    };
  },
  watch: {
    userData(newValue) {
      if (newValue) {
        cache.user = newValue;
      }
    }
  },
  methods: {
    fetchUserData() {
      if (cache.user) {
        this.userData = cache.user;
      } else {
        // 模拟异步请求获取用户数据
        setTimeout(() => {
          this.userData = { name: 'John', age: 30 };
        }, 1000);
      }
    }
  }
});

在上述代码中,当 userData 发生变化时,会将其缓存到 cache 对象中。下次获取用户数据时,先检查缓存中是否有数据,如果有则直接使用缓存数据,避免重复请求。

实时数据同步

在一些实时应用中,如聊天应用,需要实时同步数据。可以使用侦听器来监听数据的变化,并将变化的数据发送到服务器。例如:

<template>
  <div>
    <p>消息: <input v-model="message"></p>
    <button @click="sendMessage">发送消息</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: ''
    };
  },
  watch: {
    message(newValue) {
      // 模拟实时同步数据到服务器
      console.log(`正在同步消息到服务器: ${newValue}`);
    }
  },
  methods: {
    sendMessage() {
      // 模拟发送消息
      console.log(`发送消息: ${this.message}`);
      this.message = '';
    }
  }
};
</script>

在上述代码中,当 message 的值发生变化时,侦听器会模拟将消息同步到服务器。当点击发送按钮时,会发送消息并清空输入框。

注意事项

避免无限循环

在侦听器函数中,如果不小心修改了被监听的数据,可能会导致无限循环。例如:

new Vue({
  data() {
    return {
      count: 0
    };
  },
  watch: {
    count(newValue) {
      this.count = newValue + 1;
    }
  }
});

在上述代码中,当 count 发生变化时,侦听器函数会再次修改 count 的值,从而导致无限循环。要避免这种情况,需要确保在侦听器函数中不会意外修改被监听的数据,或者设置合适的条件来终止循环。

性能问题

深度监听对象或数组会带来较大的性能开销,因为它需要递归遍历所有属性。在不必要的情况下,应避免使用深度监听。如果确实需要深度监听,可以考虑使用其他方式来优化性能,例如只监听关键属性的变化,而不是整个对象或数组。

销毁侦听器

在 Vue 实例销毁时,侦听器也会被自动销毁。但是,如果在侦听器中绑定了一些外部资源(如定时器、事件监听器等),需要手动清理这些资源,以避免内存泄漏。例如:

new Vue({
  data() {
    return {
      timer: null
    };
  },
  watch: {
    someData(newValue) {
      this.timer = setInterval(() => {
        console.log(`someData 变化后定时执行: ${newValue}`);
      }, 1000);
    }
  },
  beforeDestroy() {
    if (this.timer) {
      clearInterval(this.timer);
    }
  }
});

在上述代码中,在 beforeDestroy 钩子函数中,我们手动清除了定时器,以防止内存泄漏。

通过以上对 Vue 侦听器监听数组与对象变化的详细介绍,相信你已经掌握了正确的使用姿势。在实际开发中,合理运用侦听器可以帮助我们更好地处理数据变化,提高应用的性能和用户体验。