vue.js 實現(xiàn)v-model與{{}}指令方法
上次我們已經(jīng)分析了vue.js是通過Object.defineProperty以及發(fā)布訂閱模式來進行數(shù)據(jù)劫持和監(jiān)聽,并且實現(xiàn)了一個簡單的demo。今天,我們就基于上一節(jié)的代碼,來實現(xiàn)一個MVVM類,將其與html結(jié)合在一起,并且實現(xiàn)v-model以及{{}}語法。
tips:本節(jié)新增代碼(去除注釋)在一百行左右。使用的Observer和Watcher都是延用上一節(jié)的代碼,沒有修改。
接下來,讓我們一步步來,實現(xiàn)一個MVVM類。
構(gòu)造函數(shù)
首先,一個MVVM的構(gòu)造函數(shù)如下(和vue.js的構(gòu)造函數(shù)一樣):
class MVVM {
constructor({ data, el }) {
this.data = data;
this.el = el;
this.init();
this.initDom();
}
}
和vue.js一樣,有它的data屬性以及el元素。
初始化操作
vue.js可以通過this.xxx的方法來直接訪問this.data.xxx的屬性,這一點是怎么做到的呢?其實答案很簡單,它是通過Object.defineProperty來做手腳,當你訪問this.xxx的時候,它返回的其實是this.data.xxx。當你修改this.xxx值的時候,其實修改的是this.data.xxx的值。具體可以看如下代碼:
class MVVM {
constructor({ data, el }) {
this.data = data;
this.el = el;
this.init();
this.initDom();
}
// 初始化
init() {
// 對this.data進行數(shù)據(jù)劫持
new Observer(this.data);
// 傳入的el可以是selector,也可以是元素,因此我們要在這里做一層處理,保證this.$el的值是一個元素節(jié)點
this.$el = this.isElementNode(this.el) ? this.el : document.querySelector(this.el);
// 將this.data的屬性都綁定到this上,這樣用戶就可以直接通過this.xxx來訪問this.data.xxx的值
for (let key in this.data) {
this.defineReactive(key);
}
}
defineReactive(key) {
Object.defineProperty(this, key, {
get() {
return this.data[key];
},
set(newVal) {
this.data[key] = newVal;
} //前端全棧學(xué)習(xí)交流圈:866109386
})//面向1-3年前端開發(fā)人員
}//幫助突破技術(shù)瓶頸,提升思維能力。
// 是否是屬性節(jié)點
isElementNode(node) {
return node.nodeType === 1;
}
}
在完成初始化操作后,我們需要對this.$el的節(jié)點進行編譯。目前我們要實現(xiàn)的語法有v-model和{{}}語法,v-model這個屬性只可能會出現(xiàn)在元素節(jié)點的attributes里,而{{}}語法則是出現(xiàn)在文本節(jié)點里。
fragment
在對節(jié)點進行編譯之前,我們先考慮一個現(xiàn)實問題:如果我們在編譯過程中直接操作DOM節(jié)點的話,每一次修改DOM都會導(dǎo)致DOM的回流或重繪,而這一部分性能損耗是很沒有必要的。因此,我們可以利用fragment,將節(jié)點轉(zhuǎn)化為fragment,然后在fragment里編譯完成后,再將其放回到頁面上。
class MVVM {
constructor({ data, el }) {
this.data = data;
this.el = el;//前端全棧交流學(xué)習(xí)圈:866109386
this.init();//針對1-3年前端開發(fā)人員
this.initDom();//幫助突破技術(shù)瓶頸,提升思維能力。
}
initDom() {
const fragment = this.node2Fragment();
this.compile(fragment);
// 將fragment返回到頁面中
document.body.appendChild(fragment);
}
// 將節(jié)點轉(zhuǎn)為fragment,通過fragment來操作DOM,可以獲得更高的效率
// 因為如果直接操作DOM節(jié)點的話,每次修改DOM都會導(dǎo)致DOM的回流或重繪,而將其放在fragment里,修改fragment不會導(dǎo)致DOM回流和重繪
// 當在fragment一次性修改完后,在直接放回到DOM節(jié)點中
node2Fragment() {
const fragment = document.createDocumentFragment();
let firstChild;
while(firstChild = this.$el.firstChild) {
fragment.appendChild(firstChild);
}
return fragment;
}
}
實現(xiàn)v-model
在將node節(jié)點轉(zhuǎn)為fragment后,我們來對其中的v-model語法進行編譯。
由于v-model語句只可能會出現(xiàn)在元素節(jié)點的attributes里,因此,我們先判斷該節(jié)點是否為元素節(jié)點,若為元素節(jié)點,則判斷其是否是directive(目前只有v-model),若都滿足的話,則調(diào)用CompileUtils.compileModelAttr來編譯該節(jié)點。
編譯含有v-model的節(jié)點主要有兩步:
- 為元素節(jié)點注冊input事件,在input事件觸發(fā)的時候,更新vm(this.data)上對應(yīng)的屬性值。
- 對v-model依賴的屬性注冊一個Watcher函數(shù),當依賴的屬性發(fā)生變化,則更新元素節(jié)點的value。
class MVVM {
constructor({ data, el }) {
this.data = data;
this.el = el;
this.init();
this.initDom();
}
initDom() {
const fragment = this.node2Fragment();
this.compile(fragment);
// 將fragment返回到頁面中
document.body.appendChild(fragment);
}
compile(node) {
if (this.isElementNode(node)) {
// 若是元素節(jié)點,則遍歷它的屬性,編譯其中的指令
const attrs = node.attributes;
Array.prototype.forEach.call(attrs, (attr) => {
if (this.isDirective(attr)) {
CompileUtils.compileModelAttr(this.data, node, attr)
}
})
}
// 若節(jié)點有子節(jié)點的話,則對子節(jié)點進行編譯
if (node.childNodes && node.childNodes.length > 0) {
Array.prototype.forEach.call(node.childNodes, (child) => {
this.compile(child);
})
}
}
// 是否是屬性節(jié)點
isElementNode(node) {
return node.nodeType === 1;
}
// 檢測屬性是否是指令(vue的指令是v-開頭)
isDirective(attr) {
return attr.nodeName.indexOf('v-') >= 0;
}
}
const CompileUtils = {
// 編譯v-model屬性,為元素節(jié)點注冊input事件,在input事件觸發(fā)的時候,更新vm對應(yīng)的值。
// 同時也注冊一個Watcher函數(shù),當所依賴的值發(fā)生變化的時候,更新節(jié)點的值
compileModelAttr(vm, node, attr) {
const { value: keys, nodeName } = attr;
node.value = this.getModelValue(vm, keys);
// 將v-model屬性值從元素節(jié)點上去掉
node.removeAttribute(nodeName);
node.addEventListener('input', (e) => {
this.setModelValue(vm, keys, e.target.value);
});
new Watcher(vm, keys, (oldVal, newVal) => {
node.value = newVal;
});
},
/* 解析keys,比如,用戶可以傳入
* <input v-model="obj.name" />
* 這個時候,我們在取值的時候,需要將"obj.name"解析為data[obj][name]的形式來獲取目標值
*/
parse(vm, keys) {
keys = keys.split('.');
let value = vm;
keys.forEach(_key => {
value = value[_key];
});
return value;
},
// 根據(jù)vm和keys,返回v-model對應(yīng)屬性的值
getModelValue(vm, keys) {
return this.parse(vm, keys);
},
// 修改v-model對應(yīng)屬性的值
setModelValue(vm, keys, val) {
keys = keys.split('.');
let value = vm;
for(let i = 0; i < keys.length - 1; i++) {
value = value[keys[i]];
}
value[keys[keys.length - 1]] = val;
},
}
實現(xiàn){{}}語法
{{}}語法只可能會出現(xiàn)在文本節(jié)點中,因此,我們只需要對文本節(jié)點做處理。如果文本節(jié)點中出現(xiàn){{key}}這種語句的話,我們則對該節(jié)點進行編譯。在這里,我們可以通過下面這個正則表達式來對文本節(jié)點進行處理,判斷其是否含有{{}}語法。
const textReg = /\{\{\s*\w+\s*\}\}/gi; // 檢測{{name}}語法
console.log(textReg.test('sss'));
console.log(textReg.test('aaa{{ name }}'));
console.log(textReg.test('aaa{{ name }} {{ text }}'));
若含有{{}}語法,我們則可以對其處理,由于一個文本節(jié)點可能出現(xiàn)多個{{}}語法,因此編譯含有{{}}語法的文本節(jié)點主要有以下兩步:
- 找出該文本節(jié)點中所有依賴的屬性,并且保留原始文本信息,根據(jù)原始文本信息還有屬性值,生成最終的文本信息。比如說,原始文本信息是"test {{test}} {{name}}",那么該文本信息依賴的屬性有this.data.test和this.data.name,那么我們可以根據(jù)原本信息和屬性值,生成最終的文本。
- 為該文本節(jié)點所有依賴的屬性注冊Watcher函數(shù),當依賴的屬性發(fā)生變化的時候,則更新文本節(jié)點的內(nèi)容。
class MVVM {
constructor({ data, el }) {
this.data = data;
this.el = el;
this.init();
this.initDom();
}
initDom() {
const fragment = this.node2Fragment();
this.compile(fragment);
// 將fragment返回到頁面中
document.body.appendChild(fragment);
}
compile(node) {
const textReg = /\{\{\s*\w+\s*\}\}/gi; // 檢測{{name}}語法
if (this.isTextNode(node)) {
// 若是文本節(jié)點,則判斷是否有{{}}語法,如果有的話,則編譯{{}}語法
let textContent = node.textContent;
if (textReg.test(textContent)) {
// 對于 "test{{test}} {{name}}"這種文本,可能在一個文本節(jié)點會出現(xiàn)多個匹配符,因此得對他們統(tǒng)一進行處理
// 使用 textReg來對文本節(jié)點進行匹配,可以得到["{{test}}", "{{name}}"]兩個匹配值
const matchs = textContent.match(textReg);
CompileUtils.compileTextNode(this.data, node, matchs);
}
}
// 若節(jié)點有子節(jié)點的話,則對子節(jié)點進行編譯
if (node.childNodes && node.childNodes.length > 0) {
Array.prototype.forEach.call(node.childNodes, (child) => {
this.compile(child);
})
}
}
// 是否是文本節(jié)點
isTextNode(node) {
return node.nodeType === 3;
}
}
const CompileUtils = {
reg: /\{\{\s*(\w+)\s*\}\}/, // 匹配 {{ key }}中的key
// 編譯文本節(jié)點,并注冊Watcher函數(shù),當文本節(jié)點依賴的屬性發(fā)生變化的時候,更新文本節(jié)點
compileTextNode(vm, node, matchs) {
// 原始文本信息
const rawTextContent = node.textContent;
matchs.forEach((match) => {
const keys = match.match(this.reg)[1];
console.log(rawTextContent);
new Watcher(vm, keys, () => this.updateTextNode(vm, node, matchs, rawTextContent));
});
this.updateTextNode(vm, node, matchs, rawTextContent);
},
// 更新文本節(jié)點信息
updateTextNode(vm, node, matchs, rawTextContent) {
let newTextContent = rawTextContent;
matchs.forEach((match) => {
const keys = match.match(this.reg)[1];
const val = this.getModelValue(vm, keys);
newTextContent = newTextContent.replace(match, val);
})
node.textContent = newTextContent;
}
}
結(jié)語
這樣,一個具有v-model和{{}}功能的MVVM類就已經(jīng)完成了
這里也有一個簡單的樣例(忽略樣式)。
接下來的話,可能會繼續(xù)實現(xiàn)computed屬性,v-bind方法,以及支持在{{}}里面放表達式。如果覺得這個文章對你有幫助的話,麻煩點個贊,嘻嘻。
最后,貼上所有的代碼:
class Observer {
constructor(data) {
// 如果不是對象,則返回
if (!data || typeof data !== 'object') {
return;
}
this.data = data;
this.walk();
}
// 對傳入的數(shù)據(jù)進行數(shù)據(jù)劫持
walk() {
for (let key in this.data) {
this.defineReactive(this.data, key, this.data[key]);
}
}
// 創(chuàng)建當前屬性的一個發(fā)布實例,使用Object.defineProperty來對當前屬性進行數(shù)據(jù)劫持。
defineReactive(obj, key, val) {
// 創(chuàng)建當前屬性的發(fā)布者
const dep = new Dep();
/*
* 遞歸對子屬性的值進行數(shù)據(jù)劫持,比如說對以下數(shù)據(jù)
* let data = {
* name: 'cjg',
* obj: {
* name: 'zht',
* age: 22,
* obj: {
* name: 'cjg',
* age: 22,
* }
* },
* };
* 我們先對data最外層的name和obj進行數(shù)據(jù)劫持,之后再對obj對象的子屬性obj.name,obj.age, obj.obj進行數(shù)據(jù)劫持,層層遞歸下去,直到所有的數(shù)據(jù)都完成了數(shù)據(jù)劫持工作。
*/
new Observer(val);
Object.defineProperty(obj, key, {
get() {
// 若當前有對該屬性的依賴項,則將其加入到發(fā)布者的訂閱者隊列里
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set(newVal) {
if (val === newVal) {
return;
}
val = newVal;
new Observer(newVal);
dep.notify();
}
})
}
}
// 發(fā)布者,將依賴該屬性的watcher都加入subs數(shù)組,當該屬性改變的時候,則調(diào)用所有依賴該屬性的watcher的更新函數(shù),觸發(fā)更新。
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
if (this.subs.indexOf(sub) < 0) {
this.subs.push(sub);
}
}
notify() {
this.subs.forEach((sub) => {
sub.update();
})
}
}
Dep.target = null;
// 觀察者
class Watcher {
/**
*Creates an instance of Watcher.
* @param {*} vm
* @param {*} keys
* @param {*} updateCb
* @memberof Watcher
*/
constructor(vm, keys, updateCb) {
this.vm = vm;
this.keys = keys;
this.updateCb = updateCb;
this.value = null;
this.get();
}
// 根據(jù)vm和keys獲取到最新的觀察值
get() {
// 將Dep的依賴項設(shè)置為當前的watcher,并且根據(jù)傳入的keys遍歷獲取到最新值。
// 在這個過程中,由于會調(diào)用observer對象屬性的getter方法,因此在遍歷過程中這些對象屬性的發(fā)布者就將watcher添加到訂閱者隊列里。
// 因此,當這一過程中的某一對象屬性發(fā)生變化的時候,則會觸發(fā)watcher的update方法
Dep.target = this;
this.value = CompileUtils.parse(this.vm, this.keys);
Dep.target = null;
return this.value;
}
update() {
const oldValue = this.value;
const newValue = this.get();
if (oldValue !== newValue) {
this.updateCb(oldValue, newValue);
}
}
}
class MVVM {
constructor({ data, el }) {
this.data = data;
this.el = el;
this.init();
this.initDom();
}
// 初始化
init() {
// 對this.data進行數(shù)據(jù)劫持
new Observer(this.data);
// 傳入的el可以是selector,也可以是元素,因此我們要在這里做一層處理,保證this.$el的值是一個元素節(jié)點
this.$el = this.isElementNode(this.el) ? this.el : document.querySelector(this.el);
// 將this.data的屬性都綁定到this上,這樣用戶就可以直接通過this.xxx來訪問this.data.xxx的值
for (let key in this.data) {
this.defineReactive(key);
}
}
initDom() {
const fragment = this.node2Fragment();
this.compile(fragment);
document.body.appendChild(fragment);
}
// 將節(jié)點轉(zhuǎn)為fragment,通過fragment來操作DOM,可以獲得更高的效率
// 因為如果直接操作DOM節(jié)點的話,每次修改DOM都會導(dǎo)致DOM的回流或重繪,而將其放在fragment里,修改fragment不會導(dǎo)致DOM回流和重繪
// 當在fragment一次性修改完后,在直接放回到DOM節(jié)點中
node2Fragment() {
const fragment = document.createDocumentFragment();
let firstChild;
while(firstChild = this.$el.firstChild) {
fragment.appendChild(firstChild);
}
return fragment;
}
defineReactive(key) {
Object.defineProperty(this, key, {
get() {
return this.data[key];
},
set(newVal) {
this.data[key] = newVal;
}
})
}
compile(node) {
const textReg = /\{\{\s*\w+\s*\}\}/gi; // 檢測{{name}}語法
if (this.isElementNode(node)) {
// 若是元素節(jié)點,則遍歷它的屬性,編譯其中的指令
const attrs = node.attributes;
Array.prototype.forEach.call(attrs, (attr) => {
if (this.isDirective(attr)) {
CompileUtils.compileModelAttr(this.data, node, attr)
}
})
} else if (this.isTextNode(node)) {
// 若是文本節(jié)點,則判斷是否有{{}}語法,如果有的話,則編譯{{}}語法
let textContent = node.textContent;
if (textReg.test(textContent)) {
// 對于 "test{{test}} {{name}}"這種文本,可能在一個文本節(jié)點會出現(xiàn)多個匹配符,因此得對他們統(tǒng)一進行處理
// 使用 textReg來對文本節(jié)點進行匹配,可以得到["{{test}}", "{{name}}"]兩個匹配值
const matchs = textContent.match(textReg);
CompileUtils.compileTextNode(this.data, node, matchs);
}
}
// 若節(jié)點有子節(jié)點的話,則對子節(jié)點進行編譯。
if (node.childNodes && node.childNodes.length > 0) {
Array.prototype.forEach.call(node.childNodes, (child) => {
this.compile(child);
})
}
}
// 是否是屬性節(jié)點
isElementNode(node) {
return node.nodeType === 1;
}
// 是否是文本節(jié)點
isTextNode(node) {
return node.nodeType === 3;
}
isAttrs(node) {
return node.nodeType === 2;
}
// 檢測屬性是否是指令(vue的指令是v-開頭)
isDirective(attr) {
return attr.nodeName.indexOf('v-') >= 0;
}
}
const CompileUtils = {
reg: /\{\{\s*(\w+)\s*\}\}/, // 匹配 {{ key }}中的key
// 編譯文本節(jié)點,并注冊Watcher函數(shù),當文本節(jié)點依賴的屬性發(fā)生變化的時候,更新文本節(jié)點
compileTextNode(vm, node, matchs) {
// 原始文本信息
const rawTextContent = node.textContent;
matchs.forEach((match) => {
const keys = match.match(this.reg)[1];
console.log(rawTextContent);
new Watcher(vm, keys, () => this.updateTextNode(vm, node, matchs, rawTextContent));
});
this.updateTextNode(vm, node, matchs, rawTextContent);
},
// 更新文本節(jié)點信息
updateTextNode(vm, node, matchs, rawTextContent) {
let newTextContent = rawTextContent;
matchs.forEach((match) => {
const keys = match.match(this.reg)[1];
const val = this.getModelValue(vm, keys);
newTextContent = newTextContent.replace(match, val);
})
node.textContent = newTextContent;
},
// 編譯v-model屬性,為元素節(jié)點注冊input事件,在input事件觸發(fā)的時候,更新vm對應(yīng)的值。
// 同時也注冊一個Watcher函數(shù),當所依賴的值發(fā)生變化的時候,更新節(jié)點的值
compileModelAttr(vm, node, attr) {
const { value: keys, nodeName } = attr;
node.value = this.getModelValue(vm, keys);
// 將v-model屬性值從元素節(jié)點上去掉
node.removeAttribute(nodeName);
new Watcher(vm, keys, (oldVal, newVal) => {
node.value = newVal;
});
node.addEventListener('input', (e) => {
this.setModelValue(vm, keys, e.target.value);
});
},
/* 解析keys,比如,用戶可以傳入
* let data = {
* name: 'cjg',
* obj: {
* name: 'zht',
* },
* };
* new Watcher(data, 'obj.name', (oldValue, newValue) => {
* console.log(oldValue, newValue);
* })
* 這個時候,我們需要將keys解析為data[obj][name]的形式來獲取目標值
*/
parse(vm, keys) {
keys = keys.split('.');
let value = vm;
keys.forEach(_key => {
value = value[_key];
});
return value;
},
// 根據(jù)vm和keys,返回v-model對應(yīng)屬性的值
getModelValue(vm, keys) {
return this.parse(vm, keys);
},
// 修改v-model對應(yīng)屬性的值
setModelValue(vm, keys, val) {
keys = keys.split('.');
let value = vm;
for(let i = 0; i < keys.length - 1; i++) {
value = value[keys[i]];
}
value[keys[keys.length - 1]] = val;
},
}
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
vant遇到van-sidebar數(shù)據(jù)超出不能滑動的問題
這篇文章主要介紹了vant遇到van-sidebar數(shù)據(jù)超出不能滑動的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-04-04
Vue實現(xiàn)在線預(yù)覽pdf文件功能(利用pdf.js/iframe/embed)
項目要求需要預(yù)覽pdf文件,網(wǎng)上找了很久,發(fā)現(xiàn)pdf.js的效果,這篇文章主要給大家介紹了關(guān)于Vue實現(xiàn)在線預(yù)覽pdf文件功能,主要利用pdf.js/iframe/embed來實現(xiàn)的,需要的朋友可以參考下2021-06-06
解決vue.js this.$router.push無效的問題
今天小編就為大家分享一篇解決vue.js this.$router.push無效的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-09-09

