侦听器

虽然计算属性在大多数情况下更合适,但有时也需要一个更通用的方式来响应数据的变化——这就是侦听器(Watcher)。当需要在数据变化时执行异步或开销较大的操作时,侦听器是最有用的。

基本用法

使用 watch 选项来响应数据的变化:

<div id="app">
  <p>
    问题: <input v-model="question">
  </p>
  <p>{{ answer }}</p>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    question: '',
    answer: '请输入问题'
  },
  watch: {
    // 当 question 改变时,这个函数就会运行
    question: function(newQuestion, oldQuestion) {
      this.answer = '等待输入停止...'
      this.getAnswer()
    }
  },
  methods: {
    getAnswer: _.debounce(function() {
      if (this.question.indexOf('?') === -1) {
        this.answer = '问题通常需要问号结尾 ;-)'
        return
      }
      this.answer = '思考中...'
      var vm = this
      // 模拟异步请求
      setTimeout(function() {
        vm.answer = '这是答案'
      }, 1000)
    }, 500)
  }
})
</script>

侦听器 vs 计算属性

// 使用计算属性
computed: {
  fullName: function() {
    return this.firstName + ' ' + this.lastName
  }
}

// 使用侦听器
watch: {
  firstName: function(val) {
    this.fullName = val + ' ' + this.lastName
  },
  lastName: function(val) {
    this.fullName = this.firstName + ' ' + val
  }
}

上面的例子中,计算属性更简洁。但侦听器适合以下场景:

使用侦听器的场景

  • 异步操作(API 请求)
  • 中等开销的操作
  • 数据变化时需要执行特定逻辑
  • 需要访问变化前后的值

handler 函数和 immediate 属性

默认情况下,侦听器在数据改变时才会触发。如果需要在创建时立即执行,使用 immediate

watch: {
  question: {
    handler: function(newVal, oldVal) {
      this.getAnswer()
    },
    immediate: true  // 创建时立即执行一次
  }
}

深度侦听(deep)

侦听器默认不侦听对象内部值的变化。要侦听对象内部变化,使用 deep

new Vue({
  data: {
    user: {
      name: 'John',
      age: 20
    }
  },
  watch: {
    user: {
      handler: function(newVal, oldVal) {
        console.log('user changed')
      },
      deep: true  // 深度侦听
    }
  }
})

性能提示:深度侦听会遍历对象的每个属性,开销较大。如果只需要侦听某个特定属性,使用字符串路径:

watch: {
  'user.name': function(newVal, oldVal) {
    console.log('name changed:', newVal)
  }
}

侦听数组

侦听数组变化:

new Vue({
  data: {
    items: [1, 2, 3]
  },
  watch: {
    items: {
      handler: function(newVal, oldVal) {
        console.log('items changed')
      },
      deep: true  // 如果数组元素是对象,需要深度侦听
    }
  }
})

多个侦听器

可以同时侦听多个数据:

watch: {
  firstName: function(val) {
    this.fullName = val + ' ' + this.lastName
  },
  lastName: function(val) {
    this.fullName = this.firstName + ' ' + val
  },
  age: function(newVal, oldVal) {
    console.log('age changed from', oldVal, 'to', newVal)
  }
}

实际应用场景

1. 搜索输入防抖

new Vue({
  data: {
    searchQuery: '',
    results: [],
    isSearching: false
  },
  watch: {
    searchQuery: _.debounce(function(newVal) {
      if (newVal.length < 3) return
      this.isSearching = true
      this.search(newVal)
    }, 300)
  },
  methods: {
    search: function(query) {
      var vm = this
      fetch('/api/search?q=' + query)
        .then(function(res) { return res.json() })
        .then(function(data) {
          vm.results = data
          vm.isSearching = false
        })
    }
  }
})

2. 路由参数变化

new Vue({
  watch: {
    '$route': function(to, from) {
      this.loadData(to.params.id)
    }
  },
  methods: {
    loadData: function(id) {
      // 根据 id 加载数据
    }
  }
})

3. 表单自动保存

new Vue({
  data: {
    form: {
      title: '',
      content: ''
    }
  },
  watch: {
    form: {
      handler: _.debounce(function() {
        this.autoSave()
      }, 1000),
      deep: true
    }
  },
  methods: {
    autoSave: function() {
      localStorage.setItem('draft', JSON.stringify(this.form))
    }
  }
})

4. 本地存储同步

new Vue({
  data: {
    theme: localStorage.getItem('theme') || 'light'
  },
  watch: {
    theme: function(newVal) {
      localStorage.setItem('theme', newVal)
      document.body.className = newVal
    }
  }
})

5. 验证码倒计时

new Vue({
  data: {
    countdown: 0
  },
  watch: {
    countdown: function(val) {
      if (val > 0) {
        var vm = this
        setTimeout(function() {
          vm.countdown--
        }, 1000)
      }
    }
  }
})

vm.$watch 方法

除了在 watch 选项中定义,也可以使用 $watch 方法:

var vm = new Vue({
  data: {
    a: 1
  }
})

// 返回一个取消侦听函数
var unwatch = vm.$watch('a', function(newVal, oldVal) {
  console.log('a changed:', oldVal, '->', newVal)
})

// 取消侦听
unwatch()

$watch 的选项

vm.$watch('someObject', callback, {
  deep: true,      // 深度侦听
  immediate: true  // 立即执行
})

常见错误

1. 箭头函数中的 this

// ❌ 错误:箭头函数没有自己的 this
watch: {
  question: (newVal, oldVal) => {
    this.answer = '...'  // this 不是 Vue 实例
  }
}

// ✅ 正确:使用普通函数
watch: {
  question: function(newVal, oldVal) {
    this.answer = '...'
  }
}

2. 忘记 deep 选项

// ❌ 无法侦听对象内部变化
watch: {
  user: function(newVal) {
    console.log('changed')  // 可能不会触发
  }
}

// ✅ 添加 deep 选项
watch: {
  user: {
    handler: function(newVal) {
      console.log('changed')
    },
    deep: true
  }
}

3. 异步操作未处理 this

watch: {
  question: function(newVal) {
    setTimeout(function() {
      this.answer = '...'  // ❌ this 指向错误
    }, 1000)
  }
}

// ✅ 保存 this 引用
watch: {
  question: function(newVal) {
    var vm = this
    setTimeout(function() {
      vm.answer = '...'
    }, 1000)
  }
}

// ✅ 或使用箭头函数
watch: {
  question: function(newVal) {
    var vm = this
    setTimeout(() => {
      vm.answer = '...'
    }, 1000)
  }
}

小结

特性说明
基本用法watch: { key: function(newVal, oldVal) {} }
immediate创建时立即执行一次
deep深度侦听对象内部变化
字符串路径侦听嵌套属性 'a.b.c'
$watch编程式侦听,可取消

侦听器是 Vue 响应式系统的重要组成部分,合理使用可以让应用更加灵活。