- 資料雙向綁定
- 觀察者模式
- 釋出-訂閱模式
- 兩種模式差異
- 髒值檢測
- 資料劫持
- 了解對象
-
- 資料屬性
- 通路器屬性
-
- 了解對象
- vue的做法:資料劫持+釋出訂閱
- Vue資料雙向綁定方案
- 回顧Vue生命周期
- 管理訂閱者
- 監聽資料變化
- 解析器Compile初始化訂閱者
- Vue資料雙向綁定方案
- 總結
- 參考資料
資料雙向綁定
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLwETMwIDM1EDMycmYvwVNxAjMvwFdlN3ch9CXn1Wan9Gbi9CXt92YucmblZWa55WY1JnL3d3dvw1LcpDc0RHaiojIsJye.png)
MVVM(model-view-viewmodel)類架構的一般特色就是對象屬性和UI資料的雙向綁定。方法一般有釋出訂閱模式(BackBone.js)、髒值檢測(Angular.js)、資料劫持(Vue.js)
觀察者模式
有人認為觀察者模式 == 釋出訂閱模式,其實不然。
來自維基百科的定義:
觀察者模式 在軟體設計中是一個對象(Subject),維護一個依賴清單(Observers),當任何狀态發生改變(event)自動通知(notify)它們。
/*
Subject維護了一個依賴清單,event類型作為依賴對象的key,每種event對應一個回調函數清單,每當狀态改變時,調用對應的回調函數們
Subject的原型上有下列方法:
添附:新增觀察者到依賴清單裡
解附:将已存在的觀察者從依賴清單中移除
通知:利用觀察者所提供的回調函數來通知此目标已經産生變化
*/
// 維護依賴清單
function Subject() {
this.handlers = {};
}
Subject.prototype = {
// 添附
attach: function (eventType, callback) {
var self = this;
var keys = Object.keys(self.handlers);
if (!keys.find((key) => key === eventType)) {
self.handlers[eventType] = [];
}
self.handlers[eventType].push(callback);
},
// 解附 detach
detach: () => {},
// 通知
notify: function (eventType) {
var self = this;
var args = Array.prototype.slice.call(arguments, );
// 通知 notify 直接調用回調函數,一一通知觀察者
self.handlers[eventType].forEach((handler) => {
handler.apply(self, args);
});
}
};
用觀察者模式實作一個最簡單的input與span的雙向綁定:
function updateSpan(value) {
// 更新資料
spantext.innerHTML = value;
// 釋出modelUpt事件,通知觀察者
subject.noify('modelUpt', value);
}
// 訂閱viewUpt事件
subject.attach('viewUpt', function(value){
updateSpan(value);
});
// 訂閱modelUpt事件
subject.attach('modelUpt', function(value) {
// 更新視圖中綁定的值
inputEle.value = value;
});
// 視圖資料修改,釋出viewUpt事件,通知觀察者
inputEle.addEventListener('keyup', function() {
subject.notify('viewUpt', inputEle.value);
});
問題:
-
視圖資料更改時怎麼通知觀察者?
從視圖改變資料的一般方法:表單元素的改變(input, select, checkboxes, textarea,radio等)。利用DOM的事件處理程式,為可能引發視圖資料改變的事件(change、keypress、keyup、paste)注冊處理函數,在處理函數中通知觀察者。
- 如何檢測對象屬性變更?
- 髒值檢測
- 資料劫持
釋出-訂閱模式
來自維基百科的定義
在軟體架構中,釋出-訂閱是一種消息範式,消息的發送者(稱為釋出者)不會将消息直接發送給特定的接收者(稱為訂閱者)。而是将釋出的消息分為不同的類别,無需了解哪些訂閱者(如果有的話)可能存在。同樣的,訂閱者可以表達對一個或多個類别的興趣,隻接收感興趣的消息,無需了解哪些釋出者(如果有的話)存在。
在許多釋出/訂閱系統中,釋出者釋出消息到一個中間的消息代理,然後訂閱者向該消息代理注冊訂閱,由消息代理來進行過濾。消息代理通常執行存儲轉發的功能将消息從釋出者發送到訂閱者。
釋出者和訂閱者不知道對方的存在。需要一個第三方元件,叫做資訊中介,它将訂閱者和釋出者串聯起來,它過濾和配置設定所有輸入的消息。換句話說,釋出-訂閱模式用來處理不同系統元件的資訊交流,即使這些元件不知道對方的存在。
圖檔來源: developers-club
兩種模式差異
由此可以總結出兩種模式的以下差異:
- 觀察者模式因為Subject維護着Observers隊列,對觀察者保持着記錄,但是釋出-訂閱模式中,釋出者與訂閱者不知道對方的存在,它們通過消息代理進行通信,釋出者的消息不是直接發送給訂閱者。
- 釋出訂閱模式的元件是松散耦合的,與觀察者模式相反
- 觀察者模式大多數是同步的,如上述例子,通知事件時依次調用回調函數序列的函數。釋出-訂閱模式大多數是異步的(使用消息隊列)
髒值檢測
Angular.js是通過髒值檢查的方式來對比資料是否有變更,來決定是否更新視圖,最簡單的方式就是通過setInterval()定時檢測資料變動,angular隻有在指定事件被觸發時進入髒值檢測,大體如下:
1.DOM事件,比如使用者輸入文本,點選按鈕等。(ng-click)
2.XHR響應事件(http)
3.浏覽器Location變更(location)
4.Time事件(timeout, interval)
5.執行digest()或者apply()
資料劫持
了解對象
資料屬性
資料屬性包含一個資料值的位置,在這個位置可以讀取和寫入值。
屬性名 | 描述 | 預設值 |
---|---|---|
[[Configurable]] | 能否通過delete删除屬性進而重新定義屬性,能否修改屬性的特性,能否把屬性修改為通路器屬性 | true |
[[Enumerable]] | 能否通過for-in循環傳回屬性 | true |
[[Writable]] | 能否修改屬性的值 | true |
[[Value]] | 包含這個屬性的資料值。讀取屬性的時候,從這個位置讀;寫入資料值得時候,把新值儲存在這個位置 | undefined |
要修改屬性預設的特性,使用ES5的Object.defineProperty()方法。接收三個參數:屬性所在的對象,屬性的名字,一個描述符對象。描述符對象的屬性必須是configurable,enumerable,writable,value
var person = {};
Object.defineProperty(person, 'name', {
writable: false,
value: 'May',
});
console.log(person.name);// May
person.name = 'Julia'; // 嚴格模式下報錯,非嚴格模式下指派操作被忽略
console.log(person.name);// May
通路器屬性
通路器屬性不包含資料值,包含一對getter和setter函數。在讀取通路器屬性時會調用getter,寫入通路器屬性時,會調用set函數并傳入新的值。定義通路器屬性也用Object.defineProperty()方法。
資料劫持就是利用Object.defineProperty()來劫持對象的getter和setter操作,在資料變動時執行操作。
vue的做法:資料劫持+釋出訂閱
Vue資料雙向綁定方案
回顧Vue生命周期
beforeCreate & created
在Vue的構造函數中,對傳入的options對象調用了_init函數,其中執行了以下操作。
// new Vue 執行個體初始化
initLifecycle(vm) // 初始化生命周期,給vm對象添加了$parent $root $children,_watchers等屬性,以及一些生命周期相關的辨別
initEvents(vm) // 初始化事件相關的屬性,會在這裡把父元件綁定在自定義标簽上的事件添加到子元件裡
initRender(vm) // 初始化渲染
callHook(vm, 'beforeCreate')// beforeCreate 執行beforeCreated的鈎子函數
initInjections(vm) // 初始化inject resolve injections before data/props
initState(vm) // 初始化資料
initProvide(vm) // 初始化provide resolve provide after data/props
callHook(vm, 'created') // created
initState 初始化資料,對props、methods、data、computed、watch的處理
function initState(vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) {
initProps(vm, opts.props);
}
if (opts.methods) {
initMethods(vm, opts.methods);
}
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */ );
}
if (opts.computed) {
initComputed(vm, opts.computed);
}
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
initData主要是初始化data中的資料,将資料進行Observer,監聽資料的變化
function initData(vm) {
/*得到data資料*/
var data = vm.$options.data;
data = vm._data = typeof data === 'function' ?
getData(data, vm) :
data || {};
/*判斷是否是對象*/
if (!isPlainObject(data)) {
data = {};
"development" !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
);
}
/*周遊data對象*/
// proxy data on instance
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
{
if (methods && hasOwn(methods, key)) {
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
}
}
/*保證data中的key不與props中的key重複,props優先,如果有沖突會産生warning*/
if (props && hasOwn(props, key)) {
"development" !== 'production' && warn(
"The data property \"" + key + "\" is already declared as a prop. " +
"Use prop default value instead.",
vm
);
} else if (!isReserved(key)) {
/*判斷是否是保留字段*/
/*将data上面的屬性代理到了vm執行個體上*/
proxy(vm, "_data", key);
}
}
// observe data 980
observe(data, true /* asRootData */ );
}
管理訂閱者
對訂閱者進行收集、存儲和通知
Dep構造函數
/**
* Dep類是一個釋出者,有多個訂閱者可以訂閱它
*/
var Dep = function Dep() {
this.id = uid++;
this.subs = [];
};
// 添加一個訂閱者對象
Dep.prototype.addSub = function addSub(sub) {
this.subs.push(sub);
};
// 移除一個訂閱者對象
Dep.prototype.removeSub = function removeSub(sub) {
remove(this.subs, sub);
};
// 依賴收集,當存在Dep.target的時候添加訂閱者對象
Dep.prototype.depend = function depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
};
// 通知所有訂閱者
Dep.prototype.notify = function notify() {
// stabilize the subscriber list first
var subs = this.subs.slice();
for (var i = , l = subs.length; i < l; i++) {
subs[i].update();
}
};
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
/*依賴收集完需要将Dep.target設為null,防止後面重複添加依賴。*/
Dep.target = null;
監聽資料變化
proxy代理
将data的最外層屬性代理到Vue執行個體上
/*添加代理*/
function proxy(target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
observe嘗試建立一個新的Observer執行個體(
_ob_
),Vue的響應式資料都會有一個
_ob_
的屬性作為标記,裡面存放了該屬性的觀察器,也就是Observer的執行個體,防止重複綁定。
/**
* 嘗試建立一個Observer執行個體(__ob__),
* 如果成功建立Observer執行個體則傳回新的Observer執行個體,
* 如果已有Observer執行個體則傳回現有的Observer執行個體。
*/
function observe(value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
/*建立一個新的Observer執行個體*/
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
Observer構造函數
var Observer = function Observer(value) {
this.value = value;
this.dep = new Dep();
this.vmCount = ;
// 将Observer執行個體綁定到data的__ob__屬性上面
def(value, '__ob__', this);
if (Array.isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys);
/*如果是數組則需要周遊數組的每一個成員,為其建立一個Observer執行個體(_ob_)*/
this.observeArray(value);
} else {
/*如果是對象則使用walk進行綁定*/
this.walk(value);
}
};
walk
Observer.prototype.walk = function walk(obj) {
var keys = Object.keys(obj);
/*walk方法會周遊對象的每一個屬性進行defineReactive綁定*/
for (var i = ; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
};
defineReactive 通過Object.defineProperty為資料定義上getter\setter方法,進行依賴收集後Deps會存放Watcher對象。觸發setter改變資料的時候會通知Deps訂閱者通知所有的Watcher觀察者對象進行UI的更新。
function defineReactive(
obj,
key,
val,
customSetter,
shallow
) {
// 定義一個dep對象
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === ) {
val = obj[key];
}
// 對象的子對象遞歸進行observe并傳回子節點的observe對象
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 如果對象原本就有getter則執行
var value = getter ? getter.call(obj) : val;
// 如果Dep類存在target屬性,将其添加到dep執行個體的subs數組中
// target指向一個Watcher執行個體,每個Watcher都是一個訂閱者
// Watcher執行個體在執行個體化過程中,會讀取data中的某個屬性,進而觸發目前get方法
// 并不是每次Dep.target有值時都需要添加到訂閱者管理者中去管理,需要對訂閱者去重
if (Dep.target) {
// 進行依賴收集
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
// 如果是數組對象則需要對每一個成員都進行依賴收集
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter(newVal) {
// 通過getter方法擷取目前值,與新值進行比較,一緻則不需要執行set操作
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if ("development" !== 'production' && customSetter) {
customSetter();
}
// 如果原對象有setter方法則執行setter
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
// 新值重新進行observe,保證資料響應式
childOb = !shallow && observe(newVal);
// dep對象通知所有的觀察者
dep.notify();
}
});
}
Watcher是一個觀察者對象。依賴收集後Watcher對象會被儲存在Deps中,資料變動時,Deps會通知Watcher執行個體,然後由Watcher執行個體回調進行視圖的更新。訂閱者維護着每一次更新之前的資料,然後将其和更新之後的資料進行對比,如果發生了變化,則執行回調函數,并更新訂閱者中維護的資料的值。
beforeMount & mounted
解析器Compile初始化訂閱者
Compile(HTML指令解析器),對元素節點的指令進行掃描和解析,如果存在v-model、v-on、插值等,則初始化這類節點的模闆資料,使之可以顯示在視圖上,然後初始化相應的訂閱者(Watcher),接收到屬性變化時執行回調函數更新視圖。
- 如果是元素節點(有v-指令),對元素添加監聽事件(addEventListener,對input、click、change等事件添加監聽)
- 如果是文本節點(有插值),則提取出{{}}内的data,然後根據綁定的資料對其進行初始化
beforeUpdate & updated
資料劫持:
Object.defineProperty
定義對象屬性的getter内進行依賴收集,把訂閱者添加到Dep的subs數組内,setter内通過getter方法擷取目前值,與新值進行比較,一緻則不需要執行set操作,并将改變通知給所有的訂閱者
釋出訂閱:
使用 Dep 解耦了依賴者與被依賴者之間關系的确定過程。簡單來說:
- 通過 Observer 提供的接口,周遊狀态對象,給對象的每個屬性、子屬性都綁定了一個專用的
對象。這裡的狀态對象主要指元件當中的Dep
屬性。data
- 建立三種類型的watcher:
-
- 調用 initComputed 将
屬性轉化為computed
執行個體watcher
- 調用 initWatch 方法,将
配置轉化為watch
執行個體watcher
- 調用 mountComponent 方法,為
函數綁定render
執行個體watcher
- 調用 initComputed 将
- 狀态變更後,觸發
函數,該函數再進一步觸發 Watcher 對象dep.notify()
函數,執行watcher的重新計算。update
-
對數組進行push、pop等操作時,對于數組的新對象何時進行雙向綁定,怎麼監聽數組的這些變化?
Vue的方法:重寫push、pop、shift、unshift、splice、sort、reverse這些數組的原型方法。在使用了這些方法,如果數組增加了元素(push、unshift、splice),給新元素再添加雙向綁定。同時數組元素發生變化,dep通知(notify change)所有注冊的觀察者進行響應式處理 。源碼可見observer/array.js
- 資料劫持利用的
是ES5的方法,需要支援ES5的浏覽器Object.defineProperty
- 屬性劫持的出發點是“變”,是以Vue無法很好接入immutable模式
- 訂閱者進行資料更新時會維護原資料,增加記憶體成本
總結
Vue利用的資料劫持+釋出訂閱模式,實作了視圖和對象屬性的雙向綁定。Vue3的作者宣稱他們會使用ES2015的Proxy來實作雙向綁定,在目标對象外面架設一層“攔截”,外部對該對象的通路,都會先通過這層攔截,是以提供了對外界的通路進行過濾和改寫的機制,同時Proxy除了可以直接監聽對象而非屬性、直接監聽數組變化,攔截方式還十分多樣。即使Proxy有相容性問題,但仍值得期待。
參考資料
設計模式之觀察者模式 -cnblogs
觀察者模式 -wiki
vuejs -github
觀察者模式 vs 釋出-訂閱模式 -掘金
資料雙向綁定的 分析和簡單實作 - 知乎專欄