使用Proxy和defineProperty分别构建一款MVVM框架
程序员文章站
2022-07-12 21:55:00
...
导读
这些天呢,作为前端界比较火的一件事情就是,vue 3.0
的诞生,vue 3.0
除了在用法上有些许变化外,最主要的变化,莫过于数据劫持的方式的改变;vue 3.0
使用的是es6
的Proxy
进行数据拦截的,而2.x
的版本呢,则是采用的Object.defineProperty()
这样的方式进行对数据的监听,所以呢,今天我们做个实验,什么样的实验呢?我们分别来使用这个Proxy
和defineProperty
来造一个类似于简单版的vue
这种的mvvm
框架*,好,接下来呢,我们开发阶段。
首先把这个Proxy
和defineProperty
文档的地址给贴上:
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
- http://es6.ruanyifeng.com/#docs/proxy
使用defineProperty
开发的MVVM框架
// 发布订阅Dep
class Dep{
constructor(){
this.subs = [];
}
// 添加订阅
addSub(watcher){
this.subs.push(watcher);
}
//通知执行
notify(){
this.subs.forEach(watcher => watcher.update())
}
}
// 观察者 Watcher 类实现
class Watcher{
constructor(vm, exp, callback){
this.vm = vm;
this.exp = exp;
this.callback = callback;
// 获取更改前的值
this.oldValue = this.get();
}
get(){
// 将当前的 watcher 添加到 Dep 类的静态属性上
Dep.target = this;
// 获取data上的值
let val = CompileUtil.getVal(this.vm, this.exp);
// 清空 Dep 上的 Watcher,防止重复添加
Dep.target = null;
return val;
}
update(){
// 获取新的值
let newValue = CompileUtil.getVal(this.vm, this.exp);
// 获取旧的值
let oldValue = this.oldValue;
// 如果新值和旧值不相等,就执行 callback 对 dom 进行更新
if(newValue !== oldValue){
this.callback(newValue);
}
}
}
// 模板编译类
class Compile{
constructor(el, vm){
// 获取元素
this.el = this.isElementNode(el) ? el: document.querySelector(el);
// 获取mwwm实例
this.vm = vm;
// 如果元素存在 才开始编译
if(this.el){
let fragment = this.nodeToFragment(this.el);
// 1. 把模板中的指令中的变量和{{}}中的变量替换成真实的数据
this.compileTemplate(fragment);
//2. 把编译好的 fragment 再塞回页面中
this.el.appendChild(fragment);
}
}
// 检测是否为元素节点、
isElementNode(el){
return el.nodeType === 1;
}
// 核心 将根节点转换成内存文档碎片
nodeToFragment(el){
let fragment = document.createDocumentFragment();
let firstChild;
// 循环取出节点 存放在我们的文档碎片中
while(firstChild = el.firstChild){
fragment.appendChild(firstChild);
}
return fragment;
}
// 解析文档碎片 编译模板中的变量
compileTemplate(fragment){
// 获取所有子节点(包括元素节点和文本节点)
let childNodes = fragment.childNodes;
//循环遍历每个节点
Array.from(childNodes).forEach(node => {
// 如果是元素节点
if(this.isElementNode(node)){
// 递归遍历子节点
this.compileTemplate(node);
// 处理元素节点
this.compileElement(node);
}
// 如果是文本节点
else{
this.compileText(node);
}
});
}
// 编译处理元素节点
compileElement(node){
//取出所有属性
let attrs = node.attributes;
// 遍历属性 检测是否具备`v-`
Array.from(attrs).forEach(attr => {
// console.log(attr)
let attrName = attr.name;
if(attrName.includes('v-')){
// 如果是v-attr指令,去到该属性值对应的变量exp在data里面的值
let exp = attr.value;
// 取出方法名
let [, type] = attrName.split("-");
// 调用指令对应得方法 渲染页面数据
CompileUtil[type](node, this.vm, exp);
}
})
}
// 编译处理文本节点
compileText(node){
// 获取文本节点内容
let exp = node.textContent;
let re = /{{([^}]+)}}/g;
if(re.test(exp)){
// 渲染页面数据
CompileUtil['text'](node, this.vm, exp);
}
}
}
// 数据劫持类
class Observer{
constructor(data){
this.observe(data);
}
// 添加数据监听
observe(data){
// 验证data是否为对象
if(!data || typeof data !== 'object'){
return
}
// 对data里面的数据进行深度遍历 一一劫持
Object.keys(data).forEach(key => {
// 实现数据响应劫持
this.defineReactive(data, key, data[key]);
// 递归劫持
this.observe(data[key]);
});
}
//数据响应绑定
defineReactive(data, key, value){
let self = this;
// 每个变化的数据都会对应一个数组,这个数组是存放所有更新的操作
let dep = new Dep();
//监听每一个key
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get(){ // 取值调用
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue){
// console.log(newValue)
if(newValue !== value){
self.observe(newValue); // 重新赋值与劫持
console.log(newValue)
value = newValue;
dep.notify(); // 通知数据更新
}
}
})
}
}
// 初始化类
class Mvvm{
constructor(opts){
// 获取元素和数据
this.$el = opts.el;
this.$data = opts.data;
// 判断是否有模板 如果有模板 就执行编译
if(this.$el){
//1. 数据劫持
new Observer(this.$data);
//2. 编译模板
new Compile(this.$el, this);
}
}
}
// 存储着所有的指令方法及指令对应的更新方法
let CompileUtil = {
// 更新节点数据的方法
updater: {
// 文本更新
textUpdater(node, v){
node.textContent = v;
},
// 输入框更新
modelUpdater(node, v){
node.value = v;
}
},
// 获取data里面的值
getVal(vm, exp){
// 分隔对象引用.
exps = exp.replace(/\s*/g,'').split('.');
console.log(vm.$data.me.message);
return exps.reduce((prev, next) => {
return prev[next];
}, vm.$data);
},
// 获取文本 {{ msg }} 中 msg 在 data 里面的值
getTextVal(vm, exp){
return exp.replace(/{{([^}]+)}}/g,(...args) => {
let data = this.getVal(vm, args[1]);
// console.log(data)
return data;
})
},
// 设置data值的方法、
setVal(vm, exp, newVal){
// 分隔对象引用.
exps = exp.split('.');
exps.reduce((prev, next, currentIndex) => {
// 如果当前归并的为数组的最后一项,则将新值设置到该属性
if(currentIndex === exps.length -1){
prev[next] = newVal;
return;
}
// 未到最后一个属性 继续归并
return prev[next];
},vm.$data);
console.log(vm.$data)
},
// 处理 v-model 指令的方法
model(node, vm, exp){
exp = exp.replace(/\s*/g,'');
// 1. 获取赋值的方法
let updateFn = this.updater.modelUpdater
// 2. 获取data中的对应变量的值
let val = this.getVal(vm, exp);
// 4. 监听变化决定是否更新视图
new Watcher(vm, exp, newValue => {
updateFn && updateFn(node, newValue);
})
// 5. 给node元素添加input事件监听
node.addEventListener('input', e => {
//获取输入的值
let newVal = e.target.value;
// 更新到data数据上
this.setVal(vm, exp, newVal);
});
// 3. 初始化设置值
updateFn&&updateFn(node, val);
},
// 处理文本节点上的 {{}} 方法
text(node, vm, exp){
exp = exp.replace(/\s*/g,'');
// 获取赋值的方法
let updateFn = this.updater.textUpdater;
// 获取 data 中对应的变量的值
let value = this.getTextVal(vm, exp);
// console.log(value)
// 通过正则替换,将取到数据中的值替换掉 {{ }}
exp.replace(/{{([^}]+)}}/g, (...args) => {
// 解析时遇到了模板中需要替换为数据值的变量时,应该添加一个观察者
// 当变量重新赋值时,调用更新值节点到 Dom 的方法
new Watcher(vm, args[1], newValue => {
// 如果数据发生变化,重新获取新值
updateFn && updateFn(node, newValue);
});
});
// 第一次设置值
updateFn && updateFn(node, value);
}
};
使用Proxy
开发的MVVM框架
// 发布订阅Dep
class Dep{
constructor(){
this.subs = [];
}
// 添加订阅
addSub(watcher){
this.subs.push(watcher);
}
//通知执行
notify(){
this.subs.forEach(watcher => watcher.update())
}
}
// 观察者 Watcher 类实现
class Watcher{
constructor(vm, exp, callback){
this.vm = vm;
this.exp = exp;
this.callback = callback;
// 获取更改前的值
this.oldValue = this.get();
}
get(){
// 将当前的 watcher 添加到 Dep 类的静态属性上
Dep.target = this;
// 获取data上的值
let val = CompileUtil.getVal(this.vm, this.exp);
// 清空 Dep 上的 Watcher,防止重复添加
Dep.target = null;
return val;
}
update(){
// 获取新的值
let newValue = CompileUtil.getVal(this.vm, this.exp);
// 获取旧的值
let oldValue = this.oldValue;
// console.log(this.vm, newValue, oldValue, newValue !== oldValue)
// 如果新值和旧值不相等,就执行 callback 对 dom 进行更新
if(newValue !== oldValue){
this.callback(newValue);
}
}
}
// 模板编译类
class Compile{
constructor(el, vm){
// 获取元素
this.el = this.isElementNode(el) ? el: document.querySelector(el);
// 获取mwwm实例
this.vm = vm;
console.log(vm)
// 如果元素存在 才开始编译
if(this.el){
let fragment = this.nodeToFragment(this.el);
// 1. 把模板中的指令中的变量和{{}}中的变量替换成真实的数据
this.compileTemplate(fragment);
//2. 把编译好的 fragment 再塞回页面中
this.el.appendChild(fragment);
}
}
// 检测是否为元素节点、
isElementNode(el){
return el.nodeType === 1;
}
// 核心 将根节点转换成内存文档碎片
nodeToFragment(el){
let fragment = document.createDocumentFragment();
let firstChild;
// 循环取出节点 存放在我们的文档碎片中
while(firstChild = el.firstChild){
fragment.appendChild(firstChild);
}
return fragment;
}
// 解析文档碎片 编译模板中的变量
compileTemplate(fragment){
// 获取所有子节点(包括元素节点和文本节点)
let childNodes = fragment.childNodes;
//循环遍历每个节点
Array.from(childNodes).forEach(node => {
// 如果是元素节点
if(this.isElementNode(node)){
// 递归遍历子节点
this.compileTemplate(node);
// 处理元素节点
this.compileElement(node);
}
// 如果是文本节点
else{
this.compileText(node);
}
});
}
// 编译处理元素节点
compileElement(node){
//取出所有属性
let attrs = node.attributes;
// 遍历属性 检测是否具备`v-`
Array.from(attrs).forEach(attr => {
// console.log(attr)
let attrName = attr.name;
if(attrName.includes('v-')){
// 如果是v-attr指令,去到该属性值对应的变量exp在data里面的值
let exp = attr.value;
// 取出方法名
let [, type] = attrName.split("-");
// 调用指令对应得方法 渲染页面数据
CompileUtil[type](node, this.vm, exp);
}
})
}
// 编译处理文本节点
compileText(node){
// 获取文本节点内容
let exp = node.textContent;
let re = /{{([^}]+)}}/g;
if(re.test(exp)){
// 渲染页面数据
CompileUtil['text'](node, this.vm, exp);
}
}
}
class Observer{
constructor(data){
// this.observe(data);
}
// 添加数据监听
observe(data){
// 验证data是否为对象
if(!data || typeof data !== 'object'){
return
}
let self = this;
// 每个变化的数据都会对应一个数组,这个数组是存放所有更新的操作
let dep = new Dep();
let handler = {
get: function(target, key, receiver) {
// 递归创建并返回
if (typeof target[key] === 'object' && target[key] !== null) {
return new Proxy(target[key], handler);
}
if(typeof target === 'object' && target !== null){
Dep.target && dep.addSub(Dep.target);
}
// console.log(dep);
return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver) {
console.log(target[key], value)
if(target[key] !== value){
console.log('update')
target[key] = value;
dep.notify(); // 通知数据更新
}
}
};
//
let cdata = new Proxy(data, handler);
return cdata;
}
}
// 数据劫持类
class Observer2{
constructor(data){
this.observe(data);
}
// 添加数据监听
observe(data){
// 验证data是否为对象
if(!data || typeof data !== 'object'){
return
}
// 对data里面的数据进行深度遍历 一一劫持
Object.keys(data).forEach(key => {
// 实现数据响应劫持
this.defineReactive(data, key, data[key]);
// 递归劫持
this.observe(data[key]);
});
}
//数据响应绑定
defineReactive(data, key, value){
let self = this;
// 每个变化的数据都会对应一个数组,这个数组是存放所有更新的操作
let dep = new Dep();
//监听每一个key
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get(){ // 取值调用
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue){
// console.log(newValue)
if(newValue !== value){
self.observe(newValue); // 重新赋值与劫持
console.log(newValue)
value = newValue;
dep.notify(); // 通知数据更新
}
}
})
}
}
// 初始化类
class Mvvm{
constructor(opts){
// 获取元素和数据
this.$el = opts.el;
this.$data = opts.data;
// 判断是否有模板 如果有模板 就执行编译
if(this.$el){
//1. 数据劫持
let obs = new Observer();
this.$data = obs.observe(this.$data);
//2. 编译模板
new Compile(this.$el, this);
}
}
}
// 存储着所有的指令方法及指令对应的更新方法
let CompileUtil = {
// 更新节点数据的方法
updater: {
// 文本更新
textUpdater(node, v){
node.textContent = v;
},
// 输入框更新
modelUpdater(node, v){
node.value = v;
}
},
// 获取data里面的值
getVal(vm, exp){
// 分隔对象引用.
exps = exp.replace(/\s*/g,'').split('.');
let result = exps.reduce((prev, next) => {
return prev[next];
}, vm.$data)
console.log(vm.$data.me.message)
return result;
},
// 获取文本 {{ msg }} 中 msg 在 data 里面的值
getTextVal(vm, exp){
return exp.replace(/{{([^}]+)}}/g,(...args) => {
let data = this.getVal(vm, args[1]);
// console.log(data)
return data;
})
},
// 设置data值的方法、
setVal(vm, exp, newVal){
// 分隔对象引用.
exps = exp.split('.');
exps.reduce((prev, next, currentIndex) => {
// 如果当前归并的为数组的最后一项,则将新值设置到该属性
if(currentIndex === exps.length -1){
prev[next] = newVal;
return;
}
// 未到最后一个属性 继续归并
return prev[next];
}, vm.$data);
console.log(vm.$data)
},
// 处理 v-model 指令的方法
model(node, vm, exp){
exp = exp.replace(/\s*/g,'');
// 1. 获取赋值的方法
let updateFn = this.updater.modelUpdater
// 2. 获取data中的对应变量的值
let val = this.getVal(vm, exp);
// 4. 监听变化决定是否更新视图
new Watcher(vm, exp, newValue => {
updateFn && updateFn(node, newValue);
})
// 5. 给node元素添加input事件监听
node.addEventListener('input', e => {
// console.log(e.target.value)
//获取输入的值
let newVal = e.target.value;
// 更新到data数据上
this.setVal(vm, exp, newVal);
});
// 3. 初始化设置值
updateFn&&updateFn(node, val);
},
// 处理文本节点上的 {{}} 方法
text(node, vm, exp){
exp = exp.replace(/\s*/g,'');
// 获取赋值的方法
let updateFn = this.updater.textUpdater;
// 获取 data 中对应的变量的值
let value = this.getTextVal(vm, exp);
// console.log(value)
// 通过正则替换,将取到数据中的值替换掉 {{ }}
exp.replace(/{{([^}]+)}}/g, (...args) => {
// 解析时遇到了模板中需要替换为数据值的变量时,应该添加一个观察者
// 当变量重新赋值时,调用更新值节点到 Dom 的方法
new Watcher(vm, args[1], newValue => {
// 如果数据发生变化,重新获取新值
updateFn && updateFn(node, newValue);
});
});
// 第一次设置值
updateFn && updateFn(node, value);
}
};