手寫AngularJS髒檢查機制

什麼是髒檢查

View -> Model

瀏覽器提供有User Event觸發事件的API,例如,clickchangejavascript

Model -> View

瀏覽器沒有數據監測APIAngularJS 提供了 $apply()$digest()$watch()css

其餘數據雙向綁定介紹

VUE

{{}} Object.defineProperty() 中使用 setter / getter 鉤子實現。html

Angular

[()] 事件綁定加上屬性綁定構成雙向綁定。java

怎麼手寫

你們先看運行效果,運行後,點增長,數字會+1,點減小,數字會-1,就是這麼一個簡單的頁面,視圖到底爲什麼會自動更新數據呢?瀏覽器

我先把最粗糙的源碼放出來,你們先看看,有看不懂得地方再議。app

老規矩,初始化頁面框架

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <script src="./test_01.js" charset="utf-8"></script>
    <title>手寫髒檢查</title>
    <style type="text/css"> button { height: 60px; width: 100px; } p { margin-left: 20px; } </style>
</head>

<body>
    <div>
        <button type="button" ng-click="increase">增長</button>
        <button type="button" ng-click="decrease">減小</button> 數量:
        <span ng-bind="data"></span>
    </div>
    <br>
    <!-- 合計 = <span ng-bind="sum"></span> -->
</body>

</html>
複製代碼

下面是JS源碼:1.0版本ui

window.onload = function() {
 'use strict';
    var scope = { // 至關於$scope
        "increase": function() {
            this.data++;
        },
        "decrease": function() {
            this.data--;
        },
        data: 0
    }

    function bind() {
        var list = document.querySelectorAll('[ng-click]');
        for (var i = 0, l = list.length; i < l; i++) {
            list[i].onclick = (function(index) {
                return function() {
                    var func = this.getAttribute('ng-click');
                    scope[func](scope);
                    apply();
                }
            })(i)
        }
    }

    function apply() {
        var list = document.querySelectorAll('[ng-bind]');
        for (var i = 0, l = list.length; i < l; i++) {
            var bindData = list[i].getAttribute('ng-bind');
            list[i].innerHTML = scope[bindData];
        }
    }

    bind();
    apply();
}
複製代碼

沒錯,我只是我偷懶實現的……其中還有不少bug,雖然實現了頁面效果,可是仍然有不少缺陷,好比方法我直接就定義在了scope裏面,能夠說,這一套代碼是我爲了實現雙向綁定而實現的雙向綁定。this

回到主題,這段代碼中我有用到髒檢查嗎?spa

徹底沒有。

這段代碼的意思就是bind()方法綁定click事件,apply()方法顯示到了頁面上去而已。

OK,拋開這段代碼,先看2.0版本的代碼

window.onload = function() {
    function getNewValue(scope) {
        return scope[this.name];
    }

    function $scope() {
        // AngularJS裏,$$表示其爲內部私有成員
        this.$$watchList = [];
    }

    // 髒檢查監測變化的一個方法
    $scope.prototype.$watch = function(name, getNewValue, listener) {
        var watch = {
            // 標明watch對象
            name: name,
            // 獲取watch監測對象的值
            getNewValue: getNewValue,
            // 監聽器,值發生改變時的操做
            listener: listener
        };

        this.$$watchList.push(watch);
    }

    $scope.prototype.$digest = function() {
        var list = this.$$watchList;
        for (var i = 0; i < list.length; i++) {
            list[i].listener();
        }
    }


    // 下面是實例化內容


    var scope = new $scope;
    scope.$watch('first', function() {
        console.log("I have got newValue");
    }, function() {
        console.log("I am the listener");
    })

    scope.$watch('second', function() {
        console.log("I have got newValue =====2");
    }, function() {
        console.log("I am the listener =====2");
    })

    scope.$digest();
}
複製代碼

這個版本中,沒有數據雙向綁定的影子,這只是一個髒檢查的原理。

引入2.0版本,看看在控制檯發生了什麼。

控制檯打印出了 I am the listenerI am the listener =====2 這就說明,咱們的觀察成功了。

不過,僅此而已。

咱們光打印出來有用嗎?

明顯是沒有做用的。

接下來要來改寫這一段的方法。

首先,咱們要使 listener 起到觀察的做用。

先將 listener() 方法輸出內容改變,仿照 AngularJS$watch 方法,只傳兩個參數:

scope.$watch('first', function(newValue, oldValue) {
    console.log("new: " + newValue + "=========" + "old: " + oldValue);
})

scope.$watch('second', function(newValue, oldValue) {
    console.log("new2: " + newValue + "=========" + "old2: " + oldValue);
})
複製代碼

再將 $digest 方法進行修改

$scope.prototype.$digest = function() {
    var list = this.$$watchList;
    for (var i = 0; i < list.length; i++) {
        // 獲取watch對應的對象
        var watch = list[i];
        // 獲取new和old的值
        var newValue = watch.getNewValue(this);
        var oldValue = watch.last;

        // 進行髒檢查
        if (newValue !== oldValue) {
            watch.listener(newValue, oldValue);
            watch.last = newValue;
        }

        // list[i].listener();
    }
}
複製代碼

最後將 getNewValue 方法綁定到 $scope 的原型上,修改 watch 方法所傳的參數:

$scope.prototype.getNewValue = function(scope) {
    return scope[this.name];
}

// 髒檢查監測變化的一個方法
$scope.prototype.$watch = function(name, listener) {
    var watch = {
        // 標明watch對象
        name: name,
        // 獲取watch監測對象的值
        getNewValue: this.getNewValue,
        // 監聽器,值發生改變時的操做
        listener: listener
    };

    this.$$watchList.push(watch);
}
複製代碼

