当我们需要在状态变化时执行一些 “副作用”:例如更改 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()