Vue中的watch
简答的不介绍,文档都有https://cn.vuejs.org/v2/guide/computed.html
N年没用watch,突然看到同事用上了一脸懵,理论上computed+Vuex的getters还不能满足,上watch也不能100%解决。
但是用上了发现这货还挺坑的,细细说来
watch的简单例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue-watch</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
</head>
<body>
<div id="watch-example">
<p>
Ask a yes/no question:
<input v-model="question">
</p>
<p>{{ answer }}</p>
<div style="margin-top: 20px">
{{ fullName }}
</div>
</div>
<script>
var watchExampleVM = new Vue({
el: '#watch-example',
data: {
question: '',
answer: 'I cannot give you an answer until you ask a question!',
firstName: '',
lastName: '',
fullName: '全名'
},
watch: {
// 如果 `question` 发生改变,这个函数就会运行
question: function (newQuestion, oldQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.debouncedGetAnswer()
},
firstName: {
handler(newVal, oldVal) {
console.log(`firstName update ${oldVal} -> ${newVal}`)
this.fullName = this.firstName + this.lastName
},
// immediate: true // 这里如果不添加immediate属性,则不马上执行handler
},
lastName: (newVal, oldVal) => {
console.log(`lastName update ${oldVal} -> ${newVal}`)
this.fullName = this.firstName + this.lastName
}
},
created: function () {
// `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
// 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
// AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
// `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识,
// 请参考:https://lodash.com/docs#debounce
this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
this.firstName = 'Manfred'
},
methods: {
getAnswer: function () {
if (this.question.indexOf('?') === -1) {
this.answer = 'Questions usually contain a question mark. ;-)'
return
}
this.answer = 'Thinking...'
var vm = this
axios.get('https://yesno.wtf/api')
.then(function (response) {
vm.answer = _.capitalize(response.data.answer)
})
.catch(function (error) {
vm.answer = 'Error! Could not reach the API. ' + error
})
}
}
})
</script>
</body>
</html>
- 首先是immediate这个属性,我们知道watch只是在Vue里面注册了监听器,我们可以通过Vue实例的
_watchers
属性,如watchExampleVM._watchers
来获取watchers队列。
监听器注册后是否需要马上执行一遍,默认是不执行,immediate=false
,当然也可以声明执行。常见的watch的这个属性在页面渲染的时候就需要显示,那么就一定是immediate: true
- 其次是deep这个属性,如果是对象嵌套的很深,那么deep就可以往下遍历对象的每一层属性,并监听属性,这样做开销会非常大。如下面的例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue-watch</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="root">
<p>obj.a.b.c.d: {{obj.a.b.c.d}}</p>
<p>obj.a.b.c.d: <input type="text" v-model="obj.a.b.c.d"></p>
</div>
<script>
var watchExampleVM = new Vue({
el: '#root',
data: {
obj: {
a: {
b: {
c: {
d: 123
}
}
}
}
},
watch: {
obj: {
handler(newName, oldName) {
console.log('obj.a changed', newName, oldName);
},
// deep: true,
},
},
mounted () {
this.obj.a.b.c.d = 456 // deep不设置为true不生效
}
})
</script>
</body>
</html>
如果要简单处理,我们可以这么做,这样虽然也是一层层遍历,但是只监听了最后一层的属性变化
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue-watch</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="root">
<p>obj.a.b.c.d: {{obj.a.b.c.d}}</p>
<p>obj.a.b.c.d: <input type="text" v-model="obj.a.b.c.d"></p>
</div>
<script>
var watchExampleVM = new Vue({
el: '#root',
data: {
obj: {
a: {
b: {
c: {
d: 123
}
}
}
}
},
watch: {
'obj.a.b.c.d': {
handler(newName, oldName) {
console.log('obj.a changed', newName, oldName);
}
},
},
mounted () {
this.obj.a.b.c.d = 456
}
})
</script>
</body>
</html>
例子非常简单,watch舰艇question,如果变化了就调用接口。但是这个时候问题来了,如果此时你要取消这个watch怎么办?遇到的场景是,某某组件框架有bug。框架watch了一个props属性,如果这个属性变化了,就重置组件。但是你不能去改框架底层代码。所以,只能修改实例。
watch动态注入(add watch dynamically)
watch可以在代码声明解析,也支持实例创建。
如上面例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue-watch</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="root">
<p>obj.a.b.c.d: {{obj.a.b.c.d}}</p>
<p>obj.a.b.c.d: <input type="text" v-model="obj.a.b.c.d"></p>
</div>
<script>
var watchExampleVM = new Vue({
el: '#root',
data: {
obj: {
a: {
b: {
c: {
d: 123
}
}
}
}
},
mounted () {
// 这里注册watch
const objWatch = this.$watch('obj.a.b.c.d', (newName, oldName) => {
console.log('obj.a changed', newName, oldName);
})
this.obj.a.b.c.d = 456
setTimeout(() => {
objWatch()
console.log('虽然我改变了值,但是watch被取消了,不执行了')
this.obj.a.b.c.d = 789
}, 2000)
}
})
</script>
</body>
</html>
vm.$watch
注册与代码写死是一个道理,同时值得一提的是这里会返回一个watch以便取消。动态注入的方式很容易取消watch,那么代码里写的watch要怎么取消呢?
取消实例的watch
主要是文档也翻不到,网上也搜不到对应答案,脑瓜嗡嗡作响。最终,还是觉得「Talk is cheap. Show me the code」真理大法好。
直接看源代码https://github.com/vuejs/vue/blob/2.6/src/core/observer/watcher.js#L234
简单说就是被存到vm._watchers
后,找到这个watcher,watcher的teardown
方法,就是取消watch的方法了。我可以愉快修改别人框架的Vue实例了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue-watch</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="root">
<p>obj.a.b.c.d: {{obj.a.b.c.d}}</p>
<p>obj.a.b.c.d: <input type="text" v-model="obj.a.b.c.d"></p>
</div>
<script>
var watchExampleVM = new Vue({
el: '#root',
data: {
obj: {
a: {
b: {
c: {
d: 123
}
}
}
}
},
watch: {
'obj.a.b.c.d': {
handler(newName, oldName) {
console.log('obj.a changed', newName, oldName);
}
},
},
mounted () {
this.obj.a.b.c.d = 456
setTimeout(() => {
// delete watch by use watcher.teardown()
watchExampleVM._watchers.find(item => item.expression === 'obj.a.b.c.d').teardown()
console.log('虽然我改变了值,但是watch被取消了,不执行了')
this.obj.a.b.c.d = 789
}, 2000)
}
})
</script>
</body>
</html>

欢迎关注微信公众号 【Big前端】无广告,无软文,就是这么傲娇。直推一线大厂高质量内容,不局限于前端·后台·运维相关,还包括房价🏠、信用卡💳等内容也可内推一线大厂腾讯阿里字节,对腾讯字节比较熟悉,简历可以发给我,我会给你介绍一线大厂的情况,让你更加了解一线大厂