深入了解angularjs中的$digest與$apply方法,從區別聊到使用優化


 壹 ❀ 引

如果有人問,在angularjs中修改模型數據為何視圖會同步更新呢,我想大多數人一定會回答臟檢查(Dirty Checking)相關概念。沒錯,在angularjs中作用域(scope)作為鏈接控制器(controller)與視圖(view)之間的橋梁,除了綁定數據監聽事件外,一旦有數據發生改變,scope還兼顧了臟檢測更新視圖的職責,這是我們宏觀的理解。

這就引發了一系列的問題,以點擊事件為例,為什么在angularjs中用原生click事件達不到更新視圖的效果?ng-click與原生click有何區別?ng-click觸發后angularjs又是怎么讓視圖更新的呢?$digest和$apply這兩個眼熟的方法究竟有何作用,兩者有什么區別?如果你對於這些問題感興趣,不妨靜下心來讀一讀本文,那么本文開始。

 貳 ❀ angularjs的數據綁定

現在有個需求,當我們點擊按鈕時需要更新視圖中的文本信息,當然不通過angularjs我們使用原生js做法也能輕易實現,像這樣:

<div>我的名字是:<span class="name"></span></div>
<button class="btn">click me</button>
let btn = document.querySelector('.btn');
let name = document.querySelector('.name');
btn.onclick = function () {
    name.innerHTML = '聽風是風';
};

但這樣做就有兩個問題,第一我們不得不操作DOM,第二不便於復用,如果我們希望點擊后將name字段更新到DOM不同層級的各種地方,此時獲取DOM就尤為復雜了。

而angularjs便提供了一種有效的解決方法---數據綁定,它將我們需要更新的name字段抽離成了一份數據,在使用時你不用關心這份數據與DOM結構的內在聯系,你要考慮的僅僅是在何處放置這份數據而已。

同樣還是上面的需求,我們使用angularjs就可以這么做:

<body ng-controller="myCtrl as vm">
    <div>我的名字是:<span>{{vm.name}}</span></div>
    <button class="btn" ng-click="vm.sayName()">click me</button>
</body>
angular.module('myApp', [])
    .controller('myCtrl', function () {
        let vm = this;
        vm.name = '';
        vm.sayName = function () {
            vm.name = '聽風是風';
        };
    });

我們仔細對比這兩種實現,js是click事件作為媒介找到對應的DOM並操作DOM,而angularjs通過click事件操作的卻只是數據,前者操作DOM后者操作數據。突然想起了事件驅動與數據驅動的概念,也有那么點意思了。

這就比較神奇了,當我們點擊按鈕,name的值發生了改變,同時視圖上也同步進行了刷新,angularjs是如何感知變化,又是怎么通過到視圖的呢?這就得介紹$digest循環了。

 叄 ❀ 神器的$digest

angularjs的事件循環又稱為$digest循環,循環過程中包含了數據的臟檢測,准確來說angularjs的臟檢測功能是由scope上的$digest()方法實現,這里我們先理解$digest與臟檢測的關系。

angularjs中的$digest循環主要包含了$watch列表與$evalAsync列表兩個部分,$evalAsync列表先不分析,看到$watch列表大家是不是有點想法了呢?

沒錯,這里的$watch列表就是一個包含了多個$watch監聽的數組,在scope中以$$watchers字段表示。$watch大家都不會陌生,監聽一份數據,如果發生改變則執行對應回調,而angularjs便是利用$watch監聽了我們需要交互的每份數據,只要發生改變,底層將通知視圖進行更新。

說到這大家就納悶了,我編程中明明沒加$watch,哪來的呢?其實在angularjs使用中,無論是表達式{{}}還是ng-bind,凡是與視圖上與數據交互的地方angualrjs都會幫你去注冊watch監聽,我們來看個簡單的例子:

<body ng-app="myApp">
    <div ng-controller="myCtrl as vm">
        純路人,{{vm.name}}非常{{vm.describe}}
        <div ng-bind="vm.age"></div>
    </div>
</body>
angular.module('myApp', [])
    .controller('myCtrl', function () {
        var vm = this;
        vm.name = "聽風是風";
        vm.describe = '帥';
        vm.age = 26;
    });

在這個例子中,我們定義了三個屬性,並在視圖上與之綁定,查看當前控制器scope屬性下的$$watchers屬性(這個需要谷歌插件才能查看,插件名 ng-inspect for AngularJS),可以看到類型為Array,數組中包含的三個監聽器分別對應我們前面定義的數據。一旦數據發生變化,$watch回調負責更新視圖。

一個新的問題就是,angularjs的$watch又是如何感知哪些值發生了變化呢,這就像ng-click能執行代碼是依賴了點擊行為,畢竟總不能用定時器一直監聽吧。

關於這一點就又回到了我們前面提到的$digest循環上了,angularjs在每次調用$scope.$digest()方法都會發起$digest循環,在循環中angularjs會觸發$$watchers中的每一個$watch(臟檢測),有了觸發源$watch要做的就是新舊值對比,以及發生變化后的相應操作了。

OK,到這里我們明白了調用$socpe.$digest()會觸發$digest循環,在循環中又會觸發所有$watch進行數據對比,也就是我們說的臟檢查,以及在數據變更后對視圖進行更新。

那么$apply與$digest又有什么聯系呢?我們接着說。

 肆 ❀ $apply與$digest

在angularjs開發中,大家一定有過這樣的經歷,如果一段斷碼明明修改了數據但視圖沒變化,用$scope.$apply方法包裹代碼就能解決該問題。比如我們在前文中click與ng-click的例子,這是為什么?

不賣關子,click之所以無法觸發視圖更新,這是因為click綁定函數中的函數作用域已經脫離了angularjs的上下文,angularjs的$digest循環無法感知脫離angularjs作用域的數據變化(你變了我不知道)。

使用$apply就能讓angularjs執行臟檢查的本質其實就是$apply也觸發了$digest循環,准確來說,執行$scope.$apply后會調用$rootscope.$digest,所以只要使用了$apply方法,angularjs都 會從根作用域開始遍歷每個作用域中的每個$warchers。

相比之下像angularjs中內置的事件比如ng-click都內置了$apply用於觸發$digest循環,如果我們依舊使用$apply,angularjs反而會報錯告訴你已經啟動了$apply,一個簡單的例子:

<button class="btn" ng-click="vm.sayName()">click me</button>
angular.module('myApp', [])
    .controller('myCtrl', function ($scope) {
        let vm = this;
        vm.name = '';
        vm.sayName = function () {
            $scope.$apply(function () {
                vm.name = '聽風是風';
            });
        };
    });

OK,打到這里我們知道了$apply方法執行會調用$rootscope.$digest方法,從而啟動全新的$digest循環,對所有作用域中的數據進行臟檢查。

 伍 ❀ 何時使用$apply

在angularjs controller中的任何地方都屬於angularjs的上下文,在這個上下文中直接修改變量都不需要$apply,但如果你在普通函數以及非angularjs提供的回調函數中修改變量,此時都需要結合$apply來通知angularjs進行額外的臟檢查。舉幾個例子:

1.普通事件綁定的函數內修改數據需要使用$apply,文章開頭已有舉例。

2.普通定時器回調中修改數據需要使用$apply,一般推薦使用angularjs封裝的定時器,比如$timeout:

//angularjs定時器
$timeout(() => vm.name = '聽風是風', 1000);
//普通定時器
setTimeout(() => {
    $scope.$apply(() => vm.name = '時間跳躍');
}, 2000);

在上文中我們已經說了,如果調用了$apply 等同於$rootscope.$digest,這樣性能其實是不太好的,特別是存在多個scope的情況下,我們往往更喜歡只檢測當前作用域的數據變化。

更加優化的做法是在當前作用域調用$digest,像這樣:

setTimeout(() => {
    $scope.$apply(() => vm.name = '時間跳躍');
}, 1000);

setTimeout(function () {
    vm.name = '聽風是風';
    $scope.$digest();
}, 2000);

 陸 ❀ 總

那么到這里,我們詳細介紹了$digest與$apply方法的區別,在介紹$digest循環后了解到$digest是由$apply觸發,從而也解釋了ng-click與普通click的區別。在介紹了$apply后,我們簡單提及了使用$apply的場景,我們知道它會讓angualrjs從根作用域開始臟檢測,代價較大,因此推薦使用$digest可代替。那么到這里,本文結束。

 參考

理解Angular中的$apply()以及$digest()

angularjs權威指南


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM