目录
2.async/await、Promise 和 setTimeout输出顺序
2. async/await、Promise 和 setTimeout 的区别
2.示例 1:多个 Promise 和 setTimeout
1.尾调用
尾调用优化(Tail Call Optimization, TCO)是一种编译器或解释器优化技术,用于减少函数调用时的栈空间消耗。当一个函数的最后一步是调用另一个函数时,编译器或解释器可以优化这一调用,使得当前函数的栈帧被复用,从而避免栈空间的持续增长。
尾调用优化的实际应用
1. **避免栈溢出**
在递归算法中,函数可能会频繁地自我调用,这会导致调用栈不断增长,最终可能引发栈溢出错误。尾调用优化可以有效避免这一问题。
**示例:阶乘计算**
function factorial(n) {
if (n === 0) return 1;
return n * factorial(n - 1); // 非尾调用
}
```
尾调用优化版本:
function factorial(n, acc = 1) {
if (n === 0) return acc;
return factorial(n - 1, n * acc); // 尾调用
}
通过引入累加器参数 `acc`,将递归调用改为尾调用,避免了栈溢出。
2. **提高性能**
尾调用优化通过复用栈帧,减少了函数调用的开销,从而提升了程序的性能。
3. **支持深层递归**
某些算法(如递归实现的斐波那契数列)在未优化的情况下,递归深度受栈空间限制,无法处理大规模输入。尾调用优化使得递归函数可以处理更深层次的调用。
**示例:斐波那契数列**
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
```
尾调用优化版本:
function fibonacci(n, a = 0, b = 1) {
if (n === 0) return a;
return fibonacci(n - 1, b, a + b); // 尾调用
}
通过引入辅助参数,将递归调用改为尾调用,避免了栈溢出并提升了性能。
4. **函数式编程的支持**
函数式编程语言(如 Scheme、Haskell 等)通常依赖递归而非循环来实现迭代逻辑。尾调用优化使得函数式编程语言能够高效地处理递归。
5. **状态机模拟**
尾调用优化可以用于模拟状态机,通过递归调用实现状态转移,而不会导致栈溢出。
**示例:状态机**
function processState(state) {
switch (state) {
case 'start':
console.log('Processing...');
return processState('processing');
case 'processing':
console.log('Done!');
return processState('done');
case 'done':
return state;
}
}
console.log(processState('start')); // 输出:Processing... Done! done
通过尾调用优化,状态机可以无限递归而不会导致栈溢出。
6. **异步任务调度**
尾调用优化可以用于实现异步任务链式调用,通过递归调用实现任务的顺序执行。
**示例:异步任务链**
function asyncProcess(input, callback) {
setTimeout(() => {
callback(`Processed: ${input}`);
}, Math.random() * 1000);
}
function pipeline(...fs) {
let result = null;
const execute = (index) => {
if (index < fs.length) {
asyncProcess(result, (r) => {
result = r;
execute(index + 1);
});
} else {
console.log('Pipeline completed:', result);
}
};
execute(0);
}
pipeline(
(input) => input + ' -> Step 1',
(input) => input + ' -> Step 2',
(input) => input + ' -> Step 3'
);
通过尾调用优化,异步任务可以链式调用而不会导致栈溢出。
2.async/await、Promise 和 setTimeout输出顺序
1. JavaScript 的事件循环机制
1.在深入讨论之前,简要了解 JavaScript 的事件循环机制是必要的。JavaScript 是单线程的,但它通过事件循环机制来处理异步操作,主要包括以下几个部分:
调用栈(Call Stack):用于跟踪函数的执行顺序。
任务队列(Task Queue):存放宏任务(如 setTimeout 回调)。
微任务队列(Microtask Queue):存放微任务(如 Promise 的回调、async/await 生成的回调)。
2.事件循环的基本流程是:
执行调用栈中的同步任务。
当调用栈为空时,先执行所有微任务队列中的任务。
然后从任务队列中取出一个宏任务执行。
重复上述过程。
2. async/await、Promise 和 setTimeout 的区别
- **Promise**:用于创建一个异步操作,它有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。then 和 catch 方法用于处理异步操作的结果,它们会被加入到微任务队列中。
- **async/await**:是基于 Promise 的语法糖,使得异步代码看起来更像同步代码。async 函数总是返回一个 Promise,而 await 会暂停函数的执行,直到等待的 Promise 被解决。
- **setTimeout**:用于在指定的时间后执行一个回调函数。无论延迟时间是多少,setTimeout 的回调都会被加入到宏任务队列中,等待当前调用栈和微任务队列清空后执行。
3. 执行顺序示例
1.代码示例(正常的)
console.log('开始');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
}).then(() => {
console.log('Promise 2');
});
async function asyncFunc() {
console.log('asyncFunc 开始');
await Promise.resolve();
console.log('asyncFunc 结束');
}
asyncFunc();
console.log('结束');
输出
解析:
1.同步代码执行:
console.log('开始'); 输出 开始。
遇到 setTimeout,将其回调函数加入宏任务队列,等待执行。
Promise.resolve().then(...) 创建一个立即解决的 Promise,其第一个 then 回调被加入微任务队2.列。
2.调用 asyncFunc():
输出 asyncFunc 开始。
await Promise.resolve(); 暂停 asyncFunc 的执行,并将后续代码(console.log('asyncFunc 结束');)包装成一个 Promise 的 then 回调,加入微任务队列。
输出 结束。
3.执行微任务队列:
执行第一个 Promise 的 then 回调,输出 Promise 1。
该 then 回调又添加一个新的 then 回调到微任务队列。
执行 asyncFunc 中暂停后的回调,输出 asyncFunc 结束。
执行第二个 Promise 的 then 回调,输出 Promise 2。
执行宏任务队列:
执行 setTimeout 的回调,输出 setTimeout。
2.示例 1:多个 Promise 和 setTimeout
代码示例
console.log('开始');
Promise.resolve().then(() => {
console.log('Promise 1');
setTimeout(() => {
console.log('setTimeout 1');
}, 0);
});
setTimeout(() => {
console.log('setTimeout 2');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 2');
});
console.log('结束');
输出:
解析
- 同步代码输出 开始 和 结束。
- 两个 Promise 的 then 回调被加入微任务队列,依次执行,输出 Promise 1 和 Promise 2。
- 在 Promise 1 中,setTimeout 的回调被加入宏任务队列。
- 另外一个 setTimeout 的回调也被加入宏任务队列。
- 微任务队列清空后,执行宏任务队列中的回调,按它们被添加的顺序,输出 setTimeout 2 和 setTimeout 1。
示例 2:嵌套 async/await 和 Promise
代码示例
console.log('开始');
async function func1() {
console.log('func1 开始');
await Promise.resolve();
console.log('func1 结束');
}
async function func2() {
console.log('func2 开始');
func1();
await new Promise(resolve => setTimeout(resolve, 0));
console.log('func2 结束');
}
func2();
Promise.resolve().then(() => {
console.log('Promise 1');
});
setTimeout(() => {
console.log('setTimeout');
}, 0);
console.log('结束');
输出
解释
- 1.同步代码执行:
console.log('开始'); 首先执行,输出 **"开始"**。
调用 func2(),进入 func2 函数:
输出 **"func2 开始"**。
调用 func1(),进入 func1 函数:
输出 **"func1 开始"**。
await Promise.resolve(); 将 func1 的剩余部分(console.log('func1 结束');)加入 微任务队列,并暂停 func1 的执行。
func1() 调用后,继续执行 func2:
await new Promise(resolve => setTimeout(resolve, 0)); 创建一个 宏任务(setTimeout),并将其回调加入 宏任务队列,然后暂停 func2 的执行。
- 2.继续执行同步代码:
Promise.resolve().then(() => { console.log('Promise 1'); }); 将回调加入 微任务队列。
setTimeout(() => { console.log('setTimeout'); }, 0); 将回调加入 宏任务队列(注意,这个 setTimeout 的回调会在前面的 setTimeout 之后执行,因为它们都在宏任务队列中按顺序排列)。
输出 **"结束"**。
- 3.处理微任务队列:
当前调用栈已清空,事件循环开始处理 微任务队列:
执行 Promise 1 的回调,输出 **"Promise 1"**。
接下来执行 func1 中暂停的部分,输出 **"func1 结束"**。
微任务队列现已清空。
常见概念模糊点解析
- 1.**Promise 构造函数 vs. Promise.resolve()**:
**new Promise**:构造函数中的执行器函数是同步执行的,.then 回调会被加入微任务队列。
**Promise.resolve()**:立即创建一个已解决的 Promise,其 .then 回调被加入微任务队列。
**async/await 与 Promise 的关系**:
- 2.async 函数总是返回一个 Promise。
await 会暂停函数的执行,并将后续代码作为微任务加入队列,等待 Promise 解决。
- 3.微任务队列的执行顺序:
微任务按照它们被添加到队列的顺序依次执行。
多个 Promise 的 .then 回调和 async/await 生成的回调都遵循这一规则。
- 4.**setTimeout 与 Promise 的交互**:
setTimeout 的回调是宏任务,会在当前调用栈和微任务队列清空后执行。
在 setTimeout 回调中添加的 Promise 回调会被加入新的微任务队列,等待当前宏任务完成后再执行。
- 5.嵌套 Promise 和 async/await:
嵌套的 Promise 和 async/await 会增加微任务队列的复杂性,需逐层跟踪每个异步操作的挂起点和恢复点。
3.AJAX、Axios 和 Fetch 的区别概述
1. AJAX 示例
function fetchJsonData(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const data = JSON.parse(xhr.responseText);
resolve(data);
} catch (error) {
reject(error);
}
} else {
reject(new Error('AJAX 请求失败,状态码: ' + xhr.status));
}
};
xhr.onerror = function () {
reject(new Error('AJAX 请求发生错误'));
};
xhr.send();
});
}
// 使用封装后的函数
const url = 'https://api.example.com/data';
fetchJsonData(url)
.then(data => {
console.log('AJAX 数据:', data);
})
.catch(error => {
console.error('请求失败:', error);
});
2.Axios 示例
Axios 是一个基于 Promise 的 HTTP 客户端,可以在浏览器和 Node.js 中使用。它提供了更简洁的 API,并内置了许多实用功能,如自动转换 JSON 数据、拦截请求和响应、取消请求等。
1.安装axios
npm install axios
2.js中代码示例
import axios from 'axios';
// 发起 GET 请求
axios.get('https://api.example.com/data')
.then(response => {
console.log('Axios 数据:', response.data);
})
.catch(error => {
console.error('Axios 请求失败:', error);
});
3.在 Vue 中的应用示例
<template>
<div>
<button @click="fetchData">获取数据</button>
<div v-if="loading">加载中...</div>
<div v-else>
<pre>{{ data }}</pre>
</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
loading: false,
data: null,
error: null
};
},
methods: {
fetchData() {
this.loading = true;
this.error = null;
axios.get('https://api.example.com/data')
.then(response => {
this.data = response.data;
})
.catch(error => {
this.error = `请求失败: ${error.message}`;
})
.finally(() => {
this.loading = false;
});
}
}
};
</script>
<style>
.error {
color: red;
}
</style>
4. 使用axios拦截器
// 创建 Axios 实例
const apiClient = axios.create({
baseURL: 'https://api.example.com',
timeout: 1000,
headers: { 'X-Custom-Header': 'foobar' }
});
// 添加请求拦截器
apiClient.interceptors.request.use(config => {
// 在发送请求之前做些什么
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, error => {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
apiClient.interceptors.response.use(response => {
// 对响应数据做点什么
return response;
}, error => {
// 对响应错误做点什么
return Promise.reject(error);
});
// 使用实例发起请求
apiClient.get('/data')
.then(response => {
console.log('API 数据:', response.data);
})
.catch(error => {
console.error('API 请求失败:', error);
});
3. Fetch 示例
Fetch 是现代浏览器原生提供的网络请求 API,基于 Promise,提供了比 AJAX 更简洁的语法。然而,Fetch 不会自动处理 JSON 数据,也不支持一些高级功能(如取消请求需要借助 AbortController),需要开发者手动处理。
js示例
// 发起 GET 请求
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP 错误! 状态: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Fetch 数据:', data);
})
.catch(error => {
console.error('Fetch 请求失败:', error);
});
vue代码示例
<template>
<div>
<button @click="fetchData">获取数据</button>
<div v-if="loading">加载中...</div>
<div v-else>
<pre>{{ data }}</pre>
</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
</template>
<script>
export default {
data() {
return {
loading: false,
data: null,
error: null
};
},
methods: {
fetchData() {
this.loading = true;
this.error = null;
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP 错误! 状态: ${response.status}`);
}
return response.json();
})
.then(data => {
this.data = data;
})
.catch(error => {
this.error = `请求失败: ${error.message}`;
})
.finally(() => {
this.loading = false;
});
}
}
};
</script>
<style>
.error {
color: red;
}
</style>
4.总结
- AJAX 是早期的网络请求技术,虽然兼容性好,但在现代开发中逐渐被更先进的技术取代。
- Axios 提供了更简洁和强大的 API,内置了许多实用功能,是当前最流行的网络请求库之一。
- Fetch 是现代浏览器原生提供的 API,语法简洁,但需要手动处理一些功能,适合轻量级应用或对包体积有要求的项目。