昨天同事在调试小程序生命周期onLoad
的options
参数时, 在DevTools看到的options
参数是undefined
.
async onLoad (options) {
// console.log(options);
debugger
},
而如果把onLoad
前的async
去掉, 则一切正常, 或者onLoad函数内加上console.log(options)
输出的options
是没问题的, 这个bug跟我之前看过的一篇文章在详述DevTools中inspect的原理所提出的问题很像.
这个文章的问题这要从DevTools中的一个bug说起, 先看代码:
function baz() {
const x = {a: "foo"};
function bar() {
debugger
};
bar();
}
baz();
运行后, 程序会在debugger
上停止等待调试, 然后在console中输入x
后回车查看它, 你会发现x
是undefined
.
因为bar
没有引用x
, 所以没有形成闭包. 一旦JavaScirpt的函数运行完毕便被垃圾管理清理了. v8引擎的机制是这样的, 它可以在栈上存储函数的local scope的变量或者在堆上存储一个context
对象.
如果对js的堆栈和闭包不熟悉可以参考下面的解析:
heap(堆)是没有结构的, 数据可以任意存放. heap用于复杂数据类型(引用类型)分配空间, 例如数组对象, object对象.
stack(栈)是有结构的, 每个区块按照一定次序存放(后进先出), stack中主要存放一些基本类型的变量和对象的引用, 存在栈中的数据大小与生存期必须是确定的. 可以明确知道每个区块的大小, 因此, stack的寻址速度要快于heap.
程序运行时, 每个线程分配一个stack, 每个进程分配一个heap, 也就是说, stack是线程独占的, heap是线程共用的. 此外, stack创建的时候, 大小是确定的, 数据超过这个大小, 就发生stack overflow错误, 而heap的大小是不确定的, 需要的话可以不断增加.
闭包: 如果函数B引用了函数A里的变量, 而且函数B也是在函数A里, 函数B就是闭包.
v8引擎有一个优化, 只要函数内没有闭包, v8就可以直接在栈上定位local scope的变量. 而如果存在任何闭包, 闭包里引用的变量就会被放到context
对象里(也就是被放到了堆里而不是栈里).
闭包里引用的变量为什么要放到堆里呢?
你可以让内函数从外函数里返回出来, 外函数运行完毕后便会被垃圾清理, 但内函数(闭包)所引用的变量必须还能被访问到, 所以闭包引用的变量必须放到堆内存中以便以后运行引用.
问题是DevTools的debugger
不能inspect那些放在栈内存中的变量, 所以导致那些不是闭包的变量无法被访问.
那么上边例子的情况就很好解决了, 只要在bar
函数里引用x
使之成为闭包放到堆内存中, 在调试时便可以在console中inspect到了.
function baz() {
const x = {a: "foo"};
function bar() {
console.log(x)
debugger
};
bar();
}
baz();
回到原来的话题, 这是不是跟我们遇到问题非常像, 但又有点不同, 因为如果把onLoad
前的async
去掉也是没问题的. 那么可以肯定的是, async
不知为何触发了把整个onLoad
从堆内存去掉的情况.
我所想到的只有一个可能, 那就是babel把async
转译了, 我打开了转译后的源码(不用细看, 后面有简化):
// ...
var _regenerator = _interopRequireDefault(__webpack_require__(/*! ./node_modules/@babel/runtime/regenerator */ 14));
// ...
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// ...
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); }); }; }
var _default = {
onLoad: function () {
var _onLoad = _asyncToGenerator(
/*#__PURE__*/
_regenerator.default.mark(function _callee(options) {
return _regenerator.default.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
// console.log(options);
debugger;
case 1:
case "end":
return _context.stop();
}
}
}, _callee);
}));
function onLoad(_x) {
return _onLoad.apply(this, arguments);
}
return onLoad;
}(),
// ...
};
exports.default = _default;
// ...
简化一下:
var _default = {
onload: function() {
var _onLoad = _regenerator_mark(function _callee(options) {
return _regenerator_wrap(function _callee$(_context){
debugger
})
})
function onLoad(_x) {
return _onLoad.apply(this, arguments);
}
return onLoad;
}()
}
再简化一下:
function fake_onload() {
// 假设options在这里
var options = {};
var _onLoad = function (){
debugger
return {};
}
function onLoad(_x) {
return _onLoad.apply(this, arguments);
}
return onLoad;
}
var _default = {
onload: fake_onload()
}
_default.onload()
运行调试一下看看
破案了.
虽然fake_onload
是闭包, 但是它的变量_onLoad
没有引用options
, 所以实质上options
已经在fake_onload
跑完以后消失在堆内存里了(闭包没有引用它).
参考: