前言
本文基於 Cocos Creator 2.4.5 撰寫。
🎉 普天同慶
來了來了,《源碼解讀》系列文章終於又來了!
👾 溫馨提醒
本文包含大段引擎源碼,使用大屏設備閱讀體驗更佳!
Hi There!
節點(cc.Node)作為 Cocos Creator 引擎中最基本的單位,所有組件都需要依附在節點上。
同時節點也是我們日常開發中接觸最頻繁的東西。
我們經常會需要「改變節點的排序」來完成一些效果(如圖像的遮擋)。
A Question?
😕 你有沒有想過:
節點的排序是如何實現的?
Oops!
🤯 我在分析了源碼后發現:
節點的排序並沒有想象中那么簡單!
😹 渣皮語錄
聽皮皮一句勸,zIndex 的水太深,你把握不住!
正文
節點順序 (Node Order)
🤔 如何修改節點的順序?
首先,在 Cocos Creator 編輯器中的「層級管理器」中,我們可以隨意拖動節點來改變節點的順序。

🤨 但是,在代碼中我們要怎么做呢?
我最先想到的是節點的 setSiblingIndex 函數,然后是節點的 zIndex 屬性。
我猜大多數人都不清楚這兩個方案有什么區別。
那么接下來就讓我們深入源碼,一探究竟!
siblingIndex
「siblingIndex」即「同級索引」,意為「同一父節點下的兄弟節點間的位置」。
siblingIndex 越小的節點排越前,索引最小值為 0,也就是第一個節點的索引值。
需要注意的是,實際上節點並沒有 siblingIndex 屬性,只有 getSiblingIndex 和 setSiblingIndex 這兩個相關函數。
注:本文統一使用 siblingIndex 來代指 getSiblingIndex 和 setSiblingIndex 函數。
另外,getSiblingIndex 和 setSiblingIndex 函數是由 cc._BaseNode 實現的。
💡 cc._BaseNode
大家對這個類可能會比較陌生,簡單來說
cc._BaseNode是cc.Node的基類。此類「定義了節點的基礎屬性和函數」,包括但不僅限於
setParent、addChild和getComponent等常用函數...
📝 源碼節選:
函數:cc._BaseNode.prototype.getSiblingIndex
getSiblingIndex() {
if (this._parent) {
return this._parent._children.indexOf(this);
} else {
return 0;
}
},
函數:cc._BaseNode.prototype.setSiblingIndex
setSiblingIndex(index) {
if (!this._parent) {
return;
}
if (this._parent._objFlags & Deactivating) {
return;
}
var siblings = this._parent._children;
index = index !== -1 ? index : siblings.length - 1;
var oldIndex = siblings.indexOf(this);
if (index !== oldIndex) {
siblings.splice(oldIndex, 1);
if (index < siblings.length) {
siblings.splice(index, 0, this);
} else {
siblings.push(this);
}
this._onSiblingIndexChanged && this._onSiblingIndexChanged(index);
}
},
[源碼] base-node.js#L514: https://github.com/cocos-creator/engine/blob/2.4.5/cocos2d/core/utils/base-node.js#L514
🕵️ 做了什么?
扒拉源碼后發現,siblingIndex 的本質其實很簡單。
那就是「當前節點在父節點的 _children 屬性中的下標(位置)」。
getSiblingIndex 函數返回的是「當前節點在父節點的 _children 屬性中的下標(位置)」。
setSiblingIndex 函數則是設置「當前節點在父節點的 _children 屬性中的下標(位置)」。
💡
cc._BaseNode.prototype._children節點的
_children屬性其實就是節點的children屬性。而
children屬性是一個getter,返回的是自身的_children屬性。另外
children屬性沒有實現setter,所以你直接給children屬性賦值是無效的。
zIndex
「zIndex」是「用來對節點進行排序的關鍵屬性」,它決定了一個節點在兄弟節點之間的位置。
zIndex 的值介於 cc.macro.MIN_ZINDEX 和 cc.macro.MAX_ZINDEX 之間。
另外,zIndex 屬性是在 cc.Node 內使用 Cocos 定制版 getter 和 setter 實現的。
📝 源碼節選:
屬性: cc.Node.prototype.zIndex
// 為了減少篇幅,已省略部分不相關代碼
zIndex: {
get() {
return this._localZOrder >> 16;
},
set(value) {
if (value > macro.MAX_ZINDEX) {
value = macro.MAX_ZINDEX;
} else if (value < macro.MIN_ZINDEX) {
value = macro.MIN_ZINDEX;
}
if (this.zIndex !== value) {
this._localZOrder = (this._localZOrder & 0x0000ffff) | (value << 16);
this.emit(EventType.SIBLING_ORDER_CHANGED);
this._onSiblingIndexChanged();
}
}
},
[源碼] CCNode.js#L1549: https://github.com/cocos-creator/engine/blob/2.4.5/cocos2d/core/CCNode.js#L1549
🕵️ 做了什么?
扒拉源碼后發現,zIndex 的本質其實也很簡單。
那就是「返回或設置節點的 _localZOrder 屬性」。
🧐 沒那么簡單!
有趣的是,在 getter 中並沒有直接返回 _localZOrder 屬性,而是返回了 _localZOrder 屬性右移(>>)16 位后的數值。
在 setter 中設置 _localZOrder 屬性時也並非簡單的賦值,又是進行了一頓位操作:
這里我們以二進制數的視角來分解該函數內的位操作。
- 通過
& 0x0000ffff取出原_localZOrder的「低 16 位」; - 將目標值
value「左移 16 位」; - 將左移后的
value作為「高 16 位」與原_localZOrder的「低 16 位」合並; - 最后得到一個「32 位的二進制數」並賦予
_localZOrder。
😲 嗯?
慢着!
_localZOrder又是干啥用的?咋這么繞!別急,答案在后面~
排序 (Sorting)
細心的朋友應該發現了,siblingIndex 和 zIndex 的源碼中都沒有包含實際的排序邏輯。
但是它們都有一個共同點:「最后都調用了自身的 _onSiblingIndexChanged 函數」。
_onSiblingIndexChanged
📝 源碼節選:
函數:cc.Node.prototype._onSiblingIndexChanged
_onSiblingIndexChanged() {
if (this._parent) {
this._parent._delaySort();
}
},
🕵️ 做了什么?
而 _onSiblingIndexChanged 函數內則是調用了「父節點」的 _delaySort 函數。
_delaySort
📝 源碼節選:
函數:cc.Node.prototype._delaySort
_delaySort() {
if (!this._reorderChildDirty) {
this._reorderChildDirty = true;
cc.director.__fastOn(cc.Director.EVENT_AFTER_UPDATE, this.sortAllChildren, this);
}
},
🕵️ 做了什么?
一頓操作順藤摸瓜后發現,真正進行排序的地方是「父節點」的 sortAllChildren 函數。
💡 盲生,你發現了華點!
值得注意的是,
_delaySort函數中的sortAllChildren函數調用不是立即觸發的,而是會在下一次update(生命周期)后觸發。延遲觸發的目的應該是為了避免在同一幀內的重復調用,從而減少不必要的性能損耗。
sortAllChildren
📝 源碼節選:
函數:cc.Node.prototype.sortAllChildren
// 為了減少篇幅,已省略部分不相關代碼
sortAllChildren() {
if (this._reorderChildDirty) {
this._reorderChildDirty = false;
// Part 1
var _children = this._children, child;
this._childArrivalOrder = 1;
for (let i = 0, len = _children.length; i < len; i++) {
child = _children[i];
child._updateOrderOfArrival();
}
eventManager._setDirtyForNode(this);
// Part 2
if (_children.length > 1) {
let child, child2;
for (let i = 1, count = _children.length; i < count; i++) {
child = _children[i];
let j = i;
for (;
j > 0 && (child2 = _children[j - 1])._localZOrder > child._localZOrder;
j--
) {
_children[j] = child2;
}
_children[j] = child;
}
this.emit(EventType.CHILD_REORDER, this);
}
cc.director.__fastOff(cc.Director.EVENT_AFTER_UPDATE, this.sortAllChildren, this);
}
},
[源碼] CCNode.js#L3680: https://github.com/cocos-creator/engine/blob/2.4.5/cocos2d/core/CCNode.js#L3680
上半部分 (Part 1)
隨着一步步深入,我們終於來到了關鍵部分。
現在讓我們琢磨琢磨這個 sortAllChildren 函數。
進入該函數的前半段,映入眼簾的是一行賦值語句,將 _childArrivalOrder 屬性設(重置)為 1;
緊跟其后的是一個 for 循環,遍歷了當前節點的所有「子節點」,並一一執行「子節點」的 _updateOrderOfArrival 函數。
🤨 嗯?這個 _updateOrderOfArrival 函數又是何方神聖?
_updateOrderOfArrival
📝 源碼節選:
函數:cc.Node.prototype._updateOrderOfArrival
_updateOrderOfArrival() {
var arrivalOrder = this._parent ? ++this._parent._childArrivalOrder : 0;
this._localZOrder = (this._localZOrder & 0xffff0000) | arrivalOrder;
this.emit(EventType.SIBLING_ORDER_CHANGED);
},
🕵️ 做了什么?
顯而易見的是,_updateOrderOfArrival 函數的作用就是「更新節點的 _localZOrder 屬性」。
🥱 該函數中同樣也使用了位操作:
同上,以二進制數的視角來進行分解這里的位操作。
- 將父節點的
_childArrivalOrder(前置)自增1,並賦予arrivalOrder(如無父節點則為0); - 通過
& 0xffff0000取出當前節點的_localZOrder的「高 16 位」; - 將
arrivalOrder作為「低 16 位」與當前節點的_localZOrder的「高 16 位」合並; - 最后得到一個新的「32 位的二進制數」並賦予當前節點的
_localZOrder屬性。
🤔 看到這里你是不是已經開始迷惑了?
別擔心,答案即將揭曉!
下半部分 (Part 2)
而 sortAllChildren 函數的下半部分就比較好理解了。
基本就是通過「插入排序(Insertion Sort)」來「排序當前節點的 _children 屬性(子節點數組)」。
其中主要根據子節點的 _localZOrder 屬性的值來進行排序,_localZOrder 屬性值小的子節點排前面,反之排后面。
排序的關鍵 (Key of sorting)
🤔 分析完源碼后發現,節點的排序並沒有想象中那么簡單。
我們可以先得出幾個結論:
- siblingIndex 是節點在父節點的
children屬性中的下標; zIndex是一個獨立的屬性,和 siblingIndex 沒有直接聯系;- siblingIndex 和
zIndex的改變都會觸發排序; - siblingIndex 和
zIndex共同組成了節點的_localZOrder; zIndex的權重比 siblingIndex 大;- 節點的
_localZOrder直接決定了節點的最終順序。
siblingIndex 如何影響排序 (How siblingIndex affects sorting)
我們前面有提到:
getSiblingIndex函數「返回了當前節點在父節點的_children屬性中的下標(位置)」。setSiblingIndex函數「設置了當前節點在父節點的_children屬性中的下標(位置),並通知父節點進行排序」。
隨后在父節點的 sortAllChildren 函數中的上半部分,會以這個下標作為節點 _localZOrder 的低 16 位。
🧐 所以我們可以這樣理解:
siblingIndex 是元素下標,在排序過程中,其決定了 _localZOrder 的「低 16 位」。
zIndex 如何影響排序 (How zIndex affects sorting)
我們前面有提到:
zIndex的getter「返回了_localZOrder的高 16 位」。zIndex的setter「設置了_localZOrder的高 16 位,並通知父節點進行排序」。
🧐 所以我們可以這樣理解:
zIndex 實際上只是一個軀殼,其本質是 _localZOrder 的「高 16 位」。
_localZOrder 如何決定順序 (How _localZOrder works)
父節點的 sortAllChildren 函數中根據子節點的 _localZOrder 大小來進行最終排序。
我們可以將 _localZOrder 看做一個「32 位二進制數」,其由 siblingIndex 和 zIndex 共同組成。
但是,為什么說「zIndex 的權重比 siblingIndex 大」呢?
因為 zIndex 決定了 _localZOrder 的「高 16 位」,而 siblingIndex 決定了 _localZOrder 的「低 16 位」。
所以,只有在 zIndex 相等的情況下,siblingIndex 的大小才有決定性意義。
而在 zIndex 不相等的情況下,siblingIndex 的大小就無所謂了。
🌰 舉個栗子
這里有兩個 32 位二進制數(偽代碼):
- A:
0000 0000 0000 0001 xxxx xxxx xxxx xxxx- B:
0000 0000 0000 0010 xxxx xxxx xxxx xxxx由於 B 的「高 16 位」(
0000 0000 0000 0010)比 A 的「高 16 位」(0000 0000 0000 0001)大,所以無論他們的「低 16 位」中的x是什么,B 都會永遠大於 A。
實驗一下 (Experiment)
我們可以寫個小組件來測試下 siblingIndex 和 zIndex 對於 _localZOrder 的影響。
📝 一頓打碼:
const { ccclass, property, executeInEditMode } = cc._decorator;
@ccclass
@executeInEditMode
export default class Test_NodeOrder extends cc.Component {
@property({ displayName: 'siblingIndex' })
get siblingIndex() {
return this.node.getSiblingIndex();
}
set siblingIndex(value) {
this.node.setSiblingIndex(value);
}
@property({ displayName: 'zIndex' })
get zIndex() {
return this.node.zIndex;
}
set zIndex(value) {
this.node.zIndex = value;
}
@property({ displayName: '_localZOrder' })
get localZOrder() {
return this.node._localZOrder;
}
@property({ displayName: '_localZOrder (二進制)' })
get localZOrderBinary() {
return this.node._localZOrder.toString(2).padStart(32, 0);
}
}
場景一 (Scene 1)
在 1 個節點下放置了 1 個子節點。
🖼 子節點的排序信息:

