您好,登錄后才能下訂單哦!
這篇文章主要介紹了vue雙向綁定原理實例分析的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇vue雙向綁定原理實例分析文章都會有所收獲,下面我們一起來看看吧。
vue最少需要兩個參數:模板和data。
創(chuàng)建Compiler對象,將數據渲染到模板后,掛載到指定跟節(jié)點中。
class MyVue { // 1,接收兩個參數:模板(根節(jié)點),和數據對象 constructor(options) { // 保存模板,和數據對象 if (this.isElement(options.el)) { this.$el = options.el; } else { this.$el = document.querySelector(options.el); } this.$data = options.data; // 2.根據模板和數據對象,渲染到根節(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函數將模板元素提取到內存中,方便將數據渲染到模板后,再一次性掛載到頁面中
2,模板提取到內存后,使用buildTemplate函數遍歷該模板元素
元素節(jié)點
使用buildElement函數檢查元素上以v-開頭的屬性
文本節(jié)點
用buildText函數檢查文本中有無{{}}內容
3,創(chuàng)建CompilerUtil類,用于處理vue指令和{{}},完成數據的渲染
4,到此就完成了首次數據渲染,接下來需要實現(xiàn)數據改變時,自動更新視圖。
class Compiler { constructor(vm) { this.vm = vm; // 1.將網頁上的元素放到內存中 let fragment = this.node2fragment(this.vm.$el); // 2.利用指定的數據編譯內存中的元素 this.buildTemplate(fragment); // 3.將編譯好的內容重新渲染會網頁上 this.vm.$el.appendChild(fragment); } node2fragment(app) { // 1.創(chuàng)建一個空的文檔碎片對象 let fragment = document.createDocumentFragment(); // 2.編譯循環(huán)取到每一個元素 let node = app.firstChild; while (node) { // 注意點: 只要將元素添加到了文檔碎片對象中, 那么這個元素就會自動從網頁上消失 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函數對data做Object.defineProperty處理,使得data中的每個數據都可以進行get/set監(jiān)聽
2,接下來將考慮如何在監(jiān)聽到data值改變后,更新視圖內容呢?使用觀察者設計模式,創(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)聽到數據的變化'); } } }) } }
使用觀察者設計模式,創(chuàng)建Dep和Wather類
1,使用觀察者設計模式的目的是:
解析模板,收集data中某個數據在模板中被使用的dom節(jié)點集合,當該數據改變時,更新該dom節(jié)點集合就實現(xiàn)了數據更新。
Dep:用于收集某個data屬性依賴的dom節(jié)點集合,并提供更新方法
Watcher:每個dom節(jié)點的包裹對象
attr:該dom使用的data屬性
cb:修改該dom值的回調函數,在創(chuàng)建的時候會接收
2,到這里感覺思路是沒問題了,已經是勝券在握了。那Dep和Watcher該怎么使用呢?
為每個屬性添加一個dep,用來收集依賴的dom
因為頁面首次渲染的時候會讀取data數據,這時候會觸發(fā)該data的getter,所以在此收集dom
具體如何收集呢,在CompilerUtil類解析v-model,{{}}等命令時,會觸發(fā)getter,我們在觸發(fā)之前創(chuàng)建Wather,為Watcher添加一個靜態(tài)屬性,指向該dom,然后在getter函數里面獲取該靜態(tài)變量,并添加到依賴中,就完成了一次收集。因為每次觸發(fā)getter之前都對該靜態(tài)變量賦值,所以不存在收集錯依賴的情況。
class Dep { constructor() { // 這個數組就是專門用于管理某個屬性所有的觀察者對象的 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)了數據綁定時,視圖自動更新,本來想代碼一步步實現(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參數。
1,首先將data中的數據進行遞歸遍歷,對每個屬性執(zhí)行Object.defineProperty,定義get和set函數。并為每個屬性添加一個dep數組。當get執(zhí)行時,會為調用的dom節(jié)點創(chuàng)建一個watcher存放在該數組中。當set執(zhí)行時,重新賦值,并調用dep數組的notify方法,通知所有使用了該屬性watcher,并更新對應dom的內容。
2,將模板加載到內存中,遞歸模板中的元素,檢測到元素有v-開頭的命令或者雙大括號的指令,就會從data中取對應的值去修改模板內容,這個時候就將該dom元素添加到了該屬性的dep數組中。這就實現(xiàn)了數據驅動視圖。在處理v-model指令的時候,為該dom添加input事件(或change),輸入時就去修改對應的屬性的值,實現(xiàn)了頁面驅動數據。
3,將模板與數據進行綁定后,將模板添加到真實dom樹中。
如何將watcher放在dep數組中?
在解析模板的時候,會根據v-指令獲取對應data屬性值,這個時候就會調用屬性的get方法,我們先創(chuàng)建Watcher實例,并在其內部獲取該屬性值,作為舊值存放在watcher內部,我們在獲取該值之前,在Watcher原型對象上添加屬性Watcher.target = this;然后取值,將講Watcher.target = null;這樣get在被調用的時候就可以根據Watcher.target獲取到watcher實例對象。
methods的原理
創(chuàng)建vue實例的時候,接收methods參數
在解析模板的時候遇到v-on的指令。會對該dom元素添加對應事件的監(jiān)聽,并使用call方法將vue綁定為該方法的this:vm.$methods[value].call(vm, e);
computed的原理
創(chuàng)建vue實例的時候,接收computed參數
初始化vue實例的時候,為computed的key進行Object.defineProperty處理,并添加get屬性。
關于“vue雙向綁定原理實例分析”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“vue雙向綁定原理實例分析”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業(yè)資訊頻道。
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。