前言
Vue的數(shù)據(jù)雙向綁定须揣,響應式原理,其實就是通過Object.defineProperty()結合發(fā)布者訂閱者模式來實現(xiàn)的。
我們可以先試著拆分一下Vue的核心模塊伏穆。
- Vue構造函數(shù),集中以下模塊實現(xiàn)MVVM纷纫。
- Observer 通過Object.definePropty進行數(shù)據(jù)劫持
- Dep 發(fā)布訂閱者枕扫,添加觀察者者以及在數(shù)據(jù)發(fā)生改變的時候通知觀察者
- Watcher 觀察者,對數(shù)據(jù)進行觀察以及保存數(shù)據(jù)修改需要觸發(fā)的回調
- Compiler 模板編譯器辱魁,對HTML模板進行編譯烟瞧,提取其中的變量并轉化為數(shù)據(jù)。
對于整個Vue響應式的簡單實現(xiàn)來說染簇,在文中并不能做過多的介紹参滴,只能依靠讀者自己去試,按照注釋來進行理解锻弓。推薦的學習方式就是通過本文的實現(xiàn)代碼一步一步的自己實現(xiàn)一下砾赔,然后再自己實現(xiàn)的基礎上自己編寫注釋。
正文
先看一下我們最終實現(xiàn)的效果吧
HTML
<div id="app">
<p>哈哈</p>
<h1 @click="setName">{{msg}}</h1>
<h1>{{a.b}}</h1>
<input type="text" v-model="a.b">
</div>
JavaScript
new Vue({
data() {
return {
msg: '呃呃呃呃呃',
name: '郝晨光',
a: {
b: 'bbbbb'
}
}
},
methods: {
setName() {
this.msg = '哈哈';
}
},
created() {
this.msg = '郝晨光哈哈';
console.log('實例初始化完成')
},
mounted() {
console.log('DOM掛載完成')
}
}).$mount('#app'); // 此處通過el屬性綁定也是沒有任何問題的
Vue構造函數(shù)
// Vue構造函數(shù)
function Vue(options) {
// 如果當前Vue不是通過new 關鍵字調用,就進行報錯
if(!(this instanceof arguments.callee)) {
error('Vue是一個構造函數(shù)过蹂,必須通過new關鍵字調用十绑!');
}
// 如果是的話,就接著執(zhí)行_init方法
this._init(options);
}
// 實例化Vue的方法
Vue.prototype._init = function(options) {
// 先將options保存在Vue的this.$options上
this.$options = options;
// 再拿到對應的data中的值酷勺,沒有默認為空對象
this.$data = initData(this.$options) || {};
// 拿到對應的方法本橙,沒有默認為空對象
this.$methods = this.$options.methods || {};
// 進行數(shù)據(jù)劫持
new Observer(this.$data);
// 對數(shù)據(jù)和方法進行代理
proxyData(this, this.$data);
proxyData(this, this.$methods);
// 生命周期created函數(shù)
this.$options.created.apply(this);
// 如果有el屬性的話,自動調用$mount方法脆诉,掛載到DOM節(jié)點中
if(this.$options.el) {
this.$mount(this.$options.el);
}
};
// $mount方法甚亭,將Vue實例掛載到DOM節(jié)點上
Vue.prototype.$mount = function(el) {
// 拿到對應的DOM節(jié)點
let $el = typeof el === 'string'
?
document.querySelector(el)
: el.nodeType === 1
?
el
:
error('el必須是一個選擇器或者是一個DOM節(jié)點!');
// 將DOM保存在$el屬性上
this.$el = $el;
// 通過Compiler編譯器進行編譯
new Compiler(this.$el, this);
// 調用mounted生命周期鉤子函數(shù)
this.$options.mounted.apply(this);
// 返回當前的Vue實例击胜,保證外部能夠拿到正確的Vue實例
return this;
};
// 初始化Vue實例的data
function initData(options) {
// 拿到data的數(shù)據(jù)類型
const type = typeof options.data;
// 如果是function的話亏狰,調用函數(shù)拿到對象,否則直接返回對象
return type === 'function' ? options.data() : options.data;
}
// 對data內的數(shù)據(jù)進行代理
function proxyData(target, proxy) {
// 拿到對象上的所有key值組成的數(shù)組偶摔,并進行遍歷
Object.keys(proxy).forEach(key => {
// 通過Object.defineProperty方法對數(shù)據(jù)進行代理
Object.defineProperty(target, key, {
get() {
return proxy[key];
},
set(newValue) {
proxy[key] = newValue;
}
})
});
}
// 錯誤信息
function error(info) {
throw new Error(info);
}
Observer數(shù)據(jù)劫持
// 數(shù)據(jù)劫持
function Observer(data) {
// Observer必須是一個構造函數(shù)暇唾,如果不是通過new關鍵字調用的話,
// 在內部使用new關鍵字辰斋。
if(!(this instanceof arguments.callee)) {
return new arguments.callee(data);
}
// 如果data不是一個對象的話策州,提示錯誤,
// 因為只有對象才能調用Object.defineProperty
if(!data || typeof data !== 'object') {
error('代理的data必須是一個對象')
}
// 調用observe方法
this.observe(data);
}
Observer.prototype.observe = function(data) {
if(!data || typeof data !== 'object') {
return;
}
// 獲取對象上的鍵值數(shù)組并對它進行遍歷
Object.keys(data).forEach(key => {
// 調用數(shù)據(jù)劫持方法
this.defineReactive(data, key, data[key]);
// 判斷如果當前的值還是對象的話宫仗,遞歸劫持
if(typeof data[key] === 'object') {
this.observe(data[key]); // 遞歸劫持所有的值
}
})
};
Observer.prototype.defineReactive = function(data, key, value) {
// 保存this
const _this = this;
// 添加觀察者
let dep = new Dep();
// 數(shù)據(jù)劫持
Object.defineProperty(data, key, {
enumerable: true, // 可枚舉的
configurable: true, // 可刪除的
// 代理get
get() {
// 當前Dep.target是指的Watcher(訂閱者)實例够挂,
// 向dep實例中添加Watcher實例
Dep.target && dep.addSub(Dep.target);
return value;
},
// 代理set
set(newValue) {
// 如果新的值和舊的值不相等的情況下
if(newValue !== value) {
// 重新調用observe劫持數(shù)據(jù)
_this.observe(newValue);
// 設置新的值
value = newValue;
// dep實例通知訂閱者進行修改
dep.notify();
}
}
})
};
Dep發(fā)布者
// Dep發(fā)布者將要執(zhí)行的函數(shù)統(tǒng)一存儲在一個數(shù)組中管理,
// 當達到某個執(zhí)行條件時藕夫,循環(huán)這個數(shù)組并執(zhí)行每一個成員孽糖。
function Dep() {
this.subs = [];
}
// 在發(fā)布者Dep實例上添加訂閱者
Dep.prototype.addSub = function(watcher) {
this.subs.push(watcher);
};
// 通知訂閱者進行修改
Dep.prototype.notify = function() {
// 遍歷所有的訂閱者,調用訂閱者上的update方法進行修改毅贮。
this.subs.forEach(watcher => watcher.update());
};
Watcher訂閱者
// 訂閱者
function Watcher(vm, variable, callback) {
// 保存vm實例
this.vm = vm;
// 保存需要修改的屬性
this.variable = variable;
// 保存屬性修改時需要觸發(fā)的回調
this.callback = callback;
// 保存屬性的初始值办悟,并將當前訂閱者添加到發(fā)布者上
this.value = this.get();
}
Watcher.prototype.get = function() {
// 將當前的 watcher 添加到Dep發(fā)布者的靜態(tài)屬性上
Dep.target = this;
// 獲取到當前的屬性值
let value = CompilerUtil.getValue(this.vm, this.variable);
// 在Dep發(fā)布者的靜態(tài)屬性上清除當前 watcher
Dep.target = null;
// 返回拿到的值
return value;
};
Watcher.prototype.update = function() {
// 發(fā)生修改的時候,重新獲取值
let newValue = CompilerUtil.getValue(this.vm, this.variable);
// 先獲取舊的值
let oldValue = this.value;
// 如果兩個值不等的話滩褥,調用修改DOM的回調函數(shù)
if(newValue !== oldValue) {
this.callback(newValue);
}
};
Compiler模板編譯器
// Compiler模板編譯器
function Compiler(el, vm) {
// 先拿到需要編譯的DOM節(jié)點
this.el = el.nodeType === 1 ? el : document.querySelector(el);
// 拿到當前的vm實例
this.vm = vm;
// 如果當前的el存在誉尖,就開始編譯
if(this.el) {
// 將真實的DOM轉換為文檔碎片
let fragment = this.vNodeFragment(this.el);
// 調用compile方法進行編譯
this.compile(fragment);
// 編譯完成之后再添加到真實DOM中
this.el.appendChild(fragment);
}
}
// DOM文檔片段
Compiler.prototype.vNodeFragment = function(el) {
// 創(chuàng)建文檔片段
let fragment = document.createDocumentFragment();
let firstChild;
// 遍歷當前所有的DOM子節(jié)點
while (firstChild = el.firstChild) {
// 將真實DOM節(jié)點添加到文檔片段中
fragment.appendChild(firstChild);
}
// 返回虛擬文檔片段
return fragment;
};
// 進行編譯
Compiler.prototype.compile = function(fragment) {
// 拿到文檔片段的所有子節(jié)點
// 必須通過childNodes拿,因為childNodes不會忽略文本節(jié)點铸题。
let children = fragment.childNodes;
// 轉換為真實數(shù)組并進行遍歷
Array.prototype.slice.call(children).forEach(node => {
// 如果當前是元素節(jié)點的話,繼續(xù)遞歸遍歷琢感,并編譯元素節(jié)點
if(node.nodeType === 1) {
this.compile(node); // 對當前節(jié)點內的子節(jié)點進行遞歸遍歷
this.compileElement(node); // 編譯元素節(jié)點
}else {
// 否則是文本節(jié)點丢间,就開始編譯文本
this.compileText(node);
}
})
};
// 編譯元素節(jié)點
Compiler.prototype.compileElement = function (node) {
// 獲取到元素所有的屬性
let attrs = node.attributes;
// 轉換為真實數(shù)組并進行遍歷
Array.prototype.slice.call(attrs).forEach(attr => {
// 獲取到當前的屬性名
let attrName = attr.name;
// 判斷當前的屬性是否是指令
if(attrName.includes('v-')) {
// 如果是指令的話,拿到當前的屬性值
let value = attr.value;
// 拿到當前的指令名
let [,type] = attrName.split('-');
// 對當前指令執(zhí)行編譯
CompilerUtil[type](node, this.vm, value);
// 判斷當前屬性是否是事件
}else if(attrName.includes('@')) {
// 拿到事件名稱
let event = attrName.slice(1);
// 拿到事件需要觸發(fā)的方法名稱
let method = attr.value;
// 對當前元素添加DOM事件
CompilerUtil.addEvent(node, event, method, this.vm);
}
})
};
// 編譯文本節(jié)點
Compiler.prototype.compileText = function (node) {
let content = node.textContent; // 獲取文本節(jié)點的內容
let reg = /\{\{(.+?)\}\}/g; // 匹配模板編譯器的內容
// 如果能匹配到模板編譯器
if(reg.test(content)) {
// 編譯文本節(jié)點
CompilerUtil.text(node, this.vm, content);
}
};
模板編譯工具
// 模板編譯工具對象
const CompilerUtil = {
// 文本編譯的回調函數(shù)
textUpdater(node, value) {
node.textContent = value;
},
// input編譯的回調函數(shù)
modelUpdater(node, value) {
node.value = value;
},
// 獲取vm實例中對應的值
getValue(vm, variable) {
// 獲取對象的屬性
variable = variable.split('.');
// 通過reduce方法遞歸遍歷vm.$data驹针,拿到最終在vm實例中的屬性值
return variable.reduce((prev, next) => prev[next], vm.$data);
},
// 獲取文本中變量對應的內容
getTextValue(vm, variable) {
// 通過正則匹配烘挫,拿到屬性名
let reg = /\{\{([^}]+)\}\}/g;
return variable.replace(reg, ($0, $1) => {
// 通過屬性名,調用getValue方法,獲取屬性值
return this.getValue(vm, $1);
})
},
// 設置Value
setValue(vm, variable, newValue) {
// 獲取對象的屬性名
variable = variable.split('.');
// 通過reduce方法遍歷
return variable.reduce((prev, next, index) => {
// 如果當前是匹配的屬性名的話
if(index === variable.length - 1) {
// 給當前的屬性設置值
return prev[next] = newValue;
}
// 如果不是就返回繼續(xù)計算
return prev[next];
}, vm.$data);
},
// 雙向數(shù)據(jù)綁定 v-model的簡單實現(xiàn)
model(node, vm, variable) {
// 獲取到雙向數(shù)據(jù)綁定的修改方法
let updateFn = this.modelUpdater;
// 獲取到對應的值
let value = this.getValue(vm, variable);
// 添加訂閱者饮六, 給訂閱者添加回調
new Watcher(vm, variable, newValue => {
// 當數(shù)據(jù)發(fā)生修改的時候其垄,就觸發(fā)當前回調,修改元素節(jié)點的值
updateFn && updateFn(node, newValue);
});
// 將v-model屬性從DOM節(jié)點上刪除
node.removeAttribute('v-model');
// 給當前元素節(jié)點添加input事件
node.addEventListener('input', e => {
// 拿到對應的值
let value = e.target.value;
// 設置值
this.setValue(vm, variable, value);
});
// 初次渲染的時候卤橄,也要設置一次值
updateFn && updateFn(node, value);
},
// 添加事件
addEvent(node, event, method, vm) {
// 給元素刪除事件符
node.removeAttribute('@'+event);
// 給元素添加事件
node.addEventListener(event, (...args) => {
// 調用vm上的方法绿满,并傳入?yún)?shù)
vm[method].apply(vm, args);
})
},
// 編譯文本節(jié)點的變量
text(node, vm, variable) {
// 文本節(jié)點的修改函數(shù)
let updateFn = this.textUpdater;
// 獲取到文本節(jié)點變量的值
let value = this.getTextValue(vm, variable);
// 定義正則
let reg = /\{\{(.+?)\}\}/g;
// 通過正則匹配變量,給變量添加觀察者
variable.replace(reg, ($0, $1) => {
// 當解析模板遇到變量的時候窟扑,應該使用觀察者監(jiān)聽這個變量
new Watcher(vm, $1, newValue => {
// 觀察者的回調函數(shù)喇颁,當數(shù)據(jù)發(fā)生改變就觸發(fā)該回調
updateFn && updateFn(node, newValue);
})
});
// 第一次設置值
updateFn && updateFn(node, value);
}
};
結束
參考文章鏈接:
一起學習、手寫MVVM框架
前端 實現(xiàn)一個簡易版的vue嚎货,了解vue的運行機制
JS實現(xiàn)一個簡易版的vue
如果本文對您有幫助橘霎,可以看看本人的其他文章:
前端常見面試題(十六)@郝晨光
前端常見面試題(十五)@郝晨光
前端常見面試題(十四)@郝晨光