主题
自定义指令与基本使用(Lesson6 1/3)
本页对应 Lesson06,共 6 段视频,约 2 小时 35 分。
学习目标
- 能说明自定义指令的构成(名称、参数、修饰符、值),并区分内置指令与自定义指令、局部指令与全局指令。
- 能使用
Vue.directive注册全局指令,并在组件内声明局部指令,通过实际页面验证其作用范围。 - 能使用
bind钩子接收el与binding,在挂载前操作元素,并说明数据更新时该钩子是否再次执行。 - 能使用
inserted钩子完成挂载后的 DOM 操作(如自动聚焦),并比较bind与inserted的执行时机差异。 - 能编写基于
bind的自定义防抖指令,利用binding.value、参数或修饰符配置延迟时间,并验证连续输入只触发一次。 - 能使用
watch侦听数字、字符串、对象属性和数组变化,区分函数写法与对象写法,正确使用immediate、deep,并处理数组下标赋值等不触发场景。
知识点
先分清内置指令的构成
为什么需要:自定义指令不是凭空写出来的,先要看清一个指令由哪几部分组成,后面才不会把名字、参数、修饰符、值写混。转写稿里反复强调:指令是以 v- 开头的特殊属性,结构可以理解为 v-名字[:参数][.修饰符]="值"。
怎么想:先按“有没有值、有没有参数、有没有修饰符”分类。无值的有 v-once、v-cloak;有值的有 v-if、v-show、v-html、v-text 等;有参数的有 v-bind:class、v-on:click;有修饰符的有 v-on:click.stop、v-model.number。注意转写稿里口误说成“V杠 one”,实际是 v-once。
怎么做:写指令时先确定名字,例如 v-book,其中 book 才是指令名,v- 是使用时的前缀。参数写在冒号后,修饰符写在点后,等号右边才是值。
易错点:不要把 v- 写进注册名;不要把参数和修饰符混在一起;字符串值要加引号,否则会被当成实例属性。
自定义指令的注册:局部与全局
为什么需要:内置指令不够用时,需要自己操作元素,例如自动聚焦、防抖、改样式。转写稿明确说:自定义指令和自定义事件一样,就是自己起名字。
怎么想:先判断复用范围。只在当前组件用,写局部 directives;多个组件都要用,写全局 Vue.directive。指令里 this 指向 window,所以不建议在指令里操作实例属性。
怎么做:局部写法:
js
directives: {
book(el) {
el.innerText = `《${el.innerText}》`
}
}全局写法:
js
Vue.directive('color', {
bind(el, binding) {
el.style.color = binding.value
}
})使用时:<p v-book>天龙八部</p>、<p v-color="'pink'">文字</p>。
易错点:全局是 Vue.directive,没有 s;局部是 directives,有 s。局部指令不能在别的组件直接用,转写稿里在 App 中使用组件内声明的 color 就报“指令没有找到”。
函数写法与 bind 钩子:为什么书名号会越来越多
为什么需要:转写稿先写了函数写法,结果输入框改一次,书名号就多一层。原因是数据更新后视图重新渲染,指令函数会再次执行。
怎么想:如果只想初始化一次,就用对象写法里的 bind 钩子。bind 在挂载前执行,数据更新后不会再次执行;适合加一次样式、加一次事件监听。
怎么做:
js
directives: {
book: {
bind(el) {
el.innerText = `《${el.innerText}》`
}
}
}调试过程:转写稿中先输出 mounted 和 book,发现 book 先执行,说明它在挂载前执行;输入后函数写法再次执行,el.innerText 已带书名号,于是又包一层。
易错点:bind 中 v-model 还没生效,拿不到输入框最新值;想格式化数据,优先用过滤器或计算属性,不要用指令反复改 innerText。指令针对元素,过滤器针对数据,这是本质区别。
inserted:挂载完成后再操作元素
为什么需要:自动聚焦必须等元素挂载完成。转写稿在 bind 里写 el.focus() 没效果,因为后面还会挂载更新,焦点被冲掉。
怎么想:需要真实 DOM 已插入时用 inserted。它和 mounted 都在挂载完成后执行,但 inserted 先于 mounted,且只执行一次。
怎么做:
js
directives: {
focus: {
inserted(el) {
el.focus()
}
}
}使用时:<input v-focus>。
易错点:不要在 bind 里做依赖最终 DOM 的操作;转写稿用 setTimeout 延迟也能“碰巧”生效,但更稳的是 inserted。
参数、修饰符与 binding
为什么需要:同一个指令要能传不同颜色、不同背景、不同防抖时间,就要用参数和修饰符。
怎么想:binding.value 是等号右边的值;binding.arg 是冒号后的参数;binding.modifiers 是点后的修饰符对象。
怎么做:
js
Vue.directive('style', {
bind(el, binding) {
el.style[binding.arg] = binding.value
}
})使用:<p v-style:background="'red'">内容</p>。防抖可写成 v-debounce:300="fn",在 bind 里取 Number(binding.arg) || 300。
易错点:v-color="pink" 会把 pink 当实例属性,字符串要写 v-color="'pink'";参数是 arg,修饰符在 modifiers,不要取错。
用 bind 做防抖应用
为什么需要:输入框每输入一次就发请求,次数太多。转写稿先用普通 input 事件演示:输入 123 会执行三次请求。
怎么想:防抖就是每次输入先清掉上一个定时器,只让最后一次生效。因为事件监听只加一次,所以用 bind,不用函数写法。
怎么做:
js
Vue.directive('debounce', {
bind(el, binding) {
let timer = null
el.addEventListener('input', () => {
clearTimeout(timer)
timer = setTimeout(() => {
binding.value()
}, Number(binding.arg) || 300)
})
}
})使用:<input v-debounce:500="send">。
易错点:函数写法会重复 addEventListener;定时器要保存在闭包里;binding.arg 是字符串,参与时间计算要转数字。转写稿还演示了用修饰符取时间,本质也是从 binding.modifiers 里拿配置。
先看完整示例:过滤器与自定义指令并列
下面这段代码是本节的主线:同一个 bookName 分别交给过滤器、无值指令、带值指令、带参数指令和带修饰符指令处理。先跑通,再逐个观察执行时机。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<input v-model="bookName" type="text">
<p>{{bookName}}</p>
<p>{{bookName | book}}1</p>
<p v-book>{{bookName}}1</p>
<p v-red>{{bookName}}</p>
<p v-green v-book>{{bookName}}</p>
<p v-color="'pink'">{{bookName}}</p>
<p v-color="'blue'">{{bookName}}</p>
<p v-style:background="'red'">{{bookName}}</p>
<p v-style:background.lazy="'green'">{{bookName}}</p>
</div>
</body>
<script>
// 说明:过滤器针对的是某一个数据,自定义的指令针对的是元素。
// 指令分为:内置指令以及自定义指令。
// 自定义指令分为:全局以及局部指令,通过属性directive
// 指令:以v-开头的特殊属性。
// 指令的构成:v-name:arg.modifier=value
// 1- 无值(value): v-else v-once v-pre v-cloak
// 2- 有值(value): v-if=value v-else-if=value v-show=value v-html=value v-text=value
// 3- 有参数(arg): v-bind:arg=value v-model:arg=value v-on:click=value
// 4- 有修饰符(modifier):v-model.number @click.stop
new Vue({
el:"#app",
data:{
bookName:"天龙八部"
},
filters:{
book(v){
return "《"+v+"》";
}
},
directives:{
// 定义一个名字为book的指令
// 接收的第一个参数是指令所在的元素对象。
book(el){
// 1-this指向的是window,所以在指令中不建议操作实例属性的。
// console.log(this)
// 2-自定义的指令是在挂载前执行。初次执行时,数据与视图还未进行绑定:{{bookName}}
// 3-当数据发生更改后,视图要更新,指令会再次执行,在此处通过document获取的数据是最新的。
// console.log("book",document.querySelector("p").innerText);
el.innerText = "《"+el.innerText+"》"
},
red(el){
el.style.color = "red";
},
green(el){
el.style.color = "green";
},
color(el,binds){
// v-color="'pink'"
// expression:"'pink'"--->= 右侧
// modifiers:{}---->修饰符
// name:"color"---->自定义指令的名字
// rawName:"v-color"--->带v-+name
// value:"pink"
// console.log(binds);
el.style.color = binds.value;
},
style(el,binds){
// arg:"background"--->参数
if(binds.modifiers.lazy){
setTimeout(()=>{
el.style[binds.arg]=binds.value;
},2000)
}else{
el.style[binds.arg]=binds.value;
}
}
},
mounted(){
console.log("mounted");
}
})
</script>
</html>指令是什么:从 v-name:arg.modifier=value 拆开
为什么需要:内置指令不够用时,例如“让某个元素统一变红”“给元素内容加书名号”“输入框自动聚焦”,都需要我们操作 DOM。自定义指令就是把“找到元素并操作元素”的逻辑封装成 v-xxx。
怎么想:先分清指令四类构成。无值如 v-once;有值如 v-if="ok";有参数如 v-bind:title;有修饰符如 v-model.number。完整形态可记成 v-name:arg.modifier=value。
怎么做:局部指令写在 directives 中,名字 book 对应模板里的 v-book。指令函数第一个参数 el 是指令所在元素。v-color="'pink'" 中,binds.value 才是 pink;若写 v-color="pink",Vue 会当成实例属性去找。v-style:background 中 binds.arg 是 background;.lazy 进入 binds.modifiers.lazy。
易错点:指令函数里的 this 是 window,不要在里面操作实例数据;binds.expression 是等号右侧原文,实际取颜色应优先用 binds.value。
过滤器和指令的边界:一个管数据,一个管元素
为什么需要:同样给“天龙八部”加书名号,过滤器和指令效果看似一样,但作用对象不同。过滤器针对数据,指令针对元素。
怎么想:{{bookName | book}} 只处理 bookName 这个值;v-book 处理的是元素内部整体内容。若元素里还有别的文字,el.innerText = "《"+el.innerText+"》" 会把整体包起来。
怎么做:运行后输入框改变 bookName,观察 v-book 所在 p。首次挂载前 el.innerText 还是 {{bookName}},所以可能包到模板文本;数据更新后指令再次执行,又会在旧内容上再包一层,出现书名号叠加。
易错点:转写中反复调试“为什么书名号越来越多”,根因就是普通函数写法在更新时重复执行,并且直接改 el.innerText。若只是格式化数据,用过滤器更合适;若只希望初始化做一次,应使用钩子函数 bind 或 inserted。
钩子函数 bind 与 inserted:执行时机决定成败
为什么需要:普通指令函数在初次挂载前执行,数据更新后还会再执行。自动聚焦、只绑一次事件这类需求,必须控制执行次数和时机。
怎么想:bind 在挂载前执行,数据更新后不再执行,适合“只绑一次事件、只初始化一次样式”;inserted 在挂载完成后执行一次,适合“元素已经进入 DOM 后操作”,例如 el.focus()。
怎么做:自动聚焦若写在普通函数或 bind 里,focus() 先加上,随后挂载/更新过程可能又把焦点覆盖掉;改用 inserted 后执行 el.focus() 就稳定。防抖指令也应把 addEventListener 放在 bind 中,避免每次更新重复绑定;事件回调里再用定时器清理旧定时器,最后调用 binds.value。
易错点:bind 中拿到的 el 是解析阶段的元素,不能想当然认为视图已完全更新;需要真实挂载完成后的元素,用 inserted。全局指令用 Vue.directive('color', fn),局部指令只在当前实例/组件可用。
侦听的基本语法:监听变化后做联动
为什么需要:一个数据变化后要驱动另一个数据或发请求,可以用 watch。它监听某个数据,变化时执行回调。
怎么做:函数写法 watch: { name(newValue, oldValue) { ... } };对象写法用 handler,immediate: true 表示立即执行一次,deep: true 表示深度侦听对象内部属性。对象只想监听某属性,可写 'obj.userName'。Vue 2 中直接改数组下标、直接改对象新增属性可能不触发响应式和侦听,需用 Vue.set 或替换整个数组/对象。
易错点:普通侦听对象属性变化时,若引用地址没变可能监听不到;深度侦听能听到但开销更大。数组下标直接赋值是面试常问坑,先记住用 Vue.set(arr, index, value) 或返回新数组。
先分清:函数简写与对象钩子
为什么需要:转写稿里先写了一个 v-book,用函数简写给内容加书名号。初次加载正常,但输入框改数据后,书名号越套越多。原因是函数简写不只在初始化时执行,数据更新时还会再执行。做“只初始化一次”的事情时,就要改用对象钩子,明确写到 bind。
怎么想:先判断这件事是“一次性初始化”还是“每次更新都要做”。加书名号如果依赖最新数据,应该用过滤器/计算属性;如果只是给元素做一次初始化,比如绑事件、设初始样式,才用 bind。
怎么做:把函数简写改成对象写法:
js
directives: {
book: {
bind(el) {
el.innerText = '《' + el.innerText + '》'
}
}
}易错点:bind 只执行一次,数据更新后不会再跑;不要用它做依赖最新数据的格式化。转写稿调试时在 bind 和 mounted 里都打印,控制台先输出 bind、后输出 mounted,说明 bind 在挂载前执行。bind 里的 this 指向 window,不要试图用 this 操作组件实例。
bind 的执行时机与 EL 边界
为什么需要:转写稿想给输入框自动聚焦,先在 bind 里写 el.focus(),结果没生效;用 document.querySelector 也一样。原因是 bind 执行时还没挂载完成,后面视图更新会把焦点覆盖掉。
怎么想:需要“真实 DOM 已经挂载完成”再操作,应该用 inserted。转写稿对比发现:bind 里读 v-model 绑定的值可能拿不到,inserted 里能拿到;inserted 和 mounted 都在挂载完成后执行,但 inserted 先于 mounted。
怎么做:
js
directives: {
focus: {
inserted(el) {
el.focus()
}
}
}易错点:bind 里操作焦点、测量尺寸、依赖子节点等,可能被后续挂载/更新覆盖;这类逻辑放 inserted。inserted 也只执行一次,不是每次更新都执行。
bind 做一次性事件绑定:防抖指令
为什么需要:转写稿做搜索输入,未用指令时每次输入都发请求;手写 setTimeout 但忘记 clearTimeout,仍然执行多次。自定义指令可以把“绑定一次 input 事件 + 防抖”封装起来。
怎么想:事件只绑一次,适合放 bind;防抖计时器用闭包保存,不能写 this.timer,因为 bind 中 this 是 window。回调里通过 binding.value() 调用组件方法。
怎么做:
js
directives: {
debounce: {
bind(el, binding) {
let timer = null
el.addEventListener('input', () => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
binding.value()
}, 300)
})
}
}
}模板:<input v-debounce="send">。转写稿还演示了用修饰符/参数指定时间:binding.modifiers 是对象,可用 Object.keys(binding.modifiers) 取键名再转数字;binding.arg 则接收冒号后的参数。
易错点:不要在 bind 里用 this.timer;不要重复 addEventListener;防抖时间从修饰符取时要判空并给默认值。
全局与局部、与 inserted 的选择
为什么需要:局部指令只在当前组件可用;多个组件要用就定义全局指令。转写稿在组件内写 directives 后,App 里用同名指令报“指令未找到”,改成 Vue.directive 才生效。
怎么想:一次性初始化用 bind;需要真实 DOM/挂载完成用 inserted;需要每次更新执行才用 update 或函数简写。
怎么做:
js
Vue.directive('color', {
bind(el, binding) {
el.style.color = binding.value
}
})局部写法是组件选项 directives: { color: { bind(el, binding) { ... } } }。
易错点:全局是 Vue.directive,没有 s;组件内是 directives,有 s。bind 不随数据更新,inserted 也只执行一次;别把“每次更新都要做”的逻辑误放进 bind。
从“自动聚焦”需求看自定义指令
为什么需要:输入框自动获取焦点,最直接是写 autofocus,但转写稿里明确提到:低版本浏览器可能不支持这个属性;而用 mounted 里 document.querySelector("input").focus() 或 this.$refs.inp.focus() 虽然能工作,却把“聚焦”这件事写死在某个组件里,复用性差。自定义指令正好把“操作元素”的逻辑封装成 v-focus。
怎么想:自定义指令不针对数据格式化,而针对元素本身。我们想:指令拿到元素后调用 el.focus() 即可。但关键不是“写不写得出”,而是“在哪个钩子里写”。
怎么做:在 directives 中声明 focus,先可以用函数简写验证;真正稳定写法是用对象钩子 inserted(el)。
易错点:指令名在模板里写 v-focus,在 directives 里写 focus,不要写成 v-focus 作为键名。
先复现问题:bind 阶段 el.focus() 为什么失效
为什么需要:很多同学会直接写 focus(el){ el.focus() },发现没效果,于是怀疑 focus() 方法不对。其实要理解执行时机。
怎么想:函数简写等价于 bind 钩子。转写稿的调试过程是:在 bind 中打印 document.querySelector("input").value,发现取不到 v-model 绑定后的值;此时挂载尚未完成,v-model 还没生效。更关键的是,后面还会继续挂载、更新视图,刚加上的焦点可能被更新过程“干掉”。
怎么做:转写稿里用 setTimeout(() => { document.querySelector("input").focus(); }, 200) 可以解决,因为延迟到挂载更新之后。但这是权宜方案,不够优雅。
易错点:把“代码执行了”当成“时机对了”;用全局 document.querySelector 找元素,组件多时容易找错,应优先用钩子参数 el。
inserted 钩子的执行时机与验证
为什么需要:我们需要一个“元素已经挂载完成”的时机,此时 v-model 已生效,元素也稳定,适合做一次性的 DOM 操作,比如聚焦。
怎么想:Vue 指令提供 inserted 钩子,表示被绑定元素插入父节点时调用。转写稿验证:在 inserted 中打印 el === document.querySelector("input"),结果为 true,说明 el 就是指令所在元素;再打印 document.querySelector("input").value,能看到 "字符串",说明 v-model 已绑定。inserted 只执行一次,数据更新不会再次触发。
怎么做:写成 focus: { inserted(el){ el.focus(); } }。同时可在 mounted 里打印值,观察顺序:先执行 inserted,再执行 mounted,二者都在挂载完成后。
易错点:inserted 不是数据更新钩子;如果数据变化后还要操作 DOM,应使用 updated 等钩子。转写稿也提醒:指令中 this 指向 window,不建议在指令里操作 Vue 实例属性,应围绕 el 做元素操作。
完整示例代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<input v-focus ref="inp" v-model="str" type="text">
<p>{{str}}</p>
</div>
</body>
<script>
new Vue({
el:"#app",
data:{
str:"字符串"
},
directives:{
// focus(el){
// setTimeout(()=>{
// document.querySelector("input").focus();
// },200)
// // el.focus();
// // document.querySelector("input").focus();
// // console.log(document.querySelector("input").value)
// }
focus:{
// 当挂载完成之后执行。
inserted(el){
console.log(el === document.querySelector("input"))
el.focus();
// document.querySelector("input").focus()
// console.log("inserted",document.querySelector("input").value)
}
}
},
mounted(){
// document.querySelector("input").focus();
// this.$refs.inp.focus();
console.log("mounted",document.querySelector("input").value)
}
})
</script>
</html>调试结论与易错点清单
为什么需要:把调试过程沉淀成判断标准,遇到“指令没效果”时能快速定位。
怎么想:先问“钩子时机对不对”,再问“元素找得对不对”,最后问“是否被后续更新覆盖”。
怎么做:用 inserted 做一次性 DOM 操作;用 el 而不是全局选择器;需要观察值时在 inserted 和 mounted 分别打印,确认顺序和值。转写稿的结论是:inserted 在挂载完成后执行,且只执行一次;mounted 也在挂载完成后执行,但 inserted 先于 mounted。
易错点:① 在 bind 里做聚焦,容易因挂载未完成或后续更新失效;② 用 setTimeout 掩盖时机问题;③ 忘记 inserted 只执行一次,误以为数据变化会再次触发;④ 在指令中依赖 this 访问实例属性。
先看目标:为什么需要全局指令
本节要解决的是“把自定义指令注册成全局指令”。笔记里先在组件 One 中写了局部指令的注释:
js
// 局部指定,在哪里声明只允许在哪里使用
// directives:{
// color(el,binds){
// el.style.color = binds.value;
// }
// }局部指令的问题是:它只在声明它的组件内部有效。转写稿中把 v-color 用在组件 One 里可以,但拿到外层 #app 的 <h3> 上就报“指令没有找到”。这说明:如果多个组件、甚至根实例都要用同一个指令,就应该把它注册为全局指令。全局指令的核心价值是“定义一次,多个组件都能用”,和全局过滤器、全局组件是同一类思路。
怎么想:先判断这个指令是否跨组件复用;如果只在某个组件里用,局部 directives 就够;如果要跨组件用,就在 new Vue 之前用 Vue.directive 注册。怎么做:名字不带 v-,使用时才写 v-color。易错点:Vue.directive 没有 s,不要写成 Vue.directives;全局注册要放在创建实例之前。
从局部报错推出全局注册
转写稿的调试过程很典型:组件 One 内使用 v-color="red" 时,因为组件里声明了局部 directives,所以能生效;但把同样的 v-color 写到外层 <h3> 上,控制台报指令未定义。原因不是写法错,而是“局部指令只允许在哪里声明、在哪里使用”。
于是把指令从组件内部移到全局:
js
Vue.directive("color", {
bind(el, binds) {
el.style.color = binds.value;
}
})这样 #app 里的 <h3 v-color="'green'">app</h3> 和组件 One 里的 <p v-color="red">{{str}}</p> 都能生效。怎么想:先看使用范围,再决定注册位置。怎么做:全局注册放在 new Vue 前面;组件内部不再重复声明。易错点:v-color="'green'" 里的 'green' 是字符串字面量;如果写 v-color="green",Vue 会去实例数据里找 green,找不到就得不到想要的值。
全局指令的写法与参数
全局指令有两种常见写法。笔记里注释掉的是函数简写:
js
// Vue.directive("color",function(el,binds){
// el.style.color = binds.value;
// })实际保留的是对象写法:
js
Vue.directive("color",{
bind(el,binds){
el.style.color = binds.value;
}
})怎么想:el 是指令所在的真实元素,binds 是绑定信息对象;颜色值从 binds.value 取。怎么做:在 bind 中直接操作 el.style.color。易错点:指令函数里的 this 不是 Vue 实例,转写稿中调试时 this 指向 window,所以不要指望在指令里随便操作实例数据;需要值就通过 binding.value 传进来。
用对象钩子控制执行时机
转写稿里反复强调:函数简写会在绑定和更新时被调用;如果只想在初始化时执行一次,就用对象写法里的 bind。现场验证过:bind 在挂载前执行,数据更新后不会再次执行。对于“设置一次颜色”这种需求,bind 足够;如果逻辑依赖挂载后的 DOM,就不能只靠 bind,应使用 inserted 等钩子,但本节重点是全局指令定义,不展开。
为什么需要:避免每次数据更新都重复设置同一个样式。怎么想:先问“这个指令逻辑要不要随更新重复执行”。怎么做:只初始化一次就用 bind;需要更新时再用 update 等钩子。易错点:不要以为对象写法一定比函数写法“高级”,它们解决的是执行时机问题;全局指令名不要写成 v-color,注册时写 color。
完整代码与运行检查
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<h3 v-color="'green'">app</h3>
<One></One>
</div>
</body>
<script>
// Vue.directive("color",function(el,binds){
// el.style.color = binds.value;
// })
Vue.directive("color",{
bind(el,binds){
el.style.color = binds.value;
}
})
new Vue({
el: "#app",
components: {
One: {
data() {
return {
str: "字符串",
red:"red"
}
},
template: (`
<p v-color="red">{{str}}</p>
`),
// 局部指定,在哪里声明只允许在哪里使用
// directives:{
// color(el,binds){
// el.style.color = binds.value;
// }
// }
}
}
})
</script>
</html>运行检查:<h3> 应显示绿色,组件内 <p> 应显示红色;如果外层报“指令没有找到”,说明全局指令没注册或注册位置在 new Vue 之后;如果颜色没变,检查 binds.value 是否拿到值,以及 v-color 的值是否被当成实例属性解析。易错点:组件局部 directives 与全局 Vue.directive 不要混用同名逻辑;全局指令注册一次即可,组件内不用再声明。
侦听要解决什么问题
为什么需要:有些逻辑不是模板里直接算出来的,而是“某个数据变了,我要顺手做另一件事”。转写稿里把这种能力叫“侦听/监听/监视”,意思都是监听某个数据是否改变,数据改变时驱动其他数据变化或执行其他行为。
怎么想:先明确“监听谁”,再明确“变了以后干什么”。在 Vue 2 选项式写法里,监听配置放在 watch 中,属性名就是要监听的数据名。
怎么做:先写最小例子:name 是数字,a 是空字符串;点击按钮让 name++,希望 a 变成 name + '!'。
易错点:不要一上来写复杂逻辑,先用 console.log 验证是否触发;watch 函数里的 this 指向 Vue 实例,和转写稿中打印结果一致。
函数写法:侦听数字与字符串
为什么需要:最常用的侦听写法是函数形式,适合“数据变化后执行一段逻辑”。
怎么想:把“点击按钮改数据”和“数据变化后改另一个数据”分开。转写稿中先只写 name++,发现数字变了但 a 没变;再加 watch,a 才跟着变。
怎么做:
js
data() {
return { name: 1, a: '', str: '今天有点热' }
},
watch: {
name(newVal, oldVal) {
console.log(newVal, oldVal, this)
this.a = newVal + '!'
},
str(newVal, oldVal) {
console.log(newVal, oldVal)
}
}调试时点击按钮,控制台会打印修改后的值和修改前的值,this 是 Vue 实例,所以可以写 this.a = ...。字符串 str 通过 v-model 修改后也能被侦听。
易错点:参数顺序是 newVal, oldVal,不要写反;初始值不会自动触发;函数中的 this 不要丢。
对象写法:handler 与 immediate
为什么需要:有时希望页面一加载就根据初始值算一次,而不是等数据变化后才执行。转写稿中 a 的初始值不会自动变成 name + '!',需要在 mounted 里写一次,但认为这会多一次数据更新,不如用 immediate。
怎么想:把函数写法改成对象写法,固定写 handler,再配置 immediate。
怎么做:
js
watch: {
name: {
handler(newVal, oldVal) {
this.a = newVal + '!'
},
immediate: true
}
}转写稿中验证:不加 immediate 时刷新不执行,只有数据改变才执行;加上 immediate: true 后刷新立即执行一次,之后数据改变还会再次执行。
易错点:handler 名字不能改;immediate 默认是 false;立即执行时 oldVal 通常是 undefined。
侦听对象:deep 与属性路径
为什么需要:对象里某个属性变了,普通 watch 可能收不到。转写稿中改 obj.userName、obj.age,只写 watch: { obj(...) } 不会触发,因为对象的引用地址没变。
怎么想:要么深度侦听整个对象,要么直接侦听对象属性路径。
怎么做:
js
watch: {
obj: {
handler(newVal, oldVal) {
console.log(newVal, oldVal)
},
deep: true
},
'obj.userName'(newVal, oldVal) {
console.log(newVal, oldVal)
}
}转写稿中调试:直接改属性没有输出;改整个 obj 的引用会触发;加 deep: true 后改属性会触发。但深度侦听时 newVal 和 oldVal 往往指向同一个对象,展开看到的值一样,因为引用地址没变。
易错点:属性路径要加引号;只想监听某个属性时用 'obj.userName',不要盲目深度侦听;深度侦听有性能成本。
侦听数组:下标修改的坑与 $set
为什么需要:数组很常见,但直接改下标容易“视图不更新、侦听也不触发”。转写稿中 arr: [1,2,3,4],用 push 能触发响应式和侦听;写 this.arr[3] = 100 却不触发,深度侦听也不解决。
怎么想:Vue 2 能拦截数组变更方法,但直接下标赋值不是响应式。解决思路有两个:替换整个数组改变引用,或用 this.$set。
怎么做:
js
methods: {
changeByMap() {
this.arr = this.arr.map((item, index) => index === 3 ? 100 : item)
},
changeBySet() {
this.$set(this.arr, 3, 100)
}
}转写稿中验证:push 能触发侦听;arr[3] = 100 无输出;用 map 生成新数组后能触发;用 this.$set(this.arr, 3, 100) 后视图和侦听都正常。
易错点:$set 参数是“目标、索引/键、值”;不要以为 deep: true 能救直接下标赋值;替换数组时注意返回新数组。
先明确:本节学的是 watch 侦听的基本使用
为什么需要:模板里经常出现“一个数据变了,另一个数据也要跟着变”的场景。比如 num 加一后,让 a 变成 num + '!'。如果每个按钮事件里都重复写一遍赋值,代码会重复;用 watch 可以把“谁变了以后做什么”集中配置起来。
怎么想:watch 不是事件,而是“侦听某个数据是否变化”。变化后执行函数或对象的 handler,并拿到 newValue、oldValue。
怎么做:在 Vue 实例配置里写 watch: { 数据名(newValue, oldValue){ ... } },或写成对象形式 { handler(){}, immediate, deep }。
易错点:handler 名字不能改;函数中的 this 指向 Vue 实例;immediate: true 会立即执行一次,此时初始 oldValue 往往没有意义;不要把 watch 和计算属性混为一谈。
数字侦听:函数写法与对象写法
为什么需要:转写稿里先做的是点击按钮让 num++,再让 a = num + '!'。如果直接写在模板里,重复逻辑会散落多处;放进方法里可以复用,但“数据变化后自动执行”更适合 watch。
怎么想:侦听 num,当 num 改变时把 a 改成 newValue + '!'。
怎么做:可以写函数形式:
js
num(newValue, oldValue){
this.a = newValue + "!";
}也可以写对象形式:
js
num: {
handler(newValue, oldValue){
this.a = this.num + "!";
},
immediate: true
}转写稿中通过点击按钮观察控制台,确认 newValue 是修改后的值,oldValue 是修改前的值,且 this 是 Vue 实例。
易错点:immediate: true 表示立即执行,并且之后数据改变还会再次执行;默认 false 时只有数据变化才执行。对象写法里属性名必须是 handler。
字符串与对象侦听:普通 watch 与 deep、属性路径
为什么需要:v-model 绑定字符串、对象属性时,数据会变,但“要不要触发侦听”取决于侦听方式。
怎么想:字符串 str 直接侦听即可;对象 obj 如果只改 obj.userName、obj.age,对象引用地址没变,普通 obj(newValue, oldValue) 侦听不到。
怎么做:对象有两种方案。方案一使用深度侦听:
js
obj: {
handler(newValue, oldValue){
console.log("obj", newValue, oldValue);
},
deep: true
}方案二直接侦听属性路径:
js
"obj.userName"(newValue, oldValue){
console.log("obj.userName", newValue, oldValue);
}转写稿中调试时发现:只改对象属性,视图能响应式更新,但普通 watch 不触发;加上 deep: true 或侦听 "obj.userName" 后才能触发。
易错点:深度侦听时新旧值往往指向同一个对象引用,展开后看起来一样;如果要知道具体哪个属性变了,用属性路径更直接。
数组侦听:push 可以,索引赋值要小心
为什么需要:数组很常见,但 arr[3] = 100 这种写法在 Vue 2 里不支持响应式,也不支持侦听,连深度侦听也不行。转写稿里专门用控制台验证了这一点。
怎么想:push 这类数组变异方法可以触发响应式和侦听;直接改下标不会。要改某个元素,就换数组引用或使用 $set。
怎么做:方案一换引用:
js
this.arr = this.arr.map((item, index) => {
if (index === 3) item = 100;
return item;
});方案二:
js
this.$set(this.arr, 3, 100);易错点:map 里必须 return item;$set 第一个参数是目标数组/对象,第二个是索引/键,第三个是新值。转写稿中改完后点击按钮,控制台能输出 arr 的新旧值,说明侦听恢复。
本节笔记代码保留
下面保留本节笔记中的完整示例,便于对照上面的知识点:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<button @click="num++;a=num+'!'">{{num}}</button>
<button @click="num++;a=num+'!'">{{num}}</button>
<button @click="changeNum">{{num}}</button>
<p>a:{{a}}</p>
<!--数字-->
<button @click="num++">{{num}}</button>
<hr/>
<!--字符串-->
<input type="text" v-model="str">
<hr/>
<!--对象-->
<input type="text" v-model="obj.userName">
<input type="text" v-model.number="obj.age">
<button @click="changeObj">更改obj</button>
<p>{{obj}}</p>
<hr/>
<!--数组-->
<button @click="changeArr">更改</button>
<p>{{arr}}</p>
</div>
</body>
<script>
// 通过配置watch对象可以实现侦听
new Vue({
el: "#app",
data: {
num: 1,
a: "asdfasdf",
str:"今天有点热!",
obj:{
userName:"zhangsan",
age:12
},
arr:[1,2,3,4]
},
methods: {
changeArr(){
// 支持响应式
// this.arr.push(this.arr.length+1);
// 更改某一个元素:不支持响应式,也不支持侦听(包括深度侦听也不支持)。
// this.arr[3] = 100;
// 解决方案一:通过更改数组的引用地址
// this.arr = this.arr.map((item,index)=>{
// if(index===3) item = 100;
// return item;
// })
// 解决方案二:通过实例中的$set方法。
// this.$set(this.arr,3,100);
},
changeNum() {
this.num++;
this.a = this.num + '!'
},
changeObj(){
this.obj.userName="lisi";
this.obj.age = 100;
// this.obj = {
// userName:"lisi",
// age:100
// }
}
},
watch: {
/*************数组*****************************************/
arr(newValue,oldValue){
console.log("arr",newValue,oldValue);
}
// arr:{
// handler(newValue,oldValue){
// console.log("arr",newValue,oldValue);
// },
// deep:true
// }
/*************对象*****************************************/
// 如果对象的引用地址未发生更改,用以下方法无法进行侦听。
// 如果只是更改对象的属性值,支持响应式,但是无法侦听。
// obj(newValue,oldValue){
// console.log("obj",newValue,oldValue)
// },
// 如果对象的属性发生改变,还要进行侦听,要两种方法
// 方案一:使用深度侦听
// obj:{
// handler(newValue,oldValue){
// console.log("obj",newValue,oldValue)
// },
// // 深度侦听:属性发生改变也可以侦听到对象。
// deep:true
// }
// 方案二:侦听对象的属性
// 当obj下的userName属性发生改变后,会执行
// "obj.userName"(newValue,oldValue){
// console.log("obj.userName",newValue,oldValue);
// },
// "obj.age":{
// handler(newValue,oldValue){
// console.log("obj.age",newValue,oldValue);
// }
// }
/*************字符串*****************************************/
// str(newValue,oldValue){
// console.log("str",newValue,oldValue)
// }
// str:{
// handler(newValue,oldValue){
// console.log("str",newValue,oldValue)
// },
// immediate:true
// }
/*************数字*****************************************/
// 注意:侦听的值类型可以是函数也可以是对象
// 1- 函数
// 侦听num-类型是数字
// 当num发生改变,会执行该函数。
// 接收两个参数:newValue(修改后的值),oldValue(修改前的值)
// 函数中的this指向的是Vue实例。
// num(newValue,oldValue){
// // console.log("num",newValue,oldValue,this)
// this.a = newValue+"!";
// }
// 2- 对象
// num: {
// // 句柄:要干什么。
// handler(newValue, oldValue) {
// // console.log("handler",newValue,oldValue);
// this.a = this.num + "!";
// },
// // true:代表立即执行,且数据发生改变会再次执行
// // false:默认值,只有据发生改变后才会执行
// immediate: true
// }
}
})
</script>
</html>小结与易错点复盘
本节要抓住三件事:第一,watch 用来侦听数据变化,函数写法简洁,对象写法可配 handler、immediate、deep;第二,对象只改属性时,普通侦听不触发,要用 deep 或侦听属性路径;第三,数组下标赋值 arr[3] = 100 不支持响应式和侦听,需换引用或 this.$set。调试时按转写稿的方式:点击按钮、改输入框、看控制台输出的 newValue 与 oldValue,再分别切换注释验证 deep、immediate 和 $set 的效果。
本节要解决的问题:点击按钮切换新闻列表
为什么需要:页面上有一组“体育/娱乐/财经”按钮,下方要显示对应分类的新闻。只靠静态模板无法完成切换,需要把“当前选中项”和“当前展示列表”联动起来。
怎么想:把 index 当作当前选中分类的下标;newsList 是全部数据;infoArr 是下方真正渲染的列表。点击按钮只改 index,再用 watch 侦听 index,把它对应的 infoList 赋给 infoArr。
怎么做:按钮用 v-for 渲染,:class="{active:index===i}" 控制高亮,@click="index=i" 修改下标;下方用 v-for="item in infoArr" 渲染标题。完整代码如下:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
#app button{
padding:5px;
margin:5px;
}
#app button.active{
background: red;
}
#app div{
width: 500px;
height: 300px;
border:1px solid green;
}
</style>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<button :class="{active:index===i}" @click="index=i" v-for="(item,i) in newsList" :key="i">{{item.type}}</button>
<div>
<p v-for="item in infoArr" :key="item.id">{{item.newsTitle}}</p>
</div>
</div>
</body>
<script>
new Vue({
el:"#app",
data:{
index:1,
infoArr:[],
newsList:[
{
type:"体育",
infoList:[
{
id:11,
newsTitle:"体育1"
},
{
id:12,
newsTitle:"体育2"
}
]
},
{
type:"娱乐",
infoList:[
{
id:21,
newsTitle:"娱乐1"
},
{
id:22,
newsTitle:"娱乐2"
}
]
},
{
type:"财经",
infoList:[
{
id:31,
newsTitle:"财经1"
},
{
id:32,
newsTitle:"财经2"
}
]
}
]
},
watch:{
// index(){
// this.infoArr = this.newsList[this.index].infoList;
// }
index:{
handler(){
this.infoArr = this.newsList[this.index].infoList;
},
immediate:true
}
}
})
</script>
</html>易错点:index 初始为 1,默认显示“娱乐”;infoArr 初始是空数组,如果不做初始化,首次进入页面下方不会显示内容;v-for 的 key 这里用了 i,数据稳定时可用,项目里更建议用唯一 id。
用 watch 侦听 index 的两种写法
为什么需要:点击按钮只是改了 index,并不会自动把 newsList[index].infoList 赋给 infoArr。需要“某个数据变了,就驱动另一件事”。
怎么想:watch 就是侦听/监听/监视某个数据。转写稿里反复调试到:watch 里的函数会在数据改变后执行,函数中的 this 指向 Vue 实例,所以能访问 this.newsList 和 this.index。
怎么做:第一种是函数形式:
js
watch:{
index(){
this.infoArr = this.newsList[this.index].infoList;
}
}第二种是对象形式,使用 handler 和 immediate:
js
watch:{
index:{
handler(){
this.infoArr = this.newsList[this.index].infoList;
},
immediate:true
}
}易错点:handler 名字不能随便改;immediate:true 表示立即先执行一次,默认是 false,只有数据变化才执行。本节代码用 immediate:true,是为了页面一加载就把默认分类的新闻放进 infoArr。另外不要用箭头函数写 handler,否则 this 容易不是 Vue 实例。
调试过程:先看 index,再看 infoArr
为什么需要:切换不生效时,要判断是点击没改 index,还是 watch 没执行,还是 infoArr 赋值不对。
怎么想:在 handler 里打印修改前后的值。转写稿中调试 watch 时用 newValue、oldValue 接收参数:newValue 是修改后的值,oldValue 是修改前的值。点击按钮后 index 从 1 变成 0 或 2,watch 才会触发;如果重复点击同一个按钮,index 没变,watch 不会再次执行。
怎么做:
js
index:{
handler(newValue, oldValue){
console.log(newValue, oldValue);
this.infoArr = this.newsList[this.index].infoList;
},
immediate:true
}易错点:newValue/oldValue 是形参名,可以换成别的名字,但顺序不能乱;immediate:true 第一次执行时没有“旧值”,oldValue 通常是 undefined。
侦听对象和数组时的注意点
为什么需要:项目里不只侦听数字,还会侦听字符串、对象、数组。转写稿里专门调试到:对象属性变化、数组索引赋值,普通 watch 不一定能侦听到。
怎么想:对象普通侦听看的是引用地址,直接改 obj.userName 地址没变,所以不一定触发;数组直接 arr[3] = 100 也不触发响应式和侦听。需要侦听对象某个属性时,可以写 'obj.userName';需要整体深度侦听时用 deep:true;数组改索引可用 this.$set(arr, index, value),或改一个新数组/新对象让引用地址变化。
怎么做:
js
watch:{
'obj.userName'(newValue, oldValue){
console.log(newValue, oldValue);
},
arr:{
handler(newValue){
console.log(newValue);
},
deep:true
}
}数组索引修改可写:
js
this.$set(this.arr, 3, 100);易错点:deep:true 有性能开销,不要滥用;深度侦听对象时,newValue 和 oldValue 可能指向同一个引用,打印出来看起来一样;数组的 push、pop 等变更方法通常能触发,但直接按下标赋值不行,转写稿中明确提到“数组索引赋值不支持响应式,也不支持侦听,包括深度侦听”,解决方式就是改引用地址或用 $set。
课堂画面






