20250307-vue侦听器

news/2025/3/10 23:11:49/

当我们需要在状态变化时执行一些 “副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。

我们可以使用 watch 选项在每次响应式属性发生变化时触发一个函数。(响应式属性‌是指那些能够在被修改时自动更新视图的属性)

什么是 Promise?

Promise 用于更加优雅地书写复杂的异步任务。

我们之前遇到的异步任务都是一次异步,如果需要多次调用异步函数呢?例如,如果我想分三次输出字符串,第一次间隔 1 秒,第二次间隔 4 秒,第三次间隔 3 秒:

javascript">setTimeout(function () {console.log("First");setTimeout(function () {console.log("Second");setTimeout(function () {console.log("Third");}, 3000);}, 4000);
}, 1000);

这段程序实现了这个功能,但是它是用 “函数瀑布” 来实现的。十分繁琐。

如果用 Promise 实现如下:

javascript">new Promise(function (resolve, reject) {setTimeout(function () {console.log("First");resolve();}, 1000);
}).then(function () {return new Promise(function (resolve, reject) {setTimeout(function () {console.log("Second");resolve();}, 4000);});
}).then(function () {setTimeout(function () {console.log("Third");}, 3000);
});

如上,Promise 将嵌套格式的代码变成了顺序格式的代码。

Promise 构造函数

Promise 构造函数接受一个函数作为参数,该函数是同步的并且会被立即执行,所以我们称之为起始函数。起始函数包含两个参数 resolve 和 reject,分别表示 Promise 成功和失败的状态。

起始函数执行成功时,它应该调用 resolve 函数并传递成功的结果。当起始函数执行失败时,它应该调用 reject 函数并传递失败的原因。

Promise 构造函数返回一个 Promise 对象,该对象具有以下几个方法:

  • then:用于处理 Promise 成功状态的回调函数。
  • catch:用于处理 Promise 失败状态的回调函数。
  • finally:无论 Promise 是成功还是失败,都会执行的回调函数。

示例如下:

当 Promise 被构造时,起始函数会被同步执行:

javascript">var promise = new Promise((resolve,reject) => {//异步操作setTimeout(() => {if (Math.random() < 0.5) {resolve('success');} else {reject('error');}},1000);});promise.then(result => {console.log(result);}).catch(error => {console.log(error);});

在上面的例子中,我们使用 Promise 构造函数创建了一个 Promise 对象,并使用 setTimeout 模拟了一个异步操作。如果异步操作成功,则调用 resolve 函数并传递成功的结果;如果异步操作失败,则调用 reject 函数并传递失败的原因。然后我们使用 then 方法处理 Promise 成功状态的回调函数,使用 catch 方法处理 Promise 失败状态的回调函数。

这段程序会直接输出 error 或 success。

resolve 和 reject 都是函数,其中调用 resolve 代表一切正常,reject 是出现异常时所调用的:

javascript">new Promise(function (resolve, reject) {var a = 0;var b = 1;if (b == 0) reject("Divide zero");else resolve(a / b);}).then(function (value) {console.log("a / b = " + value);}).catch(function (err) {console.log(err);}).finally(function () {console.log("End");});

Promise 类有 .then() .catch() 和 .finally() 三个方法,这三个方法的参数都是一个函数,.then() 可以将参数中的函数添加到当前 Promise 的正常执行序列,.catch() 则是设定 Promise 的异常处理序列,.finally() 是在 Promise 执行的最后一定会执行的序列。 .then() 传入的函数会按顺序依次执行,有任何异常都会直接跳到 catch 序列:

javascript">new Promise(function (resolve, reject) {console.log(1111);resolve(2222);
}).then(function (value) {console.log(value);return 3333;
}).then(function (value) {console.log(value);throw "An error";
}).catch(function (err) {console.log(err);
});

执行结果:

1111

2222

3333

An error

resolve() 中可以放置一个参数用于向下一个 then 传递一个值,then 中的函数也可以返回一个值传递给 then。但是,如果 then 中返回的是一个 Promise 对象,那么下一个 then 将相当于对这个返回的 Promise 进行操作,这一点从刚才的计时器的例子中可以看出来。

reject() 参数中一般会传递一个异常给之后的 catch 函数用于处理异常。

但是请注意以下两点:

  • resolve 和 reject 的作用域只有起始函数,不包括 then 以及其他序列;
  • resolve 和 reject 并不能够使起始函数停止运行,别忘了 return。

Promise 函数

