AngularJS 的髒值檢查和事件機制
AngularJS 的髒值檢查和事件機制都是基於作用域實現的,該文章沒有涉及的作用域的建立、銷燬等和生命週期相關的內容,只是把髒值檢查和事件機制拆出來講解一下。
本文引用的 AngularJS 的原始碼略去了很多程式碼,有需要可以直接看原始碼。
髒值檢查
在看該部分前強烈建議看下這篇文章(連結 http://www.ituring.com.cn/article/39865)對髒值檢查的實現講解很詳細,本文略去了一些不太關心的細節,只關注主要的實現原理。
AngularJS 的髒值檢查的實現主要基於作用域 $scope 的 $$watchers 屬性、$watch 和 $digest 方法,是 Sub/Pub 模式的一種實現。
$watch
function Scope() {
this.$$watchersCount = 0;
this.$$watchers = [];
}
Scope.prototype.$watch = function(watchExp, listener, objectEquality) {
// 利用依賴注入的 $parse 服務轉換成函式,用於獲取作用域裡的變數值
var get = $parse(watchExp);
var scope = this,
array = scope.$$watchers,
watcher = {
fn: listener, // 值變化後需要執行的偵聽函式
last: initWatchVal, // 最新的值
get: get,
eq: !!objectEquality // 是否進行嚴格匹配
};
if (!array) {
array = scope.$$watchers = [];
}
// we use unshift since we use a while loop in $digest for speed.
// the while loop reads in reverse order.
array.unshift(watcher);
incrementWatchersCount(this, 1);
return function deregisterWatch() {
if (arrayRemove(array, watcher) >= 0) {
incrementWatchersCount(scope, -1);
}
lastDirtyWatch = null;
};
};
$watch 是 Sub 部分,主要是往 scope 物件的 $$watchers 裡面新增 watcher 物件,注意 $$watchersCount 記錄了當前作用域和其子作用域的 watchers 的總數。
$digest
Scope.prototype.$digest = function() {
var watch, value, last, fn, get,
watchers,
length,
dirty, ttl = TTL,
next, current, target = this;
beginPhase('$digest');
// 外層 loop 在 dirty 為 true 時執行
do {
dirty = false;
current = target;
// 內層 loop 遍歷作用域樹的所有作用域,在值改變時執行對應的 fn
do {
if ((watchers = current.$$watchers)) {
length = watchers.length;
// 在 $watch 中使用 unshift 新增物件的原因
while (length--) {
watch = watchers[length];
if (watch) {
get = watch.get;
// 判斷值是否改變這裡涉及到物件、陣列、NaN 等的比較,這裡略去程式碼
if (......) {
dirty = true;
watch.last = watch.eq ? copy(value, null) : value;
fn = watch.fn;
fn(value, ......);
} else if (watch === lastDirtyWatch) {
dirty = false;
break traverseScopesLoop;
}
}
}
}
// Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have
// tests to prove it!
// this piece should be kept in sync with the
// traversal in $broadcast
if (!(next = ((current.$$watchersCount && current.$$childHead) ||
(current !== target && current.$$nextSibling)))) {
while (current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
} while ((current = next));
// `break traverseScopesLoop;` takes us to here
if ((dirty || asyncQueue.length) && !(ttl--)) {
clearPhase();
throw $rootScopeMinErr('infdig',
'{0} $digest() iterations reached. Aborting!\n' +
'Watchers fired in the last 5 iterations: {1}',
TTL, watchLog);
}
} while (dirty);
clearPhase();
};
$digest 的實現主要是兩層巢狀的 while 迴圈,外層迴圈在 dirty 為 true 時執行。內層迴圈又包含了兩個 while 迴圈,第一個迴圈去遍歷某一個作用域物件的 watchers 陣列,判斷值是否改變,若改變執行 fn,記錄 last 值並設定 dirty 為 true;第二個迴圈使用深度優先的方法遍歷作用域樹(每個作用域物件都對應一個指令,指令又對映 DOM,DOM 是樹形結構,作用域也以樹形結構實現),獲取 next 作用域;這樣內層迴圈就遍歷了所有的作用域(因為 $digest 只能由 $rootScope 呼叫,$rootScope 是作用域樹的根節點)
事件(訊息)機制
AngularJS 的指令可以理解成擴充套件的 HTML,每個指令都會注入一個作用域,指令之間的通訊是通過作用域的通訊實現,作用域的事件機制是用釋出訂閱模式實現的,主要的 API 有 $on、$emit、$broadcast。
$on
function Scope() {
this.$$listeners = {};
this.$$listenerCount = {};
}
Scope.prototype.$on = function(name, listener) {
var namedListeners = this.$$listeners[name];
if (!namedListeners) {
this.$$listeners[name] = namedListeners = [];
}
namedListeners.push(listener);
var current = this;
do {
if (!current.$$listenerCount[name]) {
current.$$listenerCount[name] = 0;
}
current.$$listenerCount[name]++;
} while ((current = current.$parent));
};
$on 是 Sub 部分,給當前作用域的事件偵聽陣列新增函式,並在其祖先元素上計數(在 $broadcast 時可以在該 count 不存在時直接返回而不必做作用域樹的深度優先的遍歷)
$emit
Scope.prototype.$emit = function(name, args) {
var scope = this;
//......
do {
namedListeners = scope.$$listeners[name] || empty;
event.currentScope = scope;
for (i = 0, length = namedListeners.length; i < length; i++) {
// if listeners were deregistered, defragment the array
if (!namedListeners[i]) {
namedListeners.splice(i, 1);
i--;
length--;
continue;
}
namedListeners[i].apply(null, listenerArgs);
}
//if any listener on the current scope stops propagation, prevent bubbling
if (stopPropagation) {
// 為什麼不直接 break
event.currentScope = null;
return event;
}
//traverse upwards
scope = scope.$parent;
} while (scope);
event.currentScope = null;
return event;
};
$emit 方法是往上拋事件,主要程式碼邏輯是迴圈取某個作用域的父級作用域,把取到的每個作用域物件的 namedListeners 陣列迴圈執行,提供了阻止“冒泡”(借用 DOM 的概念)的 API。
$broadcast
Scope.prototype.$broadcast = function(name, args) {
var target = this,
current = target,
next = target;
if (!target.$$listenerCount[name]) return event;
var listenerArgs = concat([event], arguments, 1),
listeners, i, length;
//down while you can, then up and next sibling or up and next sibling until back at root
while ((current = next)) {
event.currentScope = current;
listeners = current.$$listeners[name] || [];
for (i = 0, length = listeners.length; i < length; i++) {
// if listeners were deregistered, defragment the array
if (!listeners[i]) {
listeners.splice(i, 1);
i--;
length--;
continue;
}
listeners[i].apply(null, listenerArgs);
}
// Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it!
// this piece should be kept in sync with the traversal in $digest
// (though it differs due to having the extra check for $$listenerCount)
if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||
(current !== target && current.$$nextSibling)))) {
while (current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
}
event.currentScope = null;
return event;
};
$broadcast 往下級拋事件,又使用了深度優先遍歷作用域樹的方式獲取所有的子級作用域物件,執行回撥佇列,注意該方法不能阻止冒泡。
相關文章
- PostgreSQL的"double buffers"刷髒機制和引數SQL
- [譯]關於Angular髒值檢查你應該知道的最新指南Angular
- MySQL InnoDB檢查點機制MySql
- 檢查點機制與scn
- 深度剖析isinstance的檢查機制
- InnoDB髒頁重新整理機制
- AngularJs 鍵盤事件和滑鼠事件AngularJS事件
- Redis的事件機制Redis事件
- ios,android和javascript的UI事件機制iOSAndroidJavaScriptUI事件
- Angular效能優化 – 再談Angular 4髒值檢測Angular優化
- Angular效能優化 - 再談Angular 4髒值檢測Angular優化
- angular髒檢查原理及虛擬碼實現Angular
- 淺談JS事件機制與React事件機制JS事件React
- JS的事件物件與事件機制JS事件物件
- 關於ORACLE 和MYSQL INNODB 觸發髒資料寫的機制對比OracleMySql
- Javascript事件模型系列(二)事件的捕獲-冒泡機制及事件委託機制JavaScript事件模型
- 走進AngularJs(八) ng的路由機制AngularJS路由
- 淺談前端和移動端的事件機制前端事件
- JPA 實體髒檢查與儲存同步(Dirty & Flush)
- DOM事件機制事件
- react事件機制React事件
- redis事件機制Redis事件
- qt事件機制QT事件
- Flutter 事件機制 - Future 和 MicroTask 全解析Flutter事件
- 淺析Windows的訪問許可權檢查機制Windows訪問許可權
- jQuery的事件機制,事件物件介紹,外掛機制,多庫共存,each()jQuery事件物件
- redis的事件處理機制Redis事件
- WebSocket的事件觸發機制Web事件
- JS的事件監聽機制JS事件
- Vue和React的檢視更新機制對比VueReact
- View事件機制分析View事件
- Android 事件機制Android事件
- C# 事件機制C#事件
- JavaScript執行緒機制與事件機制JavaScript執行緒事件
- 螞蟻SOFA系列(2) - SOFABoot的Readiness健康檢查機制boot
- Oracle SCN機制解析 (SCN, checkpoint檢查點) - finalOracle
- iOS中觸控事件的傳遞和響應機制iOS事件
- AngularJS使用$sce控制程式碼安全檢查AngularJS