backbone.Model 源碼筆記

backbone.Modelcss

backbone的model(模型),用來存儲數據,交互數據,數據驗證,在view裏面能夠直接監聽model來達到model一改變,就通知視圖.html

 

這個裏面的代碼是從backbone裏面剝離出來,而後一點一點研究和調試出來的,能夠單獨運行,依賴underscore,jquery或者是zepto  event.js是剝離出來的Backbone.Eventsjquery

 

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
    <title>backbone</title>
    <style type="text/css">
        *{padding:0;margin:0;}
        .wrap{width:960px; margin: 100px auto; padding: 20px 0;}
        ul{ list-style: none;}
    </style>
</head>
<body>
    <div class="wrap">
        <div id="a1"></div>
        <div id="a2"></div>
        <div id="a3"></div>
    </div>
<script src="http://files.cnblogs.com/wtcsy/jquery.js"></script> 
<script src="http://files.cnblogs.com/wtcsy/underscore.js"></script>
<script src="http://files.cnblogs.com/wtcsy/events.js"></script>
<script>
(function(){
  // Backbone.Model
  // --------------

  // Backbone **Models** are the basic data object in the framework --
  // frequently representing a row in a table in a database on your server.
  // A discrete chunk of data and a bunch of useful, related methods for
  // performing computations and transformations on that data.

  // Create a new model with the specified attributes. A client id (`cid`)
  // is automatically generated and assigned for you.
    var Model = Backbone.Model = function(attributes, options) {
        var attrs = attributes || {};
        options || (options = {});
        //每一個molde都有一個cid 惟一的標識
        this.cid = _.uniqueId('c');
        //這個是存放設置值得hash列表
        this.attributes = {};
        //看這個model是屬於哪一個collection
        if (options.collection) this.collection = options.collection;
        //格式化參數  默認是不作變化的,能夠本身擴展parse方法實現
        if (options.parse) attrs = this.parse(attrs, options) || {};
        attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
        this.set(attrs, options);
        // 被改變了的值
        this.changed = {};
        this.initialize.apply(this, arguments);
    };

    _.extend(Model.prototype, Backbone.Events, {
        // A hash of attributes whose current and previous value differ.
        //存放 與以前attributes裏面改變了的值
        changed: null,

        //驗證失敗後返回的信息
        // The value returned during the last failed validation.
        validationError: null,

        // The default name for the JSON `id` attribute is `"id"`. MongoDB and
        // CouchDB users may want to set this to `"_id"`.
        idAttribute: 'id',

        // The function that will generate an id for a model given that model's
        // attributes.
        generateId: function (attrs) {
          return attrs[this.idAttribute];
        },

        // Initialize is an empty function by default. Override it with your own
        // initialization logic.
        // 實例化一個model的時候總會被調用的方法
        initialize: function(){},

        // Return a copy of the model's `attributes` object.
        // 複製model.的attributes的屬性
        toJSON: function(options) {
          return _.clone(this.attributes);
        },

        // Proxy `Backbone.sync` by default -- but override this if you need
        // custom syncing semantics for *this* particular model.
        sync: function() {
            return Backbone.sync.apply(this, arguments);
        },

        // Get the value of an attribute.
        get: function(attr) {
            return this.attributes[attr];
        },

        // Get the HTML-escaped value of an attribute.
        escape: function(attr) {
            return _.escape(this.get(attr));
        },

        // Remove an attribute from the model, firing `"change"`. `unset` is a noop
        // if the attribute doesn't exist.
        // 刪除model上的數據 觸發監聽 change 和 unset的回調
        unset: function(attr, options) {
            return this.set(attr, void 0, _.extend({}, options, {unset: true}));
        },

        // Clear all attributes on the model, firing `"change"`.
        clear: function(options) {
            var attrs = {};
            for (var key in this.attributes) attrs[key] = void 0;
            return this.set(attrs, _.extend({}, options, {unset: true}));
        },

        // Determine if the model has changed since the last `"change"` event.
        // If you specify an attribute name, determine if that attribute has changed.
        // 查看某個是屬性值  是否被修改了
        hasChanged: function(attr) {
            if (attr == null) return !_.isEmpty(this.changed);
            return _.has(this.changed, attr);
        },                        

        // Return an object containing all the attributes that have changed, or
        // false if there are no changed attributes. Useful for determining what
        // parts of a view need to be updated and/or what attributes need to be
        // persisted to the server. Unset attributes will be set to undefined.
        // You can also pass an attributes object to diff against the model,
        // determining if there *would be* a change.
        changedAttributes: function(diff) {
            if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
            var val, changed = false;
            var old = this._changing ? this._previousAttributes : this.attributes;
            for (var attr in diff) {
                if (_.isEqual(old[attr], (val = diff[attr]))) continue;
                (changed || (changed = {}))[attr] = val;
            }
            return changed;
        },

        // Get the previous value of an attribute, recorded at the time the last
        // `"change"` event was fired.
        //取改變了attribute以前的某個屬性值
        previous: function(attr) {
            if (attr == null || !this._previousAttributes) return null;
            return this._previousAttributes[attr];
        },

        // Get all of the attributes of the model at the time of the previous
        // `"change"` event.
        //獲取改變了attribute以前
        previousAttributes: function() {
            return _.clone(this._previousAttributes);
        },
        
        // Set a hash of model attributes on the object, firing `"change"`. This is
        // the core primitive operation of a model, updating the data and notifying
        // anyone who needs to know about the change in state. The heart of the beast.    
        set: function(key, val, options) {
            var attr, attrs, unset, changes, silent, changing, prev, current;
            if (key == null) return this;

            // Handle both `"key", value` and `{key: value}` -style arguments.
            // 根據傳參的不一樣 統一成key : value的形式
            if (typeof key === 'object') {
                attrs = key;
                options = val;
            } else {
                (attrs = {})[key] = val;
            }


            options || (options = {});

            // Run validation.
            // 若是須要驗證數據格式, 進行驗證, 驗證不經過 則返回
            
            if (!this._validate(attrs, options)) return false;

            // Extract attributes and options.
            // unset表示刪除
            // changes 是存放改變值得數組
            // changing 屬性值是否正在改變中 
            unset           = options.unset;
            silent          = options.silent;
            changes         = [];
            changing        = this._changing;
            this._changing  = true;

            //若是不是在改變值得進行中 複製this.attributes 到 this._previousAttributes
            if (!changing) {
                this._previousAttributes = _.clone(this.attributes);
                this.changed = {};
            }
            current = this.attributes, prev = this._previousAttributes;

            // For each `set` attribute, update or delete the current value.
            for (attr in attrs) {
                val = attrs[attr];
                // 若是設置的屬性的值,和當前的值不同  放到changes裏面去
                if (!_.isEqual(current[attr], val)) changes.push(attr);
                // 若是設置的值和以前的值 同樣 this.changed刪除掉該屬性  不同 添加到this.changed裏面去
                if (!_.isEqual(prev[attr], val)) {
                    this.changed[attr] = val;
                } else {
                    delete this.changed[attr];
                }
                unset ? delete current[attr] : current[attr] = val;
            }

            var prevId = this.id;
            this.id = this.generateId(current);
            if (prevId !== this.id) this.trigger('change-id', this, prevId, options);

            // Trigger all relevant attribute changes.
            // 觸發改變了屬性值的相關的回調事件
            if (!silent) {
                if (changes.length) this._pending = options;
                for (var i = 0, length = changes.length; i < length; i++) {
                    this.trigger('change:' + changes[i], this, current[changes[i]], options);
                }
            }

            // You might be wondering why there's a `while` loop here. Changes can
            // be recursively nested within `"change"` events.
            // 
            if (changing) return this;            
            if (!silent) {
                while (this._pending) {
                    options = this._pending;
                    this._pending = false;
                    this.trigger('change', this, options);
                }
            }
            this._pending = false;
            this._changing = false;
            return this;
        },

        // **parse** converts a response into the hash of attributes to be `set` on
        // the model. The default implementation is just to pass the response along.
        parse: function(resp, options) {
            return resp;
        },        
        // Check if the model is currently in a valid state.
        isValid: function(options) {
            return this._validate({}, _.extend(options || {}, { validate: true }));
        },

        // Run validation against the next complete set of model attributes,
        // returning `true` if all is well. Otherwise, fire an `"invalid"` event.
        _validate: function(attrs, options) {
            //在實例化的時候須要傳入驗證函數validate
            //而後每次設置值的時候都進行驗證
            //驗證失敗 觸發invalid的回調事件
            if (!options.validate || !this.validate) return true;
            attrs = _.extend({}, this.attributes, attrs);
            var error = this.validationError = this.validate(attrs, options) || null;
            if (!error) return true;
            this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
            return false;
        },

        // Fetch the default set of models for this collection, resetting the
        // collection when they arrive. If `reset: true` is passed, the response
        // data will be passed through the `reset` method instead of `set`.
        fetch: function(options) {
            options = options ? _.clone(options) : {};
            if (options.parse === void 0) options.parse = true;
            var success = options.success;
            var collection = this;
            options.success = function(resp) {
                var method = options.reset ? 'reset' : 'set';
                collection[method](resp, options);
                if (success) success(collection, resp, options);
                collection.trigger('sync', collection, resp, options);
            };
            wrapError(this, options);
            return this.sync('read', this, options);
        },

        // Set a hash of model attributes, and sync the model to the server.
        // If the server returns an attributes hash that differs, the model's
        // state will be `set` again.
        save: function(key, val, options) {
            var attrs, method, xhr, attributes = this.attributes;

            // Handle both `"key", value` and `{key: value}` -style arguments.
            if (key == null || typeof key === 'object') {
                attrs = key;
                options = val;
            } else {
                (attrs = {})[key] = val;
            }

            options = _.extend({validate: true}, options);

            // If we're not waiting and attributes exist, save acts as
            // `set(attr).save(null, opts)` with validation. Otherwise, check if
            // the model will be valid when the attributes, if any, are set.
            if (attrs && !options.wait) {
                if (!this.set(attrs, options)) return false;
            } else {
                if (!this._validate(attrs, options)) return false;
            }

            // Set temporary attributes if `{wait: true}`.
            if (attrs && options.wait) {
                this.attributes = _.extend({}, attributes, attrs);
            }

            // After a successful server-side save, the client is (optionally)
            // updated with the server-side state.
            if (options.parse === void 0) options.parse = true;
            var model = this;
            var success = options.success;
            options.success = function(resp) {
                // Ensure attributes are restored during synchronous saves.
                model.attributes = attributes;
                var serverAttrs = model.parse(resp, options);
                if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
                if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
                    return false;
                }
                if (success) success(model, resp, options);
                model.trigger('sync', model, resp, options);
            };
            wrapError(this, options);

            method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
            if (method === 'patch') options.attrs = attrs;
            xhr = this.sync(method, this, options);

            // Restore attributes.
            if (attrs && options.wait) this.attributes = attributes;

            return xhr;
        },
        // A model is new if it has never been saved to the server, and lacks an id.
        isNew: function() {
          return this.id == null;
        }
    });


    // Helper function to correctly set up the prototype chain, for subclasses.
    // Similar to `goog.inherits`, but uses a hash of prototype properties and
    // class properties to be extended.
      //第一個參數是要擴展到原型上的對象, 第2個參數是靜態方法擴展到構造函數上去的
    var extend = function(protoProps, staticProps) {
        var parent = this;
        var child;

        // The constructor function for the new subclass is either defined by you
        // (the "constructor" property in your `extend` definition), or defaulted
        // by us to simply call the parent's constructor.
        if (protoProps && _.has(protoProps, 'constructor')) {
            child = protoProps.constructor;
        } else {
            child = function(){ return parent.apply(this, arguments); };
        }

        // Add static properties to the constructor function, if supplied.
        //將靜態方法和 parent上的靜態方法一塊兒擴展到child上面去
        _.extend(child, parent, staticProps);

        // Set the prototype chain to inherit from `parent`, without calling
        // `parent`'s constructor function.
        //建立一個新的構造含糊Surrogate ; 
        //this.constructor = child的意思是  Surrogate實例化後的對象  讓對象的構造函數指向child
        // Surrogate的原型就是parent的原型
        // 而後實例化給child的原型,
        // 這裏不是直接從new parent給child.prototype 而是建立一個新的構造函數,我也不知道爲啥要這樣
        var Surrogate = function(){ this.constructor = child; };
        Surrogate.prototype = parent.prototype;
        child.prototype = new Surrogate;

        // Add prototype properties (instance properties) to the subclass,
        // if supplied.
        // 把第一個參數上的屬性擴展到child.prototype
        if (protoProps) _.extend(child.prototype, protoProps);

        // Set a convenience property in case the parent's prototype is needed
        // later.
        // 拿一個屬性引用父的原型, 以避免之後要用到.
        child.__super__ = parent.prototype;

        return child;
    };

    Model.extend = extend;


    // Backbone.sync
    // -------------

    // Override this function to change the manner in which Backbone persists
    // models to the server. You will be passed the type of request, and the
    // model in question. By default, makes a RESTful Ajax request
    // to the model's `url()`. Some possible customizations could be:
    //
    // * Use `setTimeout` to batch rapid-fire updates into a single request.
    // * Send up the models as XML instead of JSON.
    // * Persist models via WebSockets instead of Ajax.
    //
    // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
    // as `POST`, with a `_method` parameter containing the true HTTP method,
    // as well as all requests with the body as `application/x-www-form-urlencoded`
    // instead of `application/json` with the model in a param named `model`.
    // Useful when interfacing with server-side languages like **PHP** that make
    // it difficult to read the body of `PUT` requests.

    // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
    var methodMap = {
        'create': 'POST',
        'update': 'PUT',
        'patch':  'PATCH',
        'delete': 'DELETE',
        'read':   'GET'
    };
    // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
    // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
    // set a `X-Http-Method-Override` header.
    Backbone.emulateHTTP = false;

    // Turn on `emulateJSON` to support legacy servers that can't deal with direct
    // `application/json` requests ... will encode the body as
    // `application/x-www-form-urlencoded` instead and will send the model in a
    // form param named `model`.
    Backbone.emulateJSON = false;    

    Backbone.sync = function(method, model, options) {
        var type = methodMap[method];

        // Default options, unless specified.
        _.defaults(options || (options = {}), {
            emulateHTTP: Backbone.emulateHTTP,
            emulateJSON: Backbone.emulateJSON
        });

        // Default JSON-request options.
        var params = {type: type, dataType: 'json'};

        // Ensure that we have a URL.
        if (!options.url) {
            params.url = _.result(model, 'url') || urlError();
        }

        // Ensure that we have the appropriate request data.
        if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
            params.contentType = 'application/json';
            params.data = JSON.stringify(options.attrs || model.toJSON(options));
        }

        // For older servers, emulate JSON by encoding the request into an HTML-form.
        if (options.emulateJSON) {
            params.contentType = 'application/x-www-form-urlencoded';
            params.data = params.data ? {model: params.data} : {};
        }

        // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
        // And an `X-HTTP-Method-Override` header.
        if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
            params.type = 'POST';
            if (options.emulateJSON) params.data._method = type;
            var beforeSend = options.beforeSend;
            options.beforeSend = function(xhr) {
                xhr.setRequestHeader('X-HTTP-Method-Override', type);
                if (beforeSend) return beforeSend.apply(this, arguments);
            };
        }

        // Don't process data on a non-GET request.
        if (params.type !== 'GET' && !options.emulateJSON) {
            params.processData = false;
        }

        // If we're sending a `PATCH` request, and we're in an old Internet Explorer
        // that still has ActiveX enabled by default, override jQuery to use that
        // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
        if (params.type === 'PATCH' && noXhrPatch) {
            params.xhr = function() {
                return new ActiveXObject("Microsoft.XMLHTTP");
            };
        }

        // Pass along `textStatus` and `errorThrown` from jQuery.
        var error = options.error;
        options.error = function(xhr, textStatus, errorThrown) {
            options.textStatus = textStatus;
            options.errorThrown = errorThrown;
            if (error) error.apply(this, arguments);
        };

        // Make the request, allowing the user to override any Ajax options.
        var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
        model.trigger('request', model, xhr, options);
        return xhr;        
    };
    Backbone.$ = $;
    // Set the default implementation of `Backbone.ajax` to proxy through to `$`.
    // Override this if you'd like to use a different library.
    Backbone.ajax = function() {
        return Backbone.$.ajax.apply(Backbone.$, arguments);
    };


    var wrapError = function(model, options) {
        var error = options.error;
        options.error = function(resp) {
        if (error) error(model, resp, options);
            model.trigger('error', model, resp, options);
        };
    };    
})();