上述的 "计时器" 程序看上去比函数瀑布还要长,所以我们可以将它的核心部分写成一个 Promise 函数:

javascript">function print(delay, message) {return new Promise(function (resolve, reject) {setTimeout(function () {console.log(message);resolve();}, delay);});
}

然后我们就可以放心大胆的实现程序功能了:

javascript">print(1000, "First").then(function () {return print(4000, "Second");
}).then(function () {print(3000, "Third");
});

这种返回值为一个 Promise 对象的函数称作 Promise 函数,它常常用于开发基于异步操作的库。

then 块默认会向下顺序执行,return 是不能中断的,可以通过 throw 来跳转至 catch 中断。

什么时候适合用 Promise 而不是传统回调函数呢?当需要多次顺序执行异步操作的时候,例如,如果想通过异步方法先后检测用户名和密码,需要先异步检测用户名,然后再异步检测密码的情况下就很适合 Promise。

什么时候我们需要再写一个 then 而不是在当前的 then 接着编程。

异步函数

前面编写过这样一个函数代码

javascript">function print(delay, message) {return new Promise(function (resolve, reject) {setTimeout(function () {console.log(message);resolve();}, delay);});
}print(1000, "First").then(function () {return print(4000, "Second");
}).then(function () {print(3000, "Third");
});

我们可以用异步函数简化为

javascript">async function asyncFunc() {await print(1000, "First");await print(4000, "Second");await print(3000, "Third");
}
asyncFunc();

这样就将异步操作变得像同步操作一样容易了。

异步函数 async function 中可以使用 await 指令,await 指令后必须跟着一个 Promise,异步函数会在这个 Promise 运行中暂停,直到其运行结束再继续运行。

javascript">async function asyncFunc() {try {await new Promise(function (resolve, reject) {throw "Some error"; // 或者 reject("Some error")});} catch (err) {console.log(err);// 会输出 Some error}
}
asyncFunc();

如果 Promise 有一个正常的返回值,await 语句也会返回它:

javascript">async function asyncFunc() {let value = await new Promise(function (resolve, reject) {resolve("Return value");});console.log(value);
}
asyncFunc();

async/await 是 JS 中用于处理异步操作的一种语法糖,它使得异步代码看起来像同步代码一样直观和易读。它是用同步的语法写异步的代码。

async 用于声明一个函数是异步的。一个标记为 async 的函数总是返回一个 Promise 对象。

await 关键字仅能在 async 函数内部使用,用于等待一个 Promise 完成并返回其结果。它会暂停当前的 async 函数的执行,直到 Promise 完成,然后继续执行 async 函数,并返回解析后的 Promise 值。