一般來說,由於節點的 _childArrivalOrder 是從 1 開始的,並且在計算時會先自增 1。
所以子節點的 _localZOrder 的「低 16 位」總會比其 siblingIndex 大 2 個數。
場景二 (Scene 2)
在 1 個節點下放置了 1 個子節點,並將子節點的 zIndex 設為 1。
🖼 子節點的排序信息:

可以看到,僅僅將節點的 zIndex 屬性設為 1,其 _localZOrder 就高達 65538。
🔠 大概的計算過程如下(極為抽象的偽代碼):
1. zIndex = 1 = 0b0000000000000001
2. siblingIndex = 0
3. arrivalOrder = 1 + (siblingIndex + 1)
4. arrivalOrder = 0b0000000000000010
5. _localZOrder = (zIndex << 16) | arrivalOrder
6. _localZOrder = 0b00000000000000010000000000000000 | 0b0000000000000010
7. _localZOrder = 0b00000000000000010000000000000010 = 65538
📝 繼續簡化后的偽代碼:
_localZOrder = (zIndex << 16) | (siblingIndex + 2)
💡 By the way
當一個節點沒有父節點時,它的
arrivalOrder永遠是0。其實此時它是啥已經不重要了,畢竟沒有父節點的節點本來就不可能會被排序。
場景三 (Scene 3)
在同 1 個節點下放置了 6 個子節點,將所有子節點的 zIndex 都設為 0。
🎥 各個子節點的排序信息:

場景四 (Scene 4)
在同 1 個節點下放置了 6 個子節點,將這 6 個子節點的 zIndex 設為 0 到 5。
🎥 各個子節點的排序信息:

可以看到,zIndex 的值會直接體現在 _localZOrder 的「高 16 位」;每當 zIndex 增加 1,_localZOrder 就會增加 65537。
所以說 siblingIndex 怎么可能打得過 zIndex!
場景五 (Scene 5)
在同 1 個節點下放置了 6 個子節點,將這 6 個子節點的 zIndex 設為 0 到 5。
🎥 修改第 6 個子節點的 siblingIndex 從 0 到 4,其排序信息:

可以看到,此時無論我們怎么修改第 6 個子節點的 siblingIndex,它都會自動變回 5(也就是同級節點中的最大值)。
因為這個子節點的 zIndex 在其同級節點之中有着絕對的優勢。
不太對勁 (Something wrong)
😲 這里有一個看起來不太對勁的現象!
比如,當我們把 siblingIndex 從 5 修改為 0 時,_localZOrder 也相應從 327687 變成 327682;但是當 siblingIndex 自動變回 5 時,_localZOrder 也還是 327682,並沒有變回 327687。
🤔 為什么會這樣?
原因其實很簡單:
當我們修改節點的 siblingIndex 時會觸發排序,排序過程中會「根據節點當前時刻的 siblingIndex 和 zIndex 生成新的 _localZOrder」;
最后在父節點的 sortAllChildren 函數中會根據子節點的 _localZOrder 來對 _children 數組進行排序,此時「子節點的 siblingIndex 也會被動更新」,「但是 _localZOrder 卻沒有重新生成」。
但是,由於 zIndex 存在「絕對優勢」,這種“奇怪的現象”其實並不會影響到節點的正常排序~
總結 (Summary)
分析完源碼后,我們來總結一下。
在代碼中修改節點順序的方法主要有兩種:
- 修改節點的
zIndex屬性 - 通過
setSiblingIndex函數設置
無論使用以上哪種方法,最終都會「通過 zIndex 和 siblingIndex 的組合作為依據來進行排序」。
在多數情況下,「修改節點的 zIndex 屬性會使其 setSiblingIndex 函數失效」。
這無形中增加了編碼時的心智負擔,也增加了問題排查的難度。
引擎內的用法 (Usage in engine)
出於好奇,我在引擎源碼中搜了搜,想看看引擎內部有沒有使用到 zIndex 屬性。
結果是:只有幾處與「調試」相關的地方使用到了節點的 zIndex 屬性。

例如:預覽模式下,左下角的 Profiler 節點。

以及碰撞組件的調試框等等,這里就不在贅述了。
建議 (Suggestion)
所以,為了避免一些不必要的 BUG 和邏輯沖突。
我的建議是:
「少用甚至不用 zIndex,而優先使用 siblingIndex 相關函數。」
🥴 聽皮皮一句勸,zIndex 的水太深,你把握不住!
傳送門
更多分享
《Cocos Creator 編輯器擴展:Quick Finder》
公眾號
菜鳥小棧
😺 我是陳皮皮,一個還在不斷學習的游戲開發者,一個熱愛分享的 Cocos Star Writer。
🎨 這是我的個人公眾號,專注但不僅限於游戲開發和前端技術分享。
💖 每一篇原創都非常用心,你的關注就是我原創的動力!
Input and output.

