JavaScript中的异步编程

9次阅读

共计 6416 个字符,预计需要花费 17 分钟才能阅读完成。

异步

何为异步?

简单来说就是一个任务分成多个步骤执行,先执行某一段任务,跳出转而执行其他任务,等下一段任务准备完成后,转而回来执行下一段任务

像这种类型,把一个任务分解成多段任务 不连续 执行,就叫做异步, 连续执行 的则叫做同步

如何使得异步 看起来像是同步编程 ? 有如下几种方法

回调函数

通过拆解一个任务,分成多段,把第二段任务单独写在第二个函数内,等到需要执行这个任务时,直接调用这个函数

node.js 中常用的就是如此方法。

    fs.readFile('某个文件', function (err, data) {if (err) throw err;
        console.log(data);
    });

这是一个错误优先的回调函数(error-first callbacks), 这也是 Node.js 本身的特点之一。类似 golang 中的 err 错误处理

带来的问题:
  • 多层嵌套问题

回调带来一些问题,第一个就是多层嵌套问题,当一个问题很复杂,多段不连续,就会出现地狱嵌套问题

fs.readFile('某个文件', function (err, data) {if (err) throw err;
    fs.writeFile('某个文件',data, function (err, data) {if (err) throw err;
            fs.readFile('某个文件', function (err, data) {if (err) throw err;
                console.log("写入的是:",data)
        });
    });
});

异常处理

无法使用 try{}catch(){} 捕获错误
列子:

    try{setTimeout(()=>{callback()
            throw new Error('抛出错误')
        },1000)
    }.catch(err){console.log('看看是否走到了这里')
    }

上面的代码是无法走到 catch 内部的,由于 try{}catch 只能捕获当前任务循环内的任务抛出错误,而这个回调被存放起来,直到下一个事件环的时候才会取出,try{}catch 实在无能为力

在 node 中,已约定回调的第一个参数是抛出的异常。只是用另外的方式来捕获错误。
伪代码

    let func = function(callback){
        try{setTimeout(()=>{if(success){callback(null)
                }else{callback(new Error('错误'))
                }
            },1000)
        }catch(e){console.log('捕获错误',e);
        }
        
    }

事件监听

通常在前端操作的一般是通过 addeventLisener 监听各种事件,比如键盘事件 鼠标事件等等,

    document.addeventListener('click',function(e){console.log(e.target)
    },false)

事件发布订阅

通常把需要执行的任务先暂存起来,等达到条件或者发布的时候一一拿出来执行

  • 伪代码
class Task{construct(){this.tasks = {}
    }
    publish(event){this.tasks[event].forEach(fn=>fn())
    }
    subscribe(event,eventTask){this.tasks[event] =   this.tasks[event] ?  this.tasks[event] : []
        this.tasks[event].push(eventTask)
    }
}
let task = new Task()
task.subscribe('eat',function(){console.log('吃午饭')})
task.subscribe('eat',function(){console.log('吃晚饭')})
task.publish('eat')
  • Promise/Deferred 模式

    • 生成器 Generators/ yield

      • 当你在执行一个函数的时候,你可以在某个点暂停函数的执行,并且做一些其他工作,然后再返回这个函数继续执行,甚至是携带一些新的值,然后继续执行。
      • 上面描述的场景正是 JavaScript 生成器函数所致力于解决的问题。当我们调用一个生成器函数的时候,它并不会立即执行,而是需要我们手动的去执行迭代操作(next 方法)。也就是说,你调用生成器函数,它会返回给你一个迭代器。迭代器会遍历每个中断点。
      • next 方法返回值的 value 属性,是 Generator 函数向外输出数据;next 方法还可以接受参数,这是向 Generator 函数体内输入数据
    function* foo () {
        var index = 0;
        while (index < 2) {yield index++; // 暂停函数执行,并执行 yield 后的操作}
    }
    var bar =  foo(); // 返回的其实是一个迭代器

    console.log(bar.next());    // {value: 0, done: false}
    console.log(bar.next());    // {value: 1, done: false}
    console.log(bar.next());    // {value: undefined, done: true}

yield 具体查看 mdn 文档

yield 是一个表达式, 后面紧跟着的表达式是 next()的返回结果的 value, 而如果想给 yield 传递参数, 比如 a =yield 1, 给 a 传递值 则要 next(value))

// 例子: 
 function* foo () {
        a = yield 1
        console.log(a) // 10
    }
    var bar =  foo(); // 返回的其实是一个迭代器
    console.log(bar.next());  // {value: 1, done: false}
    console.log(bar.next(10));   // {value: undefined, done: true}

可以理解为 yield 有两步操作, 第一个 next 弹出值, 第二个 next 接收值并且执行一下段语句, 直到下一个 yield 弹出值为止

利用 yield 转换多维数组
function* iterArr(arr) {            // 迭代器返回一个迭代器对象
  if (Array.isArray(arr)) {         // 内节点
      for(let i=0; i < arr.length; i++) {yield* iterArr(arr[i]);   // (*)递归
      }
  } else {                          // 离开     
      yield arr;
  }
}

var arr = ['a', ['b',[ 'c', ['d', 'e']]]];
var gen = iterArr(arr);
arr = [...gen];   
利用 yield 解决异步问题
function* main(){
    try{let result = yield foo()
        console.log(result)
    }catch(e){console.log(e)
    }
  
}
let it = main()

function foo(params,url){
    $.ajax('www.baidu.com',
    function(err,data){if(err){it.throw(err)
        }else{it.next(data)
        }
    }
}

it.next()  // {value:undefind,done:false}

promise

Promise 对象用于表示一个异步操作的最终完成 (或失败), 及其结果值.
他有三个状态 分别是 pending resolved 以及 rejected
一旦发生状态改变, 就不可再更改了 每次 then 都另外创建一个 promise 对象

    //  伪代码
    class Promise{construct(executor){
            let self = this
            // 一个 Promise 有以下几种状态:
            // pending: 初始状态,既不是成功,也不是失败状态。// fulfilled: 意味着操作成功完成。// rejected: 意味着操作失败。this.status = 'pending' 
            this.res = undefined   // 存成功之后的值
            this.err = undefined   // 存失败之后的值
            this.onFulfilledCallback = []
            this.onRejectedCallback = []
            function resolve(res){if(self.status === 'pending'){
                    self.status = 'resolved'
                    self.res = res 
                    onFulfilledCallback.forEach(fn=>fn())
                }
            }
            function reject(err){if(self.status === 'pending'){
                    self.status = 'rejected'
                    self.err = err 
                    onRejectedCallback.forEach(fn=>fn())
                }
            }
            // executor 是带有 resolve 和 reject 两个参数的函数。Promise 构造函数执行时立即调用 executor 函数,resolve 和 reject 两个函数作为参数传递给 executor(executor 函数在 Promise 构造函数返回所建 promise 实例对象前被调用)。resolve 和 reject 函数被调用时,分别将 promise 的状态改为 fulfilled(完成)或 rejected(失败)。executor 内部通常会执行一些异步操作,一旦异步操作执行完毕(可能成功 / 失败),要么调用 resolve 函数来将 promise 状态改成 fulfilled,要么调用 reject 函数将 promise 的状态改为 rejected。如果在 executor 函数中抛出一个错误,那么该 promise 状态为 rejected。executor 函数的返回值被忽略。executor(resolve,reject)
        }
        then(onFulfilled,onRejected){
            let self = this
            return new Promise((resolve,reject)=>{if(self.status === 'resolved'){let x = onFulfilled(self.res)   // 拿到 onFulfilled 的执行结果  注意:这里执行的是 Promise.resolve() 同步代码
                    // 然后把 x 传递给下一个 then
                    resolve(x)
                }
                 if(self.status === 'rejected'){let x = onRejected(self.res)   // 拿到 onFulfilled 的执行结果  注意:这里执行的是 Promise.resolve() 同步代码
                    // 然后把 x 传递给下一个 then
                    reject(x)
                }
                if(self.status === 'pending'){self.onFulfilledCallback.push(function(){let x = onFulfilled(self.res)  // 这里的 self.res 是上一个 new Promise 上的值 此时的 onFUlfilled 相当于 fn(){let x = onFulfilled}  
                        resolve(x)
                    })
                    self.onRejectedCallback.push(function(){let x = onRejected(self.res)  // 这里的 self.res 是上一个 new Promise 上的值 此时的 onFUlfilled 相当于 fn(){let x = onFulfilled}  
                        reject(x)
                    })
                }
            })
        }
    }
使用 promise
function request(){return new Promise((resolve,reject)=>{setTimeout(()=>{resolve({data:'获得数据'})
        },1000)
    })
}
request().then(data=>{console.log(data)
})

链接:Promise

yield 配合 Promise
function foo(x,y) {
    return request("http://some.url.1/?x=" + x + "&y=" + y);
} 
function *main() {
    try {var text = yield foo( 11, 31);
        console.log(text);
    }
    catch (err) {console.error( err);
    }
} 
let it = main()
it.next()

var text = yield foo(11, 31)跟 async await 是不是很像?

编写一个生成器

生成器可以 yield 一个 promise,然后这个 promise 可以被绑定,用其完成值来恢复这个生成器的运行。

//  伪代码
function run(gen){  // 参数是一个 gen 函数
    let it = gen.apply(this)
    return Promise.resolve().then((value)=>{let next = it.next(value)
        
        return (function nextHandle(next){if(next.done === true){return next.value}else{return Promise.resolve(next.value).then(nextHandle)  // 递归
            }
        })(next)
    })

}

function* main(){function *main() {
        try {var text = yield foo( 11, 31);
            console.log(text);
        }
        catch (err) {console.error( err);
        }
    } 
}

run(main).then((data)=>{// do something}) 

AsyncFunction

AsyncFunction 构造函数用来创建新的 异步函数 对象,JavaScript 中每个异步函数都是 AsyncFunction 的对象。

注意,AsyncFunction 并不是一个全局对象,需要通过下面的方法来获取

Object.getPrototypeOf(async function(){}).constructor

语法:new AsyncFunction([arg1[, arg2[, …argN]],] functionBody)

var AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
var a = new AsyncFunction('a', 
                          'b',
                          'return await resolveAfter2Seconds(a) + await resolveAfter2Seconds(b);');
a(10, 20).then(v => {console.log(v); // 4 秒后打印 30
});

但是上面这种方式不高效 因为通过字面量创建的异步函数是与其他代码一起被解释器解析的,而 new 这种方式的函数体是单独解析的。

通过字面量创建
    async a(a,b){return await resolveAfter2Seconds(a) + await resolveAfter2Seconds(b)
    }

await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理(fulfilled),其回调的 resolve 函数参数作为 await 表达式的值,继续执行 async function。

若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。

另外,如果 await 操作符后的表达式的值不是一个 Promise,则返回该值本身。

function resolveAfter2Seconds(x) {
  return new Promise(resolve => {setTimeout(() => {resolve(x);
    }, 2000);
  });
}

async function f1() {var x = await resolveAfter2Seconds(10);
  console.log(x); // 10
}
f1();

正文完
 0

Javascript中的异步编程

9次阅读

共计 3743 个字符,预计需要花费 10 分钟才能阅读完成。

前言
最近,小伙伴 S 问了我一段代码:
const funB = (value) => {
console.log(“funB “+ value);
};

const funA = (callback) => {

setTimeout(() => {
typeof callback === “function” && callback(“is_ok!”);
}, 1000);
}

funA(funB);
他不太理解这段代码中,funB 函数作为 funA 函数的参数这样的写法。从语义上看,callback 的意思是回调,那么是说 funB 是 funA 的回调嘛?
我给他解释说,funB 函数的确是 funA 函数的回调,它会等待 funA 中前面的语句都执行完,再去执行。这是一种异步编程的写法。
小伙伴 S 还是有点不太理解。他问:异步编程是什么?除了回调函数之外,异步编程还有哪些?
别急,让我们先从概念入手,再逐个理解异步编程中的方法,看看它的前世今生。
什么是异步?
所谓 ” 异步 ”(Asynchronous),可以理解为一种不连续的执行。简单地说,就是把一个任务分成两段,先执行第一段,然后转而执行其他任务,等接到通知了,再回过头执行第二段。
我们都知道,JavaScript 是单线程的。而异步,对于 JavaScript 的重要性,则体现在非阻塞这一点上。一些常见的异步有:

onclick 在其事件触发的时候,回调会立即添加到任务队列中。

setTimeout 只有当时间到达的时候,才会将回调添加到任务队列中。

ajax 在网络请求完成并返回之后,才将回调添加到任务队列中。

接下来,我们一起来看看 Javascript 中的异步编程,具体有哪几种。
实现异步编程的方法
一、回调函数
上面不止一次提到了回调函数。它从概念上说很简单,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。它是异步编程中,最基本的方法。
举个例子,假定有两个函数 f1 和 f2,后者等待前者的执行结果。顺序执行的话,可以这样写:
f1();
f2();
但是,如果 f1 是一个很耗时的任务,该怎么办?
改写一下 f1,把 f2 写成 f1 的回调函数:
const f1 = (callback) => {
setTimeout(() => {
typeof callback === “function” && callback();
}, 1000);
}
f1(f2);
二、事件监听
onclick 的写法,在异步编程中,称为事件监听。它的思路是:如果任务的执行不取决于代码的顺序,而取决于某个事件是否发生,也就事件驱动模式。
还是 f1 和 f2 的例子,为了简化代码,这里采用 jQuery 的写法:
// 为 f1 绑定一个事件,当 f1 发生 done 事件,就执行 f2
f1.on(‘done’, f2);

// 改写 f1
function f1(){
setTimeout(() => {
// f1 的任务代码,执行完成后,立即触发 done 事件
f1.trigger(‘done’);
}, 1000);
}
它的优点是:比较容易理解,耦合度降低了。可以绑定多个事件,而且每个事件还能指定多个回调函数。
缺点是:整个程序都会变为由事件来驱动,流程会变得很不清晰。
三、发布 / 订阅
这是一种为了处理一对多的业务场景而诞生的设计模式,它也是一种异步编程的方法。vue 中 MVVM 的实现,就有它的功劳。
关于概念,我们可以这样理解,假定存在一个 ” 信号中心 ”,某个任务执行完成,就向信号中心 ” 发布 ”(publish)一个信号,其他任务可以向信号中心 ” 订阅 ”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做 ” 发布 / 订阅模式 ”(publish-subscribe pattern),又称 ” 观察者模式 ”(observer pattern)。
下面的例子,采用的是 Morgan Roderick 的 PubSubJS,这是一个无依赖的 JavaScript 插件:
import PubSub from ‘pubsub-js’;

// f2 向 ‘PubSub’ 订阅信号 ‘done’
PubSub.subscribe(‘done’, f2);

const f1 = () => {
setTimeout(() => {
// f1 执行完成后,向 ‘PubSub’ 发布信号 ‘done’,从而执行 f2
PubSub.publish(‘done’);
}, 1000);
};
f1();

// f2 完成执行后,也可以取消订阅
PubSub.unsubscribe(“done”, f2);
这种模式有点类似于“事件监听”,但是明显优于后者。因为,我们可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
四、Promise 对象
接下来,我们聊聊与 ajax 相关的异步编程方法,Promise 对象。
Promise 是由 CommonJS 提出的一种规范,它是为了解决回调函数嵌套,也就是回调地狱的问题。它不是新的语法功能,而是一种新的写法,允许将回调函数的横向加载,改成纵向加载。它的思想是,每一个异步任务返回一个 Promise 对象,该对象有一个 then 方法,允许指定回调函数。
继续改写 f1 和 f2:
const f1 = () => {
return new Promise((resolve, reject) => {
let timeOut = Math.random() * 2;
setTimeout(() => {
if (timeOut < 1) {
resolve(‘200 OK’);
} else {
reject(‘timeout in ‘ + timeOut + ‘ seconds.’);
}
}, 1000);
});
};

const f2 = () => {
console.log(‘start f2’);
};

f1().then((result) => {
console.log(result);
f2();
}).catch((reason) => {

);
例子中,用随机数模拟了请求的超时。当 f1 返回 Promise 的 resolve 时,执行 f2。
Promise 的优点是:回调函数变成了链式的写法,程序的流程可以看得很清楚。还有就是,如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不用担心是否错过了某个状态。
缺点就是:编写和理解,都相对比较难。
五、Generator
generator(生成器)是 ES6 标准引入的数据类型。它最大特点,就是可以交出函数的执行权(即暂停执行),是协程在 ES6 中的实现。
看上去它像一个函数,定义如下:
function* gen(x) {
var y = yield x + 2;
return y;
}
它不同于普通函数,函数名之前要加星号 (*),是可以暂停执行的。
整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。用 yield 语句注明异步操作需要暂停的地方。
我们来看一下 Generator 函数执行的过程:
var g = gen(1);

// {value: 3, done: false}
g.next();
// {value: undefined, done: true}
g.next();
上面代码中,调用 Generator 函数,会返回一个内部指针(即遍历器)g。这是 Generator 函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针 g 的 next 方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的 yield 语句,上例是执行到 x + 2 为止。
换言之,next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息(value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
六、async/await
这是 ES8 中提出的一种更优雅的异步解决方案,灵感来自于 C# 语言。具体可前往 细说 async/await 相较于 Promise 的优势,深入理解其原理及特性。
来看个例子,要实现一个暂停功能,输入 N 毫秒,则停顿 N 毫秒后才继续往下执行。
const sleep = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, time);
})
};

const start = async () => {
console.log(‘start’);
// 在这里使用起来就像同步代码那样直观
await sleep(1000);
console.log(‘end’);
};

start();
控制台先输出 start,稍等 1 秒后,输出结果 ok,最后输出 end。
解析一下上述代码:

async 表示这是一个 async 函数,await 只能用在这个函数里面。
await 表示在这里等待 promise 返回了结果,再继续执行。
使用起来,就像写同步代码一样地优雅。

总结
JavaScript 的异步编写方式,从 回调函数 到 async/await,感觉在写法上,每次都有进步,其本质就是一次次对语言层抽象的优化。以至于现在,我们可以像同步一样地,去处理异步。
换句话说就是:异步编程的最高境界,就是根本不用关心它是不是异步。
PS:欢迎关注我的公众号“超哥前端小栈”,交流更多的想法与技术。

正文完
 0