Skip to content

v-html与v-text与v-if v-else-if v(Lesson2 3/3) ​

本页对应 Lesson02,共 5 段视频,约 1 小时 35 分。

学习目标 ​

  • 能够使用 v-text、v-html、v-if/v-else-if/v-else、v-show 完成文本与条件渲染,并通过页面效果说明 v-if 与 v-show 在 DOM 渲染上的差异。
  • 能够使用 v-bind 动态绑定 src、href、class、style 等属性,使用 v-on 绑定 input、keydown、click 等事件,使用 v-model 完成表单双向绑定,使用 v-for 结合唯一 key 渲染列表。
  • 能够将重复的 JSONP 请求代码封装为返回 Promise 的函数,支持传入请求地址、查询参数和回调函数名,并在页面中调用该函数获取搜索数据。
  • 能够实现模拟百度搜索功能:监听输入内容发起请求、渲染返回的联想列表,并使用 setTimeout 与 clearTimeout 实现防抖,验证高频输入时只发送最后一次请求。
  • 能够实现搜索联想键盘交互:通过 keydown 判断上下方向键维护选中下标,处理边界循环并同步输入框内容;实现点击联想项回填内容并提交表单跳转。
  • 能够使用 v-if 配合 focus、blur 控制联想框显隐,并通过延迟隐藏或状态标记解决点击联想项与失焦隐藏的时序冲突,验证选项可正常点击。

知识点 ​

先明确本节目标与响应式前提 ​

为什么需要:v-text、v-html都是Vue指令,用来把实例数据输出到标签中;理解它们之前,先要理解“响应式”——视图由数据驱动,数据变,视图跟着变。
怎么想:先把new Vue({ el:"#root", data:{...} })看成数据源,页面里写指令只是“取数据并渲染”的方式。
怎么做:保留下面完整示例,重点观察userName和str两个数据。
易错点:不要以为指令和{{}}会叠加显示;指令会覆盖标签原有内容。

html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
	<script src="lib/vue.js"></script>
</head>
<body>
	<div id="root">
		<h3>v-text</h3>
		<p v-text="userName">{{str}}</p>
		<p v-text='userName'>{{str}}</p>
		<p v-text=userName>{{str}}</p>
		<p v-text=`userName`>{{str}}</p>
		<hr/>
		<h3>v-html</h3>
		<p v-html="userName">{{str}}</p>
		<p v-html='userName'>{{str}}</p>
		<p v-html=userName>{{str}}</p>
		<p v-html=`userName`>{{str}}</p>
		<template v-html="userName">123</template>
	</div>
</body>
<script>
	// 响应式:视图是通过数据进行驱动的(当数据发生改变,视图会根据所使用的数据的变化而变化)
	// 响应式即是视图会响应最新的数据。
	// v-text与v-html的共同特点:
	// 1- 值是一个字符串
	// 2- 该字符串会作为Vue实例的属性名,将对应的属性值在指令所在的标签中进行输出。
	// 3- 会覆盖原有数据。
	// 4- 属性值可以使用双引号,单引号
	// 5- 属性值也可以省略双引号,单引号
	// 6- 如果使用的是反引号会将包裹的值在标签中直接输出
	// 7- 无法与template结合使用。
	// v-text与v-html的区别:
	// v-text不支持HTML标签而v-html支持。
	new Vue({
		el:"#root",
		data:{
			str:"我是一个字符串",
			userName:"zhangsan"
		}
	})
	
</script>
</html>

v-text:把数据当纯文本输出 ​

为什么需要:有时只想输出文本,不希望数据里的<、>被当成标签解析。
怎么想:v-text="userName"表示取实例中userName的值,放进当前标签;标签里原来的{{str}}会被整体覆盖。
怎么做:把示例中四个v-text分别运行,观察双引号、单引号、无引号都能取到zhangsan;反引号写法会把包裹内容直接输出,属于不推荐写法。
易错点:v-text不支持HTML标签;若数据是<b>张三</b>,它仍按文本显示。不要把v-text当成“会解析标签”的指令。

v-html:把数据当HTML解析输出 ​

为什么需要:当数据本身就是一段HTML字符串,需要让浏览器按标签渲染时,用v-html。
怎么想:v-text和v-html取值方式相同,区别只在“输出时是否解析HTML”。
怎么做:把userName改成带标签的字符串,例如<b>zhangsan</b>,再分别看v-text与v-html的渲染差异。
易错点:v-html会解析HTML,因此不要渲染不可信内容,避免脚本或事件属性带来的安全风险;同时它同样会覆盖标签原有内容。

引号、反引号与template的坑 ​

为什么需要:HTML属性值的写法会影响Vue能否正确拿到表达式。
怎么想:双引号、单引号、无引号是HTML属性定界方式;反引号不是推荐的HTML属性定界符,容易被当成普通字符,所以笔记中表现为“直接输出包裹的值”。
怎么做:优先写v-text="userName"或v-html="userName";需要字符串字面量时,在Vue表达式内部写引号,而不是依赖HTML外层反引号。
易错点:v-text、v-html无法与<template>结合使用;<template v-html="userName">123</template>不会按预期渲染,应把指令写在真实元素上。转写稿里的“真刺P/真生P”应纠正为JSONP,“方东”应纠正为防抖,“结构负值”应纠正为结构赋值,这些属于其他节内容,本节不硬套。

调试与验证思路 ​

为什么需要:指令写错时,页面可能只是“没变化”,不一定会明显报错。
怎么想:按“先看控制台报错→再看数据是否有值→再看指令是否写在真实标签上→最后看是否被覆盖或引号影响”的顺序排查。转写稿中反复强调先看控制台、再看数据、再看渲染结果,这个调试顺序可以迁移到本节。
怎么做:修改userName的值,观察页面是否响应;再把userName改成带标签字符串,对比v-text与v-html。
易错点:不要只改样式或只改HTML结构就判断指令失效;先确认数据源和指令所在标签,再确认是否误用了反引号或template。

本节目标与完整示例代码 ​

本节学习 Vue 的条件渲染:用 v-if、v-else-if、v-else 根据数据决定“渲染哪个元素”,并对比插值表达式中的逻辑运算、三元表达式和方法调用。笔记中的完整代码如下,保留原结构,只把注释原样保留:

html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
	<script src="lib/vue.js"></script>
