面试官: 说说你对async的理解
大家好,我是小雨小雨,致力于分享有趣的、实用的技术文章。
内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步。
分享不易,希望能够得到大家的支持和关注。
TL;DR
async是generator和promise的语法糖,利用迭代器的状态机和promise来进行自更新!
如果懒得往下看,可以看下这个极其简易版本的实现方式:
// 复制粘贴即可直接运行
function stateMac (arr) {
let val;
return {
next(){
if ((val = arr.shift())) {
return {
value: val,
done: false
}
} else {
return {
done: true
}
}
}
}
}
function asyncFn(arr) {
const iterator = stateMac(arr);
function doSelf () {
const cur = iterator.next();
const value = cur.value;
if (cur.done) {
console.log('done');
return;
}
switch (true) {
case value.then && value.toString() === '[object Promise]':
value.then((result) => {
console.log(result);
doSelf();
})
break;
case typeof value === 'function':
value();
doSelf();
break;
default:
console.log(value);
doSelf();
}
}
doSelf();
}
const mockAsync = [
1,
new Promise((res) => {
setTimeout(function () {
res('promise');
}, 3000);
}),
function () {
console.log('测试');
}
];
console.log('开始');
asyncFn(mockAsync);
console.log('结束');
前言
async & await 和我们的日常开发紧密相连,但是你真的了解其背后的原理吗?
本文假设你对promise、generator有一定了解。
简述promise
promise就是callback的另一种写法,避免了毁掉地狱,从横向改为纵向,大大提升了可读性和美观。
至于promise的实现,按照promise A+规范一点点写就好了,完成后可以使用工具进行测试,确保你的写的东西是符合规范的。
具体实现原理,市面上有各种各样的写法,我就不多此一举了。
简述generator
generator就不像promise那样,他改变了函数的执行方式。可以理解为协程,就是说多个函数
互相配合完成任务。类似于这个东西:
function generator() {
return {
_value: [1, 2, 3, 4],
next() {
return {
value: this._value.shift(),
done: !this._value.length
};
}
};
}
const it = generator();
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
这只是一个demo,仅供参考。
具体请参考MDN.
async & await
照我的理解,其实就是generator和promise相交的产物,被解析器识别,然后转换成我们熟知的语法。
这次要做的就是去看编译之后的结果是什么样的。
既然如此,我们就带着问题去看,不然看起来也糟心不是~
async包装的函数会返回一个什么样的promise?
// 源代码:
async function fn() {}
fn();
// 编译后变成了一大坨:
// generator的polyfill
require("regenerator-runtime/runtime");
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
function _asyncToGenerator(fn) {
return function() {
var self = this,
args = arguments;
return new Promise(function(resolve, reject) {
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
}
_next(undefined);
});
};
}
function fn() {
return _fn.apply(this, arguments);
}
function _fn() {
_fn = _asyncToGenerator(
/*#__PURE__*/ regeneratorRuntime.mark(function _callee() {
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
case 0:
case "end":
return _context.stop();
}
}
}, _callee);
})
);
return _fn.apply(this, arguments);
}
fn();
内容也不是很多,我们一点点来看:
generator包装
fn
内部调用的是_fn
,一个私有方法,使用的apply绑定的this,并传入了动态参数。
_fn
内调用了_asyncToGenerator
方法,由于js调用栈后进先出:
读起来是这样的:fn() => _asyncToGenerator => .mark()
执行是反过来的:.mark() => _asyncToGenerator => fn()
我们先往里看,映入眼帘的是regeneratorRuntime.mark,该方法是generator的polyfill暴露的方法之一,我们去内部(require('regenerator-runtime/runtime'))简单看下这个mark是用来干什么的。
// 立即执行函数,适配commonjs和浏览器
(function (exports) {
// 暴露mark方法
exports.mark = function (genFun) {
// 兼容判断__proto__,处理老旧环境
if (Object.setPrototypeOf) {
Object.setPrototypeOf(genFun, GeneratorFunctionPrototype);
} else {
genFun.__proto__ = GeneratorFunctionPrototype;
// 设置Symbol.toStringTag,适配toString
if (!(toStringTagSymbol in genFun)) {
genFun[toStringTagSymbol] = 'GeneratorFunction';
}
}
// 设置原型
genFun.prototype = Object.create(Gp);
return genFun;
};
})(typeof module === 'Object' ? module.exports : {});
mark做了两个操作,一个是设置genFun
的__proto__,一个是设置prototype,可能有人会好奇:
__proto__不是对象上的吗?prototype不是函数上的吗?为啥两个同时应用到一个上面了
这样操作是没问题的,genFun
不仅是函数啊,函数还是对象,js中万物皆对象哦。你想想是不是可以通过Function构造函数new出一个函数?
然后开始设置__proto__和prototype,在次之前,我们来简单捋一下原型。
原型
下面是个人理解的一个说法,未查阅v8引擎,但是这样是说得通的。如果有问题,欢迎指出,一起沟通,我也会及时修改,以免误导他人!!!。
首先要知道这三个的概念:搞清对象的原型对象(proto)、构造函数的原型(prototype)、构造方法(constructor)。
方便记忆,只需要记住下面几条即可:
- prototype是构造函数(注意:构造函数也是对象嗷)上特有的属性,代表构造函数的原型。举个例子:
有一位小明同学(指代构造函数),他有自己的朋友圈子(指代prototype),通过小明可以找到小红(构造函数.prototype.小红),在通过小红的朋友圈子(prototype)还能找到小蓝,直到有一个人(指代null),孑然一身、无欲无求,莫得朋友。
上面这个关系链就可以理解为原型链。
- __proto__是每一个对象上特有的属性,指向当前对象构造函数的prototype。再举个例子:
小明家里催的急,不就就生了个大胖小子(通过构造函数{小明}创造出对象{大胖小子}),可以说这个大胖小子一出生就被众星捧月,小明的朋友们纷纷表示,以后孩子有啥事需要帮忙找我就成。这就指代对象上的__proto__
,__proto__
可以引用构造函数的任何关系。
所以说,代码源于生活~
constructor是啥呢,就是一个prototype上的属性,表示这个朋友圈子是谁的,对于小明来说: 小明.prototype.constructor === 小明。所以,当我们进行继成操作的时候,有必要修正一下constructor,不然朋友圈子就乱了~
js中函数和对象有点套娃的意思,万物皆对象,对象又是从构造函数构造而来。对于小明来说,就是我生我生我~~
来看两个判断:
proto 指向构造当前对象的构造函数的prototype,由于万物皆对象,对象又是通过构造函数构造而来。故Object通过Function构造而来,所以指向了Function.prototype
console.log(Object.__proto__ === Function.prototype); // => true
proto 指向构造当前对象的构造函数的prototype,由于万物皆对象,对象又是通过构造函数构造而来。故Function通过Function构造而来,所以指向了Function.prototype
console.log(Function.__proto__ === Function.prototype); // => true
有兴趣的朋友可以再看看这篇文章
然后,我们再来看看这张图,跟着箭头走一遍,是不是就很清晰了?
继续generator包装
mark方法会指定genFun的__proto__和prototype,完完全全替换了genFun的朋友圈以及创造genFun的构造函数的朋友圈,现在genFun就是Generator的克隆品了。
用来设置__proto__ 和 prototype的值,GeneratorFunctionPrototype,GP,我们也简单过一下:
// 创建polyfill对象
var IteratorPrototype = {};
IteratorPrototype[iteratorSymbol] = function () {
return this;
};
// 原型相关操作
// 获取对象的原型: __proto__
var getProto = Object.getPrototypeOf;
// 原生iterator原型
var NativeIteratorPrototype = getProto && getProto(getProto(values([])));
// IteratorPrototype设置为原生
if (
NativeIteratorPrototype &&
NativeIteratorPrototype !== Op &&
hasOwn.call(NativeIteratorPrototype, iteratorSymbol)
) {
// This environment has a native %IteratorPrototype%; use it instead
// of the polyfill.
IteratorPrototype = NativeIteratorPrototype;
}
// 创造原型
// Gp 为 迭代器原型
// IteratorPrototype作为原型对象
var Gp = (GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(
IteratorPrototype
));
// 更新构造函数和原型
GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype;
GeneratorFunctionPrototype.constructor = GeneratorFunction;
// toString,调用Object.toString.call的时候会返回GeneratorFunction
GeneratorFunctionPrototype[
toStringTagSymbol
] = GeneratorFunction.displayName = 'GeneratorFunction';
最后再返回经过处理的genFun,然后再回到mark函数外~
_asyncToGenerator
_asyncToGenerator
接收mark处理过的结果:
// fn 为 generator 的克隆品
function _asyncToGenerator(fn) {
return function () {
var self = this,
args = arguments;
return new Promise(function (resolve, reject) {
// 调用_callee,先看下面,一会在回来哈~
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(
gen,
resolve,
reject,
_next,
_throw,
'next',
value
);
}
function _throw(err) {
asyncGeneratorStep(
gen,
resolve,
reject,
_next,
_throw,
'throw',
err
);
}
_next(undefined);
});
};
}
regeneratorRuntime.wrap
上面的_asyncToGenerator执行后,会执行mark返回的函数:
function _callee() {
return regeneratorRuntime.wrap(function _callee$(
_context
) {
// 这里就是动态得了,也就是根据用户写的async函数,转换的记过,由于我们是一个空函数,所以直接stop了
while (1) {
switch ((_context.prev = _context.next)) {
case 0:
case 'end':
return _context.stop();
}
}
},
_callee);
}
_callee会返回wrap处理后的结果,我们继续看:
// innerFn是真正执行的函数,outerFn为被mark的函数
// self, tryLocsList未传递,为undefined
function wrap(innerFn, outerFn, self, tryLocsList) {
// If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator.
// outerFn 的原型已经被 mark重新设置,所以会包含generator相关原型
var protoGenerator =
outerFn && outerFn.prototype instanceof Generator
? outerFn
: Generator;
// 创建自定义原型的对象
var generator = Object.create(protoGenerator.prototype);
// context 实例是包含的 this.tryEntries 的
var context = new Context(tryLocsList || []);
// The ._invoke method unifies the implementations of the .next,
// .throw, and .return methods.
generator._invoke = makeInvokeMethod(innerFn, self, context);
return generator;
}
其中有个new Context()的操作,用来重置并记录迭代器的状态,后面会用到。
之后给返回generator挂载一个_invoke方法,调用makeInvokeMethod,并传入self(未传递该参数,为undefined)和context。
function makeInvokeMethod(innerFn, self, context) {
// state只有在该函数中备操作
var state = GenStateSuspendedStart; // GenStateSuspendedStart: 'suspendedStart'
// 作为外面的返回值
return function invoke(method, arg) {
// 这里就是generator相关的一些操作了,用到的时候再说
};
}
利用闭包初始化state,并返回一个invoke函数,接受两个参数,方法和值。先看到这,继续往后看。
回到之前的_asyncToGenerator
:
// 返回带有_invoke属性的generator对象
var gen = fn.apply(self, args);
之后定义了一个next和throw方法,随后直接调用_next开始执行:
function _next(value) {
asyncGeneratorStep(
gen, // 迭代器函数
resolve, // promise的resolve
reject, // promise的project
_next, // 当前函数
_throw, // 下面的_throw函数
'next', // method名
value // arg 参数值
);
}
function _throw(err) {
asyncGeneratorStep(
gen,
resolve,
reject,
_next,
_throw,
'throw',
err
);
}
_next(undefined);
其中都是用的asyncGeneratorStep,并传递了一些参数。
那asyncGeneratorStep又是啥呢:
function asyncGeneratorStep(
gen,
resolve,
reject,
_next,
_throw,
key,
arg
) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
// 出错
reject(error);
return;
}
if (info.done) {
// 如果完成,直接resolve
resolve(value);
} else {
// 否则,继续下次next调用,形成递归
Promise.resolve(value).then(_next, _throw);
}
}
代码很少,获取即将要调用的方法名(key)并传入参数,所以当前info即是:
var info = gen['next'](arg);
那next是哪来的那?就是之前mark操作中定义的,如果原生支持,就是用原生的迭代器提供的next,否则使用polyfill中定义的next。
还记得之前的makeInvokeMethod
吗?
它其实是用来定义标准化next、throw和return的:
function defineIteratorMethods(prototype) {
['next', 'throw', 'return'].forEach(function (method) {
prototype[method] = function (arg) {
return this._invoke(method, arg);
};
});
}
// Gp在之前的原型操作有用到
defineIteratorMethods(Gp);
然后当我们执行的时候,就会走到_invoke定义的invoke方法中:
function invoke(method, arg) {
// 状态判断,抛错
if (state === GenStateExecuting) {
throw new Error('Generator is already running');
}
// 已完成,返回done状态
if (state === GenStateCompleted) {
if (method === 'throw') {
throw arg;
}
// Be forgiving, per 25.3.3.3.3 of the spec:
// https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume
return doneResult();
}
// 这里就是之前定义的Context实例,下面代码没啥了,自己看吧
context.method = method;
context.arg = arg;
while (true) {
var delegate = context.delegate;
if (delegate) {
var delegateResult = maybeInvokeDelegate(delegate, context);
if (delegateResult) {
if (delegateResult === ContinueSentinel) continue;
return delegateResult;
}
}
if (context.method === 'next') {
// Setting context._sent for legacy support of Babel's
// function.sent implementation.
context.sent = context._sent = context.arg;
} else if (context.method === 'throw') {
if (state === GenStateSuspendedStart) {
state = GenStateCompleted;
throw context.arg;
}
context.dispatchException(context.arg);
} else if (context.method === 'return') {
context.abrupt('return', context.arg);
}
state = GenStateExecuting;
// innerFn就是while个循环了,使我们的代码主体
var record = tryCatch(innerFn, self, context);
if (record.type === 'normal') {
// If an exception is thrown from innerFn, we leave state ===
// GenStateExecuting and loop back for another invocation.
state = context.done
? GenStateCompleted
: GenStateSuspendedYield;
if (record.arg === ContinueSentinel) {
continue;
}
return {
value: record.arg,
done: context.done
};
} else if (record.type === 'throw') {
state = GenStateCompleted;
// Dispatch the exception by looping back around to the
// context.dispatchException(context.arg) call above.
context.method = 'throw';
context.arg = record.arg;
}
}
};
在之后,就是我们熟悉的promise相关操作了,在判断done是否为true,否则继续执行,将_next和_throw作为resolve和reject传入即可。
小结
可以看到,仅仅一个async其实做了不少工作。核心就是两个,产出一个兼容版本的generator和使用promise,回到这节的问题上,答案就是:
return new Promise(function (resolve, reject) {});
没错,就是返回一个Promise,内部会根据状态及决定是否继续执行下一个Promise.resolve().then()。
如果async函数内有很多其他操作的代码,那么while会跟着变化,利用prev和next来管理执行顺序。这里就不具体分析了,自己写个例子就明白了~
可以通过babel在线转换,给自己一个具象的感知,更利于理解。
为什么下面这种函数外的console不会等待,函数内的会等待?
async function fn() {
await (async () => {
await new Promise((r) => {
setTimeout(function () {
r();
}, 2000);
});
})();
console.log('你好');
}
fn();
console.log(123);
因为解析后的console.log(123); 是在整个语法糖之外啊,log 和 fn 是主协程序,fn内是辅协程。不相干的。
总结
有句话怎么说来着,会者不难,难者不会。所以人人都是大牛,只是你还没发力而已,哈哈~
笔者后来思考觉得这种写法完全就是回调函数的替代品,而且增加了空间,加深了调用堆栈,或许原生的写法才是效率最高的吧。
不过,需要良好的编码规范,算是一种折中的方式了。毕竟用这种方式来写业务事半功倍~
对于本文观点,完全是个人阅读后的思考,如有错误,欢迎指正,我会及时更新,避免误导他人。
拜了个拜~
面试官: 说说你对async的理解的更多相关文章
- 怎么回答面试官:你对Spring的理解?
最近看了点Spring的源码,正好来稍微扯一扯,帮一部分培训班的朋友撕开一道口子,透透气.我自己都是看的培训班视频,所以也算培训班出身吧.所以下文开口闭口"培训班",不要觉得是我在 ...
- 「每日一题」面试官问你对Promise的理解?可能是需要你能手动实现各个特性
关注「松宝写代码」,精选好文,每日一题 加入我们一起学习,day day up 作者:saucxs | songEagle 来源:原创 一.前言 2020.12.23日刚立的flag,每日一题,题目类 ...
- [每日一题]面试官问:Async/Await 如何通过同步的方式实现异步?
关注「松宝写代码」,精选好文,每日一题 时间永远是自己的 每分每秒也都是为自己的将来铺垫和增值 作者:saucxs | songEagle 一.前言 2020.12.23 日刚立的 flag,每日一 ...
- [每日一题]面试官问:谈谈你对ES6的proxy的理解?
[每日一题]面试官问:谈谈你对ES6的proxy的理解? 关注「松宝写代码」,精选好文,每日一题 作者:saucxs | songEagle 一.前言 2020.12.23 日刚立的 flag,每日一 ...
- [每日一题]面试官问:for in和for of 的区别和原理?
关注「松宝写代码」,精选好文,每日一题 时间永远是自己的 每分每秒也都是为自己的将来铺垫和增值 作者:saucxs | songEagle 一.前言 2020.12.23 日刚立的 flag,每日一 ...
- 「每日一题」有人上次在dy面试,面试官问我:vue数据绑定的实现原理。你说我该如何回答?
关注「松宝写代码」,精选好文,每日一题 时间永远是自己的 每分每秒也都是为自己的将来铺垫和增值 作者:saucxs | songEagle 来源:原创 一.前言 文章首发在「松宝写代码」 2020. ...
- 面试官: 聊一聊Babel
点击关注本公众号获取文档最新更新,并可以领取配套于本指南的 <前端面试手册> 以及最标准的简历模板. 前言 Babel 是现代 JavaScript 语法转换器,几乎在任何现代前端项目中都 ...
- 关键词:ACM & 大小端 & 面试官
关于“ACM” fender0107401 :面试了一个在ACM拿过奖的人 我问了他几个问题: 读取数组中的一个元素,计算复杂度是多少,回答不清楚. 往链表里面存一个数,不排序的情况下,计算复杂度是多 ...
- Android开发面试经——6.常见面试官提问Android题②(更新中...)
版权声明:本文为寻梦-finddreams原创文章,请关注:http://blog.csdn.net/finddreams 关注finddreams博客:http://blog.csdn.net/fi ...
随机推荐
- 安装mysql.so
1.---- cd /usr/local/src/php-5.5.34/ext/mysql/2.---- /usr/local/php5/bin/phpize3.---- ./configure ...
- java面试汇总一
第一部分 Java SE基础(1) 1.1 java的8种基本数据类型 装箱 拆箱 1.1.1 8种基本的数据类型 1.1.2装箱 拆箱 自动装箱是 Java 编译器在基本数据类型和对应的对象包 ...
- 基于Redis未授权访问的挖矿蠕虫分析
0x01 攻击方式 利用的是通用漏洞入侵服务器并获得相关权限,从而植入挖矿程序再进行隐藏. 通过对脚本的分析,发现黑客主要是利用 Redis未授权访问漏洞进行入侵.脚本里有个python函数. imp ...
- 数据挖掘算法——K-means算法
k-means中文称为K均值聚类算法,在1967年就被提出 所谓聚类就是将物理或者抽象对象的集合分组成为由类似的对象组成的多个簇的过程 聚类生成的组成为簇 簇内部任意两个对象之间具有较高的相似度,不 ...
- 关于IDEA的使用中,连接数据库console出现乱码的情况
本人在连接数据库时也不算是出现乱码,因为乱码的解决方式百度都有,但是还是没有解决我当时遇到的问题, 首先排除navicat的问题,连接选择UTF-8的编码格式, 在Idea中使用debug调试,均可以 ...
- Openstack计算Nova组件
欢迎来到虚拟机的世界,如果我们将Openstack环境里运行在各个无力节点上的各种服务看座生命体,而不是死的指令集合,那么就是一个虚拟机的世界. Openstack的计算组件,也就是Nova项目实现了 ...
- main.c(53): error: #268: declaration may not appear after executable statement in block
这个问题是在编译STM32的程序时遇到的,这个错误的原因是对于变量的声明不能放在可执行语句后面,必须在主函数开头声明变量.在程序中声明一个变量时,需要在可执行语句之前声明,否则会出现以上错误.
- Web_XML
第1章 XML简介 “当 XML(扩展标记语言)于 1998 年 2 月被引入软件工业界时,它给整个行业带来了一场风暴.有史以来第一次,这个世界拥有了一种用来结构化文档和数据的通用且适应性强的格式,它 ...
- python下使用sort()函数对目录下文件名进行多条件排序
目录 1.基础函数 2.例子解析 参考 1.基础函数 a.sort()函数 sort()函数的作用是对列表内容进行正向排序,直接在原列表进行修改,返回的是修改后的列表. lists =[1, 5, 1 ...
- Redis缓存设计与性能优化
Redis我们一般是用作缓存,扛并发:或者用于某些特定的业务场景,比如前面说到redis各种数据类型的使用场景以及redis的哨兵和集群模式. 这里主要整理了下redis用作缓存,存在的一些问题,以及 ...