感謝百度前端技術(shù)學(xué)院帶領(lǐng)我進(jìn)步!謹(jǐn)以此文善延,記錄我的學(xué)習(xí)過程~
動態(tài)數(shù)據(jù)綁定
Vue的初學(xué)者一定對于數(shù)據(jù)的動態(tài)綁定并不陌生爆安,從最簡單的需求開始一步步理解其實現(xiàn)原理,以及相關(guān)涉及到的知識點执桌。
一
let app1 = new Observer({
name: 'youngwind',
age: 25
});
let app2 = new Observer({
university: 'bupt',
major: 'computer'
});
// 要實現(xiàn)的結(jié)果如下:
app1.data.name // 你訪問了 name
app.data.age = 100; // 你設(shè)置了 age,新的值為100
app2.data.university // 你訪問了 university
app2.data.major = 'science' // 你設(shè)置了 major芜赌,新的值為 science
實際上這需要用到ES5中的Object.prototype.defineProperty
(參見文檔)這個方法,簡單敘述一下其參數(shù):
<blockquote>
Object.defineProperty(obj,key,descriptor)
:
- obj是待操作的對象仰挣;
- key是對象中待操作的屬性名
- descriptor是一個配置對象,其中有:
configurable
:配置總開關(guān)缠沈,默認(rèn)為false膘壶,只有當(dāng)其值為true時错蝴,才能動態(tài)的修改配置方法;
enumerable
:枚舉開關(guān)颓芭,默認(rèn)為false顷锰,只有當(dāng)其值為true時,該屬性才能被枚舉亡问;
value
:默認(rèn)為undefined官紫,即為本意obj[key] = value;
writable
:默認(rèn)為false,只有其值為true時州藕,屬性值才能被改寫万矾;
set/get
:為屬性提供getter和setter方法。
</blockquote>
為了實現(xiàn)第一個需求慎框,我們需要為每一個屬性均提供getter/setter:
function Observer(data) {
this.data = data;
this.makeObserver(data);
}
Observer.prototype.makeObserver = function(data){
if(typeof data !== "object"){
throw "please input object!"
}
let val;
//在對象中遍歷,只能用for..in..但是這個方法會將原型鏈上的屬性方法均會遍歷
//因此用Object.hasOwnProperty進(jìn)行過濾后添,只保留自身對象上的
for(let key in data){
if(data.hasOwnProperty(key)){
val = data[key];
//如果還是引用類型笨枯,則迭代直至所有的屬性均綁定了get/set
if(typeof val === "object"){
new Observer(val)
}
this.convert(key,val);
}
}
}
Observer.prototype.convert = function(){
Object.defineProperty(this.data,key,{
enumerable:true,
configurable:true,
get:function () {
console.log("你訪問了" + key);
return val;
},
set:function (newVal,func) {
console.log("你設(shè)置了" + key + "新的值為" + newVal);
if(newVal == val) return;
//如果值為對象,那么還得給對象里的屬性綁定setter/getter
if(typeof newVal == "object"){
new Observer(newVal);
}
val = newVal;
return val;
}
})
}
二
綁定了getter,setter后遇西,只是進(jìn)行了數(shù)據(jù)獲取/修改后的反饋馅精,并沒有涉及到觸發(fā)數(shù)據(jù)改動后的回調(diào)。我們可以用事件觸發(fā)的思路粱檀,也就是利用發(fā)布訂閱(觀察者)模式來進(jìn)行函數(shù)回調(diào)洲敢。
簡單來說:就是單獨創(chuàng)建一個中間層,用以統(tǒng)一管理事件茄蚯,該中間層給出兩個接口压彭,一個用于訂閱事件,一個用于發(fā)布事件渗常;
一個簡單的實現(xiàn):
//發(fā)布訂閱模式壮不,一個中間層(包括一個訂閱接口,一個取消接口皱碘,一個發(fā)布接口)
function PubSub(){
this.handlers = {};
}
PubSub.prototype.on = function (eventType,handler) {
if (!(eventType in this.handlers)){
this.handlers[eventType] = [];
}
this.handlers[eventType].push(handler);
return this;
}
PubSub.prototype.emit = function (eventType) {
if(!this.handlers[eventType]) return;
var handlerArgs = [].slice.call(arguments,1); //刨除eventType询一,保留其他參數(shù)
for(var i = 0; i < this.handlers[eventType].length;i++){
this.handlers[eventType][i].apply(this,handlerArgs); //實際上等于func(..rest);
}
return this;
}
PubSub.prototype.off = function (eventType) {
if(!(eventType in this.handlers)) return;
delete this.handlers[eventType];
return this;
}
Observer.prototype.$watch = function(attr, callback){
this.eventBus.on(attr, callback);
};
第二個需求變?yōu)椋?/p>
let app1 = new Observer({
name: 'youngwind',
age: 25
});
// 你需要實現(xiàn) $watch 這個 API
app1.$watch('age', function(age) {
console.log(`我的年紀(jì)變了,現(xiàn)在已經(jīng)是:${age}歲了`)
});
app1.data.age = 100; // 輸出:'我的年紀(jì)變了癌椿,現(xiàn)在已經(jīng)是100歲了'
此時健蕊,我們在第一個需求的基礎(chǔ)上,利用觀察者模式的思想添加中間層:
function Observer(data) {
this.data = data;
this.makeObserver(data);
this.eventBus = new PubSub();
}
//該方法就是給屬性綁get/set
Observer.prototype.makeObserver = function (data) {
if(typeof data !== "object"){
throw "please input object!"
}
let val;
//在對象中遍歷踢俄,只能用for..in..但是這個方法會將原型鏈上的屬性方法均會遍歷
//因此用Object.hasOwnProperty進(jìn)行過濾缩功,只保留自身對象上的
for(let key in data){
if(data.hasOwnProperty(key)){
val = data[key];
//如果還是引用類型,則迭代直至所有的屬性均綁定了get/set
if(typeof val === "object"){
new Observer(val)
}
this.convert(key,val);
}
}
};
Observer.prototype.convert = function (key,val) {
var that = this;
Object.defineProperty(this.data,key,{
enumerable:true,
configurable:true,
get:function () {
console.log("你訪問了" + key);
return val;
},
set:function (newVal,func) {
console.log("你設(shè)置了" + key + "新的值為" + newVal);
if(newVal == val) return;
if(typeof newVal == "object"){
new Observer(newVal);
}
that.eventBus.emit(key,newVal); //觸發(fā)訂閱
val = newVal;
return val;
}
})
};
//發(fā)布訂閱模式都办,一個中間層(包括一個訂閱接口掂之,一個取消接口抗俄,一個發(fā)布接口)
function PubSub(){
this.handlers = {};
}
PubSub.prototype.on = function (eventType,handler) {
if (!(eventType in this.handlers)){
this.handlers[eventType] = [];
}
this.handlers[eventType].push(handler);
return this;
}
PubSub.prototype.emit = function (eventType) {
if(!this.handlers[eventType]) return;
var handlerArgs = [].slice.call(arguments,1); //刨除eventType,保留其他參數(shù)
for(var i = 0; i < this.handlers[eventType].length;i++){
this.handlers[eventType][i].apply(this,handlerArgs); //實際上等于func(..rest);
}
return this;
}
PubSub.prototype.off = function (eventType) {
if(!(eventType in this.handlers)) return;
delete this.handlers[eventType];
return this;
}
Observer.prototype.$watch = function(attr, callback){
this.eventBus.on(attr, callback);
};
let app1 = new Observer({
name: 'youngwind',
age: 25
});
// 你需要實現(xiàn) $watch 這個 API
app1.$watch('age', function(age) {
console.log(`我的年紀(jì)變了世舰,現(xiàn)在已經(jīng)是:${age}歲了`)
});
app1.data.age = 100; // 輸出:'我的年紀(jì)變了动雹,現(xiàn)在已經(jīng)是100歲了'