</head>
<body>
<div id="root">
	<p>{{isShow}}</p>
	<p>{{isShow && "你好&&"}}</p>
	<p>{{isShow || "你好||"}}</p>
	<!-- 三元表达式	-->
	<p>{{sex===1?"男":"女"}}</p>
	<p>{{fn()}}</p>
	<!--多条件1-->
	<p>{{getAgeStr()}}</p>
	<!--多条件2-使用指令-->
	<p v-if="age>80">老年</p>
	<p v-else-if="age>40">中年</p>
	<p v-else-if="age>28">壮年</p>
	<p v-else-if="age>18">青年</p>
	<p v-else-if="age>12">少年</p>
	<p v-else-if="age>6">儿童</p>
	<p v-else>婴儿</p>
	<hr/>
	<template v-if="isLogin">您好,欢迎您的到来!</template>
	<template v-else>
		<input type="text" placeholder="请输入管理员账号">
		<input type="text" placeholder="请输入管理员密码">
	</template>
</div>
</body>
<script>
	// 注意1:v-if以及v-else-if的值是一个布尔值,如果成立则渲染指令所在的元素,不成立则不渲染
	// 注意2:v-else-if需要与v-if结合使用
	// 注意3:v-else=需要与v-if或v-else结合使用
	// 注意4:v-if,v-else-if,v-else指令所在的元素之间不要出现其它元素。
	
	new Vue({
		el:"#root",
		data:{
			isShow:true,
			sex:1,
			age:100,
			isLogin:true
		},
		methods:{
			fn(){
				return this.sex===1?"男":"女"
			},
			getAgeStr(){
				if(this.age>80) return "老年";
				if(this.age>40) return "中年"
				if(this.age>28) return "壮年"
				if(this.age>18) return "青年"
				if(this.age>12) return "少年"
				if(this.age>6) return "儿童"
				return "婴儿"
			}
		}
	})
</script>
</html>

插值表达式中的逻辑与三元:先理解“表达式能算出什么” ​

为什么需要:模板里经常要根据一个布尔值、性别或年龄显示不同文字。如果全写在 methods 里,简单判断会显得绕;如果全写 if,又不够直观。
怎么想:插值 {{ }} 里放的是一段表达式,表达式必须有返回值。&&、||、三元 条件 ? A : B 都能直接算出结果。
怎么做:{{isShow}} 显示布尔值;{{isShow && "你好&&"}} 在 isShow 为真时得到后面的字符串;{{sex===1?"男":"女"}} 按性别输出;{{fn()}} 调用方法返回结果。
易错点:{{}} 不是语句块,不能在里面写 if/else;&&、|| 返回的不一定是布尔值,要清楚当前数据下会得到什么。

v-if / v-else-if / v-else:多条件只渲染一个分支 ​

为什么需要:年龄分段这种多条件判断,如果用多个独立 v-if,可能同时成立多个;条件渲染指令能让互斥分支只显示一个。
怎么想:从上到下判断,哪个条件第一次成立,就渲染哪个元素;后面的 v-else-if、v-else 不再判断。笔记里 age:100,所以先命中 age>80,只渲染“老年”。
怎么做:写成一组相邻的 <p v-if="age>80">老年</p>、<p v-else-if="age>40">中年</p>……最后 <p v-else>婴儿</p>。
易错点:v-else-if 必须跟 v-if 结合;v-else 必须跟 v-if 或 v-else-if 结合;它们所在的元素之间不要插入其它元素,否则链会断。转写稿中把“微杠衣服、微钢 for、组册”分别纠正为 v-if、v-for、元素/标签,并提醒:v-if 不要和 v-for 写在同一个元素上,这个易错点先记住,后续再展开。

template 分组条件渲染:多个元素共用一个条件 ​

为什么需要:登录时显示一句欢迎语,未登录时显示两个输入框。如果给每个元素都写 v-if,条件会重复;包一层真实 div 又会多出无意义标签。
怎么想:<template> 是模板占位容器,本身不渲染成真实 DOM,但可以承载 v-if/v-else,把一组元素当成一个分支。
怎么做:笔记中 <template v-if="isLogin">您好,欢迎您的到来!</template>,下面紧接 <template v-else> 包住两个 input。当 isLogin:true 时只渲染欢迎语,否则渲染账号密码框。
易错点:template 上的 v-if 与下面的 v-else 仍要相邻;不要把无关元素插在两者之间。转写稿里调试下拉列表时也用到“是否渲染”的思路:用 isRender 控制显示隐藏,并发现“先隐藏后点击”会导致点击函数执行不到;这提醒我们,条件渲染为假时元素根本不存在,绑定在它上面的事件也不会正常触发。

调试与验证:改数据看渲染结果 ​

为什么需要:条件渲染不是“隐藏”,而是“不渲染”。只看代码容易误判,必须改数据观察页面。
怎么想:先把 age 改成 100、50、30、20、15、8、3,观察是否只出现一个 <p>;再把 isLogin 改成 false,观察欢迎语是否消失、两个输入框是否出现。
怎么做:在 Vue 实例 data 中修改对应值,刷新或借助开发者工具观察 DOM;确认 v-else-if 链只命中一个分支。
易错点:把 v-if 当成 CSS 隐藏;在分支之间插入 <hr/> 等元素;把 v-else 写成带表达式的形式。按笔记注释逐条核对,就能覆盖本节核心。

先分清:v-if 与 v-show 控制的是两件事 ​

为什么需要:页面上经常要按条件显示或隐藏内容。若把“是否渲染元素”和“是否显示元素”混为一谈,排查问题时就会误判 DOM 结构。

怎么想:笔记注释写得很清楚:v-if 为 true 表示渲染元素,为 false 表示不渲染元素;v-show 为 true 表示显示元素,为 false 表示隐藏元素,本质是通过 display:none 隐藏。也就是说,v-show 的元素仍在 DOM 里,只是看不见。

怎么做:先看下面这段完整示例,后面所有真假值判断都围绕它展开。本节笔记没有图片,以下保留原代码:

html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
	<script src="lib/vue.js"></script>
</head>
<body>
<div id="root">
<!--	<p v-if="isShow">v-if</p>-->
<!--	<p v-show="isShow">v-show</p>-->
<!--	<p v-show='isShow'>1</p>-->
<!--	<p v-show=isShow>2</p>-->
	<p v-show="'isShow'">3</p>
	<p v-show="''">4</p>
	<p v-show="1">5</p>
	<p v-show="0">6</p>
	<p v-show="true">7</p>
	<p v-show="false">8</p>
	<p v-show="null">9</p>
	<p v-show="undefined">10</p>
