2013年11月28日 星期四

AngularJS 玩弄手札 - 小心 filter 挖了個坑 然後在上面鋪上一些草皮

NG 很神 很淫蕩

但是不會有什麼東西只有好沒有壞

像是 filter 在不了解的情況下 就很容易不知不覺中招





首先先認清一下以下幾點事實





NG 神奇的 two-way binding 是靠 dirty check

dirty check 骨子裡是靠這頁 http://docs.angularjs.org/guide/scope 最下面的 digest loop 達成的

簡單來說

在 NG "裡面"(像是 controller)

當修改了 $scope 裡面的值

NG 會自動觸發 digest loop

然後自動幫我們更新 view 的 DOM



然而 在 NG "外面"(像是 jquery click)

即使修改了 $scope 裡面的值

NG 也"不會"自動觸發 digest loop

所以也"不會"自動幫我們更新 view 的 DOM

"但是"他會在下一次有人觸發 digest loop 的時候一起被更新



註:

一般 在NG外面 我們可以靠 $(element).scope() 取得 scope 物件

再靠 scope.$apply() 觸發 digest loop





再歸納一下上述事實要點

1. 天下沒白吃的午餐 神奇的魔法 two-way binding 這麼猛 是因為靠 dirty check

在我們觸發一些動作 事件 或 行為的時候 做檢查


2. 沒進入 digest loop 即使 scope 被滿門抄斬了 view 還在發夢





別忘了 今天要講的 好基友 filter





首先 filter 在大多數情況下非常好用 像是 formatter 跟 formatter 還有 formatter

沒錯 這意味著 當他被用在 非 formatter 的用途的時候 會變得有點糟糕

尤其是他跟另外一位 好基友 ngRepeat 一起撿肥皂的時候 更是隱藏殺機





下面是段很常見的應用 一個 array 搭 ngRepeat 產 view 然後 透過 filter 篩選項目

http://jsbin.com/oQumafE/2/embed?html,output

原本應該是這個

$scope.array = [
    { Name: '蓋倫', Title: '哥有四多藍' },
    { Name: '嘉文四世', Title: '坑爹的' },
    { Name: '趙信', Title: '長槍依在 菊花拿來' },
    { Name: '泰達米爾', Title: '五秒真男人' },
    { Name: '潘森', Title: '空降斯巴達' }
];

該死的 js逼 最近竟然不能放中文 ZZZ





為啥 filter 可以自動過濾 然後 改變 view ?

很顯然 他有進入 digest loop

不信邪 用這個可以試試

$scope.$watch(function () {
    console.log('digest!!!');
});


那 filter 又是怎樣知道 什麼時候該做過濾的檢查?

答案就是 他不知道

那...

沒錯 任何一次的 digest 都會讓 filter 跑一遍 他的 function

而且他可能不只跑一遍





所以我們有意無意的更新 scope 都會 瘋狂的跑 filter

要是我們 還不小心用了 跟 鍵盤 滑鼠 視窗 焦點 之類有關的 會猛觸發的東東

這下就悲劇了

http://jsbin.com/IseQIgo/2/embed?html,console,output



要是我們 還不小心用了 在 filter 裡面又 有意無意的更新 scope 那更慘了

這下就靠悲了

digest 觸發 filter 檢查 filter 又再觸發 digest

http://jsbin.com/UXiZawo/1/embed?html,console,output



甚至條件式隨機的 每次 return 的陣列都不同 也GG (其實我只想要每次看到隨機的資料而已呀 Q_Q)

http://jsbin.com/etojAYe/1/embed?html,console,output



就算上述 挑肛 的這些白目事都沒發生 下面這個一定天天上演

"邏輯有點兒牛逼 條件有些兒糾結 運算有點兒溫吞"

view 就會像被咬住一般 卡卡的 不太能動 無法回應

然後就是定番 電話響瞭~ 客戶怒瞭~ 奇摩幾差瞭~

其實這不是 電腦慢 瀏覽器渣 而是我們的 filter 掃得太快啦 消化不良啦





結論

1. 當我們想要將 filter 用在 非 formatter 用途上的邏輯判斷 最好三思而後行

2. 小心 好基友 ngRepeat

3. 任何細小的風吹草動 都會造成 filter 瘋狂掃射

4. 想知道 filter 到底有多快槍 可以看看 歪果神人大大文章 How Often Do Filters Execute In AngularJS





現在我們知道哪邊有坑了 但是... 還沒有越過呀 需求仍未解





小伎倆

1. 設法把複雜噁心的 "過濾" 邏輯判斷 留在 controller 裡面

2. 主動控制 觸發時機 才進行 "過濾" 邏輯判斷

3. 真的沒技 就用 $scope.$watch() 吧

雖然 watch 多了也不好 至少在 watch 裡面可以判斷 "有其必要性" 才進行 "過濾" 邏輯判斷

4. 留心那些 機關槍事件 設法實作 debounce (像這個)防止短時間內 "過濾" 邏輯判斷 被掃射

5. 打破 NG 崇拜 其實真的沒有必要任何功能都用 NG 實作

雖然死忠狂熱分子 視 J蛞蝓 如大敵 無法忍受 用了 NG 還出現 J蛞蝓

但是顯而易見 沒有銀彈 任何東西 都有長短處

NG 強在 使用 model 來 two way binding 的概念 (這是讓我們能夠快速開發的主要火力)

但是有些功能實在不必 NG 像是 廣告輪播、DOM 特效 或者其他任何 "無須" 做資料交換 對應顯示 的功能





光說不練 LIKE 政O 不是好漢 放兩個 陽春 debounce 實作



隨機條件 (原本會因為 每次結果都不同 造成 digest 10 error)


<!DOCTYPE html>
<html ng-app="app">
<head>
    <script src="Scripts/underscore.js"></script>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.2/jquery.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
    <meta charset="utf-8" />
    <title>JS Bin</title>
</head>
<body>
 
    <div ng-controller="ctrl">

        <input type="text" ng-model="keyWord" />

        <ul>
            <li ng-repeat="item in array | random">
                <span>[{{item.Name}}] {{item.Title}}</span>
            </li>
        </ul>

        <input type="button" value="jquery outside apply" class="outside" />

        <input type="button" ng-click="onClick()" value="click me" />

        <input type="button" ng-mouseover="onMouseover()" value="mouseover me" />

        <input type="button" ng-mouseenter="onMouseenter()" value="mouseenter me" />

        <input type="button" ng-mouseleave="onMouseleave()" value="mouseleave me" />

    </div>

    <script>
        angular.module('app', [])
            .controller('ctrl', function ($scope) {

                $scope.array = [
                     { Name: 'Garen', Title: 'I got four Doran' },
                     { Name: 'Jarvan IV', Title: 'Pit his father' },
                     { Name: 'Xin Zhao', Title: 'Hold pike, ass back' },
                     { Name: 'Tryndamere', Title: 'Five seconds real men' },
                     { Name: 'Pantheon', Title: 'Airborne Spartans' }
                ];

                $scope.onClick = function () {
                    //TODO Something...
                };

                $scope.onMouseover = function () {
                    //TODO Something...
                };

                $scope.onMouseenter = function () {
                    //TODO Something...
                };

                $scope.onMouseleave = function () {
                    //TODO Something...
                };
            })
            .factory('debounceFactory', function () {
                return function (predicate, wait) {
                    var debounce;
                    var arr = [];
                    return function (data) {
                        clearTimeout(debounce);
                        debounce = setTimeout(function () {
                            arr.length = 0;
                        }, wait);
                        if (arr.length === 0) {
                            for (var i = 0; i < data.length; i++) {
                                if (predicate(data[i])) {
                                    arr.push(data[i]);
                                }
                            }
                        }
                        return arr;
                    };
                }
            })
            .filter('random', function (debounceFactory) {
                var counter = 0;
                var fn = debounceFactory(function (x) {
                    return ~~(Math.random() * 2) === 0;
                });
                return function (data) {
                    console.log(++counter);
                    return fn(data);
                };
            });
    </script>
    <script>
        $('.outside').click(function () {
            var scope = $('[ng-controller="ctrl"]').scope();

            scope.onClick();

            scope.$apply();
        });
    </script>
</body>
</html>



變動原本物件 (原本會因為 scope 連動 造成 digest 10 error)


<!DOCTYPE html>
<html ng-app="app">
<head>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.2/jquery.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
    <meta charset="utf-8" />
    <title>JS Bin</title>
</head>
<body>
 
    <div ng-controller="ctrl">

        <input type="text" ng-model="keyWord" />

        <ul>
            <li ng-repeat="item in array | tick">
                <span>[{{item.Name}}] {{item.Title}}</span>
            </li>
        </ul>

        <input type="button" value="jquery outside apply" class="outside" />

        <input type="button" ng-click="onClick()" value="click me" />

        <input type="button" ng-mouseover="onMouseover()" value="mouseover me" />

        <input type="button" ng-mouseenter="onMouseenter()" value="mouseenter me" />

        <input type="button" ng-mouseleave="onMouseleave()" value="mouseleave me" />

    </div>

    <script>
        angular.module('app', [])
            .controller('ctrl', function ($scope) {

                $scope.array = [
                     { Name: 'Garen', Title: 'I got four Doran' },
                     { Name: 'Jarvan IV', Title: 'Pit his father' },
                     { Name: 'Xin Zhao', Title: 'Hold pike, ass back' },
                     { Name: 'Tryndamere', Title: 'Five seconds real men' },
                     { Name: 'Pantheon', Title: 'Airborne Spartans' }
                ];

                $scope.onClick = function () {
                    //TODO Something...
                };

                $scope.onMouseover = function () {
                    //TODO Something...
                };

                $scope.onMouseenter = function () {
                    //TODO Something...
                };

                $scope.onMouseleave = function () {
                    //TODO Something...
                };
            })
            .factory('debounceFactory', function () {
                return function (predicate, wait) {
                    var debounce;
                    var arr = [];
                    return function (data) {
                        clearTimeout(debounce);
                        debounce = setTimeout(function () {
                            arr.length = 0;
                        }, wait);
                        if (arr.length === 0) {
                            for (var i = 0; i < data.length; i++) {
                                if (predicate(data[i])) {
                                    arr.push(data[i]);
                                }
                            }
                        }
                        return arr;
                    };
                }
            })
            .filter('tick', function (debounceFactory) {
                var counter = 0;
                var fn = debounceFactory(function (x) {
                    x.Name = x.Name.split('').reverse().join('');
                    x.Title += ' la~';
                    return true;
                });
                return function (data) {
                    console.log(++counter);
                    return fn(data);
                };
            });
    </script>
    <script>
        $('.outside').click(function () {
            var scope = $('[ng-controller="ctrl"]').scope();

            scope.onClick();

            scope.$apply();
        });
    </script>
</body>
</html>

2013年11月14日 星期四