Vue 中数据双向绑定核心:
- 数据劫持
- 发布订阅
遍历 data ,通过 Object.defineProperty() 方法来劫持各个属性的 setter,getter,添加发布者,在数据变动时发布消息给订阅者,触发相应的监听回调函数
通过Observer来进行数据观测,通过Compile来解析编译模板指令,通过watcher进行数据的更新渲染
示例代码:
// 发布者 publish
class Dep{
constructor(){
// 订阅者数组
this.subs = [];
}
// 订阅方法
addSub(watcher){
// 往订阅者数组中添加新的订阅者
this.subs.push(watcher);
}
// 发布方法
notify(newVal){
// 遍历订阅者数组,调用每个订阅者的 update 方法
this.subs.forEach(sub=>{
sub.update(newVal);
})
}
}
// 订阅者 subscribe
class Watcher{
constructor(cb){
this.cb = cb;
}
// 接收到消息时的更新函数
update(newVal){
console.log('更新了....');
this.cb(newVal);
}
}
// 在数据劫持的时候,一个key劫持执行前,选new一个Dep
// new Dep()
// 第一次渲染前,先new一个watcher
// new Watcher()
// 第一次渲染的时候,添加订阅者
// addSub()
// 更新数据,执行发布
// notify()
// 防止重复添加订阅者,使用一个全局变量
let watcher = null;
class MVVM{
constructor(options){
// 配置实例上的基础属性
this.$options = options;
this.$data = this._data = options.data;
// 添加数据观测
this.observer(this.$data);
// 编译模版
this.compiler(options.el);
}
// 数据观测
observer(data){
// 获得data的所有key和value
Object.entries(data).forEach(([key, value])=>{
// 创建发布者
console.log('发布者:', key);
const dep = new Dep();
// 对key进行数据观测
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
// 设置数据
set(newVal){
// console.log('set run......');
if(newVal !== value){
// 设置新数据
value = newVal;
// 重新渲染dom
console.log('执行发布.....');
dep.notify(newVal);
}
},
// 读取数据
get(){
// console.log('get run......');
console.log('执行订阅.....');
if(watcher){
dep.addSub(watcher);
watcher = null;
}
return value;
}
});
})
}
// 编译模版
compiler(el){
// 获得实例作用的dom
const element = document.querySelector(el);
// 遍历实例作用的dom
this.compilerNode(element);
}
compilerNode(element){
// 获得需要编译的dom的每一个子节点
const childNodes = element.childNodes;
// 转为可遍历的对象,进行遍历
Array.from(childNodes).forEach(node=>{
const {nodeType, textContent} = node;
//判断是文本节点
if(nodeType === 3){
// 判断是否有插值表达式在文本节点中
let reg = /\{\{\s*(\S*)\s*\}\}/;
//有
if(reg.test(textContent)){
//数据变,dom需要更新
// 创建订阅者
console.log('订阅者:', RegExp.$1);
watcher = new Watcher((newVal)=>{
//更新数据的渲染
node.textContent = newVal;
});
// 第一次渲染dom
node.textContent = this.$data[RegExp.$1];
}
}
// 判断是标签
else if(nodeType === 1){
// 拿到标签的所有属性
let attrs = Array.from(node.attributes);
// 遍历每一属性
attrs.forEach(attr=>{
//判断属性是否是指令
if(attr.name.startsWith('v-')){
//是指令,取指令名字
let dirName = attr.name.substr(2);
if(dirName === 'model'){ //v-model="message"
let key = attr.value;
// 创建订阅者
watcher = new Watcher((newVal)=>{
node.value = newVal;
});
// 设置初始值
node.value = this.$data[key];
// 添加输入事件监听
node.addEventListener('input', (ev)=>{
this.$data[key] = ev.target.value;
});
}
else if(dirName === 'bind'){
}
}
})
}
// 有子节点,需要编译子节点
if(node.childNodes.length > 0){
// 遍历子节点
this.compilerNode(node);
}
})
}
}
使用 Object.defineProperty() 的弊端:
- Object.defineProperty() 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历
- 监测不到对象属性的添加、删除
- 监测不到数组长度的变化、给数组的某一项直接赋值也检测不到
而在 Vue 3.0 开始采用了 ES6 中新增的一个特性 Proxy(代理),它可以直接劫持整个对象,并返回一个新对象,性能和操作都会比 Object.defineProperty() 强上许多,具体可参考: