手写AngularJS脏检查机制

什么是脏检查

View -> Model

浏览器提供有User Event触发事件的API,例如,clickchangejavascript

Model -> View

浏览器没有数据监测APIAngularJS 提供了 $apply()$digest()$watch()css

其余数据双向绑定介绍

VUE

{{}} Object.defineProperty() 中使用 setter / getter 钩子实现。html

Angular

[()] 事件绑定加上属性绑定构成双向绑定。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 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而已。

相关文章
相关标签/搜索