</div>
</body>
<script>
	// v-if与v-show的值都是布尔值。
	// v-if为true表示渲染元素,false表示不渲染元素
	// v-show为true表示显示元素,false表示隐藏元素(通过display:none)
	// 注意1:空字符串,0,undefined,null作为判断条件相当于false
	// 注意2:非空字符串,1作为判断条件相当于true
	new Vue({
		el:"#root",
		data:{
			// isShow:true
		}
	})
</script>
</html>

易错点:不要因为页面上看不见,就认为 v-show 把元素删掉了。调试时打开审查元素,应能看到该元素带有 display: none,而不是消失不见。

表达式先求值,再决定显示或隐藏 ​

为什么需要:v-show 后面写的不是“固定开关”,而是一个表达式。表达式写错引号,结果会完全不同。

怎么想:先问自己:这个值在 Vue 表达式里求值后,是“真”还是“假”?笔记中的 v-show="'isShow'" 是带引号的字符串字面量,非空字符串按真处理;而 v-show="isShow" 才是去 data 里找 isShow 变量。注释掉的 v-show=isShow 在 HTML 里虽然能写,但可读性差,初学阶段建议统一加双引号。

怎么做:把笔记里的用例逐条对照:''、0、null、undefined 相当于假,元素隐藏;1、true、非空字符串相当于真,元素显示;false 隐藏。这里不要死记“字符串一定真”,空字符串是假。

易错点:最常见的错误是多套一层引号。例如想用变量却写成 v-show="'isShow'",实际判断的是字符串 isShow,不是变量。另一个错误是把 v-show="0" 当成“显示 0”,它在这里是条件表达式,结果为假。

调试顺序:先看条件,再看 DOM ​

为什么需要:显示不对时,如果直接改 CSS,很容易掩盖真正原因。转写稿里封装 JSONP 时,也是先判断配置对象里有没有 cb 属性,再决定是否拼接回调名;这里的“先判断条件成立与否,再执行对应逻辑”可以迁移到 v-show 的排查:先确认表达式结果,再看 DOM 上的 display。转写稿中的“真刺P/真生病”应理解为 JSONP,“考飞格”是 config,“s time out”是 setTimeout,这些同音字先纠正,避免读代码时误解。

怎么想:排查顺序建议是:一看模板表达式有没有写错引号;二看 data 里变量是否存在;三看审查元素里是 display:none 还是元素根本不存在。元素不存在更像 v-if 的行为,存在但隐藏才是 v-show。

怎么做:把 data 里的 isShow 打开,分别测试 v-if="isShow" 与 v-show="isShow",观察 DOM。转写稿还提到 v-if 不要和 v-for 放在同一个元素上,这类条件指令混用时先拆结构,再判断显示逻辑,能减少排查干扰。

易错点:只盯页面效果,不看 DOM;把 v-show 当成 v-if 使用;在需要频繁切换显示时仍用 v-if,或在需要彻底不渲染时误用 v-show。本节先把“真假值判断”和“display 隐藏”这两点练熟。

先明确本节重点:v-bind 是“属性由数据驱动” ​

这节标题是 v-bind(属性渲染)——重点中的重点。转写稿虽然从 express.static、JSONP 封装讲到模拟百度,但落到 Vue 上,核心是:模板里的 class、value、key 等属性不要写死,而要用 v-bind 交给数据决定。为什么需要?因为搜索联想、上下键高亮、点击回填都要求“数据一变,属性跟着变”。怎么想?先找“哪个属性会变”,再把它写成 :属性名="表达式"。怎么做?例如 :class="{ active: activeIndex === index }"、:value="wd"、:key="item.sa"。易错点:class="active" 是死字符串,:class 才是绑定;v-if 不要和 v-for 写在同一个元素上,转写稿里特意用外层容器包住 v-for。

JSONP 封装:先让请求能返回 Promise ​

为什么需要?百度联想接口跨域,普通 fetch 受同源限制,而 script 标签不受限,所以用 JSONP。怎么想?动态创建 script,把 src 拼成“接口地址 + 参数 + 回调函数名”,服务端返回 cbName({...}),浏览器就会调用挂到 window 上的函数。怎么做?转写稿里一开始报错“返回的不是 Promise”,于是补上 return new Promise;又因为只传了 cb 名却没定义全局函数而报“函数未定义”,于是用 window[fnName] = resolve 把 Promise 的成功状态交给 JSONP 回调。可整理为:

js
export default function myJSONP(url, config = {}) {
  return new Promise((resolve) => {
    const params = config.params || {};
    const cbName = config.cb || 'callback';
    const fnName = 'jsonp_' + Date.now() + Math.random().toString(36).slice(2);
    params[cbName] = fnName;

    const query = Object.keys(params)
      .map(k => `${k}=${encodeURIComponent(params[k])}`)
      .join('&');

    const script = document.createElement('script');
    script.src = url + '?' + query;
    window[fnName] = resolve;
    document.body.appendChild(script);

    script.onload = () => {
      document.body.removeChild(script);
      delete window[fnName];
    };
  });
}

易错点:函数名不要以数字开头;window[fnName] 必须先挂再插入脚本;请求完成后移除 script 并删除全局函数;参数对象要默认 {},否则 config.params 为空时拼接会报错。

模拟百度联想:v-for 渲染 + 防抖 ​

为什么需要?输入“三国”时每敲一个字都发请求,会连发多次。怎么想?只保留最后一次输入后的请求。怎么做?用 setTimeout 存到 this.timer,下次输入先 clearTimeout,再设 300ms 左右延迟;请求成功后把 res.g 赋给 this.g,模板用 v-for 渲染。转写稿里还讨论了 WD 取值位置:放在定时器回调里取,拿到的是最新值;放在外面则可能保留旧值。易错点:g 要给默认空数组;解构可写 const { g = [] } = res;v-for 的 :key 优先用接口里的唯一标识,如 item.sa,不要随便用下标。

上下键高亮:v-bind 绑定 class 与 value ​

为什么需要?用户按上下键要看到哪一项被选中。怎么想?用 activeIndex 记录当前下标,v-bind:class 判断是否加高亮类;按 38/40 改下标;越界时循环。怎么做?核心模板:

html
<div v-if="isRender">
  <p
    v-for="(item, index) in g"
    :key="item.sa"
    :class="{ active: activeIndex === index }"
    @click="select(index)"
  >
    {{ item.q }}
  </p>
</div>
js
keyup(e) {
  if (e.keyCode === 38) {
    this.activeIndex--;
    if (this.activeIndex < -1) this.activeIndex = this.g.length - 1;
  }
  if (e.keyCode === 40) {
    this.activeIndex++;
    if (this.activeIndex > this.g.length - 1) this.activeIndex = -1;
  }
  if (this.activeIndex === -1) {
    this.wd = this.keyword;
  } else {
    this.wd = this.g[this.activeIndex].q;
  }
}

