从异步到promise
一,背景
1.1,js的单线程
这一切,要从js诞生之初说起,因为js是单线程的语言。
js单线程原因:作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?当然,这可以加锁来解决,但是加锁会变得更复杂,得不偿失。
1.2,同步异步的讨论
先简单理解下同步和异步:
同步操作可以理解为不花费时间的操作,而异步操作是需要一定时间的。
单线程意味着所有的任务都只能乖乖排队执行,就和我们排队过地铁闸机一样。但是有的人刷卡,有的人刷码,有的人刷脸,所花费的时间不一样,假如一个人手机没网了,就会卡住队伍,造成堵塞。这就是单线程带来的同步阻塞问题。
为了解决这个问题,js引入了回调函数机制,对于一个IO操作,比如一个ajax,当发出一个异步请求后,程序不会阻塞在那里等待结果的返回,而是继续执行下面的代码。当请求成功获取到结果后,就会调用回调函数来处理后面的事情,这个就是异步非阻塞。在js中,异步操作的处理都是异步非阻塞。
所以可以说js是单线程异步编程语言。
如下代码:
console.log(1)
setTimeout(()=>{console.log(2)
},100)
console.log(3)
//执行结果:132
setTimeout里面执行的就是异步操作的回调函数,等待完成后执行其中的代码。类似于地铁过闸机没网的那个兄弟先站到旁边处理下网络问题,解决了再排队进闸机,这样就不会堵着后续的人。
1.3,回调函数
那如果异步操作完成后我们想要用这个结果做一些事情呢?有人说可以把代码写在异步操作的结果处理里面,但是这样代码一层包一层的,很难复用和维护。
为了解决这个问题,有人提出了回调函数的方案。
其实上文中的setTimeout里面的箭头函数就是一个回调函数,但是不够直观,这里再举个例子。
异步耗时的工作,常见的有以下这些:
定时器
建立网络连接
向文件读写数据
可以拿向文件读写数据来举例,先看如下代码:
const fs = require('fs')
function printA(){console.log(a)
}
function readFile(){fs.readFile('test.txt', (err, data) => {if (err) {console.error(err)return}// data 是二进制类型,需要转换成字符串console.log("读取到的文本内容:",data.toString())a=1})
}
let a=0
readFile()
printA()
从上文可以知道,a虽然被修改了,但它是在读文件操作(异步操作)中被修改的,因为js异步非阻塞的特性,在执行printA中的打印a时,它并没有执行,所以打印出来的a将是0。
那如何把a=1打印出来呢?
我们可以把一个函数作为参数,传给readFile,在readFile函数中执行打印操作。
const fs = require('fs')
function printA(){console.log(a)
}
function readFile(callback){fs.readFile('test.txt', (err, data) => {if (err) {console.error(err)return}// data 是二进制类型,需要转换成字符串console.log("读取到的文本内容:",data.toString())a=1callback()//这就是传进来的回调函数})
}
let a=0
readFile(printA)
这就是回调函数:函数体在完成某种操作后由内向外调用某个外部函数,因为是在函数体内部调用,所以能由内向外获取到相关变量(作用域链)。
简单点说,就是把函数当作参数,传递给异步操作完成之后再执行。
1.4,js的事件循环机制
上文说到,js的异步非阻塞机制,那它是如何实现就一个主线程,完美运行所有的同步异步代码呢?
这就需要理解下js的事件循环机制,也就是人们所说的(Event Loop),可以用下图:
常见的微任务:
Promises.(then catch finally),process.nextTick, MutationObserver
DOM渲染前触发
这里值得注意的是promise内是同步,而.then后才是微任务。
常见的宏任务:
整体代码script,setTimeout,setInterval ,setImmediate,I/O,UI renderingnew
DOM渲染后触发
也就是说,遇到异步函操作,还需要判断是宏任务还是微任务,宏任务的话,就把异步操作的结果加入宏任务队列,微任务的话,就加入到微任务队列。
于是,异步到的队列,就由原来的一个事件队列,变成了宏队列和微队列两个,而主线程空了的话,会先去微队列中查找(若在这个过程中,微队列的事件又产生的新的微任务加入队尾,也会在本次循环中进行处理,简而言是就是把每轮循环把微队列搞空),然后再去宏队列中查找(而每次宏任务只执行一个,就又轮到微任务队列)。
第一步: 主线程执行同步任务的同时,把一些异步任务放入‘任务队列’(task queue)中,等待主线程的调用栈为空时,再依次从队列出去任务去执行;
第二步:检测任务队列中的微队列是否为空,若不为空,则取出一个微任务入栈执行;然后继续执行第2步;如果微队列为空,则开始取出宏队列中的一个宏任务执行;
第三步:执行完宏队列中的一个宏任务后,会继续检测微队列是否为空,如果有新插入的任务,这继续执行第二步;如果微队列为空,则继续执行宏队列中的下一个任务,然后再继续循环执行第三步;
示例:
const fs = require('fs')
setTimeout(function callback(){console.log('2')//宏任务Promise.resolve().then(()=>{console.log(6)//宏任务中微任务})
}, 0)
setTimeout(function callback(){console.log('又一个宏任务')//宏任务
}, 0)
new Promise((resolve, reject) => {console.log('3')//同步resolve()
})
.then(res => {console.log('4');//微任务new Promise((resolve, reject) => {console.log('8')//同步fs.readF ile('test.txt', (err, data) => {if (err) {console.error(err)return}// data 是二进制类型,需要转换成字符串console.log("读取到的文本内容:",data.toString())resolve(data.toString())//宏任务})}).then(res => {new Promise((resolve, reject) => {console.log('测试')//同步resolve()}).then(res => {console.log('再测试');//微任务})})
})
console.log('5')//同步
打印的值:
3
5
4
8
2
6
又一个宏任务
读取到的文本内容: 测试文本
测试
再测试
1.5,回调函数的问题和promise的诞生
自此,js已经能够依托事件循环机制,执行所有的任务。但是因为有的任务需要依托异步任务返回的结果,所以我们采用了回调函数的解决方案,那如果有多重异步操作,并且最后的操作依赖于这几次异步操作。那么就容易层层嵌套,带来回调地狱的问题。
如下代码,我创建了三个txt文件,然后需要依次读取文件内容并打印,如果采取回调函数的写法,将是如下样子:
const fs = require('fs')
fs.readFile('test.txt', (err,data) => {let reader1=data.toString()fs.readFile('test1.txt', (err,data) => {let reader2=data.toString()fs.readFile('test2.txt', (err,data) => {let reader3=data.toString()setTimeout(()=>{let result=reader1+reader1+reader3console.log("获得的结果",result)},10)})})
})
fs模块的readFile的第二个参数,便是一个回调函数,为了按照顺序读取文本内容,则需要多层嵌套,这样只有几次倒还好,实际项目中却容易出现多层,导致代码出现回调地狱,难以阅读维护。
为了解决这个问题,promise应运而生。
回调地狱的产生,原因还是回调函数对结果的处理和异步操作终究还是在一起,并没有把分离。而引入promise的最大作用,就是把异步操作的过程和结果做到了分离,可以用promise.then()来获取和处理异步操作的结果。使用promise修改上文的代码:
const fs = require('fs')
function readFile(fileName){return new Promise((resolve,reject)=>{fs.readFile(fileName, (err,data) => {resolve(data.toString())})})
}
let res1,res2,res3
readFile('test.txt')
.then((res)=>{res1=resreturn readFile('test1.txt')
})
.then((res)=>{res2=resreturn readFile('test2.txt')
})
.then((res)=>{res3=resconsole.log("结果",res1+res2+res3)
})
这样一来,就把异步的操作和结果通过.then进行了分离。避免了回调地狱的产生。
二,promise的then方法实现
那promise到底是个啥?为何能有如此效果呢?
2.1,promise的三种状态
pending: 一个promise在resolve或者reject前就处于这个状态。
fulfilled: 一个promise被resolve后就处于fulfilled状态,这个状态不能再改变,而且必须拥有一个不可变的值(value)。
rejected: 一个promise被reject后就处于rejected状态,这个状态也不能再改变,而且必须拥有一个不可变的拒绝原因(reason)。
2.2,promise.then方法
一个promise必须拥有一个then
方法来访问他的值或者拒绝原因。then
方法有两个参数:
promise.then(onFulfilled, onRejected)
并且这两个参数有以下特征:
1,onFulfilled 和 onRejected 都是可选参数。若不存在则忽略。
2,onFulfilled在promise结束前不可调用,结束后必须被调用,其第一个参数为 promise 的终值value,且调用次数不超过一次。
3,onRejected在被拒绝前不可被调用,拒绝执行后其必须被调用,其第一个参数为 promise 的原因reason,且调用册数不超过一次。
而这个then方法还支持以下特征:
1,then 方法必须返回一个 promise 对象。这样才支持多次then链式调用。
2,then 方法可以被同一个 promise 调用多次。当 promise 成功执行时,所有 onFulfilled 需按照其注册顺序依次回调,当 promise 被拒绝执行时,所有的 onRejected 需按照其注册顺序依次回调。
2.3,先写出基本的promise,实现异步操作与结果的分离
我们平时使用promise是这样的:
const p1 = new Promise((resolve, reject) => {console.log('create a promise');setTimeout(()=>{console.log("异步")resolve('成功了');},10)})
const p2 = p1.then((res)=>{console.log(res)
})
所以promise接收一个函数,该函数有两个参数,resolve和reject,并且同步执行这个函数。
// 先定义三个常量表示状态
var PENDING = 'pending';//等待中
var FULFILLED = 'fulfilled';//执行完成
var REJECTED = 'rejected';//拒绝执行function MyPromise(fn) {this.status = PENDING; // 初始状态为pendingthis.value = null; // 初始化valuethis.reason = null; // 初始化reason//同步执行这个函数try {fn(resolve, reject);} catch (error) {reject(error);}
}
exports.MyPromise=MyPromise
可以看到fn(resolve, reject)异步操作执行完毕后,会调用resolve和reject,于是promise里面就需要创建resolve和reject提供调用。
// 这两个方法直接写在构造函数里面
function MyPromise(fn) {// ...省略前面代码...// 存一下this,以便resolve和reject里面访问var that = this;// resolve方法参数是valuefunction resolve(value) {if(that.status === PENDING) {that.status = FULFILLED;that.value = value;}}// reject方法参数是reasonfunction reject(reason) {if(that.status === PENDING) {that.status = REJECTED;that.reason = reason;}}
}
这样一来,就能在执行完promise包裹的异步操作后,调用resolve或者reject,从而改变promise的状态,并且取到结果value或原因reason。
但是,就目前的代码而言,还需要then方法来获取结果并处理。
根据我们前面的分析,then
方法可以链式调用,所以他是实例方法,而且规范中的API是promise.then(onFulfilled, onRejected)
,我们先把架子搭出来:
MyPromise.prototype.then = function(onFulfilled, onRejected) {}
如此,我们在promise中会执行异步操作,异步操作完成后会调用resolve方法,修改promise的状态,并且把结果存在promise的value上。那现在如何在then方法中取得这个结果并执行呢?
首先需要判断该异步操作已经执行完毕(通过promise的状态变成FULFILLED)。异步操作的结果这时候已经存储到promise的value上了,于是只需要then的参数是函数,把这个value传进去即可。
MyPromise.prototype.then = function(onFulfilled, onRejected) {if(this.status === FULFILLED) {onFulfilled(this.value)//将结果传入回调函数}if(this.status === REJECTED) {onRejected(this.reason);}
}
我们现在来试试现在写的promise,写出如下代码:
var MyPromise = require('./test.js').MyPromise;
const p1 = new MyPromise((resolve, reject) => {console.log('create a promise');setTimeout(()=>{console.log("异步")resolve('成功了');},10)})
console.log("直接打印",p1)
setTimeout(()=>{console.log("过两s后",p1)const aaa=p1.then((value)=>{console.log(value)})
},2000)
打印的结果:
注意到,这里我们是使用setTimeout包裹了then函数,这意味着现在只是将结果取到了then中进行处理,还不能等待异步完成后再执行then中的代码。
到这里,就可以大致知道promise到底是个啥?
可以把它当作一个异步操作的处理站点。
初始时状态是pending,当执行完毕异步操作(用户定义)后调用resolve/reject,修改状态变成fulfilled/rejected,并且把结果存储在value/reason。
然后then方法判断异步操作是否完成,完成的话,就取value/reason来完成用户接下来的操作(用户定义)。
其实还是利用的回调函数,把promsie当作中转站,将异步操作和结果隔离开来。(异步操作放在promise的入参函数中,结果放value/reason,then方法需要用到了再去这里取)。
2.4,then方法的回调函数收集与执行
实际上,我们使用promise的时候,不会像上文那样,用一个setTimeout来包裹,更多时候如下使用:
new Promise(fn).then(onFulfilled, onRejected);
上面代码then
是在实例对象一创建好就调用了,这时候fn
里面的异步操作可能还没结束,也就是说他的status
还是PENDING
,这时候肯定不能执行then里面的代码,因为结果还没有存储到value上。
那怎么在异步操作完成后再执行then里面的代码呢?之前说过,resolve和reject方法就是在异步操作完成后调用的,所以可以在这里执行then传入的回调函数。
于是修改我们的promise:
// 构造函数
function MyPromise(fn) {// ...省略其他代码...// 构造函数里面添加两个数组存储成功和失败的回调this.onFulfilledCallbacks = [];this.onRejectedCallbacks = [];function resolve(value) {if(that.status === PENDING) {// ...省略其他代码...// resolve里面将所有成功的回调拿出来执行that.onFulfilledCallbacks.forEach(callback => {callback(that.value);});}}function reject(reason) {if(that.status === PENDING) {// ...省略其他代码...// resolve里面将所有失败的回调拿出来执行that.onRejectedCallbacks.forEach(callback => {callback(that.reason);});}}
}// then方法
MyPromise.prototype.then = function(onFulfilled, onRejected) {// ...省略其他代码...// 如果还是PENDING状态,将回调保存下来if(this.status === PENDING) {this.onFulfilledCallbacks.push(onFulfilled);this.onRejectedCallbacks.push(onRejected);}
}
也就是说,promise.then做的事情,就是把传入的回调函数收集起来,然后在resolve或reject中取出收集好的回调函数来执行。
这样一来。就能保证then中的回调函数在异步操作结束后再执行。到这里我们已经初步实现了promise最核心的异步操作过程与结果的分离。
2.5,then的入参和返回
2.5.1,then的入参
之前说过,then方法的入参不是函数则忽略,其实所谓“忽略”并不是什么都不干,对于onFulfilled
来说“忽略”就是将value
原封不动的返回,对于onRejected
来说就是返回reason
,onRejected
因为是错误分支,我们返回reason
应该throw一个Error,所以这里需要重写这两个入参:
MyPromise.prototype.then = function(onFulfilled, onRejected) {// 如果onFulfilled不是函数,给一个默认函数,返回valuevar realOnFulfilled=typeof onFulfilled === 'function' ? onFulfilled : value=>value// 如果onRejected不是函数,给一个默认函数,返回reason的Errorvar realOnRejected =typeof onRejected === 'function' ? onRejected : reason=>{throw reason}//...省略其他代码
}
2.5.2,then的返回得是promise
then
的返回值必须是一个promise(这里使用promise2)。且如果onFulfilled或者onRejected抛出一个异常e,则这个新的promise2必须拒绝执行,并且返回原因e。也就是把onFulfilled和onRejected用try…catch包裹一层。
MyPromise.prototype.then = function(onFulfilled, onRejected) {// 如果onFulfilled不是函数,给一个默认函数,返回valuevar realOnFulfilled=typeof onFulfilled === 'function' ? onFulfilled : value=>value// // 如果onRejected不是函数,给一个默认函数,返回reason的Errorvar realOnRejected =typeof onRejected === 'function' ? onRejected : reason=>{throw reason}var that=thisvar promise2 = new MyPromise(function(resolve, reject) {//如果异步已经成功则直接执行回调函数,期间有报错,需要rejectif(that.status === FULFILLED) {try {realOnFulfilled(that.value);} catch (error) {reject(error);} }//如果异步已经失败则直接执行回调函数,期间有报错,需要rejectif(that.status === REJECTED) { try {realOnRejected(that.reason);} catch (error) {reject(error);}}// 如果还是PENDING状态,将回调保存下来,为了捕获回调中的错误,需要try...catch包裹一层if(that.status === PENDING) {that.onFulfilledCallbacks.push(function() {try {realOnFulfilled(that.value);} catch (error) {reject(error);}});that.onRejectedCallbacks.push(function() {try {realOnRejected(that.reason);} catch (error) {reject(error);}});}})return promise2
}
2.5.3,then的入参默认是函数
而如果onFulfilled不是函数且promise1成功执行,那么promise2必须成功执行并且返回相同的值。这时候,就需要直接透传resolve(this.value)给promise2了。
如果 onRejected
不是函数且 promise1
拒绝执行, promise2
必须拒绝执行并返回相同的原因。
MyPromise.prototype.then = function(onFulfilled, onRejected) {// 如果onFulfilled不是函数,给一个默认函数,返回valuevar realOnFulfilled=typeof onFulfilled === 'function' ? onFulfilled : value=>value// // 如果onRejected不是函数,给一个默认函数,返回reason的Errorvar realOnRejected =typeof onRejected === 'function' ? onRejected : reason=>{throw reason}var that=this//如果异步已经成功则直接执行回调函数,期间有报错,需要rejectif(this.status === FULFILLED) {var promise2 = new MyPromise(function(resolve, reject) {try {//then方法传入非函数,且成功执行,则把结果传给promise2if (typeof onFulfilled !== 'function') {resolve(that.value);} else {realOnFulfilled(that.value);resolve(that.value);}} catch (error) {reject(error);} });return promise2;}//如果异步已经失败则直接执行回调函数,期间有报错,需要rejectif(this.status === REJECTED) { var promise2 = new MyPromise(function(resolve, reject) {try {//then方法传入非函数,且成功执行,则把结果传给promise2if (typeof onRejected !== 'function') {reject(that.reason);} else {realOnRejected(that.reason);//promise1的onRejected执行成功后,promise2应该被resolve,这样promise2的状态才会是fulfilledresolve();}} catch (error) {reject(error);}});return promise2;}// 如果还是PENDING状态,将回调保存下来,为了捕获回调中的错误,需要try...catch包裹一层if(this.status === PENDING) {var promise2 = new MyPromise(function(resolve, reject) {that.onFulfilledCallbacks.push(function() {try {//如果传入的不是函数,则透传结果给promise2if (typeof onFulfilled !== 'function') {resolve(that.value);} else {realOnFulfilled(that.value);resolve(that.value);}} catch (error) {reject(error);}});that.onRejectedCallbacks.push(function() {try {//如果传入的不是函数,则透传结果给promise2if (typeof onRejected !== 'function') {reject(that.reason);} else {realOnRejected(that.reason);resolve()}} catch (error) {reject(error);}});});return promise2;}}
2.5.4,如果传入then的回调函数有返回值
如果 onFulfilled
或者 onRejected
返回一个值 x
,需要区分x的类别,运行下面的 Promise 解决过程:
MyPromise.prototype.then = function(onFulfilled, onRejected) {// 如果onFulfilled不是函数,给一个默认函数,返回valuevar realOnFulfilled=typeof onFulfilled === 'function' ? onFulfilled : value=>value// // 如果onRejected不是函数,给一个默认函数,返回reason的Errorvar realOnRejected =typeof onRejected === 'function' ? onRejected : reason=>{throw reason}var that=thisvar promise2 = new MyPromise(function(resolve, reject) {//如果异步已经成功则直接执行回调函数,期间有报错,需要rejectif(that.status === FULFILLED) {try {if (typeof onFulfilled !== 'function') {resolve(that.value);} else {var x = realOnFulfilled(that.value);resolvePromise(promise2, x, resolve, reject);}} catch (error) {reject(error);} }//如果异步已经失败则直接执行回调函数,期间有报错,需要rejectif(that.status === REJECTED) { try {if (typeof onRejected !== 'function') {reject(that.reason);} else {var x = realOnRejected(that.reason);resolvePromise(promise2, x, resolve, reject); // 调用Promise 解决过程}} catch (error) {reject(error);}}// 如果还是PENDING状态,将回调保存下来,为了捕获回调中的错误,需要try...catch包裹一层if(that.status === PENDING) {that.onFulfilledCallbacks.push(function() {try {if (typeof onFulfilled !== 'function') {resolve(that.value);} else {var x = realOnFulfilled(that.value);resolvePromise(promise2, x, resolve, reject); // 调用Promise 解决过程}} catch (error) {reject(error);}});that.onRejectedCallbacks.push(function() {try {if (typeof onRejected !== 'function') {reject(that.reason);} else {var x = realOnRejected(that.reason);resolvePromise(promise2, x, resolve, reject); // 调用Promise 解决过程}} catch (error) {reject(error);}});}})return promise2
}
在规范中还有一条:onFulfilled
和 onRejected
只有在执行环境堆栈仅包含平台代码时才可被调用。这一条的意思是实践中要确保 onFulfilled
和 onRejected
方法异步执行,且应该在 then
方法被调用的那一轮事件循环之后的新执行栈中执行。所以在我们执行onFulfilled
和 onRejected
的时候都应该包到setTimeout
里面去。
按照我个人的理解,这里包裹个setTimeout是为了让resolvePromise(promise2, x, resolve, reject);
这里传入的promise2不是undefined。
于是then方法变成:
MyPromise.prototype.then = function(onFulfilled, onRejected) {// 如果onFulfilled不是函数,给一个默认函数,返回valuevar realOnFulfilled=typeof onFulfilled === 'function' ? onFulfilled : value=>value// // 如果onRejected不是函数,给一个默认函数,返回reason的Errorvar realOnRejected =typeof onRejected === 'function' ? onRejected : reason=>{throw reason}var that=thisvar promise2 = new MyPromise(function(resolve, reject) {//如果异步已经成功则直接执行回调函数,期间有报错,需要rejectif(that.status === FULFILLED) {setTimeout(function() {try {if (typeof onFulfilled !== 'function') {resolve(that.value);} else {var x = realOnFulfilled(that.value);resolvePromise(promise2, x, resolve, reject);}} catch (error) {reject(error);} },0)}//如果异步已经失败则直接执行回调函数,期间有报错,需要rejectif(that.status === REJECTED) { setTimeout(function() {try {if (typeof onRejected !== 'function') {reject(that.reason);} else {var x = realOnRejected(that.reason);resolvePromise(promise2, x, resolve, reject); // 调用Promise 解决过程}} catch (error) {reject(error);}},0)}// 如果还是PENDING状态,将回调保存下来,为了捕获回调中的错误,需要try...catch包裹一层if(that.status === PENDING) {that.onFulfilledCallbacks.push(function() {setTimeout(function () {try {if (typeof onFulfilled !== 'function') {resolve(that.value);} else {var x = realOnFulfilled(that.value);resolvePromise(promise2, x, resolve, reject); // 调用Promise 解决过程}} catch (error) {reject(error);}},0)});that.onRejectedCallbacks.push(function() {setTimeout(function () {try {if (typeof onRejected !== 'function') {reject(that.reason);} else {var x = realOnRejected(that.reason);resolvePromise(promise2, x, resolve, reject); // 调用Promise 解决过程}} catch (error) {reject(error);}},0)});}})return promise2
}
2.6,Promise 解决过程
从上文看,Promise 解决过程是为了处理then传入的函数带返回值的情况。于是针对不同的返回值,需要有不同的处理方案。实现的原则就是一点:让then方法返回的promise2的值接收then入参函数的返回值。
所以resolvePromise这样写:
resolvePromise(promise2, x, resolve, reject); // 调用Promise 解决过程
把promise2传入,把then入参函数的返回x(可能没有)传入,然后传入promise2的resolve和reject,利用它来修改promise2的状态和值。
2.6.1,如果promise和x引用同一个对象,则用TypeError作为原因拒绝(reject)promise。
function resolvePromise(promise, x, resolve, reject) {// 如果 promise 和 x 指向同一对象,以 TypeError 为据因拒绝执行 promise// 这是为了防止死循环if (promise === x) {return reject(new TypeError('The promise and the return value are the same'));}
}
2.6.2,如果then入参函数返回的是promise
如果x是一个promise,则需要递归逐层展开返回的promise取得结果y,并且把promise2和它的resolve和reject传入,只有这样,promise2才能得到最后的结果:
function resolvePromise(promise, x, resolve, reject) {// 如果 promise 和 x 指向同一对象,以 TypeError 为据因拒绝执行 promise// 这是为了防止死循环if (promise === x) {return reject(new TypeError('The promise and the return value are the same'));}if (x instanceof MyPromise) {// 如果 x 为 Promise ,则让then返回的 promise 接受 x 的状态和结果// 也就是继续执行x,如果执行的时候拿到一个y,还要继续解析yx.then(function (y) {//注意这里传入的就是入参promise,也就是修改这个promise的状态和结果,简单写就是 resolve(y)resolvePromise(promise, y, resolve, reject);}, reject);}
}
2.6.3,如果x是个对象或者方法,这里处理的是类promise对象,即具备then方法的对象。
2.3.3另外,如果x是个对象或者方法,这里处理的是类promise对象,即具备then方法的对象。2.3.3.1 让x作为x.then2.3.3.2 如果取回的x.then属性的结果为一个异常e,用e作为原因reject promise2.3.3.3 如果then是一个方法,把x当作this来调用它, 第一个参数为 resolvePromise,第二个参数为rejectPromise,其中:2.3.3.3.1 如果/当 resolvePromise被一个值y调用,运行 [[Resolve]](promise, y)2.3.3.3.2 如果/当 rejectPromise被一个原因r调用,用r拒绝(reject)promise2.3.3.3.3 如果resolvePromise和 rejectPromise都被调用,或者对同一个参数进行多次调用,第一次调用执行,任何进一步的调用都被忽略2.3.3.3.4 如果调用then抛出一个异常e,2.3.3.3.4.1 如果resolvePromise或 rejectPromise已被调用,忽略。2.3.3.3.4.2 或者, 用e作为reason拒绝(reject)promise2.3.3.4 如果then不是一个函数,用x完成(fulfill)promise
这里规范中讲得很复杂,其实就是为了处理返回的是类promsie(thenable)对象。
如下代码:
var thenableA = {name: 'thenableA',then: function (resolve, reject) {console.log(`I'am ${this.name}`);resolve(this.name)}
}
var p1=new MyPromise((resolve, reject) => {console.log('create promise1');resolve(1)
})
p1.then((res) => {return thenableA
})
打印出来就是:
create promise1
I'am thenableA
具体的then方法实现的内容,就是把thenable对象像promise一样展开,执行其中的then方法,同样是让then方法返回的promise2的值接收最终的值。
function resolvePromise(promise, x, resolve, reject) {//...其他代码// 如果 x 为对象或者函数,即可能是thenable的对象/函数else if (x &&(typeof x === 'object' || typeof x === 'function')) {var called = false;try {// 把 x.then 赋值给 then var then = x.then;// 如果 then 是函数,则返回的其实是类promise(thenable)对象,需要展开执行其中的then方法if (typeof then === 'function') {then.call(x,function (y) {if (called) return;called = true;resolvePromise(promise, y, resolve, reject);//把它的then方法执行掉,并且修改promise2的值和状态},function (r) {if (called) return;called = true;reject(r);});} else {// 如果 then 不是函数(then返回的不是thenable对象),以 x 为参数执行 promiseif (called) return;called = true;resolve(x);}} catch (error) {// 如果取 x.then 的值时抛出错误 e ,则以 e 为据因拒绝 promise,return是为了不执行后续的代码if (called) return;called = true;return reject(error);}
}
例如上文代码:
var thenableA = {name: 'thenableA',then: function (resolve, reject) {console.log(`I'am ${this.name}`);resolve(this.name)}
}
var p1=new MyPromise((resolve, reject) => {console.log('create promise1');resolve(1)
})
var p2=p1.then((res) => {return thenableA
})setTimeout(()=>{console.log("+++++",p2)
},0)
执行之后打印的是:
create promise1
I'am thenableA
+++++ MyPromise {status: 'fulfilled',value: 'thenableA',reason: null,onFulfilledCallbacks: [],onRejectedCallbacks: []
}
可以看到thenableA的then方法被执行,并且改变了promise2的值,于是p2的value变成了’thenableA’。
2.6.4,返回的x不是函数也不是对象,甚至没有返回(x为undefined)
如果 x既不是对象也不是函数,用x完成(fulfill)promise,也就是直接resolve(x)即可。
三,验证promiseA+规范
在我们写好的MyPromsie中添加如下代码:
MyPromise.deferred = function() {var result = {};result.promise = new MyPromise(function(resolve, reject){result.resolve = resolve;result.reject = reject;});return result;
}
module.exports = MyPromise//记得这一步必须这样导出,不然会报错。
然后项目目录安装测试库:
npm i promises-aplus-tests -g
于是我们的手手写promise完整内容如下:
// 先定义三个常量表示状态
var PENDING = 'pending';
var FULFILLED = 'fulfilled';
var REJECTED = 'rejected';function MyPromise(fn) {this.status = PENDING; // 初始状态为pendingthis.value = null; // 初始化valuethis.reason = null; // 初始化reason// 构造函数里面添加两个数组存储成功和失败的回调this.onFulfilledCallbacks = [];this.onRejectedCallbacks = [];// 存一下this,以便resolve和reject里面访问var that = this;// resolve方法参数是valuefunction resolve(value) {if(that.status === PENDING) {that.status = FULFILLED;that.value = value;// resolve里面将所有成功的回调拿出来执行that.onFulfilledCallbacks.forEach(callback => {callback(that.value);});}}// reject方法参数是reasonfunction reject(reason) {if(that.status === PENDING) {that.status = REJECTED;that.reason = reason;// resolve里面将所有失败的回调拿出来执行that.onRejectedCallbacks.forEach(callback => {callback(that.reason);});}}try {fn(resolve, reject);} catch (error) {reject(error);}
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {// 如果onFulfilled不是函数,给一个默认函数,返回valuevar realOnFulfilled=typeof onFulfilled === 'function' ? onFulfilled : value=>value// // 如果onRejected不是函数,给一个默认函数,返回reason的Errorvar realOnRejected =typeof onRejected === 'function' ? onRejected : reason=>{throw reason}var that=thisvar promise2 = new MyPromise(function(resolve, reject) {//如果异步已经成功则直接执行回调函数,期间有报错,需要rejectif(that.status === FULFILLED) {setTimeout(function() {try {if (typeof onFulfilled !== 'function') {resolve(that.value);} else {var x = realOnFulfilled(that.value);resolvePromise(promise2, x, resolve, reject);}} catch (error) {reject(error);} },0)}//如果异步已经失败则直接执行回调函数,期间有报错,需要rejectif(that.status === REJECTED) { setTimeout(function() {try {if (typeof onRejected !== 'function') {reject(that.reason);} else {var x = realOnRejected(that.reason);resolvePromise(promise2, x, resolve, reject); // 调用Promise 解决过程}} catch (error) {reject(error);}},0)}// 如果还是PENDING状态,将回调保存下来,为了捕获回调中的错误,需要try...catch包裹一层if(that.status === PENDING) {that.onFulfilledCallbacks.push(function() {setTimeout(function () {try {if (typeof onFulfilled !== 'function') {resolve(that.value);} else {var x = realOnFulfilled(that.value);resolvePromise(promise2, x, resolve, reject); // 调用Promise 解决过程}} catch (error) {reject(error);}},0)});that.onRejectedCallbacks.push(function() {setTimeout(function () {try {if (typeof onRejected !== 'function') {reject(that.reason);} else {var x = realOnRejected(that.reason);resolvePromise(promise2, x, resolve, reject); // 调用Promise 解决过程}} catch (error) {reject(error);}},0)});}})return promise2
}function resolvePromise(promise, x, resolve, reject) {// 如果 promise 和 x 指向同一对象,以 TypeError 为据因拒绝执行 promise// 这是为了防止死循环if (promise === x) {return reject(new TypeError('The promise and the return value are the same'));}if (x instanceof MyPromise) {console.log("是MyPromise")// 如果 x 为 Promise ,则让then返回的 promise 接受 x 的状态和结果// 也就是继续执行x,如果执行的时候拿到一个y,还要继续解析yx.then(function (y) {//注意这里传入的就是入参promise,也就是修改这个promise的状态和结果,简单写就是 resolve(y)resolvePromise(promise, y, resolve, reject);}, reject);}// 如果 x 为对象或者函数else if (x &&(typeof x === 'object' || typeof x === 'function')) {var called = false;try {// 把 x.then 赋值给 then var then = x.then;// 如果 then 是函数,则返回的其实是类promise(thenable)对象,需要展开执行其中的then方法if (typeof then === 'function') {then.call(x,function (y) {if (called) return;called = true;resolvePromise(promise, y, resolve, reject);},function (r) {if (called) return;called = true;reject(r);});} else {// 如果 then 不是函数(then返回的不是thenable对象),以 x 为参数执行 promiseif (called) return;called = true;resolve(x);}} catch (error) {// 如果取 x.then 的值时抛出错误 e ,则以 e 为据因拒绝 promise,return是为了不执行后续的代码if (called) return;called = true;return reject(error);}} else {// 如果 x 不为对象或者函数,以 x 为参数执行 promise,没有返回时也执行这个resolve(x);}
}MyPromise.deferred = function() {var result = {};result.promise = new MyPromise(function(resolve, reject){result.resolve = resolve;result.reject = reject;});return result;
}
module.exports = MyPromise
项目根目录执行测试:
promises-aplus-tests test.js(test.js是我的文件名)
可以看到,通过了全量的测试用例。
四,promise.resolve的实现
它的作用是将现有对象转为Promise对象。
即,如果是promise则直接返回,如果不是则返回值为这个的promise。
MyPromise.resolve = function(parameter) {if(parameter instanceof MyPromise) {return parameter;}return new MyPromise(function(resolve) {resolve(parameter);});
}
即:
var promise1 = new MyPromise((resolve, reject) => {setTimeout(()=>{resolve(2)},10)
})
var p2=MyPromise.resolve(promise1)
p2.then((res)=>{console.log(res)//打印2
})
五,Promise.reject的实现
返回一个新的Promise实例,该实例的状态为rejected。Promise.reject方法的参数reason,会被传递给实例的回调函数。
MyPromise.reject = function(reason) {return new MyPromise(function(resolve, reject) {reject(reason);});
}
六,Promise.all的实现
该方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。它接受一个数组作为参数,p1
、p2
、p3
都是 Promise 实例,如果不是,就会先调用Promise.resolve
方法,将参数转为 Promise 实例,再进一步处理。当p1, p2, p3全部resolve,外层的promise才resolve,有任何一个reject,外层的promise都reject。
const p = Promise.all([p1, p2, p3]);
MyPromise.all = function(promiseList) {var resPromise = new MyPromise(function(resolve, reject) {var count = 0;var result = [];var length = promiseList.length;if(length === 0) {return resolve(result);}promiseList.forEach(function(promise, index) {MyPromise.resolve(promise).then(function(value){count++;result[index] = value;if(count === length) {//全部执行完毕后才resolve结果数组出去resolve(result);}}, function(reason){reject(reason);});});});return resPromise;
}
使用的示例:
var p1 = new MyPromise((resolve, reject) => {setTimeout(()=>{resolve(1)},10)
})
var p2 = new MyPromise((resolve, reject) => {setTimeout(()=>{resolve(2)},0)
})
var p3 = new MyPromise((resolve, reject) => {setTimeout(()=>{resolve(3)},20)
})var result=MyPromise.all([p1,p2,p3])
setTimeout(()=>{console.log(result)//打印返回的所有结果
},1000)
result.then((res)=>{console.log(res)
})
得到的结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z0tyj5fd-1680622612470)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230329225315698.png)]
七,Promise.race方法
和promise.all的用法一样,传入一个promsie数组,不同的是,promise.race是一旦有一个promise执行完毕后,就返回结果。
MyPromise.race = function(promiseList) {var resPromise = new MyPromise(function(resolve, reject) {var length = promiseList.length;if(length === 0) {return resolve();} else {for(var i = 0; i < length; i++) {//哪个先成功就直接resolve结果了,因为return了,就不执行后续的结果了,不用foreach是因为它无法returnMyPromise.resolve(promiseList[i]).then(function(value) {return resolve(value);}, function(reason) {return reject(reason);});}}});return resPromise;
}
使用的示例:
var p1 = new MyPromise((resolve, reject) => {setTimeout(()=>{resolve(1)},10)
})
var p2 = new MyPromise((resolve, reject) => {setTimeout(()=>{resolve(2)},0)
})
var p3 = new MyPromise((resolve, reject) => {setTimeout(()=>{resolve(3)},20)
})var result=MyPromise.race([p1,p2,p3])
setTimeout(()=>{console.log(result)//打印返回的所有结果
},1000)
result.then((res)=>{console.log(res)
})
八,Promise.prototype.catch
Promise.prototype.catch
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。
MyPromise.prototype.catch = function(onRejected) {this.then(null, onRejected);
}
九,总结
9.1,promise做到异步操作和结果的分离
这一点是promise存在的最大价值体现。它的实现,其实就是让then方法传入的函数在异步操作后执行即可。这里为了方便描述,我只说成功的情况。
一种情况是在调用then方法的时候,异步操作还未完成(promise的状态是pedding)。大致的流程如下:
【这里本来是动图的,但是csdn不允许上传这么大的动图,,,具体的可以看我公众号吧,嘻嘻,我最近开始写公众号啦!撒花!】
另一种情况是,在调用then方法的时候,异步操作已经完成(promise的状态已经变成fulfilled)。这时候收集器就不用收集回调函数了,而是可以直接执行,大致的流程如下:
【这里本来是动图的,但是csdn不允许上传这么大的动图,,,具体的可以看我公众号吧,嘻嘻,我最近开始写公众号啦!撒花!】
9.2,then能链式调用是因为then返回是个新的promise
我们为了解决回调地狱,使用promise时,会进行链式调用。这是因为then方法返回一个新的promise。
并且这个新的promise倾向于返回最后的处理结果。
这句话怎么理解呢?还是只说成功情况的处理。
1,then不传入函数参数时,透传promise的处理结果。
2,then传入的函数参数没有返回时,也就是返回值为undefined,那么返回的新promise的value值是undefined。
3,then传入的函数参数有返回值,且返回值是promise,则取得这个promse最后的结果,传递给它返回的promise。
4,then传入的函数参数有返回值,且返回值是thenable对象,和promise差不多,就是执行thenable对象的then方法,取得这个promise最后的结果,传递给它返回的promise。
5,then传入的函数参数有返回值,且返回值是正常数据,则传递给它返回的promise。
相关文章:

从异步到promise
一,背景 1.1,js的单线程 这一切,要从js诞生之初说起,因为js是单线程的语言。 js单线程原因:作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程&…...

Linux系统中进行JDK环境的部署
一、为什么需要部署JDK。 JDK:Java Development Kit,是用于Java语言开发的环境。 部署JDK不需要懂得Java语言,只需要掌握Linux相关命令即可。 二、部署版本与环境。 系统:安装在VMware环境下的CentOS7.6; JDK版本&a…...

Leetcode.1033 移动石子直到连续
题目链接 Leetcode.1033 移动石子直到连续 Rating : 1421 题目描述 三枚石子放置在数轴上,位置分别为 a,b,c。 每一回合,你可以从两端之一拿起一枚石子(位置最大或最小),并将其放入…...

【Java】在SpringBoot中使用事务注解(@Transactional)时需要注意的点
在SpringBoot中使用事务注解(Transactional)时需要注意的点Transactional是什么使用事务注解(Transactional)时需要注意的点Transactional是什么 Transactional是Spring框架提供的一个注解,用于声明事务边界和配置事务…...

找到序列最高位的1和最高位的0并输出位置
前言: 该题为睿思芯科笔试题,笔试时长20分钟。 题目描述 接口如下: module first_1_and_0#(parameter WIDTH 8 )(input [WIDTH-1:0] data_in ,input target ,output exist ,outpu…...

面试总结sdiugiho
一、进程与线程的区别 进程: 一个在内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程可以有多个线程; windows 任务管理器中 一个 exe 就是一个进程。 线程: 进程中的一个执行任务(控制…...

WIN10無法再使用 IE 瀏覽器打开网页解决办法
修改 Registry(只適用 Win10) 微軟已於 2023 年 2 月 14 日永久停用 Internet Explorer,會透過 Edge 的更新讓使用者開啟 IE 時自動導向 Edge,其餘如工作列上的圖示,使用的方法則是透過「品質更新」的 B 更新來達成&am…...

搭建SpringBoot和Mysql Demo
1. 引言 在上一篇文章中,介绍了如何搭建一个SpringBoot项目;本篇文章,在上一篇文章的基础上,接着介绍下怎样实现SpringBoot和MySQL的整合。在后端开发中,数据库开发是绕不开的话题,开发中很多的时间都是在…...

晶振03——晶振烧坏的原因
晶振03——晶振烧坏的原因 首先要清楚的一件事情是:晶振分为无源晶振与有源晶振两大类。基于这两类晶振的内部结构与工作原理的差异,晶振被烧坏的情况也要分为两大类: 针对无源晶振被烧坏的情况有以下两点: 1、手焊操作不当 假…...

项目管理的难点
一、项目团队建设 建设一支高效的项目团队,明确团队队员的职责是项目经理进行项目管理的首要条件,也是项目目标能否实现的关键。 1.1 学会放权 任何人都不能掌握所有的知识和技能,要敢于相信别人,让别人去做。 放权就要选择最…...

day22 ● 235. 二叉搜索树的最近公共祖先 ● 701.二叉搜索树中的插入操作 ● 450.删除二叉搜索树中的节点
问题: ● 235. 二叉搜索树的最近公共祖先 ● 701.二叉搜索树中的插入操作 ● 450.删除二叉搜索树中的节点 首先,二叉搜索树是一种常见的数据结构,它具有以下特点: 每个节点最多有两个子节点,分别为左子节点和右子节…...

ChatGPT 这个风口,普通人怎么抓住?
最近在测试ChatGPT不同领域的变现玩法,有一些已经初见成效,接下来会慢慢分享出来。 今天先给大家分享一个,看完就能直接上手的暴力引流玩法。 所需工具: 1)ChatGPT(最好是plus版,需要保证快速…...

Python代码规范:企业级代码静态扫描-代码规范、逻辑、语法、安全检查,以及代码规范自动编排(2)
本篇将总结实际项目开发中Python代码规范检查、自动编排的一些工具,特点,使用方法,以及如何在Pycharm中集成这些工具,如autoflake、yapf、black、isort、autopep8代码规范和自动编排工具。上一篇总结的pylint、pyproject-flake8、…...

acme.sh从 letsencrypt 生成SSL免费证书并自动更新证书
acme.sh 实现了 acme 协议, 可以从 letsencrypt 生成免费的证书 ACME 协议: Automatic Certificate Management Environment 自动化证书管理环境 文档: github: https://github.com/acmesh-official/acme.shgitee: https://gitee.com/neilpang/acme.sh中文文档: https://git…...

基于html+css的evenly布局
准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…...

【从零开始学习 UVM】10.5、UVM TLM —— UVM TLM Blocking Get Port
文章目录 UVM TLM Get Port Example1. 创建一个发送方类,其端口类型为 uvm_blocking_get_imp3. 创建接收器类,等待 get 方法。4. 在更高层次上连接端口及其实现Get端口阻塞行为任何组件都可以通过 TLM get 端口请求从另一个组件接收事务。发送组件应定义获取端口的实现。该实…...

English Learning - L2 第 10 次小组纠音 辅音 [m] [n] [ŋ] 半元音 [w] [j] 2023.3.29 周三
English Learning - L2 第 10 次小组纠音 辅音 [m] [n] [ŋ] [w] [j] 2023.3.29 周三共性问题more Autumn [ɔː] 舌位偏前gone evening 前后鼻音不分Hes proud of this name 双元音缺乏滑动感bank thing 中的后鼻音发成前鼻音week what yolk 元音 [iː] [ɒ] 舌位偏前 [əʊ] …...

从零开始实现一个C++高性能服务器框架----环境变量模块
此项目是根据sylar框架实现,是从零开始重写sylar,也是对sylar丰富与完善 项目地址:https://gitee.com/lzhiqiang1999/server-framework 简介 项目介绍:实现了一个基于协程的服务器框架,支持多线程、多协程协同调度&am…...

git常用命令的解释
解释git add . git add . 命令用于将当前目录下的所有文件添加到 Git 仓库的暂存区中。这个命令通常用于刚刚打开一个 Git 仓库时,或者用于将本地文件更新到远程仓库时。 具体来说,git add . 命令会将当前目录下的所有文件添加到 Git 仓库的暂存区中&am…...

DNS和CDN的区别与联系
现在互联网用户很多不能理解CDN和DNS之间的关系,他们之间到底有什么区别。对于这两者永远处于模糊的概念。其实二者是相辅相成的,二者搭配起来能使网站更加安全,快速。 用户访问未使用CDN缓存网站的过程为: 1、用户向浏览器提供要访问的域名…...

Java基础知识 | 常见面试题(中):面向对象基础
撰写成一问一答的形式,每次回答都默写,对比参考答案后,再默写出更恰当的答案。 相关内容 Java基础知识 | 常见面试题(上):基础概念和常识 Java基础知识 | 常见面试题(上):…...

勒索软件正在从 Windows 转向 Linux
听说勒索软件正在从 Windows 转向 Linux了勒索软件正在从 Windows 转向 Linux 最近几周,黑客们一直在对 Linux 企业网络部署 IceFire 勒索软件,这是一个值得注意的转变,因为它曾经是一个只针对 Windows 的恶意软件。与 Windows 相比…...

信息系统项目管理师 第11章 项目成本管理
1.管理基础 1.重要性和意义 项目管理主要受范围、时间、成本、质量的约束,项目成本管理就是要确保在批准的预算内完成项目。 如果项目建设的实际成本远远超出批准的投资预算,就很容易造成成本失控。 1.对工程项目认识不足。 2.组织制度不健全。 3.方法问题 4.技术的制约 5.需…...

XML 简介
文章目录一、XML 简介二、XML 用途总结一、XML 简介 XML 被设计用来传输和存储数据。 HTML 被设计用来显示数据。 XML 指可扩展标记语言(eXtensible Markup Language)。 可扩展标记语言(英语:Extensible Markup Language…...

ERP:华为杀入,金蝶们打颤?
配图来自Canva可画 近期,华为官方透露将在4月份推出自研MetaERP管理系统,引来不少媒体和业内人士的围观,紧接着关于华为“进军ERP市场”的解读更是不胫而走,所谓一石激起千层浪,此说法一出,直接导致了金蝶…...

Linux——总复习1
1.要注意自己处于当前那个目录位置。 2.将file1的前五行/后三行重定向、附加到file2【输出重定向】 head -5 file1 > file2 tail -3 file1 >> file2 3.ls与cat区别 ls:列出目录的目录内容,未指定目录,则列出当前工作目录的内容 -l:查…...

控制SQL*PLUS的环境和数据字典简介
可以通过使用SET命令来设置SQL*PLUS的环境变量,从而达到控制SQL*PLUS 环境的目的。 SET命令的格式如下: SET 环境变量 变量的值 可以通过使用SHOW命令来显示SQL*PLUS环境变量的配置。SHOW 命令的格式如下: SHOW 环境变量|ALL 下面用一个…...

Chapter11.3:MATLAB_SIMULINK在离散系统中的应用
该系列博客主要讲述Matlab软件在自动控制方面的应用,如无自动控制理论基础,请先学习自动控制系列博文,该系列博客不再详细讲解自动控制理论知识。 自动控制理论基础相关链接:https://blog.csdn.net/qq_39032096/category_10287468…...

过滤器Filter
什么是Filter? Filter表示过滤器,是JavaWeb三大组件(Servlet、FIlter、Listener)之一。过滤器可以把对资源的请求拦截下来,总而实现一些特殊的功能 使用过滤器后,要想访问web服务器上的资源,必须…...

MySQL数据同步ES的常用思路和方法
文章目录 1.同步双写2.异步双写3.定时任务4.数据订阅大家应该都在各种电商网站检索过商品,检索商品一般都是通过什么实现呢?搜索引擎Elasticsearch。 那么问题来了,商品上架,数据一般写入到MySQL的数据库中,那么用于检索的数据又是怎么同步到Elasticsearch的呢? 1.同步双…...