Skip to content

自定义指令与基本使用(Lesson6 1/3) ​

本页对应 Lesson06,共 6 段视频,约 2 小时 35 分。

学习目标 ​

  1. 能说明自定义指令的构成(名称、参数、修饰符、值),并区分内置指令与自定义指令、局部指令与全局指令。
  2. 能使用 Vue.directive 注册全局指令,并在组件内声明局部指令,通过实际页面验证其作用范围。
  3. 能使用 bind 钩子接收 el 与 binding,在挂载前操作元素,并说明数据更新时该钩子是否再次执行。
  4. 能使用 inserted 钩子完成挂载后的 DOM 操作(如自动聚焦),并比较 bind 与 inserted 的执行时机差异。
  5. 能编写基于 bind 的自定义防抖指令,利用 binding.value、参数或修饰符配置延迟时间,并验证连续输入只触发一次。
  6. 能使用 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。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:自定义指令基本使用(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
2bind钩子函数42:52
3bind应用18:40
4指令钩子函数-inserted12:14
5定义全局指令05:08
6侦听基本语法42:51

小结 ​

  1. 指令作用于元素,过滤器/计算属性作用于数据——同一个“加书名号”的需求,用指令会随更新层层累加,用过滤器才对;选错工具比写错代码更难排查。
  2. 钩子选择看“要不要重复执行”:bind 只跑一次(适合绑事件、设初始样式),inserted 在挂载完成后跑一次(适合 focus、依赖真实 DOM 和 v-model 已生效的操作),函数式写法每次更新都跑(只适合幂等赋值)。
  3. bind 里读不到 v-model 的值是设计使然,不是 bug;需要最新数据要么放 inserted,要么在 update 里处理。
  4. 指令拿数据靠 binding,不靠 this:value / arg / modifiers / name / expression 五个字段吃透,v-color="'pink'" 与 v-style:background.lazy 这类写法就不会再翻车。
  5. watch 的三种形态对应三种场景:函数式最简;对象式为了 handler + deep + immediate;字符串路径用于精确监听嵌套字段。默认不监听引用未变的变化,这是最常见的“失效”来源。
  6. deep: true 有代价:它要递归比较,且 newValue === oldValue,拿不到“干净”的旧值;能精确到字段就写 "obj.field"。
  7. 数组的坑是 Vue2 响应式边界:索引赋值和 length 修改不被拦截,$set 或换引用是唯一出路,deep 也救不了。
  8. 全局指令用 Vue.directive(无 s),局部指令用 directives(有 s),作用域规则和组件、过滤器完全一致——这三者的注册方式记住一套,其余照搬。