易错点:activeIndex === -1 时不能写 this.g[-1].q,否则报错;要先判断再取值。转写稿里还提到 v-if 与 v-for 同元素会出问题,所以外层用 div/template 控制显示,内层再循环。

点击与失焦:blur 先隐藏会导致 click 不执行 ​

为什么需要?点击联想项要回填并隐藏下拉。怎么想?blur 会先触发,如果立刻 isRender = false,后面的 click 就点不到元素了。怎么做?转写稿用延迟隐藏:setTimeout(() => { this.isRender = false }, 200);点击项里设置 wd 和 activeIndex。若要提交表单,注意“数据改了但视图还没更新完”,转写稿中直接用 document.querySelector 先把 input 的 value 设好再提交,避免提交旧值。易错点:延迟太短仍点不到;v-model 与手动 value 混用时要明确谁负责回填;提交前确认输入框值已同步。

v-on 的定位:把用户动作接到方法上 ​

这一节围绕 v-on 事件展开。为什么需要它?因为页面不是静态的:输入框要监听输入,按钮要监听点击,搜索框要监听键盘上下键,失焦和聚焦也要参与交互。v-on 的作用就是把 DOM 事件绑定到 Vue 实例的方法上,写法是 v-on:事件名="方法",常用简写 @事件名="方法"。

怎么想?先判断“用户做了什么动作”,再决定“这个动作要调用哪个方法”。怎么做?在模板上写 @input、@keyup、@click、@blur、@focus,在 methods 里定义对应函数。易错点是:事件处理函数不要漏写括号调用关系;需要事件对象时,方法第一个参数就是 e;不要把 v-if 和 v-for 写在同一个元素上,转写稿里就因为这个报过模板编译错误,后来拆成 template 包裹解决。

从输入框开始:@input 与事件对象 ​

模拟百度时,输入内容就要发请求。为什么不用双向绑定?转写稿里的判断是:当前只需要在输入时拿到值发请求,不需要把输入内容再渲染回页面,所以先用事件对象取值更直接。怎么做?

html
<input
  type="text"
  autocomplete="off"
  @input="search"
/>
js
search(e) {
  const wd = e.target.value.trim();
  // 用 wd 去发 JSONP 请求
}

这里 e.target.value 就是输入框当前内容,trim() 去掉左右空格。易错点:e.target 指向触发事件的元素;如果方法里还要用 this,不要用普通函数随意改变上下文。转写稿里还提到,后面做键盘上下选择时,输入框内容需要跟着变,所以又引入了 v-model="wd",这时请求参数直接用 this.wd 即可。

键盘上下选择:@keyup 与 keyCode ​

搜索建议需要按向下键选中下一项、按向上键回到上一项。为什么用 @keyup?因为要在按键抬起后处理选择。怎么想?维护一个 activeIndex 表示当前高亮项,初始设为 -1,表示没有选中。怎么做?

html
<input v-model="wd" @keyup="handleKeyUp" />
<p
  v-for="(item, index) in g"
  :key="item.sa"
  :class="{ active: activeIndex === index }"
  @click="fn(index)"
>
  {{ item.q }}
</p>
js
handleKeyUp(e) {
  if (e.keyCode === 38) {
    this.activeIndex--;
    if (this.activeIndex < -1) {
      this.activeIndex = this.g.length - 1;
    }
  } else if (e.keyCode === 40) {
    this.activeIndex++;
    if (this.activeIndex > this.g.length - 1) {
      this.activeIndex = -1;
    }
  }
}

转写稿里 38 是向上,40 是向下。易错点:下标越界要判断;activeIndex 为 -1 时不能直接 this.g[-1].q,否则报错。转写稿的修正是:只有 activeIndex !== -1 时才从数组取 q 回填输入框;等于 -1 时回到搜索关键词。

点击选择与 blur 冲突:@click、@blur、@focus ​

点击建议项要回填内容,但输入框失焦会先触发 @blur。转写稿中的调试过程很典型:点击建议项时,blur 先把列表隐藏了,导致 click 根本执行不到。为什么?因为元素已经被隐藏,点击目标不存在了。怎么想?不能一失焦就立刻隐藏,要延迟隐藏。怎么做?

html
<input
  v-model="wd"
  @blur="blurHandle"
  @focus="focusHandle"
/>
js
blurHandle() {
  setTimeout(() => {
    this.isRender = false;
  }, 200);
},
focusHandle() {
  this.isRender = true;
}

易错点:延迟不能太短,转写稿里试过 100ms 仍不稳定,后来用 200ms 或 500ms 才稳定;点击回填后还要把 isRender 重新置为 true,否则再次聚焦时列表状态不对。

高频事件与防抖:setTimeout 与 clearTimeout ​

输入 123 会连续触发多次 @input,如果每次都发请求就会发多次。为什么需要防抖?因为要等用户停止输入后再发最后一次请求。怎么想?把定时器保存在实例上,每次触发先清掉上一次。怎么做?

js
search() {
  if (this.timer) {
    clearTimeout(this.timer);
  }
  this.timer = setTimeout(() => {
    jsonp({
      url: "https://www.baidu.com/sug",
      cb: "cb",
      params: { wd: this.wd }
    }).then((res) => {
      this.g = res.g || [];
    });
  }, 300);
}

易错点:定时器要挂在 this 上,不能每次用局部变量;间隔建议 300ms 左右,太长用户会以为网站卡住;clearTimeout 要写在 setTimeout 之前。转写稿还强调,jsonp 封装里要用 window[fnName] 挂回调,fnName 不能以数字开头,最后通过 resolve 改状态,这些是事件触发后异步请求链路的配套知识。

先明确:v-model 解决的是“表单值”和“数据”同步 ​

转写稿里说的“微杠 model”,就是 v-model。这一节它并不是单独讲语法,而是在“模拟百度”的搜索框里反复出现:输入框的内容要能拿到,键盘上下选中候选项后还要把内容回填到输入框。为什么需要它?因为如果每次都写 e.target.value,数据源就不统一:输入时读 DOM,回填时又改 DOM,后面发请求、做键盘选择、做失焦隐藏时很容易乱。怎么想?把输入框内容当成一个响应式数据 wd,输入时数据变,选中时也改这个数据,视图自然跟着变。怎么做?在输入框上写 v-model="wd",发请求时用 this.wd,不再手动取 e.target.value。易错点:不要一边用 v-model,一边又手动设置 input.value;v-model 本质是 :value 加 @input 的语法糖,理解这一点后面调试才不会晕。