代码示例
示例一:自定义指令基本使用(8-1)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<input v-model="bookName" type="text">
<p>{{bookName}}</p>
<p>{{bookName | book}}1</p>
<p v-book>{{bookName}}1</p>
<p v-red>{{bookName}}</p>
<p v-green v-book>{{bookName}}</p>
<p v-color="'pink'">{{bookName}}</p>
<p v-color="'blue'">{{bookName}}</p>
<p v-style:background="'red'">{{bookName}}</p>
<p v-style:background.lazy="'green'">{{bookName}}</p>
</div>
</body>
<script>
// 说明:过滤器针对的是某一个数据,自定义的指令针对的是元素。
// 指令分为:内置指令以及自定义指令。
// 自定义指令分为:全局以及局部指令,通过属性 directive
// 指令的构成:v-name:arg.modifier=value
new Vue({
el: "#app",
data: {
bookName: "天龙八部"
},
filters: {
book(v){
return "《" + v + "》";
}
},
directives: {
// 定义一个名字为 book 的指令
// 接收的第一个参数是指令所在的元素对象。
book(el){
// 1-this 指向的是 window,所以在指令中不建议操作实例属性的。
// 2-自定义的指令是在挂载前执行。初次执行时,数据与视图还未进行绑定:{{bookName}}
// 3-当数据发生更改后,视图要更新,指令会再次执行,在此处通过 document 获取的数据是最新的。
el.innerText = "《" + el.innerText + "》";
},
red(el){
el.style.color = "red";
},
green(el){
el.style.color = "green";
},
color(el, binds){
// v-color="'pink'"
// expression:"'pink'"--->= 右侧
// modifiers:{}---->修饰符
// name:"color"---->自定义指令的名字
// rawName:"v-color"--->带 v-+name
// value:"pink"
el.style.color = binds.value;
},
style(el, binds){
// arg:"background"--->参数
if(binds.modifiers.lazy){
setTimeout(()=>{
el.style[binds.arg] = binds.value;
}, 2000)
}else{
el.style[binds.arg] = binds.value;
}
}
},
mounted(){
console.log("mounted");
}
})
</script>
</html>关键行与易错点
directives: { book(el){} }里写的是指令名,模板中用v-book;键名不要再带v-,否则要写成v-v-book。- 第一个参数
el就是指令所在的元素;第二个参数binds是对象,含value / expression / arg / modifiers / name / rawName,取值用binds.value,取参数用binds.arg。 v-color="'pink'"必须写内层单引号,否则 Vue 会把pink当作实例上的数据去取,得到undefined。v-style:background.lazy="'green'"中background是arg、lazy是modifiers,所以用el.style[binds.arg] = binds.value动态设样式,再判断binds.modifiers.lazy决定是否延迟。- 指令函数里的
this指向 window,不要指望在指令里this.bookName;指令只拿得到元素。 - 初次执行在挂载前,此时
el.innerText里还是{{bookName}}原文;数据变化后视图更新,指令会再次执行,el.innerText已是上一轮加过书名号的内容,所以会越叠越多——想格式化“数据”用过滤器,指令是用来“操作元素”的。 - 一个元素可同时挂多个指令(
v-green v-book),按书写顺序执行。
示例二:inserted 钩子函数(8-3)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<input v-focus ref="inp" v-model="str" type="text">
<p>{{str}}</p>
</div>
</body>
<script>
new Vue({
el: "#app",
data: {
str: "字符串"
},
directives: {
// 函数简写形式:在挂载前执行,此时 el.focus() 会被随后的挂载/视图更新冲掉
// focus(el){
// setTimeout(()=>{
// document.querySelector("input").focus();
// }, 200)
// }
focus: {
// 当挂载完成之后执行。
inserted(el){
console.log(el === document.querySelector("input"));
el.focus();
// document.querySelector("input").focus()
// console.log("inserted", document.querySelector("input").value)
}
}
},
mounted(){
// document.querySelector("input").focus();
// this.$refs.inp.focus();
console.log("mounted", document.querySelector("input").value);
}
})
</script>
</html>关键行与易错点
- 指令写成对象形式即可使用钩子:
bind(挂载前执行一次)、inserted(挂载完成后执行一次)、update等;写成函数只是bind的简写。 inserted里el已经是真实 DOM,且v-model已经生效,所以el.focus()能拿到焦点、el.value有值;而bind(函数简写)里document.querySelector("input").value是空串。- 如果非要用函数简写做聚焦,笔记里用
setTimeout(..., 200)兜底:因为焦点加上后,后续挂载/视图更新会把它冲掉。 el === document.querySelector("input")返回true,说明el就是指令所在元素,不必再自己查 DOM。inserted只执行一次,数据变化后不会再执行,适合做“一次性 DOM 初始化”。- 全局指令写法(8-4):
Vue.directive("color", { bind(el, binds){ el.style.color = binds.value } });注意是directive(单数、无 s),写错成directives会静默失效。局部指令只能在声明它的实例/组件内使用,跨组件用会报“指令未定义”。
示例三:侦听基本使用(9-1)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<!--数字-->
<button @click="num++">{{num}}</button>
<p>a:{{a}}</p>
<hr/>
<!--字符串-->
<input type="text" v-model="str">
<hr/>
<!--对象-->
<input type="text" v-model="obj.userName">
<input type="text" v-model.number="obj.age">
<p>{{obj}}</p>
<hr/>
<!--数组-->
<button @click="changeArr">更改</button>
<p>{{arr}}</p>
</div>
</body>
<script>
// 通过配置 watch 对象可以实现侦听
new Vue({
el: "#app",
data: {
num: 1,
a: "asdfasdf",
str: "今天有点热!",
obj: {
userName: "zhangsan",
age: 12
},
arr: [1, 2, 3, 4]
},
methods: {
changeArr(){
// 支持响应式
this.arr.push(this.arr.length + 1);
// 更改某一个元素:不支持响应式,也不支持侦听(包括深度侦听也不支持)。
// this.arr[3] = 100;
// 解决方案一:通过更改数组的引用地址
// this.arr = this.arr.map((item, index) => {
// if(index === 3) item = 100;
// return item;
// })
// 解决方案二:通过实例中的 $set 方法。
// this.$set(this.arr, 3, 100);
}
},
watch: {
/*************数字:函数写法*****************************************/
// 接收两个参数:newValue(修改后的值), oldValue(修改前的值)
// 函数中的 this 指向的是 Vue 实例。
num(newValue, oldValue){
this.a = newValue + "!";
},
/*************字符串:对象写法 + immediate**************************/
str: {
handler(newValue, oldValue){
console.log("str", newValue, oldValue);
},
// true:代表立即执行,且数据发生改变会再次执行
// false:默认值,只有数据发生改变后才会执行
immediate: true
},
/*************对象:深度侦听***************************************/
// 如果只是更改对象的属性值,支持响应式,但是无法侦听。
// 方案一:深度侦听
obj: {
handler(newValue, oldValue){
console.log("obj", newValue, oldValue);
},
deep: true
},
// 方案二:侦听对象的属性(属性名含特殊字符,必须加引号)
"obj.userName"(newValue, oldValue){
console.log("obj.userName", newValue, oldValue);
},
/*************数组*****************************************/
arr(newValue, oldValue){
console.log("arr", newValue, oldValue);
}
}
})
</script>
</html>关键行与易错点
watch是配置对象,键就是要侦听的数据名;值可以是函数(简写),也可以是带handler的对象。- 函数形式会收到
newValue / oldValue两个参数,函数内的this指向 Vue 实例(和自定义指令不同,指令里是 window)。 - 对象形式中
handler的名字不能改;immediate: true表示首次立即执行一次(此时oldValue是undefined),默认false只有数据变化才执行。 - 侦听对象的属性:
"obj.userName"(newValue, oldValue){}因属性名带点必须加引号,否则会当成表达式报错。 - 只改对象属性、引用地址没变时,普通
obj(new, old){}侦听不到,需要deep: true;但深度侦听时newValue和oldValue是同一个对象引用,打印出来值一样,这是正常现象。 - 数组的坑:
push等变更方法支持响应式也支持侦听;this.arr[3] = 100既不响应式也不触发侦听(deep 也不行),解决办法是this.arr = this.arr.map(...)(换引用地址)或this.$set(this.arr, 3, 100)。 - 类 Vue 场景下侦听器常用于“一个数据变、联动另一个数据”,如
num变化驱动a,能大幅减少重复代码。
常见错误
1. 在指令里写 this.xxx 想改实例数据,结果报 undefined 或改了没用
- 现象:
directives: { book(el){ this.a = 1 } }里this打出来是window,拿不到data,改数据视图也不动。 - 原因:函数式指令的调用上下文不是 Vue 实例,Vue 刻意不让你在指令内部反向操作实例状态。
- 修法:指令只操作
el,需要外部数据就通过v-color="'pink'"这种绑定值传入,在第二个参数binding.value里取;需要随数据变化的格式化逻辑改用filters或computed。
2. 指令里加事件监听/拼接文本,数据一改就重复叠加
- 现象:
el.innerText = "《" + el.innerText + "》"每输入一个字书名号就多一层;el.addEventListener('input', ...)输入几次就触发几次请求。 - 原因:函数式指令在每次组件更新时都会重新执行,同一元素被反复“加工”。
- 修法:需要“只做一次”的初始化(绑事件、设初始样式、取 DOM 引用)写进
bind钩子;确实要随数据更新的部分单独写update钩子,且用el.style.xxx = ...这类幂等赋值,不要基于el.innerText自我累加。
3. 用 bind 做 focus() 或读取 v-model 的值,拿不到或焦点被冲掉
- 现象:
v-focus用函数式/bind写el.focus()无效;bind里el.value是空字符串。 - 原因:
bind执行时元素尚未插入 DOM、v-model尚未把数据写进输入框,后续挂载渲染会把焦点重置掉。 - 修法:改用
inserted(el)(挂载完成、mounted之前触发,且只触发一次);若必须用bind,用setTimeout(..., 0)把操作推到渲染之后。
4. watch 侦听对象属性变化,怎么改都不触发
- 现象:
obj.userName = "lisi"视图更新了,但watch: { obj(){...} }不执行。 - 原因:改属性没换引用,
obj的引用地址没变,普通侦听按引用比较。 - 修法:三选一 ——
obj: { handler(){}, deep: true };侦听字符串路径"obj.userName"(nv, ov){};整体替换this.obj = {...}。注意deep下newValue === oldValue,别指望靠oldValue做 diff。
5. 数组下标赋值 this.arr[3] = 100 既不更新视图也不触发侦听
- 现象:
arr.push()一切正常,arr[3] = 100页面纹丝不动,加了deep: true也没用。 - 原因:Vue2 无法拦截索引赋值和
length修改,这类变更根本不进入响应系统。 - 修法:
this.$set(this.arr, 3, 100),或this.arr = this.arr.map((it,i)=> i===3 ? 100 : it)换引用。
6. 组件里 directives 声明的指令在根模板用,报 “Failed to resolve directive”
- 现象:组件
One内部定义的color指令,在#app的<h3 v-color>上报错。 - 原因:局部指令作用域仅限声明它的组件模板。
- 修法:
Vue.directive("color", { bind(el, binding){...} })全局注册(无s,注意和directives区分)。
7. 指令值没加引号 v-color=pink,报属性未定义
- 现象:控制台提示
pink is not defined,或指令收到undefined。 - 原因:
=右侧被当作表达式在实例作用域求值,pink被当成 data 属性。 - 修法:传字面量写
v-color="'pink'"(外层双引号内层单引号);传变量才直接写v-color="myColor"。
练习
练习 1(指令 vs 过滤器) 页面上有 <input v-model="bookName"> 和三个 <p>:分别用过滤器、v-book 函数式指令、v-book 的 bind 钩子输出《书名》。输入框连续输入 5 个字符,观察三者输出的差异,并解释为什么只有其中一个能保持正确结果。
练习 2(bind + inserted 分工) 实现 v-focus 指令,要求:元素挂载后自动聚焦,且之后修改其他数据导致重渲染时不重新抢焦点。再写一个 v-debounce:500="search" 指令,给输入框绑定 input 事件并做 500ms 防抖,要求:修饰符/参数可配置延迟时间,且事件只绑定一次。思考两个指令分别该用哪个钩子,为什么。
练习 3(watch 侦听对象)data: { form: { name: '', age: 0 } },要求:name 或 age 任一变化时把 form 序列化输出到控制台,且页面初次加载时也要先输出一次。分别用 deep + immediate 和侦听字符串路径两种写法实现,并说明哪种写法在“只想监听某个字段”时更合适。
练习 4(数组响应式修复)data: { list: [1,2,3,4] },写一个按钮点击后把下标 3 的值改为 100,要求视图更新且 watch 能收到通知。给出两种实现($set 与 map 换引用),并用 console.log(newValue, oldValue) 观察两种方案下 oldValue 的差别。
练习 5(综合:把重复代码抽成指令) 有 5 个按钮,点击时都要执行“改数据 + 拼字符串 + 打日志”这套逻辑,目前每个 @click 内联写一遍。请把这段重复逻辑改造成一个自定义指令 + watch 的组合方案,要求逻辑只写一处,并说明为什么这里不适合纯用指令、也不适合纯用 watch。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 自定义指令基本使用 | 33:24 |
| 2 | bind钩子函数 | 42:52 |
| 3 | bind应用 | 18:40 |
| 4 | 指令钩子函数-inserted | 12:14 |
| 5 | 定义全局指令 | 05:08 |
| 6 | 侦听基本语法 | 42:51 |
小结
- 指令作用于元素,过滤器/计算属性作用于数据——同一个“加书名号”的需求,用指令会随更新层层累加,用过滤器才对;选错工具比写错代码更难排查。
- 钩子选择看“要不要重复执行”:
bind只跑一次(适合绑事件、设初始样式),inserted在挂载完成后跑一次(适合focus、依赖真实 DOM 和v-model已生效的操作),函数式写法每次更新都跑(只适合幂等赋值)。 bind里读不到v-model的值是设计使然,不是 bug;需要最新数据要么放inserted,要么在update里处理。- 指令拿数据靠
binding,不靠this:value / arg / modifiers / name / expression五个字段吃透,v-color="'pink'"与v-style:background.lazy这类写法就不会再翻车。 watch的三种形态对应三种场景:函数式最简;对象式为了handler + deep + immediate;字符串路径用于精确监听嵌套字段。默认不监听引用未变的变化,这是最常见的“失效”来源。deep: true有代价:它要递归比较,且newValue === oldValue,拿不到“干净”的旧值;能精确到字段就写"obj.field"。- 数组的坑是 Vue2 响应式边界:索引赋值和
length修改不被拦截,$set或换引用是唯一出路,deep也救不了。 - 全局指令用
Vue.directive(无 s),局部指令用directives(有 s),作用域规则和组件、过滤器完全一致——这三者的注册方式记住一套,其余照搬。