十年網(wǎng)站開發(fā)經(jīng)驗 + 多家企業(yè)客戶 + 靠譜的建站團隊
量身定制 + 運營維護+專業(yè)推廣+無憂售后,網(wǎng)站問題一站解決
這篇文章主要介紹了vue雙向綁定原理實例分析的相關知識,內(nèi)容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇vue雙向綁定原理實例分析文章都會有所收獲,下面我們一起來看看吧。
專注于為中小企業(yè)提供網(wǎng)站制作、網(wǎng)站設計服務,電腦端+手機端+微信端的三站合一,更高效的管理,為中小企業(yè)東興免費做網(wǎng)站提供優(yōu)質(zhì)的服務。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動了成百上千家企業(yè)的穩(wěn)健成長,幫助中小企業(yè)通過網(wǎng)站建設實現(xiàn)規(guī)模擴充和轉(zhuǎn)變。

vue最少需要兩個參數(shù):模板和data。
創(chuàng)建Compiler對象,將數(shù)據(jù)渲染到模板后,掛載到指定跟節(jié)點中。
class MyVue {
// 1,接收兩個參數(shù):模板(根節(jié)點),和數(shù)據(jù)對象
constructor(options) {
// 保存模板,和數(shù)據(jù)對象
if (this.isElement(options.el)) {
this.$el = options.el;
} else {
this.$el = document.querySelector(options.el);
}
this.$data = options.data;
// 2.根據(jù)模板和數(shù)據(jù)對象,渲染到根節(jié)點
if (this.$el) {
// 監(jiān)聽data所有屬性的get/set
new Observer(this.$data);
new Compiler(this)
}
}
// 判斷是否是一個dom元素
isElement(node) {
return node.nodeType === 1;
}
}Compiler
1,node2fragment函數(shù)將模板元素提取到內(nèi)存中,方便將數(shù)據(jù)渲染到模板后,再一次性掛載到頁面中
2,模板提取到內(nèi)存后,使用buildTemplate函數(shù)遍歷該模板元素
元素節(jié)點
使用buildElement函數(shù)檢查元素上以v-開頭的屬性
文本節(jié)點
用buildText函數(shù)檢查文本中有無{{}}內(nèi)容
3,創(chuàng)建CompilerUtil類,用于處理vue指令和{{}},完成數(shù)據(jù)的渲染
4,到此就完成了首次數(shù)據(jù)渲染,接下來需要實現(xiàn)數(shù)據(jù)改變時,自動更新視圖。
class Compiler {
constructor(vm) {
this.vm = vm;
// 1.將網(wǎng)頁上的元素放到內(nèi)存中
let fragment = this.node2fragment(this.vm.$el);
// 2.利用指定的數(shù)據(jù)編譯內(nèi)存中的元素
this.buildTemplate(fragment);
// 3.將編譯好的內(nèi)容重新渲染會網(wǎng)頁上
this.vm.$el.appendChild(fragment);
}
node2fragment(app) {
// 1.創(chuàng)建一個空的文檔碎片對象
let fragment = document.createDocumentFragment();
// 2.編譯循環(huán)取到每一個元素
let node = app.firstChild;
while (node) {
// 注意點: 只要將元素添加到了文檔碎片對象中, 那么這個元素就會自動從網(wǎng)頁上消失
fragment.appendChild(node);
node = app.firstChild;
}
// 3.返回存儲了所有元素的文檔碎片對象
return fragment;
}
buildTemplate(fragment) {
let nodeList = [...fragment.childNodes];
nodeList.forEach(node => {
// 需要判斷當前遍歷到的節(jié)點是一個元素還是一個文本
if (this.vm.isElement(node)) {
// 元素節(jié)點
this.buildElement(node);
// 處理子元素
this.buildTemplate(node);
} else {
// 文本節(jié)點
this.buildText(node);
}
})
}
buildElement(node) {
let attrs = [...node.attributes];
attrs.forEach(attr => {
// v-model="name" => {name:v-model value:name}
let { name, value } = attr;
// v-model / v-html / v-text / v-xxx
if (name.startsWith('v-')) {
// v-model -> [v, model]
let [_, directive] = name.split('-');
CompilerUtil[directive](node, value, this.vm);
}
})
}
buildText(node) {
let content = node.textContent;
let reg = /\{\{.+?\}\}/gi;
if (reg.test(content)) {
CompilerUtil['content'](node, content, this.vm);
}
}
}let CompilerUtil = {
getValue(vm, value) {
// 解析this.data.aaa.bbb.ccc這種屬性
return value.split('.').reduce((data, currentKey) => {
return data[currentKey.trim()];
}, vm.$data);
},
getContent(vm, value) {
// 解析{{}}中的變量
let reg = /\{\{(.+?)\}\}/gi;
let val = value.replace(reg, (...args) => {
return this.getValue(vm, args[1]);
});
return val;
},
// 解析v-model指令
model: function (node, value, vm) {
// 在觸發(fā)getter之前,為dom創(chuàng)建Wather,并為Watcher.target賦值
new Watcher(vm, value, (newValue, oldValue) => {
node.value = newValue;
});
let val = this.getValue(vm, value);
node.value = val;
},
// 解析v-html指令
html: function (node, value, vm) {
// 在觸發(fā)getter之前,為dom創(chuàng)建Wather,并為Watcher.target賦值
new Watcher(vm, value, (newValue, oldValue) => {
node.innerHTML = newValue;
});
let val = this.getValue(vm, value);
node.innerHTML = val;
},
// 解析v-text指令
text: function (node, value, vm) {
// 在觸發(fā)getter之前,為dom創(chuàng)建Wather,并為Watcher.target賦值
new Watcher(vm, value, (newValue, oldValue) => {
node.innerText = newValue;
});
let val = this.getValue(vm, value);
node.innerText = val;
},
// 解析{{}}中的變量
content: function (node, value, vm) {
let reg = /\{\{(.+?)\}\}/gi;
let val = value.replace(reg, (...args) => {
// 在觸發(fā)getter之前,為dom創(chuàng)建Wather,并為Watcher.target賦值
new Watcher(vm, args[1], (newValue, oldValue) => {
node.textContent = this.getContent(vm, value);
});
return this.getValue(vm, args[1]);
});
node.textContent = val;
}
}Observer
1,使用defineRecative函數(shù)對data做Object.defineProperty處理,使得data中的每個數(shù)據(jù)都可以進行get/set監(jiān)聽
2,接下來將考慮如何在監(jiān)聽到data值改變后,更新視圖內(nèi)容呢?使用觀察者設計模式,創(chuàng)建Dep和Wather類。
class Observer {
constructor(data) {
this.observer(data);
}
observer(obj) {
if (obj && typeof obj === 'object') {
// 遍歷取出傳入對象的所有屬性, 給遍歷到的屬性都增加get/set方法
for (let key in obj) {
this.defineRecative(obj, key, obj[key])
}
}
}
// obj: 需要操作的對象
// attr: 需要新增get/set方法的屬性
// value: 需要新增get/set方法屬性的取值
defineRecative(obj, attr, value) {
// 如果屬性的取值又是一個對象, 那么也需要給這個對象的所有屬性添加get/set方法
this.observer(value);
// 第三步: 將當前屬性的所有觀察者對象都放到當前屬性的發(fā)布訂閱對象中管理起來
let dep = new Dep(); // 創(chuàng)建了屬于當前屬性的發(fā)布訂閱對象
Object.defineProperty(obj, attr, {
get() {
// 在這里收集依賴
Dep.target && dep.addSub(Dep.target);
return value;
},
set: (newValue) => {
if (value !== newValue) {
// 如果給屬性賦值的新值又是一個對象, 那么也需要給這個對象的所有屬性添加get/set方法
this.observer(newValue);
value = newValue;
dep.notify();
console.log('監(jiān)聽到數(shù)據(jù)的變化');
}
}
})
}
}使用觀察者設計模式,創(chuàng)建Dep和Wather類
1,使用觀察者設計模式的目的是:
解析模板,收集data中某個數(shù)據(jù)在模板中被使用的dom節(jié)點集合,當該數(shù)據(jù)改變時,更新該dom節(jié)點集合就實現(xiàn)了數(shù)據(jù)更新。
Dep:用于收集某個data屬性依賴的dom節(jié)點集合,并提供更新方法
Watcher:每個dom節(jié)點的包裹對象
attr:該dom使用的data屬性
cb:修改該dom值的回調(diào)函數(shù),在創(chuàng)建的時候會接收
2,到這里感覺思路是沒問題了,已經(jīng)是勝券在握了。那Dep和Watcher該怎么使用呢?
為每個屬性添加一個dep,用來收集依賴的dom
因為頁面首次渲染的時候會讀取data數(shù)據(jù),這時候會觸發(fā)該data的getter,所以在此收集dom
具體如何收集呢,在CompilerUtil類解析v-model,{{}}等命令時,會觸發(fā)getter,我們在觸發(fā)之前創(chuàng)建Wather,為Watcher添加一個靜態(tài)屬性,指向該dom,然后在getter函數(shù)里面獲取該靜態(tài)變量,并添加到依賴中,就完成了一次收集。因為每次觸發(fā)getter之前都對該靜態(tài)變量賦值,所以不存在收集錯依賴的情況。
class Dep {
constructor() {
// 這個數(shù)組就是專門用于管理某個屬性所有的觀察者對象的
this.subs = [];
}
// 訂閱觀察的方法
addSub(watcher) {
this.subs.push(watcher);
}
// 發(fā)布訂閱的方法
notify() {
this.subs.forEach(watcher => watcher.update());
}
}class Watcher {
constructor(vm, attr, cb) {
this.vm = vm;
this.attr = attr;
this.cb = cb;
// 在創(chuàng)建觀察者對象的時候就去獲取當前的舊值
this.oldValue = this.getOldValue();
}
getOldValue() {
Dep.target = this;
let oldValue = CompilerUtil.getValue(this.vm, this.attr);
Dep.target = null;
return oldValue;
}
// 定義一個更新的方法, 用于判斷新值和舊值是否相同
update() {
let newValue = CompilerUtil.getValue(this.vm, this.attr);
if (this.oldValue !== newValue) {
this.cb(newValue, this.oldValue);
}
}
}3,到這里就實現(xiàn)了數(shù)據(jù)綁定時,視圖自動更新,本來想代碼一步步實現(xiàn)的,但是發(fā)現(xiàn)不好處理,就把完整的class貼出來了。
其實就是監(jiān)聽輸入框的input、change事件。修改CompilerUtil的model方法。具體代碼如下
model: function (node, value, vm) {
new Watcher(vm, value, (newValue, oldValue)=>{
node.value = newValue;
});
let val = this.getValue(vm, value);
node.value = val;
// 看這里
node.addEventListener('input', (e)=>{
let newValue = e.target.value;
this.setValue(vm, value, newValue);
})
},vue雙向綁定原理
vue接收一個模板和data參數(shù)。
1,首先將data中的數(shù)據(jù)進行遞歸遍歷,對每個屬性執(zhí)行Object.defineProperty,定義get和set函數(shù)。并為每個屬性添加一個dep數(shù)組。當get執(zhí)行時,會為調(diào)用的dom節(jié)點創(chuàng)建一個watcher存放在該數(shù)組中。當set執(zhí)行時,重新賦值,并調(diào)用dep數(shù)組的notify方法,通知所有使用了該屬性watcher,并更新對應dom的內(nèi)容。
2,將模板加載到內(nèi)存中,遞歸模板中的元素,檢測到元素有v-開頭的命令或者雙大括號的指令,就會從data中取對應的值去修改模板內(nèi)容,這個時候就將該dom元素添加到了該屬性的dep數(shù)組中。這就實現(xiàn)了數(shù)據(jù)驅(qū)動視圖。在處理v-model指令的時候,為該dom添加input事件(或change),輸入時就去修改對應的屬性的值,實現(xiàn)了頁面驅(qū)動數(shù)據(jù)。
3,將模板與數(shù)據(jù)進行綁定后,將模板添加到真實dom樹中。
如何將watcher放在dep數(shù)組中?
在解析模板的時候,會根據(jù)v-指令獲取對應data屬性值,這個時候就會調(diào)用屬性的get方法,我們先創(chuàng)建Watcher實例,并在其內(nèi)部獲取該屬性值,作為舊值存放在watcher內(nèi)部,我們在獲取該值之前,在Watcher原型對象上添加屬性Watcher.target = this;然后取值,將講Watcher.target = null;這樣get在被調(diào)用的時候就可以根據(jù)Watcher.target獲取到watcher實例對象。
methods的原理
創(chuàng)建vue實例的時候,接收methods參數(shù)
在解析模板的時候遇到v-on的指令。會對該dom元素添加對應事件的監(jiān)聽,并使用call方法將vue綁定為該方法的this:vm.$methods[value].call(vm, e);
computed的原理
創(chuàng)建vue實例的時候,接收computed參數(shù)
初始化vue實例的時候,為computed的key進行Object.defineProperty處理,并添加get屬性。
關于“vue雙向綁定原理實例分析”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對“vue雙向綁定原理實例分析”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注創(chuàng)新互聯(lián)行業(yè)資訊頻道。