2017年12月5日 星期二

以 proxy 為主的 react 只適用於 backbone

(function (Backbone) {
    var hasProto = ('__proto__' in {});
    /*========================================================================*/
    //    debugger;

    var arrayProto = Array.prototype;
    var arrayMethods = Object.create(arrayProto);

    var arrayMethodsNameList = [
        'push',
        'pop',
        'shift',
        'unshift',
        'splice',
        'sort',
        'reverse'
    ];

    (function () {
        arrayMethodsNameList.forEach(function (method) {

            // cache original method
            var original = arrayProto[method];

            def(arrayMethods, method, function mutator() {
                debugger;
                var ob = this._bc_ob_;

                ob.arrayMethod = method;

                var args = [];
                var len = arguments.length;
                var canTrigger = true;

                while (len--) {
                    args[len] = arguments[len];
                }
                // 遇到 [undefined].pop() 會出問題
                var result = original.apply(this, args);

                var add = [];
                var remove = [];

                debugger;

                switch (method) {
                    case 'push':
                    case 'unshift':
                        add = args;
                        break;
                    case 'pop':
                    case 'shift':
                        remove = result;
                        break;
                    case 'splice':
                        add = args.slice(2);
                        remove = result;
                        break;
                }

                if (!Array.isArray(add)) {
                    add = (add === undefined ? [] : [add]);
                }

                if (!Array.isArray(remove)) {
                    remove = (remove === undefined ? [] : [remove]);
                }
                // 為了 proxy 而設
                ob.arrayMethod = '';

                ob.notify_1([method], {
                    add: add,
                    remove: remove,
                    key: undefined
                });


                return result;
            });
        });
    })();

    var arrayKeys = Object.getOwnPropertyNames(arrayMethods);

    /*========================================================================*/
    // Define a property
    // 預設是不可枚舉
    function def(obj, key, val, enumerable) {
        Object.defineProperty(obj, key, {
            value: val,
            enumerable: !!enumerable,
            writable: true,
            configurable: true
        });
    };
    /*========================================================================*/
    function hasOwn(obj, key) {
        return Object.prototype.hasOwnProperty.call(obj, key);
    }
    /*========================================================================*/
    /**
     * Quick object check - this is primarily used to tell
     * Objects from primitive values when we know the value
     * is a JSON-compliant type.
     */
    function isObject(obj) {
        return (obj !== null && typeof obj === 'object');
    }
    /*========================================================================*/
    // Augment an target Object or Array by intercepting
    // the prototype chain using __proto__
    function protoAugment(target, src, keys) {
        /* eslint-disable no-proto */
        target.__proto__ = src;
        /* eslint-enable no-proto */
    };
    /*========================================================================*/
    // Augment an target Object or Array by defining
    // hidden properties.
    function copyAugment(target, src, keys) {
        debugger;
        for (var i = 0, l = keys.length; i < l; i++) {
            var key = keys[i];
            def(target, key, src[key]);
        }
    };
    /*========================================================================*/
    /**
     * Attempt to create an observer instance for a value,
     * returns the new observer if successfully observed,
     * or the existing observer if the value already has one.
     */
    function _observe(value, parent) {
        debugger;
        if (!isObject(value)) {
            return value;
        }
        var ob;
        if (hasOwn(value, '_bc_ob_') && value._bc_ob_ instanceof Backbone.Observer) {
            ob = value._bc_ob_;

        } else if ((Array.isArray(value) || _.isPlainObject(value)) && Object.isExtensible(value)) {
            value = Backbone.Reactive(value, parent.options);
            ob = value._bc_ob_;
        }

        ob.$link(parent);

        return value;
    }


    ////////////////////////////////////////////////////////////////////////////
    Backbone.$buildBackbone(factory);
    ////////////////////////////////////////////////////////////////////////////
    //
    // Backbone.Reactive
    //
    // 事件有(sort, update, remove(model發出), reset)
    //
    ////////////////////////////////////////////////////////////////////////////
    function factory(root, Backbone, _, $) {
        // here
        Backbone.Reactive = makeDataReactive;

        function makeDataReactive(value, options) {
            // debugger;


            if (value == null || (typeof value !== 'object')) {
                throw new Error('cant be observer');
            }
            /*--------------------------*/
            var observe = new Backbone.Observer(value, options);

            def(value, '_bc_ob_', observe);

            // debugger;
            /*--------------------------*/

            var proxy;
            if (Array.isArray(value)) {
                var augment = hasProto ? protoAugment : copyAugment;

                // 替換 [] 的原型方法
                augment(value, arrayMethods, arrayKeys);

                // 插入 proxy
                proxy = new Proxy(value, {
                    set: function (target, key, value, p) {
                        debugger;

                        var ob = target._bc_ob_;
                        var eventName = 'change';
                        var eventNameList = [];

                        var _value = _observe(value, ob);
                        /*====================================*/
                        if (key === 'length') {
                            eventName = 'reset';
                        }
                        /*====================================*/
                        target[key] = _value;

                        eventNameList.push(eventName);

                        if (ob.arrayMethod) {
                            eventNameList.push(ob.arrayMethod);
                        }
                        /*--------------------------*/
                        if (!ob.arrayMethod) {
                            ob.notify_2(key, eventNameList, {
                                key: key,
                                add: [],
                                remove: []
                            });
                        }

                        return true;
                    }
                });

                observe.proxy = proxy;
                observe.$walkArray(value);

            } else {
                proxy = new Proxy(value, {
                    set: function (t, k, v, p) {
                        debugger;

                        var ob = t._bc_ob_;
                        var prevValue = t[k];

                        var newValue = _observe(v, ob);
                        t[k] = newValue

                        ob.notify_2(t, ['change'], {
                            key: k
                        });

                        return true;
                    }
                });
                observe.proxy = proxy;
                observe.$walkPlainObject(value);
            }
            return proxy;
        }
        ////////////////////////////////////////////////////////////////////////
        (function (fn) {
            fn.del = function () {

            };
        })(makeDataReactive);
        ////////////////////////////////////////////////////////////////////////////
        //
        // 數據的背後觀察者
        // 放在數據的(_bc_ob_)裏,並且不可枚舉
        //
        // 要加入 controller 找數據底下的 _bc_ob_
        //
        ////////////////////////////////////////////////////////////////////////////
        Backbone.Observer = Observer;

        function Observer(value, options) {
            this.cid = _.uniqueId('observer');
            /*--------------------------*/
            this.triggerOptions = {};
            /*--------------------------*/
            this.$linkParent = new Map();
            /*--------------------------*/
            this.proxy;
            this.value; // 只能是 object
            this.prevValue;
            /*--------------------------*/
            this.keys = []; // 偵測 remove
            this.prevKeys = [];
            /*--------------------------*/
            this.$listenerList = [];
            this.arrayMethod = '';
            this.eventName; // 只有是 array 的方法才會用到
            this.options = {};
            /*--------------------------*/
            this.$prevChildList = [];
            (function () {
                // debugger;

                this.options = options || {};

                this.value = value;
                this.keys = Object.keys(this.value);

                this.$cloneValue();
            }).call(this);
        };

        _.extend(Observer.prototype, Backbone.Events);

        ////////////////////////////////////////////////////////////////////////

        (function () {

            // proxy
            this.$walkPlainObject = function (obj) {

                var observe = obj._bc_ob_;

                for (var key in obj) {
                    var child = obj[key];
                    var newValue = _observe(child, observe);
                    obj[key] = newValue
                }
            };
            /*====================================================================*/
            /**
             * Observer a list of Array items.
             */
            this.$walkArray = function observeArray(items) {
                var observe = items._bc_ob_;
                for (var i = 0, l = items.length; i < l; i++) {

                    var child = items[i];
                    var newValue = _observe(child, observe);
                    items[i] = newValue;
                }
            };
            /*================================================================*/


        }).call(Observer.prototype);


        (function () {
            this.$link = function (parent) {
                // debugger;

                if (!this.$linkParent.has(parent) && this.options.link) {
                    this.$linkParent.set(parent, true);
                    this.options.link.call(this, parent, this);
                }
            };

            /*====================================================================*/
            this.$cloneValue = function () {
                // debugger;
                if (Array.isArray(this.value)) {
                    this.prevValue = this.value.slice();
                } else {
                    this.prevValue = _.extend({}, this.value);
                }

                this.prevKeys = Object.keys(this.value);
            };
            /*====================================================================*/
            this.notify_1 = function (eventList, options) {

                this.keys = Object.keys(this.value);

                this._$notify_1(eventList, options);

                this.$cloneValue();

                this.reset();
            };
            /*====================================================================*/
            // call by item
            this.notify_2 = function (key, eventList, options) {
                this.keys = Object.keys(this.value);

                this._$notify_2(key, eventList, options);

                if (!this.arrayMethod) {
                    this.$cloneValue();
                }

                this.reset();
            }
            /*====================================================================*/
            // options =>(key, add, remove)

            this._$notify_1 = function (eventList, options) {
                debugger;

                options = options || {};
                options.eventList = eventList;

                var add = options.add || [];
                var remove = options.remove || [];
                var key = options.key;
                /*--------------------------*/
                var triggerEventName = '';

                // arrayMethod

                if ((eventList.indexOf('sort') >= 0) || (eventList.indexOf('reverse') >= 0)) {
                    // sort, reverse
                    triggerEventName = 'sort';
                } else {
                    triggerEventName = 'update';
                }
                /*--------------------------*/
                var changePosition = [];
                var wantTrigger = this.$compareAllValue(changePosition);

                /*--------------------------*/
                if (!wantTrigger) {
                    return;
                }
                /*--------------------------*/
                remove.forEach(function (value, key) {
                    debugger;

                    if (_.isPlainObject(value) && (typeof value._bc_ob_ !== 'undefined')) {
                        var ob = value._bc_ob_;
                        ob.remove(this.triggerOptions, options);
                    }
                }, this);
                /*--------------------------*/
                options.changePosition = changePosition;

                // 通知 controller
                this.$notifyListener(this.triggerOptions, triggerEventName, options);

                // 通知其他註冊者
                this.trigger(this.triggerOptions, triggerEventName, options);
            };
            /*====================================================================*/
            this._$notify_2 = function (key, eventList, options) {
                debugger;

                options = options || {};
                options.eventList = eventList;

                var add = options.add || [];
                var remove = options.remove || [];
                var key = options.key;
                var wantTrigger = true;
                /*--------------------------*/
                var triggerEventName = '';


                // item發起
                if (eventList.indexOf('change') >= 0) {

                    if ((eventList.indexOf('sort') >= 0) || (eventList.indexOf('reverse') >= 0)) {
                        wantTrigger = false;
                    } else {
                        triggerEventName = 'change';
                    }

                } else if (eventList.indexOf('reset') >= 0) {
                    triggerEventName = 'reset';
                    remove = remove.concat(this.prevValue);
                }
                /*--------------------------*/
                if (!wantTrigger) {
                    return;
                }

                if (this.arrayMethod) {
                    return;
                }

                var _remove = [];

                if (key === 'length') {
                    this.$compareAllValue(_remove);
                } else {
                    wantTrigger = this.$compareValue(key, _remove);
                }

                /*--------------------------*/
                if (!wantTrigger) {
                    return;
                }
                /*--------------------------*/
                _remove.forEach(function (_k, key) {
                    debugger;
                    var value = this.prevValue[_k];

                    if (_.isPlainObject(value) && (typeof value._bc_ob_ !== 'undefined')) {
                        var ob = value._bc_ob_;
                        ob.remove(this.triggerOptions, options);
                    }
                }, this);
                /*--------------------------*/
                options.changePosition = _remove;

                // 通知 controller
                this.$notifyListener(this.triggerOptions, triggerEventName, options);

                // 通知其他註冊者
                this.trigger(this.triggerOptions, triggerEventName, options);
            };

            /*====================================================================*/
            this.toJSON = function () {
                return this.proxy;
            };
            /*================================================================*/
            this.remove = function (e, options) {
                debugger;


                // 叫那些 controller
                this.$notifyListener(e, 'remove');
                this.$listenerList.length = 0;

                // 叫那些跟隨者
                this.trigger(e, 'remove', {});

                this.stopListening();
                this.off();
            };
            /*================================================================*/
            this.reset = function () {
                // debugger;

                if (!this.arrayMethod) {
                    this.triggerOptions = {};
                }
            };
            /*====================================================================*/
            this.$compareAllValue = function (remove) {
                // debugger;


                /*--------------------------*/
                var wantTriggerList = [];

                if (Array.isArray(this.value)) {

                    var length = ((this.prevValue.length - this.value.length > 0) ? this.prevValue.length : this.value.length);

                    for (var i = 0; i < length; i++) {
                        debugger;
                        var wantTrigger = this.$compareValue(i, remove);
                        if (wantTrigger) {
                            wantTriggerList.push(i);
                        }
                    }
                } else {
                    var keyLMap = {};

                    this.keys.forEach(function (k) {
                        keyLMap[k] = true;
                    });

                    this.prevKeys.forEach(function (k) {
                        keyLMap[k] = true;
                    });

                    for (var key in keyLMap) {
                        debugger;
                        var wantTrigger = this.$compareValue(key, remove);
                        if (wantTrigger) {
                            wantTriggerList.push(i);
                        }
                    }
                }
                /*--------------------------*/
                if (wantTriggerList.length || remove.length) {
                    return true;
                } else {
                    return false;
                }

            };
            /*====================================================================*/
            this.$compareValue = function (key, remove) {
                // debugger;

                var wantTrigger = false;

                var prev = this.prevValue[key];
                var current = this.value[key];


                if (typeof prev !== typeof current) {
                    wantTrigger = true;

                    remove.push(key);

                } else if ((prev == null && current == null) ||
                    (typeof prev._bc_ob_ === 'undefined' && typeof current._bc_ob_ === 'undefined')) {
                    // 都是簡單數據

                    if (!_.isEqual(prev, current)) {
                        wantTrigger = true;
                        remove.push(key);
                    }


                } else if (prev != null && current != null &&
                    typeof prev._bc_ob_ !== 'undefined' &&
                    typeof current._bc_ob_ !== 'undefined') {
                    // 都不是簡單數據

                    if (prev._bc_ob_.cid !== current._bc_ob_.cid) {
                        wantTrigger = true;
                        remove.push(key);
                    }

                } else {
                    wantTrigger = true;
                    remove.push(key);
                }
                return wantTrigger;
            };
            /*====================================================================*/

            this.$addListener = function (controller) {
                this.$listenerList.push(controller);
            };
            /*====================================================================*/
            this.$removeListener = function (controller) {
                if (controller == null) {
                    this.$listenerList.length = 0;
                    return;
                }

                var remain = [];

                for (var i = 0, c; c = this.$listenerList[i]; i++) {
                    if (c.cid === controller.cid) {
                        continue;
                    }
                    remain.push(c);
                }
                this.$listenerList = remain;
            };
            /*====================================================================*/
            this.$notifyListener = function (e, eventName, options) {
                this.$listenerList.forEach(function (c, index) {
                    c.update(e, eventName, options);
                }, this);
            };
        }).call(Observer.prototype);
    }

})(Backbone);