MVVM
MVVM双向数据绑定是通过数据劫持+发布订阅模式Object.defineProperty
let obj={};
let theValue;
Object.defineProperty(obj,'theKeyName',{
//value:'theValue',
configurable:true, //可以配置对象,删除属性
//writable:true, //可以修改对象
enumerable:true, //可以枚举:默认情况下通过defineProperty定义的属性是不能被枚举(遍历)的
get(){ //get,set设置与writable,value互斥
return value;
},
set(value){
theValue=value;
}
});
参考Vue
<div id="app">
<h1></h1>
</div>
let mvvm = new Mvvm({
el:'#app',
data:{
title:'Mvvm'
}
});
打造MVVM
function Mvvm(options = {}){
this.$options = options;
let data = this._data = this.$options.data;
_observe(data);
}
数据劫持
- 观察对象,给对象增加
Object.defineProperty
- vue特点是不能新增不存在的属性 不存在的属性没有
get
和set
- 深度响应 每次赋予一个新的对象时会给这个新对象增加
defineProperty
function Observe(data){
// 据劫持就是给对象增加get,set
// 把data属性通过defineProperty的方式定义属性
for(let key in data){
let val = data[key];
// 递归继续向下找,实现深度的数据劫持
_observe(val);
Object.defineProperty(data,key,{
configurable:true,
get(){
return val;
},
set(value){
if(val===value){
return;
}
val = value;
// 当设置为新值后,也需要把新值再去定义成属性
_observe(value);
}
})
}
}
function _observe(data){
if(!data || typeof data !== 'object'){
return;
}
return new Observe(data);
}
数据代理
数据代理就是让我们每次拿data
里的数据时,不用每次都写一长串,如mvvm._data.a.b
这种,我们其实可以直接写成mvvm.a.b
这种显而易见的方式
function Mvvm(options = {}){
_observe(data);
// this 代理了this._data
for(let key in data){
Object.defineProperty(this,key,{
configurable:true,
get(){
return this._data[key];
},
set(value){
this._data[key]=value;
}
});
}
}
数据编译
function Mvvm(options = {}){
...
new Compile(options.el,this);
}
function Compile(el, vm) {
// 将el挂载到实例上方便调用
vm.$el = document.querySelector(el);
// 在el范围里将内容都拿到,当然不能一个一个的拿
// 可以选择移到内存中去然后放入文档碎片中,节省开销
let fragment = document.createDocumentFragment();
while (child = vm.$el.firstChild) {
fragment.appendChild(child); // 此时将el中的内容放入内存中
}
// 对el里面的内容进行替换
function replace(frag) {
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent;
let reg = /\{\{(.*?)\}\}/g; // 正则匹配{}
if (node.nodeType === 3 && reg.test(txt)) {
function replaceTxt() {
node.textContent = txt.replace(reg, (matched, placeholder) => {
console.log(placeholder); // 匹配到的分组 如:song, album.name, singer...
new Watcher(vm, placeholder, replaceTxt); // 监听变化,进行匹配替换内容
return placeholder.split('.').reduce((val, key) => {
return val[key];
}, vm);
});
};
replaceTxt();
}
});
}
replace(fragment); // 替换内容
vm.$el.appendChild(fragment); // 再将文档碎片放入el中
}
发布订阅
发布订阅主要靠的就是数组关系,订阅就是放入函数,发布就是让数组里的函数执行
// 发布订阅模式
function Dep(){
// 一个数组(存放函数的事件池)
this.subs = [];
}
Dep.prototype = {
addSub(sub){
this.subs.push(sub);
},
notify(){
// 绑定的方法,都有一个update方法
this.subs.forEach(sub=>sub.update());
}
}
// 监听函数
// 通过Watcher这个类创建的实例,都拥有update方法
function Watcher(fn){
this.fn = fn;
}
Watcher.prototype.update = function(){
this.fn();
}
let watcher= new Watcher(()=> console.log(1));
let dep = new Dep();
dep.addSub(watcher);
dep.addSub(watcher);
dep.notify(); // 1 , 1
数据更新视图
- 现在我们要订阅一个事件,当数据改变需要重新刷新视图,这就需要在
replace
替换的逻辑里来处理 - 通过
new Watcher
把数据订阅一下,数据一变就执行改变内容的操作
1 Compile
function Compile(el, vm) {
...
// 替换的逻辑
//node.textContent=txt.replace(reg,val).trim();
// 给Watcher再添加两个参数,用来取新的值(newVal)给回调函数传参
new Watcher(vm,RegExp.$1,newVal => {
node.textContent = txt.replace(reg,newVal).trim();
});
}
RegExp.$1
是RegExp
的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串
2 Watcher
function Watcher(vm,exp,fn){
this.fn = fn;
this.vm = vm;
this.exp = exp;
Dep.target = this;
let arr = exp.split('.');
let val = vm;
arr.forEach(key=>{
// 获取到this.a.b,默认就会调用get方法
val = val[key];
});
Dep.target = null;
}
// set 修改值时执行 dep.notify,其内部执行的是 watcher.update
Watcher.prototype.update = function() {
// notify的时候值已经更改了
// 再通过vm, exp来获取新的值
let arr = this.exp.split('.');
let val = this.vm;
arr.forEach(key => {
val = val[key];
});
this.fn(val);
}
3 Observe
function Observe(data){
let dep = new Dep();
...
Object.defineProperty(data,key,{
get(){
// 将watcher添加到订阅事件中 [watcher]
Dep.target&&dep.addSub(Dep.target);
return val;
},
set(newVal){
if(val === newVal){
return;
}
val = newVal;
_observe(newVal);
// 执行所有watcher的update方法
dep.notify();
}
});
}
双向数据绑定
function replace(frag){
...
if(node.nodeType === 1){
let nodeAttr = node.attributes;
Array.form(nodeAttr).forEach(attr =>{
let name = attr.name; //v-model type
let exp = attr.value;
if(name.includes('v-')){
node.value = vm[exp];
}
new Watcher(vm,exp,function(newVal){
// 当watcher触发时会自动将内容放进输入框中
node.value = newVal;
});
node.addEventListener('input',e=>{
let newVal=e.target.value;
// 相当于给this.c赋了一个新值
// 而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新
vm[exp] = newVal;
});
})
}
if(node.childNodes && node.childNodes.length){
replace(node);
}
}
总结
通过Object.defineProperty
的get
和set
进行数据劫持
通过遍历data
数据进行数据代理到this
上
通过{{}}
对数据进行编译
通过发布订阅模式实现数据与视图同步