JS 采用单线程的原因
JS 最早是运行在浏览器端的脚本语言,它的目的是为了实现页面上的动态交互,而实现页面交互的核心是 dom 操作,这也决定了它必须使用单线程模型,否则会出现复杂的线程同步问题。假定 JS 中有多个线程同步工作,其中一个线程修改了某个 dom 元素,而另一个线程同时又删除了该元素,此时浏览器无法明确该以哪个线程工作的结果为准。
任务分类
- 同步模式:任务依次执行,后面的任务必须等待前面的任务执行完毕,才能继续执行
- 异步模式:不会等待当前耗时任务结束再去执行下个任务
JS 异步编程的解决方案
回调函数
由调用者定义,交给执行者执行的函数
- 同步回调:立即执行,完全执行完成后才结束,不会放入到任务队列中,比如数组遍历相关的回调
- 异步回调:不会立即执行,会放入到任务队列中,等将来再执行,比如定时器回调,ajax 回调,promise 回调
Promise
优势
纯回调函数在执行异步操作之前就已经指定好了异步回调的函数,而 promise 指定回调函数的方式更加灵活,可以在异步任务后再指定
链式调用
借助 promise.then() 链式调用的特点,保证异步任务的扁平化。promise.then() 返回的新的 promise 的结果状态由 then 指定的回调函数执行的结果决定
- 如果抛出异常,新的 promise 变为 rejected,reason 为抛出的异常
- 如果返回的是非 promise 的任意值,新 promise 变为 resolved,value 为返回的值
- 如果返回的是另一个新的 promise,此 promise 就会成为新的 promise 的结果
promise 串连多个操作任务
new Promise((resolve, reject) => {
setTimeout(() => {
console.log("执行任务1(异步)");
resolve(1);
}, 1000);
})
.then((value) => {
console.log("任务1的执行结果:", value);
console.log("执行任务2(同步)");
return 2; // 同步任务直接返回
})
.then((value) => {
console.log("任务2的结果:", value);
// 异步任务通过 promise 进行包裹
return new Promise((resolve, reject) => {
// 启动任务3(异步)
setTimeout(() => {
console.log("执行任务3(异步)");
resolve(3);
}, 1000);
});
})
.then((value) => {
console.log("任务3的结果", value);
});
异常处理
2种异常处理的方式
ajax("/api/users.json").then(
function onFulfilled(value) {
console.log("onFulfilled", value);
return ajax("/error-url");
},
function onRejected(error) {
console.log("onRejected", error);
}
);
ajax("/api/users.json")
.then(function onFulfilled(value) {
console.log("onFulfilled", value);
return ajax("/error-url");
})
.catch(function onRejected(error) {
console.log("onRejected", error);
});
差异:每个 then 方法都是返回的一个全新的 promise 对象,catch 方法其实是给前面 then 方法返回的 promise 对象指定失败的回调,并不是给第一个 ajax 返回的 promise 对象指定的,只不过这是同一个 promise 链条,前面的 promise 异常一直会被往后传递。而通过 then 方法第二个参数指定的失败回调只是给第一个 promise 对象指定的。如果在 then 方法中返回的第二个 promise,并且在执行过程中也出现了异常,通过 then 第二个参数注册的失败回调是捕获不到的
除此之外,也可以在全局对象中注册 unhandledrejection 事件去处理没有手动被捕获的异常
// 错误可以一层层向外抛出,直到被最外层的catch捕获
new Promise((resolve, reject) => {
setTimeout(() => {
reject(1);
}, 1000);
})
.then((value) => {
return 2;
})
.then((value) => {
return 3;
})
.catch((value) => {
console.log("错误", value);
});
then 中没有写第二个错误回调,实则会隐式添加 reason=>{throw reason}
new Promise((resolve, reject) => {
setTimeout(() => {
reject(1);
}, 1000);
})
.then(
(value) => {
return 2;
},
(reason) => {
throw reason; // reject(1) 触发第二个回调,向上抛出
}
)
.then(
(value) => {
return 3;
},
(reason) => {
throw reason; // 继续抛出
}
)
.catch((value) => {
console.log("错误", value);
});
promise 异常穿透
当使用 promise 的 then 链式调用的时候,可以在最后指定失败的回调,前面任何操作出现了异常,都会传到最后失败的回调中进行处理
中断 promise 链 当使用 promise 的 then 链式调用时,想在中间中断,不再调用后面的回调函数,可以在回调函数中返回一个 pendding 状态的 promise 的对象
实现 Promise
- then 方法是可以被链式调用的, 后面 then 方法的回调函数拿到值的是上一个 then 方法的回调函数的返回值
- 一个 promise 对象可以指定多个成功/失败的回调函数,当 promise 对象改变为对应状态时都会调用,所以需要用数组存储成功/失败的回调函数
- then 方法的回调函数中不能返回自身调用的 promise 对象,会发生循环调用
- then 方法的回调函数是可选的,如果不传,相当于默认是 value => value,这样 value 值就可以一层层向后传递,直到传到最终有回调函数的地方
const PENDING = "pending"; // 等待
const FULFILLED = "fulfilled"; // 成功
const REJECTED = "rejected"; // 失败
class MyPromise {
// new Promise 中的执行器
constructor(executor) {
try {
executor(this.resolve, this.reject);
} catch (e) {
this.reject(e);
}
}
status = PENDING; // promise 状态
value = undefined; // 成功之后的值
reason = undefined; // 失败后的原因
successCallback = []; // 成功回调
failCallback = []; // 失败回调
// resolve 箭头函数确保 this 指向 promise 对象
resolve = (value) => {
// 如果状态不是等待 阻止程序向下执行 因为 promise 状态一旦改变就不会再变化了
if (this.status !== PENDING) return;
this.status = FULFILLED;
this.value = value; // 保存成功之后的值,方便在then方法中使用
while (this.successCallback.length) this.successCallback.shift()();
};
reject = (reason) => {
if (this.status !== PENDING) return;
this.status = REJECTED;
this.reason = reason; // 保存失败后的原因,方便在then方法中使用
while (this.failCallback.length) this.failCallback.shift()();
};
then(successCallback, failCallback) {
// 参数可选,不存在补充一个函数
successCallback = successCallback ? successCallback : (value) => value;
failCallback = failCallback
? failCallback
: (reason) => {
throw reason;
};
let promise2 = new MyPromise((resolve, reject) => {
// 判断状态
if (this.status === FULFILLED) {
// 用 setTimeout 进行包装,变成异步代码,从而可以获取到 promise2
setTimeout(() => {
try {
let x = successCallback(this.value);
// 判断 x 的值是普通值还是promise对象
// 如果是普通值 直接调用resolve
// 如果是 promise 对象 查看 promise 对象返回的结果
// 再根据 promise 对象返回的结果 决定调用 resolve 还是调用 reject
// setTimeout 变成异步,确保拿到 promise2
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
} else if (this.status === REJECTED) {
setTimeout(() => {
try {
let x = failCallback(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
} else {
// pending 状态需要将成功回调和失败回调存储起来
this.successCallback.push(() => {
setTimeout(() => {
try {
let x = successCallback(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.failCallback.push(() => {
setTimeout(() => {
try {
let x = failCallback(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
});
return promise2;
}
finally(callback) {
// finally 方法无论成功还是失败,都会执行
return this.then(
(value) => {
return MyPromise.resolve(callback()).then(() => value);
},
(reason) => {
return MyPromise.resolve(callback()).then(() => {
throw reason;
});
}
);
}
catch(failCallback) {
return this.then(undefined, failCallback);
}
static all(array) {
let result = [];
let index = 0;
return new MyPromise((resolve, reject) => {
function addData(key, value) {
result[key] = value;
index++;
if (index === array.length) {
resolve(result);
}
}
for (let i = 0; i < array.length; i++) {
let current = array[i];
if (current instanceof MyPromise) {
// promise 对象
current.then(
(value) => addData(i, value),
(reason) => reject(reason)
);
} else {
// 普通值
addData(i, array[i]);
}
}
});
}
static resolve(value) {
if (value instanceof MyPromise) return value;
return new MyPromise((resolve) => resolve(value));
}
}
function resolvePromise(promise2, x, resolve, reject) {
// 避免 promise 循环调用
if (promise2 === x) {
return reject(
new TypeError("Chaining cycle detected for promise #<Promise>")
);
}
if (x instanceof MyPromise) {
x.then(resolve, reject);
} else {
resolve(x);
}
}
module.exports = MyPromise;