最後定義這兩個對象:

scope.first = 1;
    scope.second = 2;
複製代碼

這個時候再運行一遍代碼,會發現控制檯輸出了 new: 1=========old: undefinednew2: 2=========old2: undefined

OK,代碼到這一步,咱們實現了watch觀察到了新值和老值。

這段代碼的 watch 我是手動觸發的,那個該如何進行自動觸發呢?

$scope.prototype.$digest = function() {
        var list = this.$$watchList;
        // 判斷是否髒了
        var dirty = true;
        while (dirty) {
            dirty = false;
            for (var i = 0; i < list.length; i++) {
                // 獲取watch對應的對象
                var watch = list[i];
                // 獲取new和old的值
                var newValue = watch.getNewValue(this);
                var oldValue = watch.last;

                // 關鍵來了,進行髒檢查
                if (newValue !== oldValue) {
                    watch.listener(newValue, oldValue);
                    watch.last = newValue;
                    dirty = true;
                }

                // list[i].listener();
            }
        }

    }
複製代碼

那我問一個問題,爲何我要寫兩個 watch 對象?

很簡單,若是我在 first 中改變了 second 的值,在 second 中改變了 first 的值,這個時候,會出現無限循環調用。

那麼,AngularJS 是如何避免的呢?

$scope.prototype.$digest = function() {
        var list = this.$$watchList;
        // 判斷是否髒了
        var dirty = true;
        // 執行次數限制
        var checkTime = 0;
        while (dirty) {
            dirty = false;
            for (var i = 0; i < list.length; i++) {
                // 獲取watch對應的對象
                var watch = list[i];
                // 獲取new和old的值
                var newValue = watch.getNewValue(this);
                var oldValue = watch.last;

                // 關鍵來了,進行髒檢查
                if (newValue !== oldValue) {
                    watch.listener(newValue, oldValue);
                    watch.last = newValue;
                    dirty = true;
                }

                // list[i].listener();
            }
            checkTime++;
            if (checkTime > 10 && checkTime) {
                throw new Error("次數過多!")
            }
        }

    }
複製代碼
scope.$watch('first', function(newValue, oldValue) {
    scope.second++;
    console.log("new: " + newValue + "=========" + "old: " + oldValue);
})

scope.$watch('second', function(newValue, oldValue) {
    scope.first++;
    console.log("new2: " + newValue + "=========" + "old2: " + oldValue);
})
複製代碼

這個時候咱們查看控制檯,發現循環了10次以後,拋出了異常。

這個時候,髒檢查機制已經實現,是時候將這個與第一段代碼進行合併了,3.0 代碼橫空出世。

window.onload = function() {
 'use strict';
    function Scope() {
        this.$$watchList = [];
    }

    Scope.prototype.getNewValue = function() {
        return $scope[this.name];
    }

    Scope.prototype.$watch = function(name, listener) {
        var watch = {
            name: name,
            getNewValue: this.getNewValue,
            listener: listener || function() {}
        };

        this.$$watchList.push(watch);
    }

    Scope.prototype.$digest = function() {
        var dirty = true;
        var checkTimes = 0;
        while (dirty) {
            dirty = this.$$digestOnce();
            checkTimes++;
            if (checkTimes > 10 && dirty) {
                throw new Error("循環過多");
            }
        }
    }

    Scope.prototype.$$digestOnce = function() {
        var dirty;
        var list = this.$$watchList;
        for (var i = 0; i < list.length; i++) {
            var watch = list[i];
            var newValue = watch.getNewValue();
            var oldValue = watch.last;
            if (newValue !== oldValue) {
                watch.listener(newValue, oldValue);
                dirty = true;
            } else {
                dirty = false;
            }

            watch.last = newValue;
        }
        return dirty;
    }


    var $scope = new Scope();
    $scope.sum = 0;
    $scope.data = 0;
    $scope.increase = function() {
        this.data++;
    };
    $scope.decrease = function() {
        this.data--;
    };
    $scope.equal = function() {

    };
    $scope.faciend = 3
    $scope.$watch('data', function(newValue, oldValue) {
        $scope.sum = newValue * $scope.faciend;
        console.log("new: " + newValue + "=========" + "old: " + oldValue);
    });

    function bind() {
        var list = document.querySelectorAll('[ng-click]');
        for (var i = 0, l = list.length; i < l; i++) {
            list[i].onclick = (function(index) {
                return function() {
                    var func = this.getAttribute('ng-click');
                    $scope[func]($scope);
                    $scope.$digest();
                    apply();
                }
            })(i)
        }
    }

    function apply() {
        var list = document.querySelectorAll('[ng-bind]');
        for (var i = 0, l = list.length; i < l; i++) {
            var bindData = list[i].getAttribute('ng-bind');
            list[i].innerHTML = $scope[bindData];
        }
    }

    bind();
    $scope.$digest();
    apply();
}
複製代碼

頁面上將 合計 放開,看看會有什麼變化。

這就是 AngularJS髒檢查機制的實現,固然,Angular 裏面確定比我要複雜的多,可是確定是基於這個進行功能的增長,好比 $watch 傳的第三個參數。

技術發展

如今 Angular 已經發展到了 Angular6,可是谷歌仍然在維護 AngularJS,並且,並不必定框架越新技術就必定越先進,要看具體的項目是否適合。

好比說目前最火的 React ,它採用的是虛擬DOM,簡單來講就是將頁面上的DOMJS裏面的虛擬DOM進行對比,而後將不同的地方渲染到頁面上去,這個思想就是AngularJS的髒檢查機制,只不過AngularJS是檢查的數據,React是檢查的DOM而已。

相關文章
相關標籤/搜索