</script>
</body>
</html>
View Code

 

Model的extendajax

Model的extend能夠建立一個新的模型,擴展你所須要的方法和屬性,這個方法在Model,View,Collection上都有.json

這裏得介紹下constructor,雖然網上已經有不少介紹了。 當一個構造函數入a = function(){}; a被實例化b,b.constructor就指向a,也就是說constructor指向它的構造函數的.可是這個屬性是能夠修改的api

extend 這個函數依賴underscore數組

    // Helper function to correctly set up the prototype chain, for subclasses.
    // Similar to `goog.inherits`, but uses a hash of prototype properties and
    // class properties to be extended.
      //第一個參數是要擴展到原型上的對象, 第2個參數是靜態方法擴展到構造函數上去的
    var extend = function(protoProps, staticProps) {
        var parent = this;
        var child;

        // The constructor function for the new subclass is either defined by you
        // (the "constructor" property in your `extend` definition), or defaulted
        // by us to simply call the parent's constructor.
        if (protoProps && _.has(protoProps, 'constructor')) {
            child = protoProps.constructor;
        } else {
            child = function(){ return parent.apply(this, arguments); };
        }

        // Add static properties to the constructor function, if supplied.
        //將靜態方法和 parent上的靜態方法一塊兒擴展到child上面去
        _.extend(child, parent, staticProps);

        // Set the prototype chain to inherit from `parent`, without calling
        // `parent`'s constructor function.
        //建立一個新的構造含糊Surrogate ; 
        //this.constructor = child的意思是  Surrogate實例化後的對象  讓對象的構造函數指向child
        // Surrogate的原型就是parent的原型
        // 而後實例化給child的原型,
        // 這裏不是直接從new parent給child.prototype 而是建立一個新的構造函數,我不知道爲啥要這樣
        var Surrogate = function(){ this.constructor = child; };
        Surrogate.prototype = parent.prototype;
        child.prototype = new Surrogate;

        // Add prototype properties (instance properties) to the subclass,
        // if supplied.
        // 把第一個參數上的屬性擴展到child.prototype
        if (protoProps) _.extend(child.prototype, protoProps);

        // Set a convenience property in case the parent's prototype is needed
        // later.
        // 拿一個屬性引用父的原型, 以避免之後要用到.
        child.__super__ = parent.prototype;

        return child;
    };

    Model.extend = extend;

 

