一、在生活中,按照字面量来理解,异步指的是,好比我先吃完苹果再看电视,这是咱们生活中理解的异步。同步就是我边吃苹果边看电视。 html
二、然而,对咱们的电脑程序来讲,同步与异步的概念偏偏跟咱们在生活中的理解彻底相反。同步指的是我先吃完苹果,而后再看电视(执行完一个事件再去执行另外一个事件,若上一个事件没执行完,下一个事件就没法执行);异步指的是我边吃苹果边看电视(多个事件能够同时执行,不会发生阻塞)。理解js中的异步和同步,若是对这两个概念混淆了,你只要想到跟咱们生活中异步与同步的理解相反就好了。ajax
单线程是JavaScript中一个比较重要的特性。这就表示在同一个时间只能作一件事情。数据库
那为何JavaScript不弄成多线程的呢?还能更加充分利用CPU呢。segmentfault
这要从JavaScript的使用场景提及,JavaScript做为浏览器语言,主要用途是经过操做DOM,与用户进行交互。后端
咱们设想一下,若是一个线程去更新某个DOM元素,而另外一个线程去删除这个DOM元素,那么浏览器该执行哪一个操做呢?这就出现冲突了。数组
所以,为了不复杂的多线陈机制,JavaScript从设计之初就选择了单线程标准。promise
单线程那就表示事件只能一个一个轮着执行,前面没执行完后面就没法执行(只能干等着,这样也过低效了)。好比,我用ajax向服务端请求一个数据(假设数据量很大,花费时间好久),这时候就会卡在那里,由于后面的只能等前一个事件把数据成功请求回来再执行接下的代码,这样对用户的体验就太不友好了。这时候 任务队列 就出场了。JS中将全部任务分为同步任务和 异步任务。 浏览器
目前我的的理解是,只有全部的同步任务执行完才会去执行异步任务。常见的异步任务有setTimeout函数,http请求,数据库查询等。多线程
(1)全部任务都在主线程上执行,造成一个执行栈(execution context stack)。
(2)主线程以外,还存在一个"任务队列"(task queue)。系统把异步任务放到"任务队 列"之中,而后继续执行后续的任务。
(3)一旦"执行栈"( 事件循环队列)中的全部任务 执行完毕,系统就会读取"任务队列"。若是这个时候,异步任务已经结束了等待状态,就会从"任务队列"进入执行栈,恢复执行。
(4)主线程不断重复上面的第三步。
详情查看此文章闭包
console.log('这里是调试1'); setTimeout(() => { console.log('这里是调试2'); },0) console.log('这里是调试3'); console.log('这里是调试4'); console.log('这里是调试5'); // 输出 这里是调试1 这里是调试3 这里是调试4 这里是调试5 这里是调试2 // setTimeout中的最后面才输出来
异步任务分为宏任务(macrotasks)和微任务(microtasks)。常见的宏任务有(setTimeout
,setInterval
,setImmediate
),常见的微任务有(Promise.then
)。在一个事件循环中,当执行完全部的同步任务后,会在任务队列中取出异步任务,这时候会优先执行微任务,接着再执行宏任务。换句话说,微任务就是为了插队而存在的。看一下下面这个例子:
setTimeout(() => console.log(1)); new Promise((resolve) => { resolve(); console.log(2); }).then(() => { console.log(3); }) console.log(4);
输入结果为:2 - 4 - 3 - 1
一、在setTimeout(...)
中能够用async(...)
,await(...)
,可是只能当前setTimeout(...)
执行栈中生效;
二、同一层级的多个setTimeout(...)
相互独立,就算setTimeout(...)
中有await(...)
也不会影响到其余setTimeout(...)
的执行;
一、在JS中默认的方式是同步的,个别是异步的(http请求,setTimeout)。假设如今有一个场景,咱们须要从后端接口获取数据(异步任务),而后对该数据进行处理,这时候咱们就须要将获取数据这一异步任务阻塞,也就是将其转化为同步任务。
二、常见的处理异步(将异步变为同步)的方式按照出现时间排序有:回调函数,Promise,async与await。
一、不管经过何种手段将 内部函数传递到这个函数定义时的 词法做用域(定义时的词法做用域指的是函数定义时的那个位置) 之外执行,内部函数都会持有对原始定义做用域的引用,不管在何处执行这个函数都会使用闭包。
二、不管函数在哪里被调用,也不管它如何被调用,它的词法做用域都只由函数被声明时所处的位置决定。
三、闭包使得函数能够继续访问定义时的词法做用域。
四、只要使用了回调函数,实际上就是在使用闭包。
---《你不知道的JavaScript》
一、通常来讲,block(块级)做用域被执行完后,里面的数据就被回收了。
二、做用: 将某个块级做用域里面的变量能够被外部使用。
三、词法做用域:静态做用域,查找做用域的顺序是按照函数定义时的位置来决定的。
四、全局做用域是在V8启动过程当中就建立了,且一直保存在内存中不会被销毁的,直到V8退出。而函数做用域是在执行该函数时建立的,当函数执行结束以后,函数做用域就随之被销毁了。
咱们来看一个闭包的案例:
function foo() { let a = 2; function bar() { console.log(a); a++; } return bar; } let baz = foo(); baz(); // 2 baz(); // 3
函数foo(...)
中返回了一个名为bar
的函数,接着 let baz = foo();
就将bar(...)
这个函数赋值给了baz
,因此在必定程度上baz至关于bar(...)
,接着咱们就执行baz
(在bar
定义时的词法做用域之外执行了)(词法做用域: 函数在哪里定义,他的词法做用域就在哪里);这也就印证了咱们前面说的,将一个内部函数传递到这个函数所在的词法做用域之外执行,因此就产生了闭包,由于咱们在外面获取到了foo函数内部的变量a,同时这也是闭包产生的效果。
三、总结:
四、只要使用了回调函数,实际上就是在使用闭包。下面咱们来看一个案例。
function wait(message) { setTimeout(function timer() { console.log(message); }, 1000); } wait('Hello, World');
将一个内部函数(名为timer)传递给setTimeout(...)
。timer具备涵盖wait(...)
做用域的闭包,所以还保有对变量message
的引用。wait(...)
执行1000毫秒后,它的内部做用域并不会消失,timer函数依然保有wait(...)
做用域的闭包。由于它能够引用到message
这个变量。
五、下面咱们来看一个稍微复杂点的案例(利用回调函数进行同步传值):
// 模拟获取ajax请求的数据(异步) function getAjaxData(cb) { // 用setTimeout实现异步请求 setTimeout(function() { // 假设data是咱们请求获得的数据 咱们须要将数据发送给别人 const data = "请求获得的数据"; cb(data); }, 1000) } // 获取ajax请求的响应数据并对数据进行处理 getAjaxData(function handleData(tempData) { tempData = tempData + '666'; console.log(tempData); // 请求获得的数据666 });
将handleData(...)
做为参数传进getAjaxData(...)
中,所以cb(data)
中的data就做为参数传进了handleData(...)
中,这样也就达到了传值的做用了。
六、回调函数存在的问题:信任问题。以上面的例子进行改进。
function getAjaxData (cb) { // 用setTimeout实现异步请求 setTimeout(function () { // 假设data是咱们请求获得的数据 咱们须要将数据发送给别人 const data = "请求获得的数据"; cb(data); cb(data); }, 1000) } // 获取ajax请求的响应数据并对数据进行处理 getAjaxData(function handleData (tempData) { tempData = tempData + '666'; console.log(tempData); // 请求获得的数据666 });
假设getAjaxData(...)
这个方法是由第三方库引进来的,咱们并不清楚里面的代码逻辑细节,这样的话handleData(...)
的执行就存在不肯定性,好比上面我增长了一个cb(data)
,这handleData(...)
就会执行两次,固然这不是咱们想要的效果,所以回调的处理就不可控了。
回调最大的问题是控制反转,它会致使信任链的彻底断裂。
由于回调函数内部的调用状况是不肯定的,可能不调用,也可能被调用了屡次。
---《你不知道的JavaScript》
Promise的出现正是为了解决这个问题。
七、备注: 关于js中内存回收的问题
Promise一旦决议(resolve
),一直保持其决议结果不变
解决回调调用过早的问题
解决回调调用过晚的问题
解决回调未调用的问题
解决调用次数过少或过多
一、new Promise 时,须要传递一个executor执行器,执行器马上执行;
二、executor 接受两个参数,分别是resolve和reject;
三、promise 只能从 pending 到 rejected,或者从 pending 到 fulfilled;
四、promise 的状态一旦确认,就不会再改变;
五、promise都有then方法,then接受两个参数,分别是promise成功的回调 onFulfilled,和promise失败的回调onRejected;
六、若是调用then时,promise已经成功,则执行onFulfilled,并将promise的值做为参数传递进去。若是promise已经失败,那么执行onRejected,并将promise失败的缘由做为参数传递进去。若是promise的状态是pending,须要将onFulfilled和onRejected函数存放起来,等待状态肯定后,再依次将对应的函数执行;
七、then的参数onFulfilled和onRejected能够缺省;
八、promise能够then屡次,promise的then方法返回一个promise;
九、若是then返回的是一个结果,那么就会把这个结果做为参数,传递给下一个then的成功的回调(onFulfilled);
十、若是then中抛出异常,那么就会把这个异常做为参数,传递给下一个then的失败的回调(onRejected);
十一、若是then返回的是一个promise,那么就会等这个promise执行完,promise若是成功,就走下一个then的成功,若是失败,就走下一个then的失败;
一、当promise中有异步任务:若是promise中有异步任务的话,那他的status为pedding,以后全部的then都会放到待办任务数组里面。
二、链式调用的实现:then方法返回一个promise。
三、当then中有异步任务:两种状况:
resolve(...)
以后,将状态改变以后才会再执行后面的(遍历待办任务数组),也就是至关于将then(...)
中的异步任务阻塞了。一、实现promise(...)
方法,该方法有一个回调函数exectue
参数 ;
当咱们new promise((resolve, reject) => {});
时,将会执行该回调函数 ;
而且resolve
对应到promise(...)
中的res(...)
;
reject
对应到promise(...)中的rej(...)
;
当咱们执行到resolve(...)
时,才会执行res(...)
;
function promise(exectue) { const res = (value) => { } const rej = (reason) => { } exectue(res, rej); } // resolve对应的是promise(...)中的res(...) // reject对应的是promise(...)中的rej(...) // 所以只有执行了resolve(...)或reject(...) 才改变status的值 const test = new promise((resolve, reject) => { console.log('这里是调试1'); setTimeout(() => { console.log('这里是调试2'); resolve(); }, 3000) }) // 这里是调试1 // 这里是调试2 // 执行了res
二、promise(...)
方法中维护几个变量,用于存储执行节点的状态&数据:
fulfilled
:标志任务是否已完成状态;pedding
:标志任务是否正在进行中状态(未完成状态);status
:标志当前执行节点的状态,节点的初始状态为pedding
;value
:存储promise
状态成功时的值;reason
:存储promise
状态失败时的值;onFulfilledCallbacks
:数组,存储成功的回调任务;onRejectedCallbacks
:数组,存储失败的回调任务;function promise(exectue) { this.fulfilled = 'fulfilled'; this.pedding = 'pedding'; this.rejected = 'rejected'; this.status = this.pedding; this.value; this.reason; this.onFulfilledCallbacks = []; this.onRejectedCallbacks = []; const res = (value) => { if(this.status === this.pedding) { // 执行了resolve就要改变status为fulfilled this.status = this.fulfilled; this.value = value; // 遍历执行放在待办任务数组中的事件 this.onFulfilledCallbacks.forEach(fn => fn()); } } const rej = (reason) => { if(this.status === this.pedding) { // 执行了reject就要改变status为rejected this.status = this.rejected; this.reason = reason; this.onRejectedCallbacks.forEach(fn => fn()); } } exectue(res, rej); }
三、实现then(...)
,为符合链式调用,then(...)
方法必须返回一个promise(...)
:
promise.prototype.then = function (onFulfilled, onRejected) { // this指向promise(...)对象 const that = this; const promise2 = new promise((resolve, reject) => { if(that.status === that.fulfilled) { onFulfilled(); resolve(); } if(that.status === that.pedding) { } if(that.status === that.rejected) { } }) return promise2; } // 调用 const test = new promise((resolve, reject) => { console.log('这里是调试1'); resolve(); }) test.then(() => { console.log('这里是调试2'); }).then(() => { console.log('这里是调试3'); }) // 这里是调试1 // 这里是调试2 // 这里是调试3
四、到这里,咱们已实现了基本的链式调用的功能了,那若是在promise(...)
中是一个异步事件时,而且咱们须要阻塞这个异步任务(将异步转化为同步),预计的效果是只有执行了resolve(...)
才能执行then(...)
中的代码,能够怎么实现呢?
当promise(...)
中是须要阻塞的异步任务时,那么当执行到then(...)
时,此时的status
为pedding
,须要将链式调用的执行节点根据Fulfilled
或Rejected
两种状况分别添加到onFulfilledCallbacks
或onRejectedCallbacks
这两个变量中,等到异步任务执行完,resolve(...)
以后才轮到链式调用节点的执行,改进代码:
promise.prototype.then = function (onFulfilled, onRejected) { // this指向promise(...)对象 const that = this; const promise2 = new promise((resolve, reject) => { if(that.status === that.fulfilled) { onFulfilled(); resolve(); } if(that.status === that.pedding) { console.log('这里是调试4'); this.onFulfilledCallbacks.push(() => { onFulfilled(); resolve(); }) } if(that.status === that.rejected) { this.onRejectedCallbacks.push(() => { onRejected(); reject(); }) } }) return promise2; } // 调用 const test = new promise((resolve, reject) => { setTimeout(() => { console.log('这里是调试1'); resolve(); }, 1000) }) test.then(() => { console.log('这里是调试2'); }).then(() => { console.log('这里是调试3'); }) // 这里是调试4 // 这里是调试4 // 这里是调试1 // 这里是调试2 // 这里是调试3
五、接下来,这里又有一个场景,当咱们在then(...)
方法中有异步任务,咱们一样想要让该异步任务阻塞,又该怎么弄呢?按照咱们上面的逻辑是阻塞不了then(...)
方法中的异步任务的,你们能够尝试一下。
const test = new promise((resolve, reject) => { setTimeout(() => { console.log('这里是调试1'); resolve(); }, 1000) }) test.then(() => { setTimeout(() => { console.log('这里是调试2'); }, 2000) }).then(() => { console.log('这里是调试3'); }) // 输出 // 这里是调试4 // 这里是调试4 // 这里是调试1 // 这里是调试3 // 这里是调试2
要想阻塞then(...)
中的异步任务,这里得分红两种状况来讨论:一种是要阻塞异步任务,须要在then(...)
返回一个promise(...)
,另外一种是不须要阻塞异步任务,那就不须要返回任何值。继续改进代码:
promise.prototype.then = function (onFulfilled, onRejected) { // this指向promise(...)对象 const that = this; const promise2 = new promise((resolve, reject) => { if(that.status === that.fulfilled) { let x = onFulfilled(that.value); if(x && typeof x === 'object' || typeof x === 'function') { let then = x.then; if(then) { then.call(x, resolve, reject); } else { resolve(x); } } if(that.status === that.pedding) { this.onFulfilledCallbacks.push(() => { // onFulfilled(...) 为then(...)方法中的回调函数 let x = onFulfilled(that.value); // 执行了then(...)方法中的回调函数并将返回值赋给x // 判断then(...)中回调函数返回值类型 是否为function if(x && typeof x === 'object' || typeof x === 'function') { let then = x.then; // 若是then存在 说明返回了一个promise if(then) { // 这里采用的方法是将resolve放在返回的promise的then里面执行 . // 这样只有当then(...)方法中的异步事件执行完才resolve(...) 很牛皮 // call(...)绑定this的指向 then.call(x, resolve, reject); } } else { resolve(x); } }) } if(that.status === that.rejected) { this.onRejectedCallbacks.push(() => { let x = onRejected(that.reason); if(x && typeof x === 'object' || typeof x === 'function') { let then = x.then; if(then) { then.call(x, resolve, reject); } else { resolve(x); } }) } }) return promise2; } const test = new promise((resolve, reject) => { setTimeout(() => { console.log('这里是调试1'); resolve(); }, 1000) }) test.then(() => { // 若要阻塞then(...)中的异步任务须要return一个promise return new promise((resolve1, reject1) => { setTimeout(() => { console.log('这里是调试2'); resolve1(); }, 2000) }) }).then(() => { console.log('这里是调试3'); }) // 这里是调试1 // 这里是调试2 // 这里是调试3
六、到这里,关于promise
核心的内容基本已经实现了,再看一下如下案例:
new promise(function(resolve){ console.log('这里是调试1'); resolve(); }).then(function(){ console.log('这里是调试2'); }); console.log('这里是调试3'); // 这里是调试1 // 这里是调试2 // 这里是调试3
new Promise(function(resolve){ console.log('这里是调试1'); resolve(); }).then(function(){ console.log('这里是调试2'); }); console.log('这里是调试3'); // 这里是调试1 // 这里是调试3 // 这里是调试2
能够发现,咱们的promise
和原生的Promise
输出结果不同。根据Promise
规范,在.then(...)
方法中执行的程序属于微任务
(异步任务分为微任务
和宏任务
,通常的异步事件属于宏任务,微任务比宏任务先执行,但咱们本身实现的promise
并不支持这个规则,原生的Promise
才支持),所以须要将其转化为异步任务,下面咱们用setTimeout(...)
来模拟实现一下异步任务(原生Promise
并非用setTimeout(...)
实现的)
promise.prototype.then = function (onFulfilled, onRejected) { const that = this; const promise2 = new promise((resolve, reject) => { if (that.status === that.fulfilled) { setTimeout(() => { let x = onFulfilled(that.value); if (x && typeof x === 'object' || typeof x === 'function') { let then = x.then; if (then) { then.call(x, resolve, reject); } } else { resolve(x); } }) } if (that.status === that.pedding) { that.onFulfilledCallbacks.push(() => { setTimeout(() => { let x = onFulfilled(that.value); if (x && typeof x === 'object' || typeof x === 'function') { let then = x.then; if (then) { then.call(x, resolve, reject); } } else { resolve(x); } }) }) that.onRejectedCallbacks.push(() => { setTimeout(() => { let x = onRejected(that.reason); if (x && typeof x === 'object' || typeof x === 'function') { let then = x.then; if (then) { then.call(x, resolve, reject); } } else { resolve(x); } }) }) } if (that.status === that.rejected) { setTimeout(() => { let x = onRejected(that.reason); if (x && typeof x === 'object' || typeof x === 'function') { let then = x.then; if (then) { then.call(x, resolve, reject); } } else { resolve(x); } }) } }) return promise2; } // 这里是调试1 // 这里是调试3 // 这里是调试2
七、再来看一个案例:
const test = new promise((res, rej) => { setTimeout(() => { console.log('这里是调试1'); res(); }, 4000); }) // 回调函数push进test.onFulfilledCallbacks数组中 test.then(() => { return new promise((res1) => { setTimeout(() => { console.log('这里是调试2'); res1(); }, 3000); }) }) // 回调函数也是push进test.onFulfilledCallbacks数组中,所以两个then(...)是相互独立的,执行顺序按照常规的来处理 test.then(() => { console.log('这里是调试3'); }) // 输出 // 这里是调试1 // 这里是调试3 // 这里是调试2
async
函数必定会返回一个Promise对象。若是一个async函数的返回值看起来不是promise,那么它将会被隐式得包装在一个promise中;yield
暂停函数,只有等前面的promise
决议以后才执行next(...)
,程序才继续日后面执行,表现出来的就是暂停程序的效果;一、一种特殊对象,用于为迭代过程设计的专有接口,简单来讲就相似与遍历一个对象;
二、调用返回一个结果对象,对象中有next()方法;
三、next()方法返回一个对象: value: 表示下一个要返回的值;done:没有值能够返回时为true,不然为false;
四、迭代器简易内部实现:
function createIterator(items) { let i = 0; return { next: function() { let done = (i >= items.length); // items[i++] 至关于 items[i]; i++; let value = !done ? items[i++] : undefined; return { value: value, done: done // true表示后面没有值了 }; } }; } // 只建立了一个实例 所以iterator一直是同一个 let iterator = createIterator([1,2,3]); console.log(iterator.next()); // { value: 1, done: false } console.log(iterator.next()); // { value: 2, done: false } console.log(iterator.next()); // { value: 3, done: false } console.log(iterator.next()); // { value: undefined, done: true }
一、一个返回迭代器的函数;实现了一种顺序、看似同步的异步流程控制表达风格;
二、关键字yield:表示函数暂停运行;
三、过程:每当执行完一条yield语句后函数就暂停了,直到再次调用函数的next()方法才会继续执行后面的语句;
四、使用yield关键字能够返回任何值或表达式( 表示要传递给下一个过程的值);
五、注意点:
function *createInterator() { yield 1; yield 2; yield 3; } // 生成器的调用方式与普通函数相同,只不过返回一个迭代器 let iterator = createInterator(); console.log(iterator.next()); // { value: 1, done: false } console.log(iterator.next()); // { value: 2, done: false } console.log(iterator.next()); // { value: 3, done: false } console.log(iterator.next()); // { value: undefined, done: true }
function *foo(x) { const y = x * (yield); return y; } const it = foo(6); // 启动foo(...) it.next(); const res = it.next(7); // next(...)的数量老是比yield的多一个 console.log(res); // 42
首先,传入6做为参数。而后调用it.next(...)
,这会启动*foo(...)
;
接着,在*foo(...)
内部,开始执行语句const y = x ...
,但随后就遇到了一个yield
表达式。它就会在这一点上暂停*foo(...)
(在赋值语句中间),并不是在本质上要求调用代码为yield
表达式提供一个结果值。接下来,调用it.next(7)
,这一句把值7传回做为被暂停的yield
表达式结果。
因此,这是赋值语句实际上就是const y = 6 * 7
。如今,return y
返回值42做为调用it.next(7)
的结果。
注意,根据你的视角不一样,yield
和next(...)
调用有一个不匹配。通常来讲,须要的next(...)
调用要比yield
语句多一个,前面的代码片断就有一个yield
和两个next(...)
调用。
由于第一个next(...)
老是用来启动生成器,并运行到第一个yield
处。不过,是第二个next(...)
调用完成第一个被暂停的yield
表达式,第三个next(...)
完成第二个yield
,以此类推。
yield(...)
做为一个表达式,能够发出消息响应next(...)
;
next(...)
也能够向暂停的yield
表达式发送值。注意:前面咱们说到,第一个next(...)
老是用来启动生成器的,此时没有暂停的yield
来接受这样一个值,规范和浏览器都会默认丢弃传递给第一个next(...)
的任何东西。所以,启动生成器必定要用不带参数的next(...)
;
function *foo(x) { const y = x * (yield "Hello"); return y; } const it = foo(6); let res = it.next(); // 第一个next(),并不传入任何东西 console.log(res.value); // Hello res = it.next(7); // 向等待的yield传入7 console.log(res.value); // 42
async
&await
是基于生成器+Promise封装的语法糖,使用async
修饰的function
实际上会被转化为一个生成器函数,并返回一个决议的Promise(...)
,await
对应到的是yield
,所以await
只能在被async
修饰的方法中使用
下面看一个例子:这是一个常见的使用async/await
的案例
// 模拟多个异步状况 function foo1() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(100); // 若是没有决议(resolve)就不会运行下一个 }, 3000) }) } function foo2() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(200); }, 2500) }) } async function test() { const f1 = await foo1(); const f2 = await foo2(); console.log('这里是调试1'); console.log(f1); console.log(f2); return 'haha'; } test().then((data) => { console.log('这里是调试2'); console.log(data); }); // 这里是调试1 // 100 // 200 // 这里是调试2 // haha
接着咱们来看一下如何将这个案例中的async/await用生成器+Promise来实现:
将test(...)
转化为生成器函数:
function *test() { const f1 = yield foo1(); const f2 = yield foo2(); console.log('这里是调试1'); console.log(f1); console.log(f2); return 'haha'; }
实现方法run(...)
,用于调用生成器*test(...)
:
function run(generatorFunc) { return function() { const gen = generatorFunc(); // async 默认返回Promise return new Promise((resolve, reject) => { function step(key, arg) { let generatorResult; try { // 至关于执行gen.next(...) generatorResult = gen[key](arg); } catch (error) { return reject(error); } // gen.next() 获得的结果是一个 { value, done } 的结构 const { value, done } = generatorResult; if (done) { return resolve(value); } else { return Promise.resolve(value).then(val => step('next', val), err => step('throw', err)); } } step("next"); }) } } const haha = run(test); haha().then((data) => { console.log('这里是调试2'); console.log(data); }); // 这里是调试1 // 100 // 200 // 这里是调试2 // haha