Gadzan

uni-app开发小程序onLoad的options参数在debug时undefined的问题

昨天同事在调试小程序生命周期onLoadoptions参数时, 在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后回车查看它, 你会发现xundefined.

因为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跑完以后消失在堆内存里了(闭包没有引用它).


参考:

  1. javascript内存管理(堆和栈)和javascript运行机制
  2. 从ES6生成器(Generator)原理解读,到理解ES7的asyn...await...
  3. Why does Chrome debugger think closed local variable is undefined?

打赏码

知识共享许可协议 本作品采用知识共享署名 4.0 国际许可协议进行许可。

评论