本篇自訂一個vue,逐步實現資料的雙向綁定,給大家透過實例來一步步搞懂vue雙向綁定原理,希望對大家有幫助!
#vue最少需要兩個參數:模板和data。 【相關推薦:vue.js影片教學】
建立Compiler對象,將資料渲染到模板後,掛載到指定跟節點中。
class MyVue { // 1,接收两个参数:模板(根节点),和数据对象 constructor(options) { // 保存模板,和数据对象 if (this.isElement(options.el)) { this.$el = options.el; } else { this.$el = document.querySelector(options.el); } this.$data = options.data; // 2.根据模板和数据对象,渲染到根节点 if (this.$el) { // 监听data所有属性的get/set new Observer(this.$data); new Compiler(this) } } // 判断是否是一个dom元素 isElement(node) { return node.nodeType === 1; } }
#Compiler
元素節點
##文本節點
用buildText函數檢查文字中有無{{}}內容#3,建立CompilerUtil類,用於處理vue指令和{{}},完成資料的渲染4,到此就完成了首次資料渲染,接下來需要實作資料變更時,自動更新視圖。 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.创建一个空的文档碎片对象
let fragment = document.createDocumentFragment();
// 2.编译循环取到每一个元素
let node = app.firstChild;
while (node) {
// 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
fragment.appendChild(node);
node = app.firstChild;
}
// 3.返回存储了所有元素的文档碎片对象
return fragment;
}
buildTemplate(fragment) {
let nodeList = [...fragment.childNodes];
nodeList.forEach(node => {
// 需要判断当前遍历到的节点是一个元素还是一个文本
if (this.vm.isElement(node)) {
// 元素节点
this.buildElement(node);
// 处理子元素
this.buildTemplate(node);
} else {
// 文本节点
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) {
// 在触发getter之前,为dom创建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) {
// 在触发getter之前,为dom创建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) {
// 在触发getter之前,为dom创建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) => {
// 在触发getter之前,为dom创建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;
}
}
2,接下來將考慮如何在監聽到data值改變後,更新視圖內容呢?使用觀察者設計模式,建立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); // 第三步: 将当前属性的所有观察者对象都放到当前属性的发布订阅对象中管理起来 let dep = new Dep(); // 创建了属于当前属性的发布订阅对象 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('监听到数据的变化'); } } }) } }
使用觀察者設計模式,建立Dep和Wather類別
2,到這裡感覺思路是沒問題了,已經是勝券在握了。那Dep和Watcher該怎麼使用呢?
為每個屬性添加一個dep,用來收集依賴的dom
因為頁面首次渲染的時候會讀取data數據,這時候會觸發該data的getter,所以在此收集dom具體如何收集呢,在CompilerUtil類別解析v-model,{{}}等指令時,會觸發getter ,我們在觸發之前創建Wather,為Watcher添加一個靜態屬性,指向該dom,然後在getter函數裡面獲取該靜態變量,並添加到依賴中,就完成了一次收集。因為每次觸發getter之前都會對該靜態變數賦值,所以不存在收集錯依賴的情況。
class Dep { constructor() { // 这个数组就是专门用于管理某个属性所有的观察者对象的 this.subs = []; } // 订阅观察的方法 addSub(watcher) { this.subs.push(watcher); } // 发布订阅的方法 notify() { this.subs.forEach(watcher => watcher.update()); } }
class Watcher { constructor(vm, attr, cb) { this.vm = vm; this.attr = attr; this.cb = cb; // 在创建观察者对象的时候就去获取当前的旧值 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,到這裡就實現了資料綁定時,視圖自動更新,本來想程式碼一步步實現的,但是發現不好處理,就把完整的class貼出來了。 實作視圖驅動資料
其實就是監聽輸入框的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); }) },
methods的原理
建立vue實例的時候,接收methods參數
在解析模板的時候遇到v-on的指令。會對該dom元素新增對應事件的監聽,並使用call方法將vue綁定為該方法的this:vm.$methods[value].call(vm, e);
computed的原理
建立vue實例的時候,接收computed參數
初始化vue實例的時候,為computed的key進行Object .defineProperty處理,並加入get屬性。
(學習影片分享:web前端)
以上是一文帶你深入解析vue雙向綁定原理(徹底搞懂它)的詳細內容。更多資訊請關注PHP中文網其他相關文章!