javascript"><template><p>Ask a yes/no question:<input v-model="question" :disabled="loading" /></p><p>{{answer}}</p>
</template><script>
export default {data() {return {question: '',answer: 'Questions usually contain a question mark. ;-)',loading: false}},watch: {//每当 question 改变时,这个函数就会执行question(newQuestion, oldQuestion) {if (newQuestion.includes('?')) {this.getAnswer();}}},methods: {async getAnswer() {this.loading = true;this.answer = "Thiking...";try {const res = await fetch('https://yesno.wtf/api')this.answer = (await res.json()).answer} catch (error) {this.answer ='Error! Could not reach the API. ' + error} finally {this.loading = false}},}
}
</script><style></style>

watch 选项也支持把键设置成用 . 分隔的路径:

javascript"><template><p>Ask a yes/no question:<input v-model="one.two.question" :disabled="loading" /></p><p>{{answer}}</p>
</template><script>
export default {data() {return {one: {two: {question: '',}},answer: 'Questions usually contain a question mark. ;-)',loading: false}},watch: {//每当 question 改变时,这个函数就会执行'one.two.question'(newQuestion, oldQuestion) {if (newQuestion.includes('?')) {this.getAnswer();}}},methods: {async getAnswer() {this.loading = true;this.answer = "Thiking...";try {const res = await fetch('https://yesno.wtf/api')this.answer = (await res.json()).answer} catch (error) {this.answer ='Error! Could not reach the API. ' + error} finally {this.loading = false}},}
}
</script><style></style>

深层侦听器

watch 默认是浅层的:被侦听的属性,仅在被赋新值时,才会触发回调函数——而嵌套属性的变化不会触发。如果想侦听所有嵌套的变更,你需要深层侦听器。

浅层:

javascript"><template><button @click="changeTwo">点击</button>
</template><script>
export default {data() {return {one: {two: ''},}},watch: {//每当 one 改变时,这个函数就会执行one(newOne, oldOne) {console.log("one 旧值:" + oldOne + "新值:" + newOne)}},methods: {changeTwo() {//点击无效this.one.two = "two 改变"//点击有效this.one = {two:'two新值'}}}
}
</script><style></style>

深层:

javascript"><template><button @click="changeTwo">点击</button>
</template><script>
export default {data() {return {one: {two: '旧值',},}},watch: {//每当 one 改变时,这个函数就会执行one:{handler(newOne, oldOne) {//注意:在嵌套的变更中,//只要没有替换对象本身即one,//那么这里的 'newValue' 和 'oldValue' 想通过console.log("one 旧值:" + JSON.stringify(oldOne) + " 新值:" + JSON.stringify(newOne))},//one 的嵌套改变,上边函数也会调用deep:true}},methods: {changeTwo() {this.one.two = "two新"}}
}
</script><style></style>

deep 选项还可以时一个数字,表示最大遍历深度——即 Vue 应该遍历对象嵌套属性的级数。

即时回调的侦听器

在创建侦听器时,立即执行一遍回调。

javascript">export default {// ...watch: {question: {handler(newQuestion) {// 在组件实例创建时会立即调用},// 强制立即执行回调immediate: true}}// ...
}

回调函数的初次执行就发生在 created 钩子之前。Vue 此时已经处理了 data、computed 和 methods 选项,所以这些属性在第一次调用时就是可用的。

一次性侦听器

javascript">export default {watch: {source: {handler(newValue, oldValue) {// 当 `source` 变化时,仅触发一次},once: true}}
}

副作用清理

有时我们可能会在侦听器中执行副作用,例如异步请求:

javascript">	watch: {one(newOne) {setTimeout(()=>{console.log("最新值": + newOne);},2000);}}

但如果在打印完成之前 one 发生了变化怎么办?当上一个打印完成时,它仍会使用已经过时的 one。理想情况下,我们希望能够在 one 变为新值时取消过时的请求。

我们可以使用 onWatcherCleanup() API来注册一个清理函数,当侦听器失效并准备重新运行时会被调用:

在处理网络请求时,经常会遇到需要中途需要取消请求的情况。这时候,AbortController API 就显得尤为重要了。

AbortController 提供了一个信号对象 AbortSignal,该对象可以用来取消与 Fetch API相关的操作。当我们创建 AbortController 实例时,会自动生成一个与之关联的 AbortSignal 对象。我们可以将这个 AbortSignal 对象作为参数传递给 fetch 函数,从而实现对网络请求的取消控制。

javascript">//创建 AbortController 实例:const controller = new AbortController();//获取 AbortSignal 对象:const signal = controller.signal;//使用 signal 对象发起 fetch 请求//在调用 fetch 函数时,我们将 signal 对象作为选项对象的 signal 属性传递进去:fetch(url, { signal }).then(response => {// 处理响应数据}).catch(error => {if (error.name === 'AbortError') {console.log('Fetch 请求已被取消');} else {// 处理其他错误}});

取消 fetch 请求

javascript">controller.abort();

调用 abort 方法后,与该 AbortController 实例关联的 fetch 请求会被中断,并在 Promise 链中抛出一个带有 name 属性为 AbortError 的错误。

javascript"><template></template><script>
import { onWatcherCleanup } from 'vue'
export default {data() {return {id: 0,}},watch: {id(newId) {const controller = new AbortController();fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {// 回调逻辑})onWatcherCleanup(()=>{//终止过期调用controller.abort()});}},methods: {}
}
</script><style></style>

onWatcherCleanup 必须在 watchEffect 效果函数或 watch 回调函数的同步执行期间调用:你不能在异步函数的 await 语句之后调用它。

也可以用 onCleanup 函数,

javascript">export default {watch: {id(newId, oldId, onCleanup) {// ...onCleanup(() => {// 清理逻辑})}}
}

回调的触发时机

当更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。

类似于组件更新,用户创建的侦听器回调函数也会被批量处理以避免重复调用。例如,如果我们同步将一千个项目推入被侦听的数组中,我们可能不希望侦听器触发一千次。

默认情况下,侦听器回调会在父组件更新(如有)之后、所属组件的 DOM 更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的 DOM,那么 DOM 将处于更新前的状态。

如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM,你需要指明 flush:'post' 选项:

javascript">export default {// ...watch: {key: {handler() {},flush: 'post'}}
}

还可以创建一个同步触发的侦听器,它会在 Vue 进行任何更新之前触发:

javascript">export default {// ...watch: {key: {handler() {},flush: 'sync'}}
}

this.$watch()

也可以使用组件实例的 $watch() 方法来命令式地创建一个侦听器:

javascript">export default {created() {this.$watch('question', (newQuestion) => {// ...})}
}

如果要在特定条件下设置一个侦听器,或者只侦听响应用户交互的内容,这方法很有用。它还允许你提前停止该侦听器。

停止侦听器

用 watch 选项或者 $watch() 实例方法声明的侦听器,会在宿主组件卸载时自动停止。因此,在大多数场景下,你无需关心怎么停止它。

在少数情况下,你的确需要在组件卸载之前就停止一个侦听器,这时可以调用 $watch() API 返回的函数:

javascript">const unwatch = this.$watch('foo', callback)// ...当该侦听器不再需要时
unwatch()

http://www.ppmy.cn/news/1578198.html

相关文章

使用 Docker 部署 Nginx,配置后端 API 轮询与多个子域名前端应用

使用 Docker 部署 Nginx&#xff0c;配置后端 API 轮询与多个子域名前端应用 在这篇博客中&#xff0c;我们将介绍如何通过 Docker 部署 Nginx 服务器&#xff0c;并配置 多个后端 API 的轮询负载均衡&#xff0c;同时通过 子域名 部署多个不同的前端应用。Nginx 将作为反向代…

利用pdf.js+百度翻译实现PDF翻译,创建中文PDF

基于JavaScript的PDF文档解析与智能翻译系统开发实践 一、功能预览 1.1 PDF加载 1.2 PDF翻译 二、系统架构设计 2.1 PDF智能翻译系统架构设计 层级模块名称功能描述技术实现呈现层Canvas渲染器PDF文档可视化渲染PDF.js + 动态视口计算 + 矩阵变换

C++修炼之路:初识C++

Hello大家好&#xff01;很高兴我们又见面啦&#xff01;给生活添点passion&#xff0c;开始今天的编程之路&#xff01; 我的博客&#xff1a;<但凡. 我的专栏&#xff1a;《编程之路》、《数据结构与算法之美》、《题海拾贝》 欢迎点赞&#xff0c;关注&#xff01; 引言 …

Locker 是 Godot 的一个开源插件,它提供了一种快速且可扩展的方式来使用不同的策略保存和加载数据,并且具有开箱即用的 JSON 和加密功能。

一、软件介绍 文末提供下载 Locker 插件是在 Godot 4.3 中创建的框架&#xff0c;旨在简化在 Godot 项目中保存、加载和管理数据的过程。该插件的主要目标之一是对用户自定义开放&#xff0c;允许使用不同的用户定义策略来访问数据。并且具有开箱即用的 JSON 和加密功能。 二、…

web—HTML

什么是web ●Web:全球广域网&#xff0c;也称为万维网(www World Wide Web),能够通过浏览器访问的网站。 在浏览器中呈现精美的网页。 1.网页由那几部分组成&#xff1f; >文字、图片、视频、音频、超链接&#xff0c;&#xff0c;&#xff0c; 2.我们看到的网页&#xf…

Android Dagger2 原理深度剖析

一、绪论 1.1 依赖注入在 Android 开发中的重要性 在 Android 开发的复杂生态中&#xff0c;构建高效、可维护和可测试的应用程序是开发者们始终追求的目标。依赖注入&#xff08;Dependency Injection&#xff0c;简称 DI&#xff09;作为一种关键的设计模式&#xff0c;在达…

vue知识点(1)

ref和reactive的区别 ref 用途&#xff1a;用于创建一个响应式的基本类型&#xff08;string、number、boolean&#xff09;或者引用类型&#xff08;object、array&#xff09;的数据。 返回值&#xff1a;返回一个带有.value属性的对象&#xff0c;访问或者修改数据时需要通…

AI 智能:开拓未知疆域的科技先锋

在当今科技迅猛发展的浪潮中&#xff0c;AI 智能无疑是最耀眼的弄潮儿&#xff0c;持续重塑着我们生活与工作的方方面面。然而&#xff0c;在这片广袤的技术海洋里&#xff0c;还有诸多潜藏在深处、尚未被广泛挖掘与讨论的领域&#xff0c;它们代表着 AI 智能未来发展的新方向&…