MVVM雙向繫結機制的原理和程式碼實現
MVVM
一句話就是 vm層(檢視模型層)通過介面從後臺m層(model層)請求資料
vm層繼而和v(view層)實現資料的雙向繫結
資料繫結
單向繫結:資料 =>檢視
雙向繫結:檢視 <=> 資料
雙向資料繫結無非就是在單向繫結的基礎上給
可輸入元素新增了change事件,來動態修改model和view
實現資料繫結的做法大致有如下幾種:
釋出者-訂閱者模式 backbone.js
髒值檢查 angular.js
資料劫持 vue.js
釋出者-訂閱者模式
一般通過sub pub的方式實現資料和檢視的繫結監聽更新資料的方式通常是vm.set(‘property’,value);
髒值檢查
一般通過檢測對比的方式檢視資料是否有變更,來決定是否更新檢視,
最簡單的就是通過setInterval()計時器來定時檢測資料變動,
但是angular不會這麼low,它只有在指定的事件觸發時才進入髒值檢測,比如DOM事件,XHR響應事件等
資料劫持
vue則是採用資料劫持結合釋出訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter getter,在資料變動時釋出訊息給訂閱者,觸發監聽回撥
基本流程:
MVVM作為繫結入口,整合Observer,Compile和watcher;
通過Observer監聽資料變化,通過Compile來解析編譯模板指令
利用watcher搭建Observer和Compile之間的通訊達到雙向繫結
程式碼實現
建立index.html
<body>
<div id="app">
<h2>{{person.name}}--{{person.age}}</h2>
<h3>{{person.sex}}</h3>
<div v-text='person.name'></div>
<div v-html='htmlStr'></div>
<input type="text" v-model='msg' />
<ul>
<li>{{msg}}</li>
<li>22</li>
<li>33</li>
<li>44</li>
</ul>
<button v-on:click='funs'>按鈕1</button>
<button @click='funs'>{{msg}}</button>
</div>
<!-- <script src="./myVue.js"></script>官方vue.js檔案 -->
<!-- 引入自定義js檔案 -->
<script src="./Obsever.js"></script>
<script src="./myVue.js"></script>
<script>
let vm = new MyVue({
el: '#app',
data: {
person: {
name: '小張',
age: 23,
sex: '男',
},
msg: '實現mvvm原理',
htmlStr: '網頁'
},
methods: {
funs() {
console.log(this)
this.person.name = '2333'
}
}
})
</script>
</body>
myVue.js
建立一個MyVue類
//MyVue
class MyVue {
//構造
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$option = options;
//如果節點存在
if (this.$el) {
//1.資料劫持
new Observe(this.$data)
//2.實現一個指令解析器
new Compile(this.$el, this);
//新增代理
this.proxyData(this.$data);
}
}
//新增代理
proxyData(data) {
for (const key in data) {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newVal) {
data[key] = newVal
}
})
}
}
}
MyVue類中
1.例項化一個指令解析器
2.資料劫持
3.新增物件代理
建立一個指令解析器
作用:
1.初始化檢視
1.訂閱資料變化,繫結更新函式
//指令解析器
class Compile {
constructor(el, vm) {
//節點傳給el
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
//1.獲取文件碎片物件,放入記憶體中減少迴流和重繪
//文件碎片物件 :document.createDocumentFragment() 用於暫時存放建立的dom元素
//將需要新增的大量元素 先新增到文件碎片,文件碎片新增到需要插入的位置,大大減少dom操作
const fragment = this.nodeFragment(this.el);
// console.log(fragment);
//2.編譯模板
this.compile(fragment);
//3.追加子元素到根元素
this.el.appendChild(fragment);
}
//編譯模板
compile(fragment) {
const childNode = fragment.childNodes;
//遍歷每個子節點
[...childNode].forEach(childItem => {
if (this.isElementNode(childItem)) {
// console.log('元素節點', childItem);
//處理節點元素
this.compileElementNode(childItem);
} else {
// console.log('文字節點', childItem);
//處理文字元素
this.compileElementText(childItem)
}
//遍歷子節點下的子節點
if (childItem.childNodes && childItem.childNodes.length) {
this.compile(childItem);
}
})
}
//處理節點元素
compileElementNode(node) {
//獲取節點屬性
const attributes = node.attributes;
//遍歷節點每個屬性
[...attributes].forEach(item => {
const { name, value } = item;
//是否指令
if (this.isDirecative(name)) { //v-text v-html v-model v-on:click
// console.log(item)
const [, dir] = name.split('-'); //text html model on:click
const [dirName, eventName] = dir.split(':');//text html model on click
//更新檢視,資料驅動檢視
compileUtil[dirName](node, value, this.vm, eventName);
//刪除有指令的標籤屬性
node.removeAttribute('v-' + dir);
}
//是否事件
else if (this.isEventName(name)) { //@click='fun'
const [, dir] = name.split('@');
const eventName = dir.split('=');
compileUtil['on'](node, value, this.vm, eventName);
}
// console.log(name, value)
})
}
//處理文字元素
compileElementText(node) {
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
console.log(content);
compileUtil['text'](node, content, this.vm);
}
}
//轉化為文件節點
nodeFragment(el) {
//建立文件物件
const f = document.createDocumentFragment();
let firstChild;
//迴圈裝入第一子節點
while (firstChild = el.firstChild) {
f.appendChild(firstChild);
}
//
return f;
}
//是否是事件
isEventName(attrName) {
return attrName.startsWith('@')
}
//是否是指令
isDirecative(attrName) {
return attrName.startsWith('v-');
}
//判斷是否節點
isElementNode(node) {
return node.nodeType === 1;
}
}
新增一個指令物件,處理v-html v-model v-on v-text等指令
const compileUtil = {
//指令(節點,名稱,MyVue物件)
text(node, expr, vm) {
//獲取MyVue物件 data的某一屬性的值
console.log(expr)
// const value = this.getVal(expr, vm);
let value;
//去除{{}}
if (expr.indexOf('{{') !== -1) {
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
//掛在一個觀察者
new Watcher(vm, args[1], (newVal) => {
console.log(this.getContenVal(expr, vm))
this.updater.textUpdater(node, this.getContenVal(expr, vm));
})
return this.getVal(args[1], vm);
})
} else {
console.log(this.getContenVal(expr, vm))
value = this.getVal(expr, vm);
}
//更新函式
this.updater.textUpdater(node, value);
},
html(node, expr, vm) {
const value = this.getVal(expr, vm);
//掛在一個觀察者
new Watcher(vm, expr, (newVal) => {
this.updater.htmlUpdater(node, newVal);
})
this.updater.htmlUpdater(node, value);
},
model(node, expr, vm) {
const value = this.getVal(expr, vm);
//資料=>檢視
new Watcher(vm, expr, (newVal) => {
this.updater.modelUpdater(node, newVal);
})
//檢視=>資料=>檢視
node.addEventListener('input', (e) => {
this.setVal(expr, vm, e.target.value);
})
this.updater.modelUpdater(node, value);
},
//事件
on(node, expr, vm, eventName) {
let fn = vm.$option.methods && vm.$option.methods[expr];
node.addEventListener(eventName, fn.bind(vm), false);
},
//更新函式
updater: {
modelUpdater(node, value) {
node.value = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
textUpdater(node, value) {
//返回指定節點的文字內容
node.textContent = value;
}
},
//
setVal(expr, vm, inputVal) {
return expr.split('.').reduce((data, currentVal) => {
data[currentVal] = inputVal;
return data[currentVal];
}, vm.$data);
},
//獲取值
getVal(expr, vm) {
//person.name
//[person,name]
return expr.split('.').reduce((data, currentVal) => {
return data[currentVal]
}, vm.$data);
},
//
getContenVal(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(args[1], vm);
})
}
}
在每個指令執行時候為每個物件掛載一個觀察者new Watcher()
2.資料劫持
Object.defineProperty劫持資料的get和set方法
//資料劫持
class Observe {
constructor(data) {
this.observer(data);
}
//遍歷所有屬性
observer(data) {
if (data && typeof data === 'object') {
console.log(Object.keys(data))
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
})
}
}
//資料劫持
defineReactive(data, key, value) {
//遞迴遍歷子屬性
this.observer(value);
//新增dep容器
const dep = new Dep();
//劫持所有屬性 為每個屬性新增get set方法
Object.defineProperty(data, key, {
enumerable: true,//是否可遍歷
configurable: false,//是否可更改編寫
get() {
//訂閱資料變化時,往Deo中新增觀察者
Dep.target && dep.addSub(Dep.target);
return value;
},
set: (newVal) => {
//對新值也進行劫持
this.observer(newVal);
if (newVal !== value) {
value = newVal;
}
//通知觀察者去跟新
dep.notify();
}
})
}
}
訂閱資料時,往Deo中為每個屬性新增觀察者
資料變化時通知Dep的屬性觀察者去更新
建立一個Dep容器
//*Obsever.js*
class Dep {
constructor() {
this.subs = [];
}
//收集觀察者
addSub(watcher) {
this.subs.push(watcher);
}
//通知觀察者去跟新
notify() {
console.log('觀察者', this.subs);
this.subs.forEach(watcher => {
//通知觀察者更新資料
watcher.updata();
})
}
}
Dep容器的作用是去儲存所有觀察者,當資料變化時,通知觀察者去跟新
觀察者
class Watcher {
constructor(vm, expr, callback) {
this.vm = vm;
this.expr = expr;
this.callback = callback;
//先把舊值儲存
this.oldVal = this.getOldVal();
}
//儲存舊值
getOldVal() {
//掛載觀察者
Dep.target = this; //target暫存的this,this指向Watcher物件,把物件push進dep就銷燬
//獲取舊值
const oldVal = compileUtil.getVal(this.expr, this.vm);
//清除觀察者
Dep.target = null;
return oldVal;
}
//更新資料
updata() {
//獲取變化值
const newVal = compileUtil.getVal(this.expr, this.vm);
//如果改變則執行回撥
if (newVal !== this.oldVal) {
this.callback(newVal)
}
}
}
相關文章
- vue雙向繫結的原理及實現雙向繫結MVVM原始碼分析VueMVVM原始碼
- 剖析Vue原理&實現雙向繫結MVVMVueMVVM
- WPF之AvalonEdit實現MVVM雙向繫結MVVM
- mvvm-simple雙向繫結簡單實現MVVM
- 梳理vue雙向繫結的實現原理Vue
- 雙向資料繫結實現原理
- Vue、MVVM、MVC、雙向繫結VueMVVMMVC
- vue資料雙向繫結的實現原理Vue
- Vue雙向繫結原理,教你一步一步實現雙向繫結Vue
- 2 理解 Angular 的雙向繫結機制Angular
- Vue雙向繫結實現Vue
- vue雙向繫結原理Vue
- 雙向繫結的極簡實現
- Vue雙向繫結的實現原理系列(一):Object.definepropertyVueObject
- vue實現prop雙向繫結Vue
- javascript實現資料的雙向繫結(手動繫結)JavaScript
- 簡要實現vue雙向繫結Vue
- angular雙向繫結簡單實現Angular
- javascript實現雙向資料繫結JavaScript
- Vue資料雙向繫結原理Vue
- vue雙向資料繫結原理Vue
- [JS] 資料雙向繫結原理JS
- js 實現vue的雙向資料繫結JSVue
- vue中的雙向資料繫結原理Vue
- 簡單實現一個雙向繫結
- vue 雙向繫結(v-model 雙向繫結、.sync 雙向繫結、.sync 傳物件)Vue物件
- 【.NET6+WPF】WPF使用prism框架+Unity IOC容器實現MVVM雙向繫結和依賴注入框架UnityMVVM依賴注入
- Vue 雙向資料繫結原理分析Vue
- 通俗易懂圖解MVVM和RAC雙向繫結介紹(附Demo)圖解MVVM
- Vue 中實現雙向繫結的 4 種方法Vue
- JavaScript實現簡單的雙向資料繫結JavaScript
- 基於vue實現的雙向資料繫結Vue
- angularjs中的雙向繫結原理解析AngularJS
- 5分鐘教你實現Vue雙向繫結Vue
- Vue父子元件雙向繫結傳值的實現方法Vue元件
- 通過原生js實現資料的雙向繫結JS
- vue實踐:元件雙向繫結Vue元件
- 論MVVM偽框架結構和MVC中M的實現機制MVVM框架MVC