<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          聽皮皮一句勸zIndex 的水太深,你把握不?。?/h1>

          共 13971字,需瀏覽 28分鐘

           ·

          2021-08-10 22:07


          前言

          本文基于 Cocos Creator 2.4.5 撰寫。

          ?? 普天同慶

          來了來了,《源碼解讀》系列文章終于又來了!

          ?? 溫馨提醒

          本文包含大段引擎源碼,使用大屏設(shè)備閱讀體驗更佳!

          Hi There!

          節(jié)點(cc.Node)作為 Cocos Creator 引擎中最基本的單位,所有組件都需要依附在節(jié)點上。

          同時節(jié)點也是我們?nèi)粘i_發(fā)中接觸最頻繁的東西。

          我們經(jīng)常會需要「改變節(jié)點的排序」來完成一些效果(如圖像的遮擋)。

          A Question?

          ?? 你有沒有想過:

          節(jié)點的排序是如何實現(xiàn)的?

          Oops!

          ?? 我在分析了源碼后發(fā)現(xiàn):

          節(jié)點的排序并沒有想象中那么簡單!

          ?? 渣皮語錄

          聽皮皮一句勸,zIndex 的水太深,你把握不??!


          正文

          節(jié)點順序 (Node Order)

          ?? 如何修改節(jié)點的順序?

          首先,在 Cocos Creator 編輯器中的「層級管理器」中,我們可以隨意拖動節(jié)點來改變節(jié)點的順序。

          拖動排序

          ?? 但是,在代碼中我們要怎么做呢?

          我最先想到的是節(jié)點的 setSiblingIndex 函數(shù),然后是節(jié)點的 zIndex 屬性。

          我猜大多數(shù)人都不清楚這兩個方案有什么區(qū)別。

          那么接下來就讓我們深入源碼,一探究竟!

          siblingIndex

          「siblingIndex」即「同級索引」,意為「同一父節(jié)點下的兄弟節(jié)點間的位置」。

          siblingIndex 越小的節(jié)點排越前,索引最小值為 0,也就是第一個節(jié)點的索引值。

          需要注意的是,實際上節(jié)點并沒有 siblingIndex 屬性,只有 getSiblingIndexsetSiblingIndex 這兩個相關(guān)函數(shù)。

          注:本文統(tǒng)一使用 siblingIndex 來代指 getSiblingIndexsetSiblingIndex 函數(shù)。

          另外,getSiblingIndexsetSiblingIndex 函數(shù)是由 cc._BaseNode 實現(xiàn)的。

          ?? cc._BaseNode

          大家對這個類可能會比較陌生,簡單來說 cc._BaseNodecc.Node 的基類。

          此類「定義了節(jié)點的基礎(chǔ)屬性和函數(shù)」,包括但不僅限于 setParent、addChildgetComponent 等常用函數(shù)...

          ?? 源碼節(jié)選:

          函數(shù):cc._BaseNode.prototype.getSiblingIndex

          getSiblingIndex() {
            if (this._parent) {
              return this._parent._children.indexOf(this);
            } else {
              return 0;
            }
          },

          函數(shù):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, 0this);
              } 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

          ???? 做了什么?

          扒拉源碼后發(fā)現(xiàn),siblingIndex 的本質(zhì)其實很簡單。

          那就是「當前節(jié)點在父節(jié)點的 _children 屬性中的下標(位置)」。

          getSiblingIndex 函數(shù)返回的是「當前節(jié)點在父節(jié)點的 _children 屬性中的下標(位置)」。

          setSiblingIndex 函數(shù)則是設(shè)置「當前節(jié)點在父節(jié)點的 _children 屬性中的下標(位置)」。

          ?? cc._BaseNode.prototype._children

          節(jié)點的 _children 屬性其實就是節(jié)點的 children 屬性。

          children 屬性是一個 getter,返回的是自身的 _children 屬性。

          另外 children 屬性沒有實現(xiàn) setter,所以你直接給 children 屬性賦值是無效的。

          zIndex

          「zIndex」是「用來對節(jié)點進行排序的關(guān)鍵屬性」,它決定了一個節(jié)點在兄弟節(jié)點之間的位置。

          zIndex 的值介于 cc.macro.MIN_ZINDEXcc.macro.MAX_ZINDEX 之間。

          另外,zIndex 屬性是在 cc.Node 內(nèi)使用 Cocos 定制版 gettersetter 實現(xiàn)的。

          ?? 源碼節(jié)選:

          屬性: cc.Node.prototype.zIndex

          // 為了減少篇幅,已省略部分不相關(guān)代碼
          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

          ??? 做了什么?

          扒拉源碼后發(fā)現(xiàn),zIndex 的本質(zhì)其實也很簡單。

          那就是「返回或設(shè)置節(jié)點的 _localZOrder 屬性」。

          ?? 沒那么簡單!

          有趣的是,在 getter 中并沒有直接返回 _localZOrder 屬性,而是返回了 _localZOrder 屬性右移(>>)16 位后的數(shù)值。

          setter 中設(shè)置 _localZOrder 屬性時也并非簡單的賦值,又是進行了一頓位操作:

          這里我們以二進制數(shù)的視角來分解該函數(shù)內(nèi)的位操作。

          1. 通過 & 0x0000ffff 取出原 _localZOrder 的「低 16 位」;
          2. 將目標值 value「左移 16 位」;
          3. 將左移后的 value 作為「高 16 位」與原 _localZOrder 的「低 16 位」合并;
          4. 最后得到一個「32 位的二進制數(shù)」并賦予 _localZOrder。

          ?? 嗯?

          慢著!_localZOrder 又是干啥用的?咋這么繞!

          別急,答案在后面~

          排序 (Sorting)

          細心的朋友應該發(fā)現(xiàn)了,siblingIndex 和 zIndex 的源碼中都沒有包含實際的排序邏輯。

          但是它們都有一個共同點:「最后都調(diào)用了自身的 _onSiblingIndexChanged 函數(shù)」。

          _onSiblingIndexChanged

          ?? 源碼節(jié)選:

          函數(shù):cc.Node.prototype._onSiblingIndexChanged

          _onSiblingIndexChanged() {
            if (this._parent) {
              this._parent._delaySort();
            }
          },

          ??? 做了什么?

          _onSiblingIndexChanged 函數(shù)內(nèi)則是調(diào)用了「父節(jié)點」的 _delaySort 函數(shù)。

          _delaySort

          ?? 源碼節(jié)選:

          函數(shù):cc.Node.prototype._delaySort

          _delaySort() {
            if (!this._reorderChildDirty) {
              this._reorderChildDirty = true;
              cc.director.__fastOn(cc.Director.EVENT_AFTER_UPDATE, this.sortAllChildren, this);
            }
          },

          ??? 做了什么?

          一頓操作順藤摸瓜后發(fā)現(xiàn),真正進行排序的地方是「父節(jié)點」的 sortAllChildren 函數(shù)。

          ?? 盲生,你發(fā)現(xiàn)了華點!

          值得注意的是,_delaySort 函數(shù)中的 sortAllChildren 函數(shù)調(diào)用不是立即觸發(fā)的,而是會在下一次 update(生命周期)后觸發(fā)。

          延遲觸發(fā)的目的應該是為了避免在同一幀內(nèi)的重復調(diào)用,從而減少不必要的性能損耗。

          sortAllChildren

          ?? 源碼節(jié)選:

          函數(shù):cc.Node.prototype.sortAllChildren

          // 為了減少篇幅,已省略部分不相關(guān)代碼
          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)

          隨著一步步深入,我們終于來到了關(guān)鍵部分。

          現(xiàn)在讓我們琢磨琢磨這個 sortAllChildren 函數(shù)。

          進入該函數(shù)的前半段,映入眼簾的是一行賦值語句,將 _childArrivalOrder 屬性設(shè)(重置)為 1;

          緊跟其后的是一個 for 循環(huán),遍歷了當前節(jié)點的所有「子節(jié)點」,并一一執(zhí)行「子節(jié)點」的 _updateOrderOfArrival 函數(shù)。

          ?? 嗯?這個 _updateOrderOfArrival 函數(shù)又是何方神圣?

          ~_updateOrderOfArrival

          ?? 源碼節(jié)選:

          函數(shù):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 函數(shù)的作用就是「更新節(jié)點的 _localZOrder 屬性」。

          ?? 該函數(shù)中同樣也使用了位操作:

          同上,以二進制數(shù)的視角來進行分解這里的位操作。

          1. 將父節(jié)點的 _childArrivalOrder(前置)自增 1,并賦予 arrivalOrder(如無父節(jié)點則為 0);
          2. 通過 & 0xffff0000 取出當前節(jié)點的 _localZOrder 的「高 16 位」;
          3. arrivalOrder 作為「低 16 位」與當前節(jié)點的 _localZOrder 的「高 16 位」合并;
          4. 最后得到一個新的「32 位的二進制數(shù)」并賦予當前節(jié)點的 _localZOrder 屬性。

          ?? 看到這里你是不是已經(jīng)開始迷惑了?

          別擔心,答案即將揭曉!

          >下半部分 (Part 2)

          sortAllChildren 函數(shù)的下半部分就比較好理解了。

          基本就是通過「插入排序(Insertion Sort)」來「排序當前節(jié)點的 _children 屬性(子節(jié)點數(shù)組)」。

          其中主要根據(jù)子節(jié)點的 _localZOrder 屬性的值來進行排序,_localZOrder 屬性值小的子節(jié)點排前面,反之排后面。

          排序的關(guān)鍵 (Key of sorting)

          ?? 分析完源碼后發(fā)現(xiàn),節(jié)點的排序并沒有想象中那么簡單。

          我們可以先得出幾個結(jié)論:

          1. siblingIndex 是節(jié)點在父節(jié)點的 children 屬性中的下標;
          2. zIndex 是一個獨立的屬性,和 siblingIndex 沒有直接聯(lián)系;
          3. siblingIndex 和 zIndex 的改變都會觸發(fā)排序;
          4. siblingIndex 和 zIndex 共同組成了節(jié)點的 _localZOrder;
          5. zIndex 的權(quán)重比 siblingIndex 大;
          6. 節(jié)點的 _localZOrder 直接決定了節(jié)點的最終順序。

          siblingIndex 如何影響排序 (How siblingIndex affects sorting)

          我們前面有提到:

          • getSiblingIndex 函數(shù)「返回了當前節(jié)點在父節(jié)點的 _children 屬性中的下標(位置)」。
          • setSiblingIndex 函數(shù)「設(shè)置了當前節(jié)點在父節(jié)點的 _children 屬性中的下標(位置),并通知父節(jié)點進行排序」。

          隨后在父節(jié)點的 sortAllChildren 函數(shù)中的上半部分,會以這個下標作為節(jié)點 _localZOrder 的低 16 位。

          ?? 所以我們可以這樣理解:

          siblingIndex 是元素下標,在排序過程中,其決定了 _localZOrder 的「低 16 位」。

          zIndex 如何影響排序 (How zIndex affects sorting)

          我們前面有提到:

          • zIndexgetter「返回了 _localZOrder 的高 16 位」。
          • zIndexsetter「設(shè)置了 _localZOrder 的高 16 位,并通知父節(jié)點進行排序」。

          ?? 所以我們可以這樣理解:

          zIndex 實際上只是一個軀殼,其本質(zhì)是 _localZOrder 的「高 16 位」。

          _localZOrder 如何決定順序 (How _localZOrder works)

          父節(jié)點的 sortAllChildren 函數(shù)中根據(jù)子節(jié)點的 _localZOrder 大小來進行最終排序。

          我們可以將 _localZOrder 看做一個「32 位二進制數(shù)」,其由 siblingIndex 和 zIndex 共同組成。

          但是,為什么說「zIndex 的權(quán)重比 siblingIndex 大」呢?

          因為 zIndex 決定了 _localZOrder 的「高 16 位」,而 siblingIndex 決定了 _localZOrder 的「低 16 位」。

          所以,只有在 zIndex 相等的情況下,siblingIndex 的大小才有決定性意義。

          而在 zIndex 不相等的情況下,siblingIndex 的大小就無所謂了。

          ?? 舉個栗子

          這里有兩個 32 位二進制數(shù)(偽代碼):

          • 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(320);
            }

          }

          >場景一 (Scene 1)

          在 1 個節(jié)點下放置了 1 個子節(jié)點。

          ?? 子節(jié)點的排序信息:

          zIndex 0

          一般來說,由于節(jié)點的 _childArrivalOrder 是從 1 開始的,并且在計算時會先自增 1。

          所以子節(jié)點的 _localZOrder 的「低 16 位」總會比其 siblingIndex 大 2 個數(shù)。

          >場景二 (Scene 2)

          在 1 個節(jié)點下放置了 1 個子節(jié)點,并將子節(jié)點的 zIndex 設(shè)為 1。

          ?? 子節(jié)點的排序信息:

          zIndex 1

          可以看到,僅僅將節(jié)點的 zIndex 屬性設(shè)為 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

          ?? 繼續(xù)簡化后的偽代碼:

          _localZOrder = (zIndex << 16) | (siblingIndex + 2)

          ?? By the way

          當一個節(jié)點沒有父節(jié)點時,它的 arrivalOrder 永遠是 0

          其實此時它是啥已經(jīng)不重要了,畢竟沒有父節(jié)點的節(jié)點本來就不可能會被排序。

          >場景三 (Scene 3)

          在同 1 個節(jié)點下放置了 6 個子節(jié)點,將所有子節(jié)點的 zIndex 都設(shè)為 0。

          ?? 各個子節(jié)點的排序信息:

          zIndex 0 & siblingIndex 0~5

          >場景四 (Scene 4)

          在同 1 個節(jié)點下放置了 6 個子節(jié)點,將這 6 個子節(jié)點的 zIndex 設(shè)為 05。

          ?? 各個子節(jié)點的排序信息:

          zIndex 0~5

          可以看到,zIndex 的值會直接體現(xiàn)在 _localZOrder 的「高 16 位」;每當 zIndex 增加 1,_localZOrder 就會增加 65537。

          所以說 siblingIndex 怎么可能打得過 zIndex!

          >場景五 (Scene 5)

          在同 1 個節(jié)點下放置了 6 個子節(jié)點,將這 6 個子節(jié)點的 zIndex 設(shè)為 05

          ?? 修改第 6 個子節(jié)點的 siblingIndex04,其排序信息:

          zIndex 5 & siblingIndex 0~4

          可以看到,此時無論我們怎么修改第 6 個子節(jié)點的 siblingIndex,它都會自動變回 5(也就是同級節(jié)點中的最大值)。

          因為這個子節(jié)點的 zIndex 在其同級節(jié)點之中有著絕對的優(yōu)勢。

          ~不太對勁 (Something wrong)

          ?? 這里有一個看起來不太對勁的現(xiàn)象!

          比如,當我們把 siblingIndex5 修改為 0 時,_localZOrder 也相應從 327687 變成 327682;但是當 siblingIndex 自動變回 5 時,_localZOrder 也還是 327682,并沒有變回 327687

          ?? 為什么會這樣?

          原因其實很簡單:

          當我們修改節(jié)點的 siblingIndex 時會觸發(fā)排序,排序過程中會「根據(jù)節(jié)點當前時刻的 siblingIndex 和 zIndex 生成新的 _localZOrder」;

          最后在父節(jié)點的 sortAllChildren 函數(shù)中會根據(jù)子節(jié)點的 _localZOrder 來對 _children 數(shù)組進行排序,此時「子節(jié)點的 siblingIndex 也會被動更新」,「但是 _localZOrder 卻沒有重新生成」。

          但是,由于 zIndex 存在「絕對優(yōu)勢」,這種“奇怪的現(xiàn)象”其實并不會影響到節(jié)點的正常排序~

          總結(jié) (Summary)

          分析完源碼后,我們來總結(jié)一下。

          在代碼中修改節(jié)點順序的方法主要有兩種:

          1. 修改節(jié)點的 zIndex 屬性
          2. 通過 setSiblingIndex 函數(shù)設(shè)置

          無論使用以上哪種方法,最終都會「通過 zIndex 和 siblingIndex 的組合作為依據(jù)來進行排序」。

          在多數(shù)情況下,「修改節(jié)點的 zIndex 屬性會使其 setSiblingIndex 函數(shù)失效」。

          這無形中增加了編碼時的心智負擔,也增加了問題排查的難度。

          引擎內(nèi)的用法 (Usage in engine)

          出于好奇,我在引擎源碼中搜了搜,想看看引擎內(nèi)部有沒有使用到 zIndex 屬性。

          結(jié)果是:只有幾處與「調(diào)試」相關(guān)的地方使用到了節(jié)點的 zIndex 屬性。

          Usage in engine

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

          Profiler Node

          以及碰撞組件的調(diào)試框等等,這里就不在贅述了。

          建議 (Suggestion)

          所以,為了避免一些不必要的 BUG 和邏輯沖突。

          我的建議是:

          「少用甚至不用 zIndex,而優(yōu)先使用 siblingIndex 相關(guān)函數(shù)。」

          ?? 聽皮皮一句勸,zIndex 的水太深,你把握不?。?/em>


          公眾號

          菜鳥小棧

          ?? 我是陳皮皮,一個還在不斷學習的游戲開發(fā)者,一個熱愛分享的 Cocos Star Writer。

          ?? 這是我的個人公眾號,專注但不僅限于游戲開發(fā)和前端技術(shù)分享。

          ?? 每一篇原創(chuàng)都非常用心,你的關(guān)注就是我原創(chuàng)的動力!

          Input and output.


          瀏覽 60
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報

          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  自拍偷拍激情视频 | 亚色综合网 | 婷婷五月丁香五月 | 天天干在线影院 | 日本精品人妻无码免费大全 |