公司决定项目开发全部转向Cocos Creator 作为学习开发了15个月Layabox的自然而然要切换引擎,带来的是一些新的挑战(入坑)和学习(填坑)的机会,我重新打开之前的学习整理的Cocos的资料,牛人博客和论坛上大家写的教程和讨论的话题,虽然这些学习不难,但时间紧,任务重,为了快速进入状态,还是有些迫切的,读到官方文档,关于循环引用和属性延迟这个小节,当时有些疑惑,查阅了一些资料,觉得值得记录一下。

Cocos 官方是这样解释和给出方法

属性延迟定义

如果两个类相互引用,脚本加载阶段就会出现循环引用,循环引用将导致脚本加载出错:

  • Game.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var Item = require("Item");

    var Game = cc.Class({
    properties: {
    item: {
    default: null,
    type: Item
    }
    }
    });

    module.exports = Game;
  • Item.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var Game = require("Game");

    var Item = cc.Class({
    properties: {
    game: {
    default: null,
    type: Game
    }
    }
    });

    module.exports = Item;

上面两个脚本加载时,由于它们在 require 的过程中形成了闭环,因此加载会出现循环引用的错误,循环引用时 type 就会变为 undefined。
因此我们提倡使用以下的属性定义方式:

  • Game.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var Game = cc.Class({
    properties: () => ({
    item: {
    default: null,
    type: require("Item")
    }
    })
    });

    module.exports = Game;
  • Item.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var Item = cc.Class({
    properties: () => ({
    game: {
    default: null,
    type: require("Game")
    }
    })
    });

    module.exports = Item;

这种方式就是将 properties 指定为一个 ES6 的箭头函数(lambda 表达式),箭头函数的内容在脚本加载过程中并不会同步执行,而是会被 CCClass 以异步的形式在所有脚本加载成功后才调用。因此加载过程中并不会出现循环引用,属性都可以正常初始化。

箭头函数的用法符合 JavaScript 的 ES6 标准,并且 Creator 会自动将 ES6 转义为 ES5,用户不用担心浏览器的兼容问题。

你可以这样来理解箭头函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 箭头函数支持省略掉 `return` 语句,我们推荐的是这种省略后的写法:

properties: () => ({ // <- 箭头右边的括号 "(" 不可省略
game: {
default: null,
type: require("Game")
}
})

// 如果要完整写出 `return`,那么上面的写法等价于:

properties: () => {
return {
game: {
default: null,
type: require("Game")
}
}; // <- 这里 return 的内容,就是原先箭头右边括号里的部分
}

// 我们也可以不用箭头函数,而是用普通的匿名函数:

properties: function () {
return {
game: {
default: null,
type: require("Game")
}
};
}

我自己用的是typescript项目 查阅得到的两个答案:

查阅结果

ts版本下的表现形式代码部分(无报错)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// A.ts
import { B } from "./B";
const { ccclass, property } = cc._decorator;
@ccclass
export class A extends cc.Component {
@property(B)
x: B = null
}

// B.ts
import { A } from "./A";
const { ccclass, property } = cc._decorator;
@ccclass
export class B extends cc.Component {
@property(A)
x: A = null
}

特别说明:

在ts中,import语句是没有循环引用问题的,因为import输出的是一个值的引用,只有在具体使用时才会去取值,在这篇文章中有更具体的描述http://es6.ruanyifeng.com/#docs/module-loader#ES6-模块与-CommonJS-模块的差异。

但是通过@property()定义的属性,是直接通过编辑器初始化的,相当于直接使用了,则会造成循环引用的问题。

解决方案

  1. 设计好的分层,避免循环引用(老项目要重构许久)

  2. 传入cc.Node,在onLoad()中实现属性的延迟定义
    这个方法就比较简单了,逻辑也比较顺畅,很适合新手去使用,实现方案如下所示:

  3. import { A } from "./A";
    const { ccclass, property } = cc._decorator;
    @ccclass
    export class B extends cc.Component {
    
        @property(cc.Node)
        x: A = null
    
        onLoad() {
            this.x = this.x.getComponent(A)
        }
    }
  4. 通过箭头函数实现属性的延迟定义(和cocos 官方不谋而合)

后记

写代码尽量保持单向引用文件,如果不能引用的可以通过拆分重新设计结构,其实,后面的方法都是亡羊补牢,真正好的方法是避免灾难的发生。

另外关于阮一峰博客的文章由于时间关系没有读太细,后续我会再认真读一下,把理解写出来。

参考链接