前段时间面试,考察比较多的是 js 异步编程方面的相关知识点,如今,正好轮到自己分享技术,所以想把 js 异步编程学习下,做个总结。
下面这个 demo 概括了大多数面试过程中遇到的问题:
- for (var i = 0; i < 3; i++) {
- setTimeout(function() {
- console.log('timeout' + i);
- })
- }
- new Promise(function(resolve) {
- console.log('promise1');
- for (var i = 0; i < 1000; i++) {
- i == 99 && resolve();
- }
- console.log('promise2');
- }).then(function() {
- console.log('then1');
- })
- console.log('global1');
通过验证可以得知这个 demo 的结果为:
可是为什么会是这样的结果,我们可能需要先了解下下面两个知识点
浏览器的内核是多线程的,他们在内核的控制下互相配合以保持同步,一个浏览器至少实现三个常驻的线程:javascript 引擎线程,GUI 渲染线程,浏览器事件触发线程。
1)js 引擎,基于事件驱动单线程执行的,js 引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个 JS 线程在运行 JS 程序。
2)GUI 线程,当界面需要重绘或由于某种操作引发回流时,该线程就会执行。它和 JS 引擎是互斥的。
3)浏览器事件触发线程,当一个事件被触发时,该线程会把事件添加到待处理队列的队尾,等待 js 引擎的处理,这些事件可来自 JavaScript 引擎当前执行的代码块如,setTimeOut, 也可以来自浏览器内核的其他线程如鼠标点击,AJAX 异步请求等,但由于 JS 的单线程关系,所有这些事件都得排队等待 JS 引擎处理。
1)任务队列又分为 macro-task(宏任务)与 micro-task(微任务),
在最新标准中,它们被分别称为 task 与 jobs。
2)macro-task 大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
3)micro-task【先执行】大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5 新特性)
setTimeout/Promise 等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
事件循环的顺序,决定了 JavaScript 代码的执行顺序。它从 script(整体代码) 开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空 (只剩全局),然后执行所有的 micro-task。当所有可执行的 micro-task 执行完毕之后。循环再次从 macro-task 开始,找到其中一个任务队列执行完毕,然后再执行所有的 macro-task,这样一直循环下去。
通过这个事件循环的顺序,我们就知道,为什么上面提到的面试题为什么是这样的输出结果了。
接下来我们看下三类异步编程的实现。
demo1:
- // 一个简单的封装
- function want() {
- console.log('这是你想要执行的代码');
- }
- function fn(want) {
- console.log('这里表示执行了一大堆各种代码');
- // 其他代码执行完毕,最后执行回调函数
- want && want();
- }
- fn(want);
demo2:
- //callback hell
- doSomethingAsync1(function() {
- doSomethingAsync2(function() {
- doSomethingAsync3(function() {
- doSomethingAsync4(function() {
- doSomethingAsync5(function() {
- // code...
- });
- });
- });
- });
- });
可以发现一个问题,在回调函数嵌套层数不深的情况下,代码还算容易理解和维护,一旦嵌套层数加深,就会出现 "回调金字塔" 的问题,就像 demo2 那样,如果这里面的每个回调函数中又包含了很多业务逻辑的话,整个代码块就会变得非常复杂。从逻辑正确性的角度来说,上面这几种回调函数的写法没有任何问题,但是随着业务逻辑的增加和趋于复杂,这种写法的缺点马上就会暴露出来,想要维护它们实在是太痛苦了,这就是 "回调地狱 (callback hell)"。
回调函数还有一个问题就是我们在回调函数之外无法捕获到回调函数中的异常,一般我们用 try catch 来捕捉异常,我们尝试下捕捉回调中的异常
可以看到,不能捕捉到 callback 中的异常。
事件监听是一种非常常见的异步编程模式,它是一种典型的逻辑分离方式,对代码解耦很有用处。通常情况下,我们需要考虑哪些部分是不变的,哪些是容易变化的,把不变的部分封装在组件内部,供外部调用,需要自定义的部分暴露在外部处理。从某种意义上说,事件的设计就是组件的接口设计。
1)jQuery 事件监听
- $('#btn').on('myEvent', function(e) {
- console.log('There is my Event');
- });
- $('#btn').trigger('myEvent');
2)发布 / 订阅模式
- var PubSub = function(){
- this.handlers = {};
- };
- PubSub.prototype.subscribe = function(eventType, handler) {
- if (!(eventType in this.handlers)) {
- this.handlers[eventType] = [];
- }
- this.handlers[eventType].push(handler); //添加事件监听器
- return this;//返回上下文环境以实现链式调用
- };
- PubSub.prototype.publish = function(eventType) {
- var _args = Array.prototype.slice.call(arguments, 1);
- for (var i = 0, _handlers = this.handlers[eventType]; i < _handlers.length; i++) {
- _handlers[i].apply(this, _args);//遍历事件监听器
- }
- return this;
- };
- var event = new PubSub;//构造PubSub实例
- event.subscribe('list', function(msg) {
- console.log(msg);
- });
- event.publish('list', {data: ['one,', 'two']});
- //Object {data: Array[2]}
这种模式实现的异步编程,本质上还是通过回调函数实现的,所以 3.1 中提到的回调嵌套和无法捕捉异常的问题还是存在的,接下来我们看 ES6 提供的 Promise 对象,是否解决这两个问题。
ES 6 中原生提供了 Promise 对象,Promise 对象代表了某个未来才会知道结果的事件 (一般是一个异步操作),并且这个事件对外提供了统一的 API,可供进一步处理。
使用 Promise 对象可以用同步操作的流程写法来表达异步操作,避免了层层嵌套的异步回调,代码也更加清晰易懂,方便维护,也可以捕捉异常。
一个简单例子:
- function fn(num) {
- return new Promise(function(resolve, reject) {
- if (typeof num == 'number') {
- resolve();
- } else {
- reject();
- }
- }).then(function() {
- console.log('参数是一个number值');
- }).then(null,
- function() {
- console.log('参数不是一个number值');
- })
- }
- fn('haha');
- fn(1234);
为什么 Promise 可以这样实现异步编程,在这我们简单分析下 Promise 实现过程:
1)极简 Promise 雏形
- // 极简promise雏形
- function Promise(fn) {
- var value = null,
- callbacks = []; //callbacks为数组,因为可能同时有很多个回调
- this.then = function(onFulfilled) {
- callbacks.push(onFulfilled);
- };
- function resolve(value) {
- callbacks.forEach(function(callback) {
- callback(value);
- });
- }
- fn(resolve);
- }
2)加入延时处理
- // 极简promise雏形,加入延时处理
- function Promise(fn) {
- var value = null,
- callbacks = []; //callbacks为数组,因为可能同时有很多个回调
- this.then = function(onFulfilled) {
- callbacks.push(onFulfilled);
- };
- function resolve(value) {
- setTimeout(function() {
- callbacks.forEach(function(callback) {
- callback(value);
- });
- },
- 0)
- }
- fn(resolve);
- }
3)加入状态判断
- // 极简promise雏形,加状态判断
- function Promise(fn) {
- var state = 'pending',
- value = null,
- callbacks = [];
- this.then = function(onFulfilled) {
- if (state === 'pending') {
- callbacks.push(onFulfilled);
- return this;
- }
- onFulfilled(value);
- return this;
- };
- function resolve(newValue) {
- value = newValue;
- state = 'fulfilled';
- setTimeout(function() {
- callbacks.forEach(function(callback) {
- callback(value);
- });
- },
- 0);
- }
- fn(resolve);
- }
4)链式 promise
- // 极简promise雏形,链式promise
- function Promise(fn) {
- var state = 'pending',
- value = null,
- callbacks = [];
- this.then = function(onFulfilled) {
- return new Promise(function(resolve) {
- handle({
- onFulfilled: onFulfilled || null,
- resolve: resolve
- });
- });
- };
- function handle(callback) {
- if (state === 'pending') {
- callbacks.push(callback);
- return;
- }
- //如果then中没有传递任何东西
- if (!callback.onResolved) {
- callback.resolve(value);
- return;
- }
- var ret = callback.onFulfilled(value);
- callback.resolve(ret);
- }
- function resolve(newValue) {
- if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
- var then = newValue.then;
- if (typeof then === 'function') {
- then.call(newValue, resolve);
- return;
- }
- }
- state = 'fulfilled';
- value = newValue;
- setTimeout(function() {
- callbacks.forEach(function(callback) {
- handle(callback);
- });
- },
- 0);
- }
- fn(resolve);
- }
利用 Promise 的知识,对 ajax 进行一个简单的封装。看看会是什么样子:
- //demo3 promise封装ajax
- var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';
- function getJSON(url) {
- return new Promise(function(resolve, reject) {
- var XHR = new XMLHttpRequest();
- XHR.open('GET', url, true);
- XHR.send();
- XHR.onreadystatechange = function() {
- if (XHR.readyState == 4) {
- if (XHR.status == 200) {
- try {
- var response = JSON.parse(XHR.responseText);
- resolve(response);
- } catch(e) {
- reject(e);
- }
- } else {
- reject(new Error(XHR.statusText));
- }
- }
- }
- })
- }
- getJSON(url).then(resp = >console.log(resp));
除了串行执行若干异步任务外,Promise 还可以并行执行异步任务。
当有一个 ajax 请求,它的参数需要另外 2 个甚至更多请求都有返回结果之后才能确定,那么这个时候,就需要用到 Promise.all 来帮助我们应对这个场景。
Promise.all 接收一个 Promise 对象组成的数组作为参数,当这个数组所有的 Promise 对象状态都变成 resolved 或者 rejected 的时候,它才会去调用 then 方法。
- // demo4 promise.all
- var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';
- var url1 = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-03-26/2017-06-10';
- function renderAll() {
- return Promise.all([getJSON(url), getJSON(url1)]);
- }
- renderAll().then(function(value) {
- console.log(value); //将得到一个数组,里面是两个接口返回的值
- })
结果:
有些时候,多个异步任务是为了容错。比如,同时向两个 URL 读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用 Promise.race() 实现。
与 Promise.all 相似的是,Promise.race 都是以一个 Promise 对象组成的数组作为参数,不同的是,只要当数组中的其中一个 Promsie 状态变成 resolved 或者 rejected 时,就可以调用. then 方法了
- // demo5 promise.race
- function renderRace() {
- return Promise.race([getJSON(url), getJSON(url1)]);
- }
- renderRace().then(function(value) {
- console.log(value);
- })
这里 then() 传的 value 值将是接口返回比较快的接口数据,另外一个接口仍在继续执行,但执行结果将被丢弃。
结果:
Generator 函数是协程在 ES 6 中的实现,最大特点就是可以交出函数的执行权(暂停执行)。
注意:在 node 中需要开启 --harmony 选项来启用 Generator 函数。
整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。
看个简单的例子:
- function * gen(x) {
- var y = yield x + 2;
- return y;
- }
- var g = gen(1);
- var r1 = g.next(); // { value: 3, done: false }
- console.log(r1);
- var r2 = g.next() // { value: undefined, done: true }
- console.log(r2);
需要注意的是 Generator 函数的函数名前面有一个 "*"。
上述代码中,调用 Generator 函数,会返回一个内部指针 (即遍历器)g,这是 Generator 函数和一般函数不同的地方,调用它不会返回结果,而是一个指针对象。调用指针 g 的 next 方法,会移动内部指针,指向第一个遇到的 yield 语句,上例就是执行到 x+2 为止。
换言之,next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息(value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
对 Generator 函数,只有一个感性认知,没有实践过,所以就先介绍到这了,后面还有 ES7 新的知识点 async await,看了下网上的资料,理解得还不够,希望后面自己接触得更多再来这里补上,未完待续...
参考资料:
1) http://www.jianshu.com/p/12b9f73c5a4f
2) http://www.jianshu.com/p/fe5f173276bd
3) https://mengera88.github.io/2017/05/18/Promise 原理解析 /
4) http://www.cnblogs.com/nullcc/p/5841182.html
来源: http://www.cnblogs.com/wj204/p/7398676.html