Model的set  set(attributes, [options])app

在調用set的方法,流程以下,在set的時候,看是否須要驗證,須要驗證則驗證,驗證成功就繼續往下執行。而後再copy一個attributes的屬性,賦值給this._previousAttributes,而後比較傳入的參數跟attributes,這裏有2中若是值同樣則從this.changed裏面刪除,不同則添加,this.changed老是保存此次跟上次之間值發生變化的那些屬性. 而後再看時候設置了unset屬性,若是設置了該屬性從attributes刪除該值,不然在attributes修改或者添加該值 而後看那些屬性的值發生了變化,變化的屬性值觸發change:key的監聽回調(key是屬性名),而後再觸發change監聽的回調less

在調用set方法的時候大概用到了一下幾個屬性ide

_previousAttributes

changed

_changing

changed

_previousAttributes  這個屬性老是記錄model修改以前的數據

changed                  這個屬性老是記錄此次set操做後,對於上次的數據,修改了哪些數據,就是保存修改的數據  changed顧名思義,已改變的

_changing               這個屬性若是爲true表示正在進行set中,false表示沒有對model進行數據的操做

set方法能夠傳入2個參數,也能夠傳入3個參數,其實傳入就是支持對象的入參,和key,value的入參,若是第一個參數是對象就當作2個參數來處理,若是第一個參數是字符串,就會當成3個參數來處理,另一個參數options,裏面有幾個屬性會常常用到unset,silent

