Vue基本原理 (三)
3、響應式原理
核心思想:Object.defineProperty(obj, key, {set, get})
function defineReact(obj, key, value){ Object.defineProperty(obj, key, { set: function(newValue){ console.log(`觸發setter`); value = newValue; console.log(value); }, get: function(){ console.log(`觸發getter`); return value; } }); }
這裡是針對data資料的屬性的響應式定義,但是如何去實現vue例項vm繫結data每個屬性,通過以下方法:
function observe(obj, vm){
Object.keys(obj).forEach((key) => {
defineReact(vm, key, obj[key]);
})
}
vue的建構函式:
function Vue(options){ this.data = options.data; let id = options.el; observe(this.data, this); // 將每個data屬相繫結到Vue的例項上this }
通過以上我們可以實現Vue例項繫結data屬性。
如何去實現Vue,通常我們例項化Vue是這樣的:
var vm = new Vue({
el: 'container',
data: {
msg: 'Hello world!',
inpText: 'Input text'
}
});
console.log(vm.msg); // Hello world!
console.log(vm.inpText); // Input text
實現以上效果,我們必須在vue內部初始化虛擬Dom
function Vue(options){
this.data = options.data;
let id = options.el;
observe(this.data, this); // 將每個data屬相繫結到Vue的例項上this
//------------------------新增以下程式碼
let container = document.getElementById(id);
let fragment = virtualDom(container, this); // 這裡通過vm物件初始化
container.appendChild(fragment);
}
這是我們再對Vue進行例項化,則可以看到以下頁面:
至此我們實現了dom的初始化,下一步我們在v-model元素新增監聽事件,這樣就可以通過view層的操作來修改vm對應的屬性值。在compile編譯的時候,可以準確的找到v-model屬相元素,因此我們把監聽事件新增到compile內部。
function compile(node, data){
let reg = /\{\{(.*)\}\}/g;
if(node.nodeType === 1){ // 標籤
let attr = node.attributes;
for(let i = 0, len = attr.length; i < len; i++){
// console.log(attr[i].nodeName, attr[i].nodeValue);
if(attr[i].nodeName === 'v-model'){
let name = attr[i].nodeValue;
node.value = data[name];
// ------------------------新增監聽事件
node.addEventListener('keyup', function(e){
data[name] = e.target.value;
}, false);
// -----------------------------------
}
}
if(node.hasChildNodes()){
node.childNodes.forEach((item) => {
compile(item, data);
});
}
}
if(node.nodeType === 3){ // 文字節點
if(reg.test(node.nodeValue)){
let name = RegExp.$1;
name = name.trim();
node.nodeValue = data[name];
}
}
}
這一步我們操作頁面輸入框,可以看到以下效果,證明監聽事件新增有效。
var subscribe_1 = {
update: function(){
console.log('This is subscribe_1');
}
};
var subscribe_2 = {
update: function(){
console.log('This is subscribe_2');
}
};
var subscribe_3 = {
update: function(){
console.log('This is subscribe_3');
}
};
到這裡我們已經實現了MVVM的,即Model -> vm -> View || View -> vm -> Model 中間橋樑就是vm例項物件。
4、觀察者模式原理
觀察者模式也稱為釋出者-訂閱者模式,這樣說應該會更容易理解,更加形象。
訂閱者:
var subscribe_1 = {
update: function(){
console.log('This is subscribe_1');
}
};
var subscribe_2 = {
update: function(){
console.log('This is subscribe_2');
}
};
var subscribe_3 = {
update: function(){
console.log('This is subscribe_3');
}
};
三個訂閱者都有update方法。
釋出者:
function Publisher(){
this.subs = [subscribe_1, subscribe_2, subscribe_3]; // 新增訂閱者
}
Publisher.prototype = {
constructor: Publisher,
notify: function(){
this.subs.forEach(function(sub){
sub.update();
})
}
};
釋出者通過notify方法對訂閱者廣播,訂閱者通過update來接受資訊。
例項化publisher:
var publisher = new Publisher();
publisher.notify();
這裡我們可以做一箇中間件來處理髮布者-訂閱者模式:
var publisher = new Publisher();
var middleware = {
publish: function(){
publisher.notify();
}
};
middleware.publish();
5、觀察者模式嵌入
到這一步,我們已經實現了:
1、修改v-model屬性元素 -> 觸發修改vm的屬性值 -> 觸發set
2、釋出者新增訂閱 -> notify分發訂閱 -> 訂閱者update資料
接下來我們要實現:更新檢視,同時把訂閱——釋出者模式嵌入。
釋出者:
function Publisher(){
this.subs = []; // 訂閱者容器
}
Publisher.prototype = {
constructor: Publisher,
add: function(sub){
this.subs.push(sub); // 新增訂閱者
},
notify: function(){
this.subs.forEach(function(sub){
sub.update(); // 釋出訂閱
});
}
};
訂閱者:
考慮到要把訂閱者繫結data的每個屬性,來觀察屬性的變化,引數:name引數可以有compile中獲取的name傳參。由於傳入的node節點型別分為兩種,我們可以分為兩訂閱者來處理,同時也可以對node節點型別進行判斷,通過switch分別處理。
function Subscriber(node, vm, name){
this.node = node;
this.vm = vm;
this.name = name;
}
Subscriber.prototype = {
constructor: Subscriber,
update: function(){
let vm = this.vm;
let node = this.node;
let name = this.name;
switch(this.node.nodeType){
case 1:
node.value = vm[name];
break;
case 3:
node.nodeValue = vm[name];
break;
default:
break;
}
}
};
我們要把訂閱者新增到compile進行虛擬dom的初始化,替換掉原來的賦值:
function compile(node, data){
let reg = /\{\{(.*)\}\}/g;
if(node.nodeType === 1){ // 標籤
let attr = node.attributes;
for(let i = 0, len = attr.length; i < len; i++){
// console.log(attr[i].nodeName, attr[i].nodeValue);
if(attr[i].nodeName === 'v-model'){
let name = attr[i].nodeValue;
// --------------------這裡被替換掉
// node.value = data[name];
new Subscriber(node, data, name);
// ------------------------新增監聽事件
node.addEventListener('keyup', function(e){
data[name] = e.target.value;
}, false);
}
}
if(node.hasChildNodes()){
node.childNodes.forEach((item) => {
compile(item, data);
});
}
}
if(node.nodeType === 3){ // 文字節點
if(reg.test(node.nodeValue)){
let name = RegExp.$1;
name = name.trim();
// ---------------------這裡被替換掉
// node.nodeValue = data[name];
new Subscriber(node, data, name);
}
}
}
既然是對虛擬dom編譯初始化,Subscriber要初始化,即Subscriber.update,因此要對Subscriber作進一步的處理:
function Subscriber(node, vm, name){
this.node = node;
this.vm = vm;
this.name = name;
this.update();
}
Subscriber.prototype = {
constructor: Subscriber,
update: function(){
let vm = this.vm;
let node = this.node;
let name = this.name;
switch(this.node.nodeType){
case 1:
node.value = vm[name];
break;
case 3:
node.nodeValue = vm[name];
break;
default:
break;
}
}
};
釋出者新增到defineReact,來觀察資料的變化:
function defineReact(data, key, value){
let publisher = new Publisher();
Object.defineProperty(data, key, {
set: function(newValue){
console.log(`觸發setter`);
value = newValue;
console.log(value);
publisher.notify(); // 釋出訂閱
},
get: function(){
console.log(`觸發getter`);
if(Publisher.global){ //這裡為什麼來新增判斷條件,主要是讓publisher.add只執行一次,初始化虛擬dom編譯的時候來執行
publisher.add(Publisher.global); // 新增訂閱者
}
return value;
}
});
}
這一步將訂閱者新增到釋出者容器內,對訂閱者改造:
function Subscriber(node, vm, name){
Publisher.global = this;
this.node = node;
this.vm = vm;
this.name = name;
this.update();
Publisher.global = null;
}
Subscriber.prototype = {
constructor: Subscriber,
update: function(){
let vm = this.vm;
let node = this.node;
let name = this.name;
switch(this.node.nodeType){
case 1:
node.value = vm[name];
break;
case 3:
node.nodeValue = vm[name];
break;
default:
break;
}
}
};
點選這裡=》 效果圖