-
Creator 版本:2.0.9
-
目标平台: N/A 编译后都可以正常运行,仅仅是场景编辑时报错
-
详细报错信息,包含调用堆栈:
-
重现方式:循环依赖就可以重现,我会提供demo和详细的代码错误
-
出现概率:100%
现象:
在层级管理器中点击特定节点(我提供的Demo中就是GameNode节点),控制台会报错:
getComponent: Type must be non-nil
at Object.cc.errorID (D:\Program Files (x86)\CocosCreator_2.0.9\resources\engine\bin\.cache\dev\cocos2d\core\CCDebug.js:167:16)
at getConstructor (D:\Program Files (x86)\CocosCreator_2.0.9\resources\engine\bin\.cache\dev\cocos2d\core\utils\base-node.js:29:20)
at cc_Node.getComponent (D:\Program Files (x86)\CocosCreator_2.0.9\resources\engine\bin\.cache\dev\cocos2d\core\utils\base-node.js:399:35)
at HTMLElement.query-node-info (D:\Program Files (x86)\CocosCreator_2.0.9\resources\app.asar\editor\builtin\scene\panel\messages\scene-query.js:1:1331)
at Object.e._dispatch (D:\Program Files (x86)\CocosCreator_2.0.9\resources\app.asar\editor-framework\lib\renderer\panel.js:1:1941)
at EventEmitter.o.on.s (D:\Program Files (x86)\CocosCreator_2.0.9\resources\app.asar\editor-framework\lib\renderer\ipc.js:1:2917)
at emitMany (events.js:127:13)
at EventEmitter.emit (events.js:204:7)
在我的样例中,报错的原因是:
IndexController类有一个类型为GameNode的属性。
同时GameNode类有一个IndexController的属性。
通过关联绑定以后,产生了循环依赖。类似于Spring的依赖注入,属性的依赖注入是不会产生错误的,所以编译以后是可以正常运行的,但是Creator的编辑器就报错了。
这个错误导致:
1、添加的用户脚本如果有循环依赖那么该属性就不能关联节点。界面显示该属性为:Null ---- Create,点击Create按钮没反应。
2、一旦我通过GameNode的属性检查器删除了其IndexController属性的关联,那么GameNode的IndexController属性就会变成:Null ---- Create,点击Create按钮没有任何反应。
以下是我使用开发人员工具跟的CocosCreator源码,便于开发人员修复这个问题:
通过electron提供的Chrome开发人员工具,我检查了这个有问题的属性:
<ui-node class="flex-1" type="undefined" typename="IndexController" droppable="node" tabindex="-1"></ui-node>
发现其type是undefined
然后通过跟踪vue的源码。。找到pushWatcher。。。找到dumpNode。。。找到__attr__属性中IndexController的没有任何default和type的信息(IndexController$_$type=null)(在attribute.js中的setClassAttr方法:proto[propName + DELIMETER + key] = value;):
在
BugTest\temp\quick-scripts\assets\Script\Node\GameNode.js的15行打断点:
var IndexController_1 = require("../Controller/IndexController");
发现返回值IndexController_1的的值为:{__esModule: true}
而正常情况下应该返回:{__esModule: true, default: function},其中default应该是IndexController类的构造方法。
通过分析require内部的核心代码:在module.js的413行的Module._load方法(省略部分代码):
// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call `NativeModule.require()` with the
// filename and return the result.
// 3. Otherwise, create a new module for the file and save it to the cache.
// Then have it load the file contents before returning its exports
// object.
Module._load = function(request, parent, isMain) {
......
var filename = Module._resolveFilename(request, parent, isMain);
var cachedModule = Module._cache[filename]; // ①缓存中读取
if (cachedModule) {
return cachedModule.exports;
}
......
var module = new Module(filename, parent);
......
Module._cache[filename] = module; // ⑤⑤⑤⑤⑤⑤⑤
tryModuleLoad(module, filename); //②加载该模块
return module.exports;
};
实际的流程是这样:
相关代码为:
BugTest\temp\quick-scripts\assets\Script\Node\GameNode.js
BugTest\temp\quick-scripts\assets\Script\Controller\IndexController.js
为了便于说明,下面描述以文件名就是上面的绝对路径对应的文件
1、系统初始化,调用app.asar\editor\page\project-scripts.js中的loadCommon方法加载所有\temp\quick-scripts\assets\下的js文件(加载的顺序可能会不一样,我这边是先IndexController再加载GameNode)
2、系统使用Module._compile(IndexController中的代码, IndexController.js路径)对IndexController中的代码进行编译
3、调用IndexController.js内部的
__define(__module.exports, __require, __module);
// !!!!!!方法中的部分代码
"use strict";
cc._RF.push(module, '1cc6bz/GJtB1qeEg6Qt50cB', 'IndexController', __filename);
// Script/Controller/IndexController.ts
Object.defineProperty(exports, "__esModule", {
value: true
});
// 下面这部分就是根据ts文件的import、@ccclass、@property等注解自动转换的代码。
var GameNode_1 = require("../Node/GameNode"); //③③③③③③③③③③③③③③③
var _a = cc._decorator,
ccclass = _a.ccclass,
property = _a.property;
var IndexController = /** @class */(function (_super) {
__extends(IndexController, _super);
function IndexController() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.gameNode = null;
return _this;
// update (dt) {}
}
// LIFE-CYCLE CALLBACKS:
IndexController.prototype.onLoad = function () {};
IndexController.prototype.start = function () {};
__decorate([
property(GameNode_1.default)
], IndexController.prototype, "gameNode", void 0);
IndexController = __decorate([
ccclass
], IndexController);
return IndexController;
}(cc.Component));
exports.default = IndexController; // ⑥⑥⑥⑥⑥
cc._RF.pop();
4、当执行到上面③③③③的地方,又会去加载GameNode.js
5、在require(‘GameNode.js’)的内部,会调用Module._compile(GameNode中的代码, GameNode.js路径)对GameNode.js中的代码进行编译
6、同样会执行到GameNode.js中的__define()方法(差异点就是其中④④④处的代码):
"use strict";
cc._RF.push(module, 'e00f6YQPKZJAbzKscQyGhyI', 'GameNode', __filename);
// Script/Node/GameNode.ts
Object.defineProperty(exports, "__esModule", {
value: true
});
var IndexController_1 = require("../Controller/IndexController"); // ④④④④④④④④
var _a = cc._decorator
, ccclass = _a.ccclass
, property = _a.property;
var GameNode = /** @class */
(function(_super) {
__extends(GameNode, _super);
function GameNode() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.indexController = null;
return _this;
// update (dt) {}
}
// LIFE-CYCLE CALLBACKS:
GameNode.prototype.onLoad = function() {}
;
GameNode.prototype.start = function() {}
;
__decorate([property(IndexController_1.default)], GameNode.prototype, "indexController", void 0); //⑦⑦⑦⑦⑦
GameNode = __decorate([ccclass], GameNode);
return GameNode;
}(cc.Component));
exports.default = GameNode;
cc._RF.pop();
由于GameNode又依赖了IndexController,因此在④处我们可以看到又require(IndexController)了。
7、④处的代码内部会调用到①处的Module._load(IndexController)方法,从缓存中获取cachedModule,此时IndexController的cachedModule已经存在了,因为Module._load()中⑤⑤⑤⑤是先将module放进了缓存再加载的,开发者其实已经很小心了,因为如果不这么写,循环依赖就直接死递归了。但是这样的结果就导致了④④④处获取的是尚未加载完毕的IndexController,因为IndexController的__define()方法必须执行⑥⑥⑥,也就是exports.default = IndexController;赋值以后才算执行完毕!!!!!!!
8、④④④的IndexController_1 拿到的是没有加载完毕的IndexController对象,因此⑦⑦⑦处使用IndexController_1.default获取到的就是null,
然后导致最终编辑器中拿到的类型就是undefined。
虽然有些人肯定会说,你不要循环依赖啊,确实,去掉循环依赖确实可以解决这个问题,但是循环依赖的场景真的很多啊,就好像我们开发代码,类A引用类B,类B又引用了类A,只要不是构造函数相互依赖:只是属性相互依赖肯定不会有问题啊,比如Spring的依赖注入就允许有依赖。原理和cocos_creator最终的运行时实现一样(虽然编辑器报错了,但是代编译出来的代码最终执行的时候是肯定OK的):先创建对象,然后再依赖注入。而不是根据import去递归加载。
cocos的开发人员也可以考虑使用这一思路去修改,想办法让import部分的require代码延迟执行,即:先把一个IndexController加载完毕exports.default = IndexController; // ⑥⑥⑥⑥⑥,然后再去
var GameNode_1 = require("../Node/GameNode");,
进而
_decorate(
property(GameNode_1.default)
], IndexController.prototype, "gameNode", void 0)
是否就可以修复这个问题????
这样还可以解决空import的资源浪费问题,即:虽然ts代码import了其它类,但是代码中实际并没有用到,那么就不应该去加载import的类,代码中还是会经常出现这种情况的,Java的import也是这么处理的,即:只有当类真正被用到的时候才会去加载这个类,如果仅仅是import,即使是import的不存在的类,运行是也不会报错(编译时会)。
BugTest.zip (8.2 KB)

川子 你好强大