unset 表示把這個屬性取消

silent 表示把是否觸發綁定該屬性監聽的回調,不設置或者設置成false都會觸發change:attr,設置爲true表示不觸發監聽回調

一個例子

var m = new Backbone.Model();
m.set({
    name : "xxoo",
    age  : 18,
    info : "wa haha!!!"
});
//監聽age的變化若是  當age改變時  小於18和大於等於18彈出的結果是不同的
m.on("change:age",function(model,val){
    if(val>=18){
        alert("cheng nian le -_-");
    }else{
        alert("ni hai xiao ^_^");
    }
});
m.set({age:12}); //ni hai xiao ^_^
m.set({age:20}) //cheng nian le -_-

//若是改變age 的時候不想觸發監聽的函數 能夠設置silent
m.set({age:20},{silent:true})


//取消一個屬性
m.set({info:1},{unset:true})
console.log(m)

//若是想監聽全部的屬性的變化能夠用直接綁定chagne
m.on("change",function(model){console.log(model)})

m.set({age:1})
m.set({name:2})

 

Model的unset  model.unset(attribute, [options])  zzzzz

就是刪除一個屬性,實現其實很簡單就是調用set方法,把options裏面的silent設置成true

             var m = new Backbone.Model()
             m.set("haha",123)
             m.unset("haha")
             //一次只能刪除一個  參數不能傳遞對象 若是要刪除多個就調用屢次unset,若是要刪除全部的,就調用clear

 

