瀏覽器提供有User Event
觸發事件的API
,例如,click
,change
等javascript
瀏覽器沒有數據監測API
。 AngularJS
提供了 $apply()
,$digest()
,$watch()
。css
{{}}
Object.defineProperty()
中使用 setter
/ getter
鉤子實現。html
[()]
事件綁定加上屬性綁定構成雙向綁定。java
你們先看運行效果,運行後,點增長,數字會+1,點減小,數字會-1,就是這麼一個簡單的頁面,視圖到底爲什麼會自動更新數據呢?瀏覽器
我先把最粗糙的源碼放出來,你們先看看,有看不懂得地方再議。markdown
老規矩,初始化頁面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
版本框架
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 listener
和 I 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: undefined
和 new2: 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
,簡單來講就是將頁面上的DOM
和JS
裏面的虛擬DOM
進行對比,而後將不同的地方渲染到頁面上去,這個思想就是AngularJS
的髒檢查機制,只不過AngularJS
是檢查的數據,React
是檢查的DOM
而已。