在模拟百度中,从手动取值改成 v-model ​

转写稿前面先写了 @input,通过 e.target.value 获取输入内容;后面在“模拟百度完成”里改成双向绑定:输入框绑定 wd,发送请求时直接用 this.wd。这样改的好处是,键盘上下选择时不用再操作 DOM,只要改 this.wd,输入框就会显示新值。转写稿中明确出现的片段可以整理为:

html
<input v-model="wd" @input="search" @keyup="handleKeyUp">

这里要注意:@input 负责触发搜索,@keyup 负责判断上下键;v-model 只负责把输入框的值同步到 wd。易错点:如果搜索函数里还写 e.target.value,就会和 v-model 形成两个数据源;应该统一读 this.wd。另外,转写稿里提到“v-if 不要和 v-for 出现在同一个元素上”,这是列表渲染的易错点,和 v-model 不是一回事,但同属模板指令边界,别混在一起。

键盘上下选择:改数据,不要直接改 DOM ​

转写稿用 keyup 判断 38 和 40:38 向上,40 向下。核心状态是 activeIndex,初始值设为 -1,表示没有选中项。向下时 activeIndex++,向上时 activeIndex--;到边界要处理:向下超过 this.g.length - 1 就回到 -1,向上小于 -1 就回到数组最后一项。选中后回填输入框时,不能直接写 this.g[this.activeIndex].q 就完事,因为 activeIndex 可能是 -1,取数组会得到 undefined,转写稿里就出现了“数据报错”。正确思路是先判断:如果 activeIndex === -1,回填搜索关键词;否则再取 this.g[this.activeIndex].q。代码片段按转写稿整理为:

js
this.wd = this.g[this.activeIndex].q

易错点:边界判断要放在取值之前;-1 不是数组下标,不能直接取值;上下键的判断只负责改 activeIndex,回填 wd 的逻辑最好统一放在后面,避免条件分支里重复写。

失焦隐藏和点击选择的冲突:blur 先执行了 ​

转写稿里有一个很典型的调试过程:输入框失焦时要把下拉列表隐藏,但点击候选项时,blur 先触发,列表先被隐藏,结果 click 的 P 元素点不到,点击函数压根没有执行。为什么?因为 blur 把列表隐藏后,鼠标点到的元素已经不存在了。怎么想?要么延迟隐藏,要么用标记区分“正在点击候选项”。转写稿先用了 setTimeout 延迟隐藏,100ms 不明显,500ms 可用;也讨论了 isClick 标记:点击候选项时先设置标记,blurHandler 里判断标记再决定是否隐藏。易错点:延迟隐藏是权宜方案,时间太短会点不到,太长会显得卡;更稳的是标记方案。转写稿还提到点击后要把 isClick 改回 false,否则下一次失焦又不好使。

防抖和异步更新:v-model 不负责这些 ​

搜索框每输入一个字符就发请求,会高频触发。转写稿用防抖解决:clearTimeout 取消上一次定时器,再 setTimeout 只执行最后一次,建议 300–500ms。易错点:setTimeout 里取 this.wd 拿到的是最后一次值;如果把取值放到定时器外面,闭包会保留旧值。另一个易错点是异步更新:改完数据后立即 submit 表单,视图还没更新,提交的仍是旧值;转写稿用 querySelector 先设置 input.value 再提交,或者等视图更新。记住:v-model 只同步表单值和数据,防抖、键盘选择、失焦隐藏、提交时机都要另外处理。

一、先明确:v-for 解决“同一结构重复渲染”的问题 ​

为什么需要: 模拟百度搜索时,接口返回的是一组候选词,页面要把它们逐条显示出来。若手写多个 <p>,数据一变就要改结构,既繁琐又容易漏。
怎么想: 先把“数据”看成数组,再把“数组中的每一项”映射成一段相同的 DOM 结构,这就是 v-for 的职责。
怎么做: 在模板中用 v-for 遍历数组,例如转写稿中最终渲染搜索结果的写法可整理为:

html
<template v-if="isRender">
  <p
    v-for="(item, index) in g"
    :key="item.sa"
    :class="{ active: index === activeIndex }"
    @click="fn(index)"
  >
    {{ item.Q }}
  </p>
</template>

其中 g 是搜索结果数组,item.Q 是候选词内容,index 用于高亮和点击取值。
易错点: 不要在没有数据时硬渲染;转写稿中先把 g 的初始值设为空数组,等请求成功后再赋值,避免 undefined 参与遍历。

二、数据从请求到列表:先确认数组结构,再交给 v-for ​

为什么需要: v-for 只负责“按数组渲染”,它不负责请求数据。若数据结构没看清,模板里写错字段就会空白或报错。
怎么想: 转写稿的调试顺序是:先发 JSONP 请求,console.log 出响应体,确认里面有一个数组字段(如 g),再决定 v-for 遍历谁。
怎么做: 在 data 中准备初始状态,请求成功后更新状态:

js
data() {
  return {
    g: [],
    activeIndex: -1,
    WD: '',
    isRender: false
  }
}
// 请求成功后(按转写稿整理)
this.g = res.g

模板中再写 v-for="(item, index) in g"。转写稿中强调:初始值给空数组,是因为最终得到的数据结构就是数组;状态更新后视图会重新渲染。
易错点: 一是字段名写错,比如把 res.g 写成别的;二是忘记给默认值,导致未请求时遍历报错;三是刷新时模板未编译会出现双大括号闪烁,转写稿中记录可用 v-cloak 解决,课上先记住这个现象。

三、key 不要随便用下标:优先使用数据里的唯一标识 ​

为什么需要: v-for 需要 key 帮助 Vue 识别每一项。若 key 不稳定,列表更新时可能复用错节点,影响高亮、点击等状态。
怎么想: 转写稿中先考虑用下标,后来发现返回对象里有 sa 这类唯一标识,于是改为 :key="item.sa"。思路是:数据本身有唯一值,就优先用它。
怎么做:

html
<p v-for="(item, index) in g" :key="item.sa">
  {{ item.Q }}
</p>

易错点: 不要把 key 写成 item.Q 或随手写死;若确实没有唯一字段,才退回使用 index,但要明白它只适合简单、不频繁重排的列表。

四、v-if 与 v-for 不要挤在同一元素:用 template 包一层 ​

为什么需要: 转写稿中做“是否渲染候选列表”时,需要条件控制和循环同时存在。若把 v-if 和 v-for 写在同一个元素上,容易产生优先级和可读性问题。
怎么想: 明确提醒:v-if 不要和 v-for 出现在同一个元素上,先用 template 包条件,再在内部元素上写 v-for。
怎么做:

html
<template v-if="isRender">
  <p v-for="(item, index) in g" :key="item.sa">
    {{ item.Q }}
  </p>
</template>

易错点: 不要把 v-if 直接塞到 v-for 的 <p> 上;另外转写稿中调试键盘选择时发现,失去焦点后立即隐藏列表会导致点击项执行不到,后来用延迟隐藏兜底,这属于交互细节,但根因仍是“先隐藏了就点不到”,写列表交互时要留意执行顺序。

一、为什么要把 JSONP 再封装一层 ​

上一节我们已经能用 script 标签跨域拿数据了,但每次都要手写创建标签、拼 src、挂 onload、删标签、挂全局回调,代码重复度极高。转写里说“这里面的代码相似度很高,我建议把它封装一下”,目的就是:页面里只写“请求哪个地址、带什么参数”,其余细节全部收进一个文件。

二、先想清楚封装后的调用姿势 ​

我们希望页面里能写成这样:

js
JSONP('https://www.baidu.com/sug', {
  params: { wd: '三国' }
}).then(res => {
  console.log(res)
})

所以封装函数至少接收两个参数:请求地址 url、配置对象 config。config 里放查询参数 params;另外还要允许指定“服务端回调函数名”和“回调参数名”,因为百度用 cb,京东可能用别的名字。

三、动手写 myJSONP.js ​

新建 myJSONP.js,默认导出一个函数,并且必须返回 Promise:

js
export default function JSONP(url, config = {}) {
  return new Promise((resolve, reject) => {
    const cb = config.cb || 'cb'
    let fnName = config.fnName || ('jsonp_' + Math.random().toString().slice(2))
    // 后面拼参数、创建 script
  })
}

易错点:转写里第一次写的时候忘了 return new Promise,页面一调用就报“不是一个 Promise”。只要调用方用 .then,封装函数就必须返回 Promise。

四、拼查询字符串 ​

params 是一个对象,要变成 wd=三国&cb=jsonp_xxx 这种形式:

js
const params = config.params || {}
if (config.fnName) {
  fnName = config.fnName
} else {
  params[cb] = fnName
}
const query = Object.keys(params)
  .map(key => key + '=' + params[key])
  .join('&')
url += '?' + query

怎么想:先判断调用方有没有传 fnName;没传才用随机串,并把它塞进 params[cb],这样服务端才知道回调函数叫什么。易错点:随机函数名不要以数字开头,所以前面拼一个 jsonp_;另外 params 要给默认空对象,否则调用方不传时 params[cb] 直接报错。

五、创建 script 并挂回调 ​

js
const script = document.createElement('script')
script.src = url
document.body.appendChild(script)

script.onload = function () {
  document.body.removeChild(script)
  delete window[fnName]
}

window[fnName] = resolve

为什么把 resolve 直接赋给 window[fnName]:服务端返回的脚本会执行 fnName(数据),而 resolve 接收一个参数,正好把数据交给 Promise,状态变成成功。这比再包一层函数更简洁。

六、模拟百度:输入即搜索 ​

页面里引入时要注意:没有 webpack,浏览器要求写扩展名,并且要加 type="module":

html
<script type="module">
  import JSONP from './js/myJSONP.js'
</script>

输入框监听 input 事件,拿到内容后发请求:

js
search() {
  JSONP('https://www.baidu.com/sug', {
    params: { wd: this.WD }
  }).then(res => {
    this.list = res.g || []
    this.keyword = this.WD
  })
}

res.g 是数组,每一项里有 q(候选词)和 sa(唯一标识)。渲染时用 :key="item.sa",不要用下标。

七、防抖:只发最后一次请求 ​

每敲一个字就发一次请求,输入“三国”会发好几次。用 setTimeout + clearTimeout:

js
clearTimeout(this.timer)
this.timer = setTimeout(() => {
  this.search()
}, 300)

怎么想:每次输入先清掉上一次定时器,只有停下来 300ms 才真正发请求。转写里强调“300 到 500 毫秒”,太长用户会以为网站坏了。易错点:this.timer 要挂在实例上,不能写在函数局部,否则清不掉。

八、上下键选中与提交 ​

给 input 加 keydown,keyCode 为 38(上)、40(下)时改 activeIndex:

js
if (e.keyCode === 40) {
  this.activeIndex++
  if (this.activeIndex > this.list.length - 1) {
    this.activeIndex = -1
  }
} else if (e.keyCode === 38) {
  this.activeIndex--
  if (this.activeIndex < -1) {
    this.activeIndex = this.list.length - 1
  }
}

选中项高亮用 :class="{ active: index === activeIndex }"。最后统一根据 activeIndex 回填输入框:为 -1 时用 this.keyword,否则用 this.list[this.activeIndex].q。易错点:不要在下标为 -1 时还去取 this.list[-1].q,会报 undefined。

九、失焦隐藏与延迟的坑 ​

输入框失焦时隐藏候选框,但点击候选项会先触发 blur,候选框一隐藏就点不到了。转写里的做法是延迟隐藏:

js
blurHandle() {
  setTimeout(() => {
    this.isShow = false
  }, 200)
}

点击候选项时先把 q 赋给 WD,再调用 form.submit() 跳转。易错点:不要先改数据再立刻提交表单,因为视图还没更新,输入框里的值可能没变;可以先用 querySelector 拿到 input,把 value 设好再提交。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一:v-text 与 v-html ​

html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
	<script src="lib/vue.js"></script>
</head>
<body>
	<div id="root">
		<h3>v-text</h3>
		<p v-text="userName">{{str}}</p>
		<p v-text='userName'>{{str}}</p>
		<p v-text=userName>{{str}}</p>
		<p v-text=`userName`>{{str}}</p>
		<hr/>
		<h3>v-html</h3>
		<p v-html="userName">{{str}}</p>
		<p v-html='userName'>{{str}}</p>
		<p v-html=userName>{{str}}</p>
		<p v-html=`userName`>{{str}}</p>
		<template v-html="userName">123</template>
	</div>
