先上效果图
Vue的双向绑定采用数据劫持配合发布者——订阅者模式,通过Object.defineproperty()
劫持各个属性getter和setter,数据变动时,发布消息给依赖收集器Dep,然后通知观察者,做出对应的回调,更新视图
在案例中MyVue作为入口,整合Observer,Compile,Watcher,通过Observer来监听数据变化通过Compile来编译模板,利用Watcher来连接Compile和Observer,来实现双向绑定
MyVue作为入口,经过Compile解析之后渲染到页面,同时添加观察者Observer来监听数据变化,当数据变化时通知依赖收集器Dep,将变化分发给watcher然后再渲染视图
数据劫持
示例代码
const obj = {};
function property(obj, key, value) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
get() {
return '姓名:' + value;
},
set(newVal) {
value = newVal;
console.log('修改了数据');
}
})
}
property(obj, 'name', '张三');
console.log(obj.name);
运行图
当数据读取时会加上“姓名:”,数据修改时会提示修改数据,此时对象obj除了name之外,还有name的getter和setter两个方法
代码
入口
class MyVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 观察者
new Observer(this.$data);
// 指令解析器
new Compile(this.$el, this)
}
}
Compile
指令解析器,解析网页元素中的双大括号、v-model、v-on……等元素。对指令节点进行判断,然后遍历其所有属性,看是否有匹配的指令的属性。首先要获得dom元素,然后对含有dom元素上含有指令的节点进行处理,建一个fragment片段,将需要解析的dom元素存到fragment片段中在做处理,接下来需要遍历所有节点,对含有指令的节点进行特殊的处理
class Compile {
constructor(el, vm) {
// 赋值/获取节点
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 获取子元素文档碎片
const frag = this.fragment(this.el);
// console.log(frag);
// 编译模板
this.compile(frag);
// 追加子元素到根元素
this.el.appendChild(frag);
}
compile(fragment) {
const children = fragment.childNodes;
[...children].forEach(child => {
if (this.isElementNode(child)) {
// 元素节点
this.compileElement(child);
} else {
// 文本节点
this.complieText(child);
}
if (child.childNodes && child.childNodes.length) {
this.compile(child)
}
});
}
complieText(text) {
const content = text.textContent;
if(/\{\{(.+?)\}\}/.test(content)) {
compileUtil['text'](text, content, this.vm);
}
}
compileElement(node) {
const attributes = node.attributes;
[...attributes].forEach(attribute => {
const {
name,
value
} = attribute;
if (this.isDire(name)) {
const [, dirctive] = name.split('-');
const [dirName, eventName] = dirctive.split(':');
// 更新视图
compileUtil[dirName](node, value, this.vm, eventName);
// 删除指令
node.removeAttribute('v-' + dirctive);
} else if(this.isEventName(name)) {// 判断@符号
let [, eventName] = name.split('@');
compileUtil['on'](node, value, this.vm, eventName);
} else if(this.isBindName(name)) {
let [, attr] = name.split(':');
compileUtil['bind'](node, value, this.vm, attr);
}
})
}
isBindName(name) {
return name.startsWith(':')
}
isEventName(name) {
return name.startsWith('@')
}
isDire(name) {
return name.startsWith('v-')
}
fragment(el) {
// 创建文档碎片
const nodeFrag = document.createDocumentFragment();
while (el.firstChild) {
nodeFrag.appendChild(el.firstChild);
}
return nodeFrag;
}
isElementNode(node) {
return node.nodeType === 1;
}
}
Observer
遍历循环对所有属性值进行监听,并对其进行数据劫持。通过observe()方法进行遍历向下找到所有的属性,并通过defineReactive()方法进行数据劫持监听。知道监听器Observer是在get函数中执行了添加订阅者的操作的,所以我们只需要在订阅者Watcher在初始化时触发相对应的get函数来执行添加订阅者的操作,借助依赖收集器
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(data, key, value) {
this.observer(value);
const depend = new Dep();
// 劫持
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
// 数据初始化的时候添加订阅
Dep.target && depend.addSub(Dep.target)
return value;
},
set: (newValue) => {
this.observer(newValue);
if(newValue !== value) {
value = newValue;
}
// 通知Dep变化
depend.notify();
}
})
}
}
Dep
依赖收集器,主要负责添加订阅以及发布通知
class Dep {
// 收集watcher,通知watcher
constructor() {
this.subs = [];
}
// 收集watcher
addSub(watcher) {
this.subs.push(watcher);
}
// 通知watcher更新
notify() {
console.log("观察者", this.subs);
this.subs.forEach(watcher => {
watcher.update()
})
}
}
Watcher
将Compile和Observer关联起来
class Watcher {
constructor(vm, express, callback) {
this.vm = vm;
this.express = express;
this.callback = callback;
// 保存旧值
this.oldValue = this.getOldValue();
}
getOldValue() {
Dep.target = this;
let old = compileUtil.getValue(this.express, this.vm);
Dep.target = null;
return old;
}
update() {
const newValue = compileUtil.getValue(this.express, this.vm);
if(newValue !== this.oldValue) {
this.callback(newValue);
}
}
}
上面图中的updater放在了compileUtil中
const compileUtil = {
// 获取data中的对象值
getValue(express, vm) {
return express.split('.').reduce((data, currentVal) => {
// console.log(currentVal);
return data[currentVal]
}, vm.$data)
},
setValue(express, vm, inputValue) {
return express.split('.').reduce((data, currentVal) => {
data[currentVal] = inputValue;
}, vm.$data)
},
// 区分开{{}}--{{}}
getContentValue(express, vm) {
return express.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm);
})
},
// v-text
text(node, express, vm) {
let value;
if(express.indexOf('{{') !== -1) {
// {{msg}}
value = express.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 绑定观察者
new Watcher(vm, args[1], (newValue) => {
this.updater.textUpdater(node, this.getContentValue(express, vm));
})
return this.getValue(args[1], vm);
})
} else {
value = this.getValue(express, vm);
}
this.updater.textUpdater(node, value);
},
// v-html
html(node, express, vm) {
let value = this.getValue(express, vm);
new Watcher(vm, express, (newValue) => {
this.updater.htmlUpdater(node, newValue);
});
this.updater.htmlUpdater(node, value);
},
// v-model
model(node, express, vm) {
// 数据-视图
const value = this.getValue(express, vm);
new Watcher(vm, express, (newValue) => {
this.updater.modelUpdater(node, newValue);
});
// 视图-数据-视图
node.addEventListener('input', e => {
this.setValue(express, vm, e.target.value);
})
this.updater.modelUpdater(node, value);
},
// v-on
on(node, express, vm, event) {
const f = vm.$options.methods && vm.$options.methods[express];
node.addEventListener(event, f.bind(vm), false);
},
bind(node, express, vm, attribute) {
const value = this.getValue(express, vm);
this.updater.bindUpdater(node, attribute, value);
},
updater: {
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
modelUpdater(node, value) {
node.value = value;
},
bindUpdater(node, attribute, value) {
node.setAttribute(attribute, value);
}
}
}