Devil May Code...

Vergil's Blog

前言

一般来说,AJAX请求与普通HTTP请求没有任何的区别。但jQuery框架会在Request header加上X-Requested-With:XMLHttpRequest

但这并不是一个标准,所以并非所有AJAX请求都会发送这个request header。原生的XMLHttpRequest对象就没有这个东西。

AngularJS的$http服务也不会发送这个header。

由于jQuery的流行,很多PHP框架会以此来判断这个请求是否AJAX。

例如:

LaravelIlluminate\Http\Request::ajax()方法。该类继承自Symfony框架的Symfony\Component\HttpFoundation\Request并使用了它的isXmlHttpRequest()来判断是否AJAX请求。

国内著名的ThinkPHP框架,IS_AJAX常量也是以此来判断是否AJAX。

define('IS_AJAX', ((isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') 

阅读剩余部分...


上一篇博文提到,是由于jQuery.val()方法设置DOM元素的值的时候,并没有触发AngularJS的$digest循环。然后我写了一个directive来解决这个问题。

那是否可以有一个更加通用的方法,让AngularJS适应其他使用val()方法设置值的jQuery插件呢?如何让val()自动触发DOM事件??

jQuery Hooks

豆浆油条求助,得知jQuery有一个API,称为“hook(钩子)”(怪自己不去看官方文档)。

Hooks可以使用在val()css()attr()prop()这些getter/setter方法上。如果你设置了hook,当你调用这些函数的时候,jQuery会触发你hook定义的方法。

Hooks签名如下:

var someHook = {
    get: function(elem) {
        // obtain and return a value
        return "something";
    },
    set: function(elem, value) {
        // do something with value
    }
}

.val() Hooks

valHooks允许我们拦截.val()的功能

HTML:

<input type="text" id="t" />

JavaScript:

$.valHooks.text = {
    set: function(elem, val) {
        elem.value = 'hello  ' + val;
        return true;
    },
    get: function(elem) {
        return 'fuck';
    }
}

$('#t').val('world!');      //设置值为:hello world!
$('#t').val();              //返回:fuck

.css() Hooks

以后补充

.prop()和.attr() Hooks

以后补充

使用valHooks让jQuery.val()与AngularJS模型数据同步

知道Hooks之后,我们就可以拦截.val()的功能,在设置好值之后,触发一个让AngularJS进入$degist的情况。比如,触发input事件

Example:

<!doctype html>
<html ng-app="app">
    <head>
        <script src="http://cdn.bootcss.com/jquery/2.2.0/jquery.min.js"></script>
        <script src="http://cdn.bootcss.com/angular.js/1.4.8/angular.min.js"></script>
        <script>
            $(function(){
                $('#setvalue').on('click', function() {
                    $('#input').val('hello world!');
                });

                $.valHooks.text = {
                    set: function(elem, val) {
                        elem.value = val;   //把DOM元素设置为传进来的val值
                        $(elem).trigger('input');  //触发input事件
                        return true;
                    }
                };
            });
        </script>
    </head>
    <body>
        <div>
            <input type="text" ng-model="value" id="input" />
            <button type="button" id="setvalue">jQuery set value</button>
            <p>ng model value is:{{value}}</p>
        </div>
    </body>
</html>

OK,那么现在,使用val()方法设置元素的值之后,也可能让AngularJS的模型同步了。

一般情况下,这样子确实可以解决问题,但如果你不想让它触发input事件呢?比如说,你的input事件有另外的功能。

我的思路就是,触发一个自定义事件,写一个directive监听该事件。

Example:

<!doctype html>
<html ng-app="app">
    <head>
        <script src="http://cdn.bootcss.com/jquery/2.2.0/jquery.min.js"></script>
        <script src="http://cdn.bootcss.com/angular.js/1.4.8/angular.min.js"></script>
        <script>
            $(function(){
                $('#setvalue').on('click', function() {
                    $('#input').val('hello world!');
                });

                $.valHooks.text = {
                    set: function(elem, val) {
                        elem.value = val;
                        $(elem).trigger('jquery.set.value');    //触发自定义事件
                        return true;
                    }
                };
            });
        </script>
    </head>
    <body>
        <div>
            <!-- 注意这个input标签使用了jqSetValue组件 -->
            <input type="text" ng-model="value" id="input" jq-set-value />
            <button type="button" id="setvalue">jQuery set value</button>
            <p>ng model value is:{{value}}</p>
        </div>
        <script>
            angular.module('app', []).directive('jqSetValue', function() {
                return {
                    restrict: 'AC',
                    require: '?ngModel',
                    link: function(scope, element, attrs, ngModel) {
                         //设置自定义事件的回调函数
                        element.on('jquery.set.value', function(event) {
                            if(!ngModel) return;
                            scope.$apply(function(){
                                ngModel.$setViewValue($(element).val());
                            });
                        });
                    }
                };
            });
        </script>
    </body>
</html>

       在AngularJS的术语中,把检测模型变化的过程称为$digest循环。这个名字来源于Scope实例上的$digest方法。这种方法被作为$apply中的重要一步来调用,它会检测注册在所有作用域上的所有监视对象。

       为什么AngularJS中要有$digest循环呢?它又是如何判断模型发生变化的?$digest循环的存在是为了解决下面的两个问题。

  • 判定模型的哪些部分发生变化,以及DOM中的哪些属性应该被更新。这一步的目的是让检测模型变化的过程对开发者保持尽可能的简单。在开发时,我们只需要修改属性值,之后AngularJS指令会自动找出网页中哪些部分应该被重绘。
  • 减少不必要的重绘以提升应用性能,避免UI界面闪烁。为了实现这点,AngularJS会将DOM重绘尽可能推迟到最后一刻,此时模型趋于稳定(所有模型值都已完成运算,时刻准备驱动UI重绘)。

       要理解AngularJS如何实现这种效果,首先要明白web浏览器只有一个UI线程。浏览器中当然还有其他线程(如负责网络相关操作的线程),但只有一个线程用于渲染DOM元素、监视DOM事件,以及执行JavaScript代码。浏览器不停地在JavaScript执行环境和DOM渲染环境之间切换。

       AngularJS确保在将控制权交还给DOM渲染环境之前,所有的模型值都已完成运算且已“稳定”。这种方法保证了UI一次性完成重绘,而不会为了响应某个单独的模型变化而不停重绘。这保证了更快的执行速度(因为运行环境的切换很少)和更好的视觉效果(所有重绘一次完成)。每个单独的模型属性变化都出发一次UI重绘会让界面变得很慢,而且会出现明显的闪烁。

解剖$watch

       AngularJS使用脏检查(dirty checking)机制来判定某个模型值是否真正发生了变化。脏检查的工作机制是将之前保存的模型值和能导致模型发生变化的事件(DOM事件、XHR事件等)发生后计算的新模型值做对比。

       再来看一下,注册一个新的模型监视基本语法如下:

$scope.$watch(watchExpression, modelChangeCallback);

       当作用域添加一个新的$watch时,AngularJS会运算watchExpression表达式,然后在内部将运算所得的值存储起来。紧接着进入$digest循环,watchExpression会被再次运算,运算所得的新值会和之前保存的值进行对比。modelChangeCallback只会在新值与旧值不同时才会执行。这个新值也会被保存起来以备下一次对比使用,整个过程可以一直这样持续下去。

       作为开发者,我们对自己手动注册的监视(watches)很清楚。但我们还要明白任何指令(AngularJS的内置核心指令和任何第三方指令)都可以设置自己的监视。任何插值表达式({{wapression}})也会在作用域上注册一个新的监视。

模型的稳定性

       如果模型上的任何一个监视器都检测不到任何变化,AngularJS就认为该模型是稳定的(此时就可以进行UI渲染工作了)。只要一个监视器的一个变化,就足以使整个$digest循环变“脏”(dirty),迫使AngularJS进入下一轮循环。此时使用了“一颗老鼠屎坏了一锅汤”的原则,当然这么做是有充足的理由的。

       AngularJS会持续执行$digest循环,反复运算所有作用域上的所有监视,知道没有发现任何变化为止。连续几轮$digest循环是必要的,因为模型监视回调会有一些副作用。如果只是简单地设置一个回调,当见识的模型值变化时执行,那就可能改变我们已经运算过且认为是稳定的那些模型。

       我们来考虑一个简单的表单例子,该表单有两个字段Start和End。在这个表单中结束时间(End)应该永远晚于开始时间(Start)。

<div>
    <form>
        Start date: <input ng-model="startDate">
        End date: <input ng-model="endDate">
    </form>
</div>

       为了保证在模型中endDate永远晚于StartDate,可以注册一个监视,代码如下:

function oneDayAhead(dateToIncrement){
    return dateToIncrement.setDate(dateToIncrement.getDate() + 1);
}

$scope.$watch('startDate', function(newValue){
    if(newValue <= $scope.startDate) {
        $scope.endDate = oneDayAhead($scope.startDate);
    }
});

       在上面的监视中,我们让两个模型值之间产生一个依赖关系,其中一个模型的变化(startDate)可以触发另一个变化。这个例子就能说明模型变化回调的副作用,一个模型变化时可能会让另一个已被认为“稳定”的模型值也发生变化。

       对脏检查算法的深入理解,会让你明白一个watchExpression在每次$digest循环中都会被至少运算两次。可以通过创建下面代码来验证这一点:

<input ng-model="name">
{{getName()}}

       getName()是在作用域上的一个函数

$scope.getName = function(){
    console.log('dirty-checking');
    return $scope.name;
}

       如果运行上面的代码,注意观察控制台的输出,就会发现<input>的每次变化都会输出两条日志。

任何一个$digest循环至少都会运行一次,一般情况下会运行两次。这意味着每一个被监视的表达式在每个$digest循环中都会被运算两次(在浏览器离开JavaScript环境转向UI渲染之前)

不稳定的模型

有些情况下,$digest执行两次循环也不足以确定模型的稳定性。更糟糕的是,有可能会出现永远无法确定模型稳定性的模型的情况!我们来看下面的例子:

<span>Random Value: {{random()}}</span>

       random()函数定义如下:

$scope.random = Math.random;

       监视表达式等于Math.random(),这样会(极大可能)导致每次$digest循环运算所得的值都不一样。这表示每次检查的结果都是“脏”的,需要启动新一轮检查。这会导致一遍又一遍地循环,直到AngularJS认为这个模型是“不稳定”的,然后强制跳出$digest循环。

AngularJS默认最多会执行10次循环,之后就会声明该模型是不稳定的,然后中断$digest循环。

       在中断$digest之后,AngularJS会抛出一个错误(使用$exceptionHandler服务来处理,该服务用来在控制台记录错误)。抛出的错误中会包含5个最新的不稳定的监视信息(其中含有这些表达式的新值和旧值)。大多数情况下只会有一个不稳定的监视模型,所以很容易找出罪魁祸首。

       在$digest循环被中止之后,JavaScript线程就会离开“AngularJS世界”。此时,没有什么能够阻挡浏览器对渲染的向往。之后,用户就会看到使用$digest循环最后一次运行所得到的模型值渲染的页面。

即使$digest循环运行超过10次上限,页面还是会被渲染。在你观察控制台日志之前,这种错误很难定位,所以它可能会被忽略一段时间。即便如此,还是应该追踪并解决与不稳定模型有关的问题

$digest循环和作用域的层级

        每次$digest循环都会从$rootScope开始,重新计算所有作用域上的所有监视表达式。这一眼看上去有点违反直觉,有些人可能会说只要重新计算指定作用域及其子作用域上的表达式足矣。不幸的是,这样做会导致UI和模型不同步。原因就是子作用域上的变化可能会影响父作用域上的变。请看下面的例子,含有两个作用域(一个是$rootScope,另一个是ng-controller指令创建的):

<body ng-app ng-init="user = {name: 'Superhero'}">
    Name in parent: {{user.name}}
    <div ng-controller="ChildCtrl">
        Name in child; {{user.name}}
        <input ng-model="user.name">
    </div>
</body>

       模型(user.name)的变化是被子作用域(ng-controller)触发的,但是这个变化也改变了$rootScopr上对应的属性。

       这种情况迫使AngularJS次那个$rootScope开始逐级往下(使用深度优先遍历)运算所有的监视表达式。如果AngularJS只是运算某个作用域上(加上它的子作用域)的监视表达式,那么,一旦模型有变化,应用中就存在模型和实际显示不同步的风险,比如我们讨论的这个例子, Name in Parent:{{user.name}}中的插值表达式就不会被运算,显示结果也就不同步。

在每次$digest循环中,AngularJS都需要运算所有作用域上的所有监视表达式。这种过程从$rootScope开始,然后以深度优先便利所有子作用域。


摘自《精通AngularJS》 11.2理解AngularJS的内部运作机制


       当AngularJS首次向公众发布之后,就有许多关于它的模型变化监控算法的“阴谋论”。其中最被津津乐道的一种是,怀疑AngularJS使用了某种轮询机制。这种机制可能每隔一小段时间就去检查模型值的变化,如果发现变化,就重绘DOM。所有这些猜测都是错误的。

AngularJS没有使用任何的轮询算法来定期检查模型变化

       AngularJS的模型变化监控机制背后的思路其实是“善后”(Observeat the end of the day),因为引发模型变化的情况是有限的。这些情况包括:

  • DOM事件(例如,用户修改了input的值,然后点击一个按钮,调用了一个JavaScript函数或执行其他操作);
  • XHR相应触发回调;
  • 浏览器的地址变化;
  • 计时器(setTimeout, setInterval)触发回调。

       的确,如果上面的任何一种情况都未发生(用户与页面没有发生任何交互,没有任何XHR响应,没有计时器结束),那么监控模型的变化是没有意义的。此时页面上什么事情也没发生,模型也没有变化,重绘DOM也就毫无必要了。

       AngularJS只会在被明确告知的情况下才会启动它的模型监控机制。为了让这种复杂的机制运作起来,需要在scope对象上执行$apply方法。

       回到之前的simple-model执行,可以在input值每次发生变化后执行$apply方法(这样每次按键的变化都会传播给模型。这也是ng-model执行的模型行为)。

.directive('simpleModel', function($parse){
    return function(scope, element, attrs){
        var modelGetter = $parse(attrs.simpleModel);
        var modelSetter = modelGetter.assign;

        //从model更新DOM
        scope.$watch(modelGetter, function(newVal, oldVal){
            element.val(newVal);
        });

        //从DOM更新model
        element.bind('input', function(){
            scope.$apply(function(){
                modelSetter(scope, element.val());
            });
        });
    }
});

       使用AngularJS的内置指令时,我们完全无需调用$apply方法,就可以看到很多“奇迹”,这一点很让人惊讶。但实际上,内置指令的实现代码中调用了$apply方法。也就是说,标准的指令和服务已经帮我们处理好了模型变化的监控工作($http、$timeout、$location等)。

当在一个作用域上调用$apply方法后,AngularJS就会启动模型变化监控机制。在网络通信、DOM事件、JavaScript计时或浏览器地址发生变化之后,AngularJS标准的服务和指令就会调用$apply方法。


摘自《精通AngularJS》 11.2理解AngularJS的内部运作机制


Powered by Typecho.