相信只要你去面试vue,都会被问到vue的双向数据绑定,你要是就说个mvvm就是视图模型模型视图,只要数据改变视图也会同时更新!那你离被pass就不远了!那么读完本文后,相信你就可以说出很多东西了!
vue的mvvm由两部分组成,中间通过watcher来连接
- 数据劫持:通过Object.defineProperty来给每个数据属性加上get和set方法,每次取值的时候都会调用get方法,每次赋值的时候都会触发set方法;并且有new Dep(); 在每次取值调用get方法的时候存放watcher,每次赋值的时候依次调用watcher的updata方法。
- 模版编译:通过模版的内容取出指令例如 v-model 取出数据例如{{message}},将值替换
- Watcher:在编译的时候遇到数据需要编译的就要new Watcher,这个watcher实例身上有该vm, expr, cb,还有value,并且存在一个数组中保存好,以便后面调用,在我们改动数据的时候就会触发上面被劫持数据的set方法,set方法中会看一下改没改值,如果改了 先接着劫持一下数据(万一是对象就必须地递归深度劫持),然后通知刚刚存的所有watcher,这里有个数据更新了快来看看啊,执行watcher中的updata方法,updata方法会执行每个watcher实例的cb(new Watcher的时候存下来的)cb是是啥呢?其实就是更新了节点
// 文本更新
textUpdater(node, value) {
node.textContent = value
},
// 输入框更新
modelUpdater(node, value) {
node.value = value;
}
复制代码
MVVM.js
class MVVM{
constructor(options) {
this.$el = options.el;
this.$data = options.data;
if(this.$el) {
// 数据劫持 就是把对象的所有属性 改成get和set方法
new Observer(this.$data);
// 将对象的属性代理到this身上
this.proxyData(this.$data);
new Compile(this.$el,this)
}
}
//代理 将this.$data代理到this身上,这样我们赋值的时候就可以直接写this.xx不需要再写this.$data.xx
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this,key,{
get(){
return data[key]
},
set(newValue){
data[key] = newValue
}
})
});
}
}
复制代码
下面放observer.js的代码并说明:
class Observer{
constructor(data){
this.observe(data) //数据加get和set方法 这样我们就可以监听数据变化了
}
observe(data) {
// 要对这个data数据将原有的属性改成set和get的形式
if(!data || typeof data !== 'object'){
return;
}
// 要将数据 一一劫持 先获取取到data的key和value
Object.keys(data).forEach(key=>{
// 劫持
this.defineReactive(data,key,data[key]);
this.observe(data[key]);// 深度递归劫持
});
}
defineReactive(data,key,val) {
let that = this;
let dep = new Dep(); // 每个变化的数据 都会对应一个数组,这个数组是存放所有更新的操作
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get(){ //取值的时候会触发该方法
Dep.target && dep.addSub(Dep.target); //这里的dep其实是一个闭包,每个key都对应独自的dep //后面我们会看到这个Dep.target其实就是watcher实例 watcher实例身上有vm,绑定的数据,和回调(更新节点数据)
return val
},
set(newVal){ //设置值的时候会触发
if(val != newVal) {
that.observe(newVal) //如果是对象继续劫持
val = newVal;
dep.notify(); // 通知所有相关人 数据更新了
}
}
})
}
}
//类似发布订阅
class Dep{
constructor(){
this.subs = []
}
addSub(watcher){
this.subs.push(watcher)
}
notify(){
this.subs.forEach(watcher=>watcher.update());
}
}
复制代码
下面放watcher.js并说明:
class Watcher{
constructor(vm, expr, cb){
this.vm = vm;
this.expr = expr;
this.cb = cb;
this.value = this.get();
}
getVal(vm, expr) { // 获取实例上对应的数据
expr = expr.split('.'); // [message,a]
return expr.reduce((prev, next) => { // vm.$data.a
return prev[next];
}, vm.$data);
}
get(){
Dep.target = this;
let value = this.getVal(this.vm,this.expr);//特别要说明的就是这个地方
//取值就会触发observer.js中的get方法 在这个时候给所有的watcher都给存下来
Dep.target = null;
return value;
}
// 对外暴露的方法
update(){
let newValue = this.getVal(this.vm, this.expr);
let oldValue = this.value;
if(newValue != oldValue){
this.cb(newValue); // 对应watch的callback
}
}
}
复制代码
下面放compile.js并说明:
class Compile{
constructor(el,vm) {
this.el = this.isElementNode(el) ? el: document.querySelector(el);
this.vm = vm;
if(this.el) {
// 如果这个元素能获取到 我们才开始编译
// 1.先把这些真实的DOM移入到内存中 fragment
let fragment = this.node2fragment(this.el);
// 2.编译 => 提取想要的元素节点 v-model 和文本节点 {{}}
this.compile(fragment);
// 3.把编译号的fragment在塞回到页面里去
this.el.appendChild(fragment);
}
}
// 辅助方法
// 是不是元素
isElementNode(node) {
return node.nodeType === 1;
}
// 是不是指令
isDirective(name) {
return name.includes('v-');
}
// 核心方法
compile(fragment){
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach(node => {
if (this.isElementNode(node)) {
// 元素
this.compileElement(node);
this.compile(node)
} else {
// 文本
this.compileText(node);
}
})
}
compileElement(node){
// 取出元素的所有属性 看看需不需要编译
let attrs = node.attributes;
for(var attr of attrs) {
let attrName = attr.name;
if(this.isDirective(attrName)) {
let expr = attr.value;
let [,type] = attrName.split('-')
CompileUtil[type](node, this.vm, expr);
}
}
}
compileText(node){
let expr = node.textContent;
let reg = /\{\{([^}]+)\}\}/g
if (reg.test(expr)) {
// node this.vm.$data text
CompileUtil['text'](node, this.vm, expr);
}
}
node2fragment(el){
let fragment = document.createDocumentFragment();
let firstChild;
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild)
}
return fragment;
}
}
CompileUtil = {
getVal(vm, expr) { // 获取实例上对应的数据
expr = expr.split('.'); // [message,a]
return expr.reduce((prev, next) => { // vm.$data.a
return prev[next];
}, vm.$data);
},
getTextVal(vm, expr) { // 获取编译文本后的结果
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
return this.getVal(vm, arguments[1]);
})
},
// node节点,vm实例 expr(message.a) v-text="message.a"
text(node,vm,expr){
let updateFn = this.updater['textUpdater'];
let value = this.getTextVal(vm, expr);
expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
new Watcher(vm, arguments[1],(newValue)=>{
//将每一个绑定的数据形成一个watcher --> observer的取值操作 -get-> Dep.target && dep.addSub(Dep.target);
// 如果数据变化了,文本节点需要重新获取依赖的属性更新文本中的内容
updateFn && updateFn(node,this.getTextVal(vm,expr));
});
})
updateFn && updateFn(node, value)
},
setVal(vm,expr,value){ // [message,a]
expr = expr.split('.');
// 收敛
return expr.reduce((prev,next,currentIndex)=>{
if(currentIndex === expr.length-1){
return prev[next] = value; //设置值了会触发observer中的set方法
}
return prev[next];
},vm.$data);
},
model(node,vm,expr) {
let updateFn = this.updater['modelUpdater'];
// 这里应该加一个监控 数据变化了 应该调用这个watch的callback
new Watcher(vm,expr,(newValue)=>{
// 当值变化后会调用cb 将新的值传递过来 ()
updateFn && updateFn(node, this.getVal(vm, expr));
});
node.addEventListener('input',(e)=>{
let newValue = e.target.value;
this.setVal(vm,expr,newValue)
})
updateFn && updateFn(node, this.getVal(vm, expr));
},
updater: {
// 文本更新
textUpdater(node, value) {
node.textContent = value
},
// 输入框更新
modelUpdater(node, value) {
node.value = value;
}
}
}
复制代码
至此,一个简单的mvvm就实现了!三个js各自分工明确,互相配合!当然还有很多不足之处,欢迎各位提出宝贵的意见或建议,也希望能帮助到你从中获得一些知识,谢谢大家的关注!