vue.js響應式原理解析與實現—實現v-model與{{}}指令
我們已經分析了vue.js是透過Object.defineProperty以及釋出訂閱模式來進行資料劫持和監聽,並且實現了一個簡單的demo。今天,我們就基於上一節的程式碼,來實現一個MVVM類,將其與html結合在一起,並且實現v-model以及{{}}語法。
tips:本節新增程式碼(去除註釋)在一百行左右。使用的Observer和Watcher都是延用上一節的程式碼,沒有修改。
接下來,讓我們一步步來,實現一個MVVM類。
建構函式
首先,一個MVVM的建構函式如下(和vue.js的建構函式一樣):
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進行資料劫持 new Observer(this.data); // 傳入的el可以是selector,也可以是元素,因此我們要在這裡做一層處理,保證this.$el的值是一個元素節點 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; } }) } // 是否是屬性節點 isElementNode(node) { return node.nodeType === 1; } }
在完成初始化操作後,我們需要對this.$el的節點進行編譯。目前我們要實現的語法有v-model和{{}}語法,v-model這個屬性只可能會出現在元素節點的attributes裡,而{{}}語法則是出現在文字節點裡。
fragment
在對節點進行編譯之前,我們先考慮一個現實問題:如果我們在編譯過程中直接操作DOM節點的話,每一次修改DOM都會導致DOM的迴流或重繪,而這一部分效能損耗是很沒有必要的。因此,我們可以利用fragment,將節點轉化為fragment,然後在fragment裡編譯完成後,再將其放回到頁面上。
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); } // 將節點轉為fragment,透過fragment來操作DOM,可以獲得更高的效率 // 因為如果直接操作DOM節點的話,每次修改DOM都會導致DOM的迴流或重繪,而將其放在fragment裡,修改fragment不會導致DOM迴流和重繪 // 當在fragment一次性修改完後,在直接放回到DOM節點中 node2Fragment() { const fragment = document.createDocumentFragment(); let firstChild; while(firstChild = this.$el.firstChild) { fragment.appendChild(firstChild); } return fragment; } }
實現v-model
在將node節點轉為fragment後,我們來對其中的v-model語法進行編譯。
由於v-model語句只可能會出現在元素節點的attributes裡,因此,我們先判斷該節點是否為元素節點,若為元素節點,則判斷其是否是directive(目前只有v-model),若都滿足的話,則呼叫CompileUtils.compileModelAttr來編譯該節點。
編譯含有v-model的節點主要有兩步:
為元素節點註冊input事件,在input事件觸發的時候,更新vm(this.data)上對應的屬性值。
對v-model依賴的屬性註冊一個Watcher函式,當依賴的屬性發生變化,則更新元素節點的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)) { // 若是元素節點,則遍歷它的屬性,編譯其中的指令 const attrs = node.attributes; Array.prototype.forEach.call(attrs, (attr) => { if (this.isDirective(attr)) { CompileUtils.compileModelAttr(this.data, node, attr) } }) } // 若節點有子節點的話,則對子節點進行編譯 if (node.childNodes && node.childNodes.length > 0) { Array.prototype.forEach.call(node.childNodes, (child) => { this.compile(child); }) } } // 是否是屬性節點 isElementNode(node) { return node.nodeType === 1; } // 檢測屬性是否是指令(vue的指令是v-開頭) isDirective(attr) { return attr.nodeName.indexOf('v-') >= 0; } }const CompileUtils = { // 編譯v-model屬性,為元素節點註冊input事件,在input事件觸發的時候,更新vm對應的值。 // 同時也註冊一個Watcher函式,當所依賴的值發生變化的時候,更新節點的值 compileModelAttr(vm, node, attr) { const { value: keys, nodeName } = attr; node.value = this.getModelValue(vm, keys); // 將v-model屬性值從元素節點上去掉 node.removeAttribute(nodeName); node.addEventListener('input', (e) => { this.setModelValue(vm, keys, e.target.value); }); new Watcher(vm, keys, (oldVal, newVal) => { node.value = newVal; }); }, /* 解析keys,比如,使用者可以傳入 * * 這個時候,我們在取值的時候,需要將"obj.name"解析為data[obj][name]的形式來獲取目標值 */ parse(vm, keys) { keys = keys.split('.'); let value = vm; keys.forEach(_key => { value = value[_key]; }); return value; }, // 根據vm和keys,返回v-model對應屬性的值 getModelValue(vm, keys) { return this.parse(vm, keys); }, // 修改v-model對應屬性的值 setModelValue(vm, keys, val) { keys = keys.split('.'); let value = vm; for(let i = 0; i實現{{}}語法
{{}}語法只可能會出現在文字節點中,因此,我們只需要對文字節點做處理。如果文字節點中出現{{key}}這種語句的話,我們則對該節點進行編譯。在這裡,我們可以透過下面這個正規表示式來對文字節點進行處理,判斷其是否含有{{}}語法。
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 }}'));若含有{{}}語法,我們則可以對其處理,由於一個文字節點可能出現多個{{}}語法,因此編譯含有{{}}語法的文字節點主要有以下兩步:
找出該文字節點中所有依賴的屬性,並且保留原始文字資訊,根據原始文字資訊還有屬性值,生成最終的文字資訊。比如說,原始文字資訊是"test {{test}} {{name}}",那麼該文字資訊依賴的屬性有this.data.test和this.data.name,那麼我們可以根據原本資訊和屬性值,生成最終的文字。
為該文字節點所有依賴的屬性註冊Watcher函式,當依賴的屬性發生變化的時候,則更新文字節點的內容。
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)) { // 若是文字節點,則判斷是否有{{}}語法,如果有的話,則編譯{{}}語法 let textContent = node.textContent; if (textReg.test(textContent)) { // 對於 "test{{test}} {{name}}"這種文字,可能在一個文字節點會出現多個匹配符,因此得對他們統一進行處理 // 使用 textReg來對文字節點進行匹配,可以得到["{{test}}", "{{name}}"]兩個匹配值 const matchs = textContent.match(textReg); CompileUtils.compileTextNode(this.data, node, matchs); } } // 若節點有子節點的話,則對子節點進行編譯 if (node.childNodes && node.childNodes.length > 0) { Array.prototype.forEach.call(node.childNodes, (child) => { this.compile(child); }) } } // 是否是文字節點 isTextNode(node) { return node.nodeType === 3; } }const CompileUtils = { reg: /{{s*(w+)s*}}/, // 匹配 {{ key }}中的key // 編譯文字節點,並註冊Watcher函式,當文字節點依賴的屬性發生變化的時候,更新文字節點 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); }, // 更新文字節點資訊 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和{{}}功能的MVVM類就已經完成了。有興趣的小夥伴可以上去看下(也可以star or fork下哈哈哈)。
這裡也有一個(忽略樣式)。
接下來的話,可能會繼續實現computed屬性,v-bind方法,以及支援在{{}}裡面放表示式。如果覺得這個文章對你有幫助的話,麻煩點個贊,嘻嘻。
最後,貼上所有的程式碼:
class Observer { constructor(data) { // 如果不是物件,則返回 if (!data || typeof data !== 'object') { return; } this.data = data; this.walk(); } // 對傳入的資料進行資料劫持 walk() { for (let key in this.data) { this.defineReactive(this.data, key, this.data[key]); } } // 建立當前屬性的一個釋出例項,使用Object.defineProperty來對當前屬性進行資料劫持。 defineReactive(obj, key, val) { // 建立當前屬性的釋出者 const dep = new Dep(); /* * 遞迴對子屬性的值進行資料劫持,比如說對以下資料 * let data = { * name: 'cjg', * obj: { * name: 'zht', * age: 22, * obj: { * name: 'cjg', * age: 22, * } * }, * }; * 我們先對data最外層的name和obj進行資料劫持,之後再對obj物件的子屬性obj.name,obj.age, obj.obj進行資料劫持,層層遞迴下去,直到所有的資料都完成了資料劫持工作。 */ new Observer(val); Object.defineProperty(obj, key, { get() { // 若當前有對該屬性的依賴項,則將其加入到釋出者的訂閱者佇列裡 if (Dep.target) { dep.addSub(Dep.target); } return val; }, set(newVal) { if (val === newVal) { return; } val = newVal; new Observer(newVal); dep.notify(); } }) } }// 釋出者,將依賴該屬性的watcher都加入subs陣列,當該屬性改變的時候,則呼叫所有依賴該屬性的watcher的更新函式,觸發更新。class Dep { constructor() { this.subs = []; } addSub(sub) { if (this.subs.indexOf(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(); } // 根據vm和keys獲取到最新的觀察值 get() { // 將Dep的依賴項設定為當前的watcher,並且根據傳入的keys遍歷獲取到最新值。 // 在這個過程中,由於會呼叫observer物件屬性的getter方法,因此在遍歷過程中這些物件屬性的釋出者就將watcher新增到訂閱者佇列裡。 // 因此,當這一過程中的某一物件屬性發生變化的時候,則會觸發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進行資料劫持 new Observer(this.data); // 傳入的el可以是selector,也可以是元素,因此我們要在這裡做一層處理,保證this.$el的值是一個元素節點 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); } // 將節點轉為fragment,透過fragment來操作DOM,可以獲得更高的效率 // 因為如果直接操作DOM節點的話,每次修改DOM都會導致DOM的迴流或重繪,而將其放在fragment裡,修改fragment不會導致DOM迴流和重繪 // 當在fragment一次性修改完後,在直接放回到DOM節點中 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)) { // 若是元素節點,則遍歷它的屬性,編譯其中的指令 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)) { // 若是文字節點,則判斷是否有{{}}語法,如果有的話,則編譯{{}}語法 let textContent = node.textContent; if (textReg.test(textContent)) { // 對於 "test{{test}} {{name}}"這種文字,可能在一個文字節點會出現多個匹配符,因此得對他們統一進行處理 // 使用 textReg來對文字節點進行匹配,可以得到["{{test}}", "{{name}}"]兩個匹配值 const matchs = textContent.match(textReg); CompileUtils.compileTextNode(this.data, node, matchs); } } // 若節點有子節點的話,則對子節點進行編譯。 if (node.childNodes && node.childNodes.length > 0) { Array.prototype.forEach.call(node.childNodes, (child) => { this.compile(child); }) } } // 是否是屬性節點 isElementNode(node) { return node.nodeType === 1; } // 是否是文字節點 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 // 編譯文字節點,並註冊Watcher函式,當文字節點依賴的屬性發生變化的時候,更新文字節點 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); }, // 更新文字節點資訊 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屬性,為元素節點註冊input事件,在input事件觸發的時候,更新vm對應的值。 // 同時也註冊一個Watcher函式,當所依賴的值發生變化的時候,更新節點的值 compileModelAttr(vm, node, attr) { const { value: keys, nodeName } = attr; node.value = this.getModelValue(vm, keys); // 將v-model屬性值從元素節點上去掉 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; }, // 根據vm和keys,返回v-model對應屬性的值 getModelValue(vm, keys) { return this.parse(vm, keys); }, // 修改v-model對應屬性的值 setModelValue(vm, keys, val) { keys = keys.split('.'); let value = vm; for(let i = 0; i原文出處:https://www.cnblogs.com/chenjg/p/9548473.html
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3486/viewspace-2813972/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- vue.js響應式原理解析與實現Vue.js
- 深度解析vue.js響應式原理解析與實現Vue.js
- Vue響應式原理與模擬實現Vue
- Vue 響應式實現原理Vue
- vue響應式資料的實現原理解析Vue
- 解析 iOS 動畫原理與實現iOS動畫
- Vue 3 響應式原理及實現Vue
- 自己實現一個VUE響應式--VUE響應式原理Vue
- memcached分散式原理與實現分散式
- 深入解析 ResNet:實現與原理
- 前端響應式佈局原理與實踐前端
- Vue響應式原理以及簡單實現Vue
- v-model的實現原理
- vue原生指令v-model實現自定義樣式の多選與單選Vue
- 分散式鎖實現原理與最佳實踐分散式
- Lombok 原理與實現Lombok
- LRU原理與實現
- Redis、Zookeeper實現分散式鎖——原理與實踐Redis分散式
- Vue 原始碼解析(例項化前) - 響應式資料的實現原理Vue原始碼
- Vue 原始碼解析(例項化前) – 響應式資料的實現原理Vue原始碼
- vue響應式原理學習(二)— Observer的實現VueServer
- 第142篇:原生js實現響應式原理JS
- 探索 Vue.js 響應式原理Vue.js
- Dubbo 實現原理與原始碼解析系列 —— 精品合集原始碼
- Redis分散式鎖的使用與實現原理Redis分散式
- 堆的原理與實現
- 熔斷原理與實現
- @weakify 與 @strongify 實現原理
- vue響應式原理學習(三)— Watcher的實現Vue
- Vue 進階系列(一)之響應式原理及實現Vue
- Vue指令實現原理Vue
- Promise 原理解析與實現(遵循Promise/A+規範)Promise
- OpenStack設計與實現(二)Libvirt簡介與實現原理
- vysor原理與程式碼實現
- React基礎與原理實現React
- 富集分析的原理與實現
- 前端模板的原理與實現前端
- jsonp的原理與實現JSON