圖片來源: 剖析Vue實現原理 - 如何實現雙向綁定mvvmjavascript
<div id="app">
<input type="text" v-model="text">
<input type="text" v-model="man.name">
<div v-html="text"></div>
<div>{{man.name}}{{man.age}}{{text}}</div>
</div>
複製代碼
var app = new Vue({
el: "#app",
data: {
man: {
name: '小白',
age: 20
},
text: 'hello world',
}
})
複製代碼
上面代碼,也許對於你再熟悉不過了html
基於這樣的形式,咱們須要對數據進行掛載,將data
的數據掛載到對應的DOM上前端
首先建立一個類來接收對象參數options
vue
class Vue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
if(this.$el) {
new Compile(this.$el,this) ////模板解析
}
}
}
複製代碼
Compile
類,用於模板解析,它的工做內容主要爲如下幾點java
class Compile{
constructor(el,vm) {
// 建立文檔碎片,接收el的裏面全部子元素
// 解析子元素中存在v-開頭的屬性及文本節點中存在{{}}標識
// 將vm中$data對應的數據掛載上去
}
}
複製代碼
基礎代碼:node
class Compile {
constructor(el,vm) {
this.el = this.isElementNode(el)?el:document.querySelector(el);
this.vm = vm;
let fragment = this.node2fragment(this.el);
this.compile(fragment)
}
isDirective(attrName) {
return attrName.startsWith('v-'); //判斷屬性中是否存在v-字段 返回 布爾值
}
compileElement(node) {
let attributes = node.attributes;
[...attributes].forEach(attr => {
let {name,value} = attr
if(this.isDirective(name)) {
let [,directive] = name.split('-')
CompileUtil[directive](node,value,this.vm);
}
})
}
compileText(node) {
let content = node.textContent;
let reg = /\{\{(.+?)}\}/;
if(reg.test(content)) {
CompileUtil['text'](node,content,this.vm);
}
}
compile(fragment) {
let childNodes = fragment.childNodes;
[...childNodes].forEach(child => {
if(this.isElementNode(child)) {
this.compileElement(child);
this.compile(child);
}else{
this.compileText(child);
}
})
document.body.appendChild(fragment);
}
node2fragment(nodes) {
let fragment = document.createDocumentFragment(),firstChild;
while(firstChild = nodes.firstChild) {
fragment.appendChild(firstChild);
}
return fragment
}
isElementNode(node) {
return node.nodeType === 1;
}
}
CompileUtil = {
getValue(vm,expr) {
// 解析表達式值 獲取vm.$data內對應的數據
let value = expr.split('.').reduce((data,current) => {
return data[current]
},vm.$data)
return value
},
model(node,expr,vm) {
let data = this.getValue(vm,expr);
this.updater['modeUpdater'](node,data);
},
html(node,expr,vm){
let data = this.getValue(vm,expr);
this.updater['htmlUpdater'](node,data);
},
text(node,expr,vm){
let content = expr.replace(/\{\{(.+?)}\}/g, (...args) => {
return this.getValue(vm,args[1]);
})
console.log(content)
this.updater['textUpdater'](node,content);
},
updater:{
modeUpdater(node,value){
node.value = value;
},
textUpdater(node,value){
node.textContent = value;
},
htmlUpdater(node,value){
node.innerHTML = value;
}
}
}
複製代碼
上面已經完成了對模板的數據解析,接下來再對數據的變動進行監聽,實現雙向數據綁定git
class Vue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
if(this.$el) {
new Compile(this.$el,this);
new Observer(this.$data); //新增 數據劫持
}
}
}
複製代碼
Observer
類,用於監聽數據,它的工做內容主要爲如下幾點github
class Observer{
constructor(el,vm) {
// 利用Object.defineProperty監聽全部屬性
// 遞歸循環監聽全部傳入的對象
}
}
複製代碼
基礎代碼:閉包
class Observer {
constructor(data) {
this.observer(data);
}
observer(data) {
if(!data||typeof data !== 'object') return
for(let key in data) {
this.defineReactive(data,key,data[key]);
}
}
defineReactive (obj,key,value) {
this.observer(value);
Object.defineProperty(obj,key,{
get: () => {
return value;
},
set: (newValue) => {
if(newValue !== value) {
this.observer(newValue);
value = newValue;
}
}
})
}
}
複製代碼
將監聽到的數據變動,實時的更替上去架構
首先咱們須要一個Watcher
類,它的工做內容以下
class Watcher {
// 存儲當前觀察屬性對象的數據
// 當前觀察屬性對象數據變動時,更新數據
}
複製代碼
基礎代碼:
class Watcher {
/* vm 對象實例 expr 須要監聽的對象表達式 cb 更新數據的回調函數 */
constructor(vm,expr,cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
this.oldValue = this.get();
}
get() {
let value = CompileUtil.getValue(this.vm,this.expr);
return value;
}
update() {
let newValue = CompileUtil.getValue(this.vm,this.expr);
if(this.oldValue !== newValue) {
this.cb(newValue)
}
}
}
複製代碼
再來一個發佈訂閱Dep
的類
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
複製代碼
接下來,讓咱們把Watcher
和Dep
類關聯起來
在CompileUtil
的model
、text
方法中分別新建Watcher
實例
Watcher
在接收到Dep
的廣播時,須要一個對應的回調函數,更新數據
CompileUtil = {
getValue(vm,expr) {
// 解析表達式值 獲取vm.$data內對應的數據
let value = expr.split('.').reduce((data,current) => {
return data[current]
},vm.$data)
return value
},
...
model(node,expr,vm) {
let data = this.getValue(vm,expr);
//新增 觀察者
new Watcher(vm,expr,(newValue) => {
this.updater['modeUpdater'](node,newValue);
})
this.updater['modeUpdater'](node,data);
},
html(node,expr,vm){
let data = this.getValue(vm,expr);
//新增 觀察者
new Watcher(vm,expr,(newValue) => {
this.updater['htmlUpdater'](node,newValue);
})
this.updater['htmlUpdater'](node,data);
},
...
text(node,expr,vm){
let content = expr.replace(/\{\{(.+?)}\}/g, (...args) => {
/* 新增 觀察者 匹配多個{{}}字段 */
new Watcher(vm,args[1],() => {
this.updater['textUpdater'](node,this.getContentValue(vm,expr));
})
return this.getValue(vm,args[1]);
})
this.updater['textUpdater'](node,content);
},
}
複製代碼
實例化一個Watcher
的同時會調用this.get()
方法,this.get()
在取值時,會觸發被監聽對象的getter
class Watcher {
...
get() {
// 在Dep設置一個全局屬性
Dep.target = this;
// 取值會觸發被監聽對象的getter函數
let value = CompileUtil.getValue(this.vm,this.expr);
Dep.target = null;
return value;
}
...
}
複製代碼
來到Observer
中,此時在get
函數中,咱們就能夠將Watcher
實例放進Dep
的容器subs
中
這裏dep,利用了閉包的特性,每次廣播不會通知全部用戶,提升了性能
class Observer {
...
defineReactive (obj,key,value) {
this.observer(value);
let dep = new Dep()
Object.defineProperty(obj,key,{
get: () => {
//新增 訂閱
Dep.target && dep.addSub(Dep.target);
return value;
},
set: (newValue) => {
if(newValue !== value) {
console.log('監聽',newValue)
this.observer(newValue);
value = newValue;
//廣播
dep.notify();
}
}
})
}
}
複製代碼
此時,Watcher
和Dep
已造成關聯,一旦被監聽的對象數據發生變動,就會觸發Dep
的notify
廣播功能,進而觸發Watcher
的update
方法執行回調函數!
測試:
setTimeout(function(){
app.$data.test = "123"
},3000)
複製代碼
結果:
到這裏,咱們已經完成了最核心的部分,數據驅動視圖,可是衆所周知,v-model
是能夠視圖驅動數據的,因而咱們再增長一個監聽事件
CompileUtil = {
...
setValue(vm,expr,value) {
//迭代屬性賦值
expr.split('.').reduce((data,current,index,arr) => {
if(index == arr.length - 1){
data[current] = value
}
return data[current]
},vm.$data)
},
model(node,expr,vm) {
let data = this.getValue(vm,expr);
new Watcher(vm,expr,(newValue) => {
this.updater['modeUpdater'](node,newValue);
})
//事件監聽
node.addEventListener('input', el => {
let value = el.target.value;
console.log(value)
this.setValue(vm,expr,value)
})
this.updater['modeUpdater'](node,data);
},
...
}
複製代碼
效果以下:
最後爲Vue實例添加一個屬性代理的方法,使訪問vm
的屬性代理爲訪問vm._data
的屬性
class Vue {
constructor(options) {
...
this.$data = options.data;
Object.keys(this.$data).forEach(key => {
this.proxyKeys(key);
})
...
}
proxyKeys(key) {
console.log(key)
Object.defineProperty(this,key,{
enumerable: true,
configurable: false,
get: () => {
return this.$data[key];
},
set: (newValue) => {
console.log('newValue',newValue)
this.$data[key] = newValue;
}
})
}
}
複製代碼
大功告成! 源碼:github.com/luojinxu520…
目前,Vue 的反應系統是使用 Object.defineProperty 的 getter 和 setter。 可是,Vue 3 將使用 ES2015 Proxy 做爲其觀察者機制。 這消除了之前存在的警告,使速度加倍,並節省了一半的內存開銷。 爲了繼續支持 IE11,Vue 3 將發佈一個支持舊觀察者機制和新 Proxy 版本的構建。
參考 :