Model的validate和_validate

驗證,在設置值得時候會進行驗證,設置值得時候必須帶上屬性validate好比,m.set("test",11,{"validate":true})。validate是用戶本身設定的驗證函數,_validate是進行驗證時的操做,驗證失敗後會觸發invalid監聽的回調

var testModel = Backbone.Model.extend({
    validate:function(obj){
        if(obj.test>10){
            return "測試值不能大於10";
        }
        if(obj.age<18){
            return "不能小魚18歲";
        }
    }
})
var m = new testModel()
m.on("invalid",function(model,errText){alert(errText)}); //對設置值失敗的統一處理函數,只要綁定了invalid,設置值失敗了都會進這個地方
m.set("test",11,{"validate":true})  //這個設置會失敗的 
m.set("test",9,{"validate":true}) //設置會成功
m.set("test",2)//該值不會進行驗證,由於沒有設置{"validate":true}

m.set("age",12,{"validate":true}) //這個測試當驗證失敗的時候他們回調用invalid的監聽

 

 Model的一些屬性和方法  cid    changed    _previousAttributes  hasChanged

每一個modl實例化的時候都會建立一個cid 保證model的惟一性.

Model裏面有一些屬性和方法是專門對比上一次和修改後以前的差別的 

changed 是屬性 一個object  保存上一次和此次相比發生變化的值

_previousAttributes 是一個對象  保存上一次的attributes

hasChanged  是一個方法 判斷一個屬性,這一次跟上一次是否發生了變化

var m = new Backbone.Model();
m.set({
    a:1,
    b:1,
    c:1,
    d:1,
    e:1
});

m.set({
    a:2,
    b:3
});

//打印出來的只有a,b由於只有a,b發生了變化
console.log(m.changed)

//作個對比能夠看到 _previousAttributes保存的修改前的值
console.log(m._previousAttributes)
console.log(m.attributes)

//能夠看到a返回的true  c則是false
m.hasChanged("a")
m.hasChanged("c")

 

Model提供了跟後臺交互的方法snyc fetch save destory方法 依賴jquery或者zepto,用到的也是$.ajax,基本上就是在作了一些事件的監聽, 在工做中我都不用到

相關文章
相關標籤/搜索