剖析手寫Vue,你也可以手寫一個MVVM框架#
郵箱:563995050@qq.com
github: https://github.com/xiaoqiuxiong
作者:肖秋雄(eddy)
溫馨提示:感謝閱讀,筆者創作辛苦,如需轉載請自覺注明出處哦
Vue MVVM響應式原理剖釋
Vue是采用數據劫持配合發布者和訂閱者模式,通過Object.definerProperty()來劫持各個屬性的setter和setter,在數據變動時,發布消息給依賴收集器Dep,去通知觀察者Watcher,觸發對應的解釋模板回調函數去更新視圖。
詳細點說就是MVVM作為綁定的入口,整合了Observer,Compile和Watcher三者,通過Observer來劫持且監聽數據,通過Compile來解釋編譯模板指令,然后利用Watcher搭建Observer,Compile之間的聯系,達到數據變化=>視圖更新,視圖交互變化=>數據變化=>視圖更新雙向綁定的效果。
示列:
手寫Vue源碼
inex.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>手寫Vue</title>
</head>
<body>
<div id="app">
<h2>{{ person.name }} -- {{ person.age }}</h2>
<h3>{{ person.favorite }}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{ msg }}</h3>
<div v-text='person.name'></div>
<div v-text='msg'></div>
<div v-html='htmlStr'></div>
<input type="text" v-model='msg'>
<button v-on:click="handlerClick">點擊按鈕</button>
<br>
<br>
<div>
數量:
<button v-on:click="sub">-</button>
{{num}}
<button v-on:click="add">+</button>
</div>
</div>
<script src="./Observer.js"></script>
<script src="./MVue.js"></script>
<script>
let vm = new MVue({
el: '#app',
data: {
num: 1,
person: {
name: '小馬哥',
age: 18,
favorite: '喜歡大長腿妹妹!'
},
msg: '學習手寫Vue框架',
htmlStr: '<h3>我愛學習vue</h3>'
},
methods: {
handlerClick() {
this.msg = '66'
this.person = {
name: '大馬哥',
age: 99,
favorite: '喜歡大哥哥!'
}
},
add() {
this.num++
},
sub() {
if(this.num === 1) return
this.num = this.num -1
}
}
})
</script>
</html>
MVue.js
// 入口方法
class MVue {
constructor(options) {
this.$options = options
this.$el = options.el
this.$data = options.data
if (this.$el) {
// 1.實現一個數據觀察者
new Observer(this.$data)
// 2.實現一個指令解釋器
new Compile(this.$el, this)
// 代理this.$data => 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
}
})
}
}
}
// 指令解釋器
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
// 1.獲取文檔碎片對象,放入內存中可以減少頁面回流和重繪
const fragment = this.node2Fragment(this.el)
// console.log(fragment);
// 2.編譯模板
this.compile(fragment)
// 3.追加子元素到根元素
this.el.appendChild(fragment)
}
compile(fragment) {
// 1.獲取每一個子節點
let childNodes = fragment.childNodes
childNodes = this.convertToArray(childNodes)
childNodes.forEach(child => {
if (this.isElementNode(child)) {
// console.log('元素節點', child);
this.compileElement(child)
} else {
// console.log('文檔節點', child);
this.compileText(child)
}
if (child.childNodes && child.childNodes.length) {
this.compile(child)
}
});
}
isDirective(name) {
return name.startsWith('v-')
}
isElementNode(node) {
// 判斷是否是元素節點
return node.nodeType === 1
}
convertToArray(nodes) {
// 將childNodes返回的數據轉化為數組的方法
var array = null;
try {
array = Array.prototype.slice.call(nodes, 0);
} catch (ex) {
array = new Array();
for (var i = 0, len = nodes.length; i < len; i++) {
array.push(nodes[i]);
}
}
return array;
}
compileElement(node) {
// console.log(node);
// <div v-text="msg"></div>
const attributes = node.attributes
// console.log(attributes);
this.convertToArray(attributes).forEach(attr => {
const { name, value } = attr
// console.log(value);
if (this.isDirective(name)) {
// 是一個指令
const [, dirctive] = name.split('-')
const [dirName, eventName] = dirctive.split(':')
// console.log(dirName, eventName);
// 更新數據 數據驅動視圖
compileUtil[dirName](node, value, this.vm, eventName)
// 刪除標簽上的指令
node.removeAttribute('v-' + dirctive)
}
})
}
compileText(node) {
// 匹配雙大括號 {{}}
const content = node.textContent
if (/\{\{(.+?)\}\}/.test(content)) {
// console.log(content);
compileUtil['text'](node, content, this.vm)
}
}
node2Fragment(el) {
// 創建文檔碎片
let f = document.createDocumentFragment()
while (el.firstChild) {
f.appendChild(el.firstChild)
}
return f
}
}
const compileUtil = {
text(node, expr, vm) {
let value
if (expr.indexOf('{{') !== -1) {
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 綁定觀察者,將來數據發生變化,觸發這里的回調函數去更是對應的視圖
new Watcher(vm, args[1], (newVal) => {
this.updater.textUpdater(node, this.getContentVal(expr, vm))
})
return this.getValue(args[1], vm)
})
} else {
value = this.getValue(expr, vm)
new Watcher(vm, expr, (newVal) => {
this.updater.textUpdater(node, newVal)
})
}
this.updater.textUpdater(node, value)
},
html(node, expr, vm) {
let value = this.getValue(expr, vm)
new Watcher(vm, expr, (newVal) => {
this.updater.htmlUpdater(node, newVal)
})
this.updater.htmlUpdater(node, value)
},
model(node, expr, vm) {
const value = this.getValue(expr, vm)
// 綁定更新函數 數據=>視圖
new Watcher(vm, expr, (newVal) => {
this.updater.modelUpdater(node, newVal)
})
// 視圖=>數據=>視圖
node.addEventListener('input', e => {
// 設置值
this.setValue(expr, vm, e.target.value)
})
this.updater.modelUpdater(node, value)
},
on(node, expr, vm, eventName) {
let fn = vm.$options.methods && vm.$options.methods[expr]
node.addEventListener(eventName, fn.bind(vm), false)
},
getValue(expr, vm) {
expr = expr.replace(/\s+/g, "")
return expr.split('.').reduce((data, currentVal) => {
// console.log(currentVal);
return data[currentVal]
}, vm.$data)
},
setValue(expr, vm, newVal) {
expr = expr.replace(/\s+/g, "")
return expr.split('.').reduce((data, currentVal) => {
data[currentVal] = newVal
}, vm.$data)
},
updater: {
textUpdater(node, value) {
node.textContent = value
},
htmlUpdater(node, value) {
node.innerHTML = value
},
modelUpdater(node, value) {
node.value = value
}
},
getContentVal(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm)
})
}
}
Observer.js
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
// 先把舊值保存起來
this.oldVal = this.getOldVal()
}
getOldVal() {
Dep.target = this
const oldVal = compileUtil.getValue(this.expr, this.vm)
Dep.target = null
return oldVal
}
update() {
const newVal = compileUtil.getValue(this.expr, this.vm)
this.cb(newVal)
}
}
class Dep {
constructor() {
// 定義觀察者數組
this.subs = []
}
// 收集觀察者
addSub(watcher) {
this.subs.push(watcher)
}
// 通知觀察者去更新視圖
notify() {
// console.log('通知了觀察者');
this.subs.forEach(w => w.update())
}
}
// 數據劫持監聽
class Observer {
constructor(data) {
this.observer(data)
}
observer(data) {
if (data && typeof data === 'object') {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
}
defineReactive(obj, key, value) {
// 遞歸遍歷,直到最后一個值不是對象
this.observer(value)
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
get() {
// 訂閱數據變化時,往Dep中添加觀察者
Dep.target && dep.addSub(Dep.target)
return value
},
set: (newVal) => {
this.observer(newVal)
// 重新更新值之前先對新值劫持監聽
value = newVal
// 告訴Dep通知變化
dep.notify()
}
})
}
}
感謝閱讀,筆者創作辛苦,如需轉載請自覺注明出處哦