學習 kityminder & angular (十四) event 和 scope.$apply

回顧 event 機制 

先回顧一下之前看的 core/event.js, 其提供了 minder 的事件機制 (event) 支持: node

// 表示一個腦圖中發生的事件
class MinderEvent {
  ctor(type, parms, canstop): 構造一個腦圖事件, type 是一個名字字符串, 如 `contentchange'.
  getPosition(): 若是事件是從一個 kity 事件派生的,會有 `getPosition()` 獲取事件發生的座標
  getTargetNode(): 當發生的事件是鼠標事件時,獲取事件位置命中的腦圖節點
  stopPropagation(): 中止(向上)傳播.
  preventDefault(): 取消缺省處理.
  ... 其它輔助函數略...
}

這個類從功能上看, 模仿了 DOM 的事件, 提供了基本類型信息, 以及一些輔助獲取信息的函數. angularjs

// 當 minder 對象構造時, 調用指定 hook.
// 註冊函數實如今 minder.js 中, 方法是在一個閉包數組 _initHooks[] 中添加該函數,
//   當 minder.ctor() 時, 調用 hooks[] 中每個函數.
Minder.registerInitHook( _initEvents );


extend class Minder {
  _initEvents(): 初始化 event 組件(部分)所需的內部數據. 實際初始化 _eventCallbacks{} 對象.
  _resetEvents(): 估計不會用到.
  
  on(names, callback): {  // names 能夠是多個事件 type, 用空格分隔.
    names.split(/s/).foreach { |type|
      This._listen(type, callback);
    }
  },
  _listen(type, callback) {
    // ... 將 callback 函數添加到 this._eventCallbacks{} 對象中名爲 type 的隊列中. 簡寫爲:
    this._ec{}.type[] += callback;
  },
  off(names, callback): 與 on() 是反操做, 細節略.
 
  fire(type, params): {  // 發佈事件.
    var e = new MinderEvent(...); // 構造事件實例
     this._fire(e);  // 發佈實現.
    return this;
  }
  _fire(e): {  //  發佈事件的實現.
    // 從前面 _listen() 已經知道, 名爲 type 的事件回調函數在...
    var callbacks[] = this._eventCallbacks[type].copy(); // 複製一份.
    foreach (cb in callbacks) 
      => this.cb(e)
    return e.shouldStopPropagation();
  }
}

這是一個典型的註冊/發佈事件的模型. 原理上沒有要說的, 主要是實現的一點點細節問題. 例如:
函數 _fire() 返回的值被 fire() 函數拋棄, 那 shouldStopPropagation() 語義如何實現呢...? 算法

另外, 在發佈的時候都會"複製"一個 event callbacks[] 數組, 我看不如不要複製, 而是添加的時候採用複製後添加
的方式也許效率更高, 更節省點內存. express

====== api

待求解的問題

問本身一個問題: 當界面選中一個節點(或取消選中), 工具欄的變化是如何產生的?
思路多是哪一個呢? 
   1. minder 發佈事件, 某個地方監聽後更新UI;
   2: toolbar 設置一個 timer, 按期更新UI.
   3: toolbar 的 ng-disabled 背後作了未知偵聽/計算過程, 從而改變了按鈕 enabled/disabled 狀態. 數組

查看 ng-disabled 文檔: https://docs.angularjs.org/api/ng/directive/ngDisabled
  該指令設置元素的 disabled 屬性, 根據給出的表達式. 瀏覽器

調整 undo-redo ng-disabled='debug_can_undo()', 而後調試加入一些 console.log() 語句, 觀察: 閉包

當選中一個節點時, 會有 minder 事件 'focus', 'selectionchange', 'beforerender', 'noderender' 被髮布
出來. 而後就是 9 次連續的 debug_can_undo() 方法調用, 按鈕狀態被設置. 那麼這些是如何發生的呢? app

研究一下這四個事件都是誰在監聽: dom

1. 事件 focus: 沒有偵聽者;
2. 事件 selection-change: 兩個偵聽者:
    (1) kityminder-core 內部一個;
    (2) kityminder-editor/src/runtime/input.js:75
3. 事件 before-render: 一個偵聽者: 在 kityminder-core 內部.
4. 事件 node-render: 一個偵聽者: 在 kityminder-core 內部.

更進一步, 咱們 hack 掉 fire() 方法, 使得其一個事件也不發佈出去(或不發佈 selectionchange 事件), 觀察結果,
結果是 can_undo() 仍然會被調用 ( 9 次), 那麼看起來不是在事件中更新工具條狀態的了.

換一個思路:

記得看 angularjs 的某篇文章中提到, angularjs 會處理整個網頁的 mouse,key 消息, 而後更新整個界面, 是
這樣的機制嗎? 讓咱們去看書和搜索文章 --- 彷佛要調用 scope.$apply() 方法使得 AngularJS 更新界面.

搜索了一下整個 kityminder-editor 部分, 發現 service/commandBinder, service/resourceService,
  directive/kityminderEditor, directive/noteEditor, notePreviewer, resourceEditor, searchBox
這些地方有. 那些對話框咱們暫時未使用到, 估計不會是它們產生 $apply() 調用.

雖然如今對 AngularJS 的 service 概念還一無所知, 但仍是先看看 commandBinder.js 看看是作什麼的.
裏面大體是這樣:

angular.module(...)
  .service(估計是服務名='commandBinder', function() {
    return {
      bind: function(...) {
        minder.on('interactchange', function() {  // 沒見到發佈此事件, 因此...?
          這裏會調用 scope.$apply();
        });
      } 
    };
  });

爲了實驗, 咱們註釋掉 scope.$apply(), 發現有趣的一幕, can_undo() 方法被調用次數變爲 4 次.
在 scope.$apply() 前面加上 console.log(), 再次實驗. 結果顯示有 3 次 `interactchange' 事件發生!

只能是前面咱們攔截 minder.fire() 的方式不對. 再換一種方式, 根據咱們前面對 kityminder 的 event 系統的知識,
咱們此次攔截更底層的 _fire() 方法, 並過濾掉不關心的消息. 再次觀察, 發現事件 `interactchange' 以後 "總會"
發生工具條 can_undo() 的調用, 根據點擊的地方不一樣, 有時調用 9 次, 或 5 次不一樣.

 

爲了理解 $apply() 等 scope 上的幾個方法, 讓咱們去翻書吧. 打開找到《精通 AngularJS》一書第 293 頁:

Scope.$apply -- 打開 AngularJS 世界的鑰匙

難道咱們隨意想了解的一個問題就接觸到了鑰匙? 無論鑰匙不鑰匙, 問題老是要解決的. 繼續看...

當 AngularJS 首次向公衆發佈以後, 就有許多關於它的模型變化監控算法的 "陰謀論". 其中最被津津樂道的一種
是, 懷疑 AngularJS 使用了某種輪詢機制. (前面咱們說起的 toolbar timer 算是輪詢機制, 我也是陰謀論者麼?)
書上說: 這猜想是錯誤的!

AngularJS 模型變化監控 背後的思路是 "善後" (observeat the end of the day), 由於引起模型變化的狀況
能夠被窮舉出來:
   1. DOM 事件. 如 click, char 事件
   2. XHR 回調事件.
   3. 瀏覽器地址變化.
   4. 定時器事件. (timeout, interval)

AngularJS 只會在被明確告知的狀況下才會啓動它的模型監控機制. 爲了讓這種監控機制運轉起來, 須要在
scope 對象上執行 $apply 方法. (須要模型主動調用 $apply ...) AngularJS 內置指令和服務實現調用了
$apply() 方法, 它們內部已經處理好了監控工做.

 

深刻 $digest 循環

在 AngularJS 中, 檢測模型變化的過程成爲 $digest 循環. 注: digest 在 IT 中可理解爲摘要(算法), 如 MD5, SHA.
該方法會檢測註冊在全部做用域上的全部監視 ($watch) 對象.

存在 $digest 循環的緣由:
   1. 斷定模型哪些部分發生了變化, 以及 DOM 中的哪些屬性應該被更新.
   2. 減小沒必要要的重繪, 以提高性能, 減小 UI 閃爍.

AngularJS 在(執行完 JavaScript) 交還控制權給 DOM 渲染部分以前, 確保全部的模型值都已完成計算且已 "穩定".
這保證了 UI 一次性完成重繪. 若是每一個單獨的屬性變化都重繪一次, 就會致使性能低下和界面閃爍.

AngularJS 使用髒檢查 (dirty checking) 機制來斷定某個模型值是否發生了變化. 工做機制是將以前保存的模型
值和能致使模型變化的事件發生後計算的新模型值作對比.

註冊一個新的模型監視基本語法:
   scope.$watch(watchExpression, modelChangeCallback)

看成用域上添加一個新的 $watch 時, AngularJS 會計算 watch-expression, 而後將結果保存到內部.
在後續的 $digest 循環中, watch-expression 會被再次計算, 計算所得的新值和舊值進行對比. 回調函數
model-change-callback 只會在新值vs舊值不一樣時纔會調用.

須要知道的是, 不只咱們能夠本身註冊 $watch, 任何指令均可以設置本身的 $watch.
(能夠明顯地猜想, ng-disabled 會註冊 $watch).

實驗: 讓咱們在瀏覽器中觀察 scope 的 $$watchers[] 字段, 能夠發現對於 undo-redo 按鈕的 ng-disabled
註冊的 $watcher 的 .last 屬性記錄了該屬性最後的值, .exp 記錄了表達式 "debug_can_undo()", 若是繼續
深刻, 還能發現更多驚喜的細節! 可是隻能略了.

模型的穩定性

若是模型上任何一個監視器都檢測不到任何變化了, 則 AngularJS 就認爲該模型是穩定的. 只要一個監視器有
變化, 就會再次使整個 $digest 循環變 dirty (我猜會再循環一遍, 直到 dirty = false 才中止).

AngularJS 會持續執行 $digest 循環, 反覆運算全部做用域上的全部監視, 直到沒有發現任何變化爲止.
(這就解釋了爲何 debug_can_undo() 函數會被調用多達 9 次, 只要有一個監視器發生變化, 就會再調用一次).
實驗觀察: 在選中一個節點狀態下, 選擇另外一個節點, debug_can_undo() 只調用 5 次.
緣由猜想: 只有 4 個 scope 中的監視器發生變化, 加上本身調用 1 次, 而後總計調用 5 次.

實驗2: 選中另外一個 toolbar 的 tab, 此時 undo-redo 按鈕 `不顯示出來'. 此時點擊節點, debug_can_undo()
  仍然會被調用(屢次).

不穩定的模型如 random() 怎麼辦?

AngularJS 默認最多會執行 10 次循環, 以後就會聲明該模型是不穩定的, 而後中斷 $digest 循環.
(那咱們看到的 debug_can_undo() 最多顯示 9 次是這個緣由嗎...? )

 

小結

綜上所述, 工具條狀態更新流程爲:
   1. kityminder 在點擊等操做時發佈 'interactchange' 事件;
   2. 該事件被 commandBinder 服務偵聽, 並調用 angular scope.$apply() 方法;
   3. AngularJS 會進入 $digest 循環, 調用各個 $watcher (如內建指令 ng-disabled 設置的), 直到狀態穩定;
   4. 更新 UI, toolbar 的按鈕 disable/enable 狀態變化/或不變.

因此, 當心點性能問題, 可能某個方法會被調用 N 次, 在不知道的狀況下.

相關文章
相關標籤/搜索