</body>
<script>
	// 响应式:视图是通过数据进行驱动的(当数据发生改变,视图会根据所使用的数据的变化而变化)
	// 响应式即是视图会响应最新的数据。
	// v-text与v-html的共同特点:
	// 1- 值是一个字符串
	// 2- 该字符串会作为Vue实例的属性名,将对应的属性值在指令所在的标签中进行输出。
	// 3- 会覆盖原有数据。
	// 4- 属性值可以使用双引号,单引号
	// 5- 属性值也可以省略双引号,单引号
	// 6- 如果使用的是反引号会将包裹的值在标签中直接输出
	// 7- 无法与template结合使用。
	// v-text与v-html的区别:
	// v-text不支持HTML标签而v-html支持。
	new Vue({
		el:"#root",
		data:{
			str:"我是一个字符串",
			userName:"zhangsan"
		}
	})
</script>
</html>

要点解释与易错点:

  • v-text="userName" 中的 userName 是Vue 实例的属性名,不是字符串字面量;页面输出的是 data 里 userName 的值 zhangsan。
  • 指令会覆盖标签原有内容,所以 <p v-text="userName">{{str}}</p> 里的 {{str}} 不会显示。
  • 属性值可以用双引号、单引号,也可以省略引号;但用反引号时不会当属性名解析,而是把包裹的内容直接在标签中输出,这是最典型的易错点。
  • v-text 不解析 HTML 标签,v-html 才会把内容当 HTML 渲染;v-html 有 XSS 风险,不要渲染用户输入。
  • 指令不能写在 <template> 上,<template v-html="userName">123</template> 是无效用法。
  • el:"#root" 必须与页面中 id="root" 的挂载点对应,且 lib/vue.js 路径要正确,否则指令不会生效。

示例二:v-if / v-else-if / v-else 条件渲染 ​

html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
	<script src="lib/vue.js"></script>
</head>
<body>
<div id="root">
	<p>{{isShow}}</p>
	<p>{{isShow && "你好&&"}}</p>
	<p>{{isShow || "你好||"}}</p>
	<!-- 三元表达式	-->
	<p>{{sex===1?"男":"女"}}</p>
	<p>{{fn()}}</p>
	<!--多条件1-->
	<p>{{getAgeStr()}}</p>
	<!--多条件2-使用指令-->
	<p v-if="age>80">老年</p>
	<p v-else-if="age>40">中年</p>
	<p v-else-if="age>28">壮年</p>
	<p v-else-if="age>18">青年</p>
	<p v-else-if="age>12">少年</p>
	<p v-else-if="age>6">儿童</p>
	<p v-else>婴儿</p>
	<hr/>
	<template v-if="isLogin">您好,欢迎您的到来!</template>
	<template v-else>
		<input type="text" placeholder="请输入管理员账号">
		<input type="text" placeholder="请输入管理员密码">
	</template>
</div>
</body>
<script>
	// 注意1:v-if以及v-else-if的值是一个布尔值,如果成立则渲染指令所在的元素,不成立则不渲染
	// 注意2:v-else-if需要与v-if结合使用
	// 注意3:v-else=需要与v-if或v-else结合使用
	// 注意4:v-if,v-else-if,v-else指令所在的元素之间不要出现其它元素。

	new Vue({
		el:"#root",
		data:{
			isShow:true,
			sex:1,
			age:100,
			isLogin:true
		},
		methods:{
			fn(){
				return this.sex===1?"男":"女"
			},
			getAgeStr(){
				if(this.age>80) return "老年";
				if(this.age>40) return "中年"
				if(this.age>28) return "壮年"
				if(this.age>18) return "青年"
				if(this.age>12) return "少年"
				if(this.age>6) return "儿童"
				return "婴儿"
			}
		}
	})
</script>
</html>

要点解释与易错点:

  • 插值 {{ }} 里可以写表达式(&&、||、三元),但不能写 if 语句;多分支要么抽成 methods 里的函数,要么改用指令。
  • v-if 为真才渲染该元素,为假时元素根本不存在于 DOM 中;age:100 时只会渲染"老年"那一个 p。
  • 分支必须连续:v-if、v-else-if、v-else 所在的元素之间不能插入其它元素,否则分支链断开、v-else 会失效。
  • v-else-if 必须跟在 v-if(或上一个 v-else-if)之后,v-else 必须跟在 v-if/v-else-if 之后,不能单独使用。
  • <template> 上可以用 v-if/v-else 做"包裹多个元素"的条件渲染,它本身不产生额外标签;这一点与 v-text/v-html 不同。
  • methods 里访问 data 必须写 this.sex、this.age,漏写 this 会取不到值。

示例三:v-show ​

html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
	<script src="lib/vue.js"></script>
</head>
<body>
<div id="root">
<!--	<p v-if="isShow">v-if</p>-->
<!--	<p v-show="isShow">v-show</p>-->
<!--	<p v-show='isShow'>1</p>-->
<!--	<p v-show=isShow>2</p>-->
	<p v-show="'isShow'">3</p>
	<p v-show="''">4</p>
	<p v-show="1">5</p>
	<p v-show="0">6</p>
	<p v-show="true">7</p>
	<p v-show="false">8</p>
	<p v-show="null">9</p>
	<p v-show="undefined">10</p>
</div>
</body>
<script>
	// v-if与v-show的值都是布尔值。
	// v-if为true表示渲染元素,false表示不渲染元素
	// v-show为true表示显示元素,false表示隐藏元素(通过display:none)
	// 注意1:空字符串,0,undefined,null作为判断条件相当于false
	// 注意2:非空字符串,1作为判断条件相当于true
	new Vue({
		el:"#root",
		data:{
			// isShow:true
		}
	})
</script>
</html>

要点解释与易错点:

  • v-show 无论真假元素始终在 DOM 中,只是通过 display:none 隐藏;v-if 是真正的不渲染。频繁切换用 v-show,条件很少变化用 v-if。
  • v-show="'isShow'" 加了引号就是字符串字面量,非空字符串为真,所以第 3 行会显示;这和 v-show="isShow"(取 data 中的属性)完全不同,是本节最容易踩的坑。
  • 假值判定:''、0、null、undefined、false 都相当于 false;1、非空字符串、true 相当于 true。所以第 4、6、8、9、10 行被隐藏。
  • 示例中 data 里的 isShow 被注释掉了,因此注释掉的几行 v-show="isShow" 若取消注释,取到的是 undefined,同样会被隐藏,不会报错。
  • v-show 不支持 <template> 元素(template 不渲染真实节点,无法挂 display),需要包裹多个元素的条件显示请用 v-if + <template>。

常见错误 ​

1. 表单标签写成 <from>,控制台报「Unknown custom element」

  • 现象:页面白屏或警告 "Did you register the component correctly?",模板里那块内容整个不渲染。
  • 原因:HTML 没有 from 标签,Vue 编译时把它当成一个未注册的子组件去解析。
  • 修法:改成 <form>;提交时给表单加 name="f",用 document.f 或 querySelector('form') 拿到表单再 submit()。

2. 用 import 引入自己封装的 jsonp 模块,浏览器直接报 SyntaxError

  • 现象:Cannot use import statement outside a module,整段脚本不执行。
  • 原因:没走 webpack/vite 的裸页面里,<script> 默认是普通脚本,不认 ES Module 语法。
  • 修法:给 script 加 type="module";同时 import 路径必须写全后缀 ./js/myJsonp.js,不能省 .js。

3. jsonp 回调名用 'cb' + Math.random() 直接拼

  • 现象:偶尔报 xxx is not a function,或者原本能用的全局函数被顶掉。
  • 原因:随机串可能以数字开头(cb0.123...),且 cb 前缀太常见,容易和页面已有全局名撞车;另外回调函数声明在函数体内部却忘了挂到 window 上,服务端回调时根本找不到。
  • 修法:加唯一前缀(如 jsonp_ + 时间戳 + 随机数),并把回调显式挂 window[fnName] = resolve,用完 delete window[fnName]。

4. 防抖里把 e.target.value 写进定时器回调内部

  • 现象:输入 123 只发一次请求没错,但请求参数永远是最后那个 123,看起来"防抖生效了却拿不到当时的输入"。
  • 原因:定时器回调延迟执行,执行时 e.target.value 已被重新读取为最新值;把取值写在 setTimeout 外面才是靠闭包保留当次的值。
  • 修法:const wd = e.target.value.trim() 放在 setTimeout 之前,回调里只用 wd;同时每次触发先 clearTimeout(this.timer) 再 this.timer = setTimeout(...)。

5. 键盘上下选中时 activeIndex 变成 -1,取值报错

  • 现象:一直按 ↓ 循环到底后再按,控制台 Cannot read property 'Q' of undefined,输入框被清空。
  • 原因:this.list[this.activeIndex] 在 activeIndex === -1 时是 undefined,再取 .Q 就崩。
  • 修法:把"改下标"和"改输入框内容"拆成两步;取内容前先判断 activeIndex === -1 就用搜索关键词回填,否则才取 this.list[activeIndex].Q。

6. 失焦隐藏下拉列表,导致选项点击无效

  • 现象:鼠标点下拉项没反应,只能靠键盘选。
  • 原因:blur 先于 click 触发,列表已被 v-if 移除,点击目标消失,click 根本派发不到。
  • 修法:blur 里用 setTimeout(() => this.isRender = false, 200) 延迟隐藏;或把选项的触发事件从 click 换成 mousedown。

7. v-if 和 v-for 写在同一个元素上

  • 现象:列表渲染异常或报错提示优先级问题。
  • 原因:Vue2 中 v-for 优先级高于 v-if,条件拿不到循环项,且每次循环都做一次判断,浪费性能。
  • 修法:外层套 <template v-if="isRender">,内层 <p v-for="(item, index) in list">,两者分层。

8. 改完数据立刻 form.submit(),提交的还是旧值

  • 现象:点击下拉项跳转后,搜索框里还是上一次的关键词。
  • 原因:改 data 只是改了数据,DOM 更新是异步的,submit() 同步执行时视图还没重新渲染。
  • 修法:提交前直接操作 DOM —— document.querySelector('input').value = this.list[index].Q,再 submit();或把提交逻辑放进 this.$nextTick()。

9. v-text / v-html 的值用了反引号

  • 现象:页面上直接显示 userName 四个字母,而不是 zhangsan。
  • 原因:反引号在这里不是引号语法的一部分,Vue 把它当成字面量字符串。
  • 修法:只用双引号/单引号或不加引号,值必须是 data 里的属性名;另外注意 v-text/v-html 会覆盖标签内原有内容,且都不能和 <template> 搭配。

练习 ​

  1. 指令辨析:给定 isShow: false、msg: '<b>hi</b>'、age: 30,分别用 v-text、v-html、v-if/v-else-if、v-show 写出四种输出,并在注释里说明哪些标签被真正移出 DOM、哪些只是 display:none。

  2. jsonp 封装改造:把课上写的 jsonp(url, config) 改造成支持三个参数 jsonp(url, params, callbackKey),要求:params 为对象自动拼成查询串、callbackKey 缺省时不追加回调参数(兼容京东这类固定回调名的接口),并返回 Promise。

  3. 防抖 + 竞态:给搜索框实现 300ms 防抖,同时保证快速输入时后发的请求先返回也不会覆盖先发请求的结果(提示:给每次请求编号,回调里比对序号)。

  4. 下拉交互补全:基于 jsonp 搜索结果实现完整下拉:↓/↑ 循环选中并高亮(activeClass)、Enter 提交、失焦 200ms 后隐藏、鼠标点击选项跳转。要求点击和键盘两条路径行为一致。

  5. 边界自测:造三组数据(空数组、只有 1 条、20 条),验证 ↑/↓ 到边界后不会出现 undefined 报错,且输入框内容始终与高亮项一致;把发现的每个问题写成一句"现象 → 原因 → 修法"。

原视频对照 ​

#视频时长
1封装30:51
2模拟百度17:23
3模拟百度防抖05:26
4模拟百度完成25:07
5扩展16:30

小结 ​

  1. v-text / v-html / v-if 系列 / v-show 的取值最终都落在 data 上的属性名,引号写法可以省,但写成反引号就等于写死字面量。
  2. v-if 是真删真建,v-show 只是切 display;空字符串、0、null、undefined 在条件判断中一律等价于 false。
  3. v-if 与 v-for 永远不要写在同一个元素上,用 <template> 做外层条件、内层循环,顺带解决了多个元素的批量渲染。
  4. jsonp 的本质是「动态 script + 全局回调 + 用完即删」,回调名要唯一且不能以数字开头,delete window[fnName] 这一步漏了就会一直占内存。
  5. 防抖的三要素是 clearTimeout → 重新计时 → 回调里只用闭包捕获的旧值;间隔取 300ms 左右,太长用户会以为网站卡死。
  6. 视图更新是异步的,凡是在「改数据之后立刻读 DOM 或提交表单」的场景,要么直接操作 DOM,要么交给 $nextTick。
  7. 交互类 bug 八成出在事件顺序上:blur 先于 click,所以下拉、弹层这类"点了就消失"的组件,隐藏动作必须延迟或换 mousedown。
  8. 所有涉及数组下标的逻辑,先处理 -1 和越界两种情况,再谈循环和取值,否则一定在边界上崩。