参考地址:
https://mp.weixin.qq.com/s/fRFnuBqdHn5kBgWiPQR7ew
什么是MVVM--MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。
在vue中,是通过Object.defineProperty(数据劫持) + 观察者模式(发布+订阅)实现的数据之间双向绑定的
Object.defineProperty的用法
let obj = {};
let song = '发如雪';
obj.singer = '周杰伦';
Object.defineProperty(obj, 'music', {
// 1. value: '七里香',
configurable: true, // 2. 可以配置对象,删除属性
// writable: true, // 3. 可以修改对象
enumerable: true, // 4. 可以枚举
// ☆ get,set设置时不能设置writable和value,它们代替了二者且是互斥的
get() { // 5. 获取obj.music的时候就会调用get方法
return song;
},
set(val) { // 6. 将修改的值重新赋给song
song = val;
}
});
// 下面打印的部分分别是对应代码写入顺序执行
console.log(obj); // {singer: '周杰伦', music: '七里香'} // 1
delete obj.music; // 如果想对obj里的属性进行删除,configurable要设为true 2
console.log(obj); // 此时为 {singer: '周杰伦'}
obj.music = '听妈妈的话'; // 如果想对obj的属性进行修改,writable要设为true 3
console.log(obj); // {singer: '周杰伦', music: "听妈妈的话"}
for (let key in obj) {
// 默认情况下通过defineProperty定义的属性是不能被枚举(遍历)的
// 需要设置enumerable为true才可以
// 不然你是拿不到music这个属性的,你只能拿到singer
console.log(key); // singer, music 4
}
console.log(obj.music); // '发如雪' 5
obj.music = '夜曲'; // 调用set设置新的值
console.log(obj.music); // '夜曲' 6
仿VUE手写MVVM开始
先看个脑图:-- 代码的大致流程 (也可以不看,我其实就是瞎画的)
图的摘抄地址:https://blog.csdn.net/feng_strong/article/details/74331823
先看下外部如何调用Mvvm类呢?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="school.name">
<input type="text" v-model="a">
{{a}}
<div>
{{school.aa}}{{school.bb}}
<span>
{{school.bb}}
</span>
</div>
</div>
<script type="module">
import {Mvvm} from './mvvm.js'
window.vm = new Mvvm({
el:'#app',
data:{
school:{
name:'zz',
aa: 22,
bb: 33
},
a:22
},
computed:{
getName(){
return this.school.name + '呵呵';
}
}
})
</script>
</body>
</html>
Mvvm类的实现
根据脑图不难看出,Mvvm中主要做了几件事情:
1.将外部传达的参数option中抽离出el和data.单独绑到Mvvm实例上。抽离出来的主要原因是其他地方会疯狂用到data和el值。
文章中文字描述的部分所有关于data默认就是$data, el默认就是$el
2.new Observal(this); 数据劫持 -- 为什么要数据劫持呢?为了data中的值全部用Object.defineProperty增加get,set函数。(vue内部的数据拦截实现就是用的defineProperty)-- 最重要的为了实时监听data中数据的变化。get中添加订阅者(watch).set中只要值发生变化就 触发订阅者的 赋值函数。把更改的值动态的响应到页面。-- 这里看不懂没关系。一会有代码
3. 将data中的数据绑定到vm实例上一份,因为vue中直接用this.xx 等同于调用this.data.xx的属性。为了和vue保持一致。就写了这一操作。-- 你要是不喜欢这步。你可以不要,你开心就好,这步也不重要
4.编译模板 -- 就是排查el中的html 元素,其中要是有{{xx.xx}} <input v-model="xx.xx">这种。就把它摘出来。经过一番加工。把它替换成data中的值。
根据下边的基础类,我就说这么多。下边就上述的2,3,4项。我详细的说明下
// 基础类
export class Mvvm{
constructor(option){
// option 外部传的参数
this.$el = option.el;
this.$data = option.data;
// 这个根元素 存在
if(this.$el){
// 数据劫持,给所有的增加defineProperty -- set,get
new Observal(this);
// 将$data中的数据绑定到vm实例上一份
this.proxVM(this, this.$data);
// 编译模板
new Compiler(this.$el, this);
}
}
}
为了方便理解--我先说编译模板,为什么要编译模板,刚才说过了。就是想把id='app'中的v-model和{{}}v-html,v-on这些乱七八糟的东西替换成data各个对应的值。(vue绑定的el为#app,所以这里主要职责就是替换#app中的内容)
编译模板 -- 代码:
new Compiler(this.$el, this); // this.$el就是 #app, this就是 new Mvvm实例
// 编译模板(模板编译)
class Compiler{
constructor(el,vm){ // el 值 #app vm 就是 new Mvvm()
this.vm = vm; // 将vm和el 绑定到Compiler实例上是为了方便调用没有其他的意思
// 判断el属性 是不是一个元素 如果不是元素 那就获取它 如果是元素 则直接用
this.el = this.isElementNode(el) ? el : document.querySelector(el);
// 把当前节点中的元素 获取到 放到内存中 (避免每次回流和重绘)
let fragment =this.node2fragment(this.el);
/*
fragment中的存放的就是原来#app中的内容
编译模板 -- 把节点中的内容进行替换
*/
this.compile(fragment);
// 把内容再塞到#app中,这用#app中都是替换后的内容了
this.el.appendChild(fragment);
}
// Compiler中的静态变量
static CompilerTypeUtil = {
getValue(vm, value){ // data vue中的data集合 value 为 v-model中的值 v-model="school.aa"
let data = vm.$data;
let val = value.split('.').reduce((lastObj,currentKey) =>{
return lastObj[currentKey];
},data);
return val;
},
// 给$data中数据和vm中的数据赋值 modelValue:school.a.b value:更新的值 obj: this.$data或vm
setValueSecond(obj, modelValue, value){
let arr = modelValue.split('.');
// index当前索引 arr为遍历的数组 currentKey当前元素 obj上一次返回的值
arr.forEach((currentKey,index) =>{
// school.a.b -- > 证明遍历到b了。这时候直接赋值
if( index >= arr.length -1 ){
obj[currentKey] = value;
}else{
obj = obj[currentKey];
}
})
},
// 用来更新$data中数据和vm中的数据
setValue(modelValue, vm, value){
let obj = vm.$data
this.setValueSecond(obj, modelValue, value);
this.setValueSecond(vm, modelValue, value);
},
// 给输入框的value赋值
// compileObj -- Compiler实例, value -- v-model值, node -- 指定节点
// modelValue:"school.aa" node: <input v-model="school.aa"/>
model(vm, modelValue, node){ // value 值 school.aa
// 这里一会重点说下 // 给school.aa增加观察者,如果稍后数据更新会触发回调方法,会拿新的输入框赋值
new Watch(vm, modelValue, (newVal) => {
// newVal -- 从 $data.school.aa 的值(更改后的值)
this.setInput(node, newVal); // setInput 就是 给input的value属性 赋值
/*
这的setValue是什么意思呢? 是既给$data.school.aa更新值。又给vm.school.aa更新值。
这么写主要是为了vm.xx的值改了可以更新到$data.xx上、反之$data.xx的值改了可以更新到vm.xx上
*/
this.setValue(modelValue, vm, newVal);
});
// 在v-model的input框中 绑定Inputs事件。用来更新$data中数据 input绑定input事件。每次改变以后,动态更新$data中的数据
node.addEventListener('input', (e)=>{
let value = e.target.value;
this.setValue(modelValue, vm, value);
})
// 第一次编译的时候走这里,之后数据更新会走到watch中的回调中。就不会走到这里了
let val = this.getValue(vm, modelValue);
this.setInput(node, val);
},
// 给v-html赋值
html(){ // node.html
},
// 给文本赋值
text(vm, value){
let textVal = this.getValue(vm, value);
return textVal;
},
setInput(node,value){ // 给 文本框赋值 node -- input元素 value--input值
node.value = value;
},
setText(node, text){
node.textContent = text;
}
}
// 判断是否是指令
isDirective(name){
// startsWith --》 es6新属性,判断是否以v-开头。
return name.startsWith('v-');
}
compileEleNode(node){ // 替换v-model中内容 属性中带v-xx 的元素 例如:<input v-model="school.aa"/>
/* var reg = /v-model\s*=\s*\"(.+?)\"/g;
var ss = 'v-model="2222" ssss v-model="2222" '
ss.match(reg); */
// node.attributes 获取节点的属性
// Array.from或[...xx]方式可以将 伪数组转化成数组
Array.from(node.attributes).forEach(item =>{
let {name,value} = item; // item 对象, item中有name和value属性。在这只我只需要name和value的属性。
// 所以用了解构赋值。name:"v-model" value:"school.name"
if(this.isDirective(name)){ // 判断是否以v-开头,如果以v-开头,证明是自定义的属性(不是html自己的属性)
let [direct, funName] = name.split('-'); // 拆分 v-model, split拆分 拆分成 direct 为 v funName 为 model
// model/html Compiler.CompilerTypeUtil.model(this.vm, value, node);
Compiler.CompilerTypeUtil[funName](this.vm, value, node);
}
})
}
compileEleTextNode(node){ // 替换{{}}中内容
/* var reg = /\{\{(.+?)\}\}/g;
var ss = '{{11}}dddd{{6666}}'
ss.match(reg); */
var reg = /\{\{(.+?)\}\}/g;
let str = node.textContent; // 获取文本节点中的内容
if(reg.test(str)){
// 每返回一个值就会替换{{}}的位置
let textVal = str.replace(reg,(...args)=>{
/* args[1] -- school.aa 或 school.bb
str 为 {{school.aa}}{{school.bb}}
reg -- /\{\{(.+?)\}\}/g
node div(文本元素)
*/
// 因为repace是相当于循环替换,文本内容 为 {{school.aa}}{{school.bb}} replace中就会循环两次,
// 分别将 school.aa 和 school.bb 加入观察者模式,当值发生变化时触发
new Watch(this.vm, args[1], () =>{
this.regroupReg(this.vm, str, reg, node);
});
// 分别获取school.aa 和 school.bb 对应的值
let textValue = Compiler.CompilerTypeUtil.text(this.vm, args[1]);
// 返回 并替换正则匹配到的内容 -- 这是replace函数的特性
return textValue;
})
// 给文本赋值
Compiler.CompilerTypeUtil.setText(node, textVal);
}
}
/*
一个div中有可能会存在两个{{}}, 例如{{a}}{{b}}当a发生变化。
需要把a和b重新都替换一下。然后重新用node.textContent是改变整个div文本中的值。
*/
regroupReg(vm, str, reg, node){
let textVal = str.replace(reg,(...args)=>{
let reqChild = args[1]; // school.aa 或 school.bb
let textValue = Compiler.CompilerTypeUtil.text(vm, reqChild);
Compiler.CompilerTypeUtil.setValue(reqChild, vm, textValue);
return textValue;
})
// 给文本赋值
Compiler.CompilerTypeUtil.setText(node, textVal);
}
compile(node){ // 用来编译内存中的dom节点 -- node 是 fragment
let childNodes = node.childNodes //获取node的儿子元素
Array.from(childNodes).forEach(ele =>{ // Array.from(childNodes)或者[...childNodes]把类数组转化成数组 遍历
if(ele.childNodes && ele.childNodes.length > 0){ // 查看有没有儿子元素。如果有继续递归
this.compile(ele);
}
if(this.isElementNode(ele)){ // 验证是否元素节点
// 编译元素节点
this.compileEleNode(ele); // 递归编译元素节点
}else if(this.isTextNode(ele)){ // 验证是不是文本节点
// 编译文本节点
this.compileEleTextNode(ele); // 递归编译文本节点
}
});
}
// 把节点移动到内存中
node2fragment(node){
/*
创建一个文档碎片 可以对它做添加,删除,更改 节点的操作。
一般就是在文档碎片上添加完了 以后,在添加到html中这样可避免回流之类。
*/
let fragment = document.createDocumentFragment();
let firstChild;
// node 就是 document.querySelect('#app')
/*
while(firstChild = node.firstChild) 每次获取#app第一个元素(儿子节点)把这个节点放到文档碎片中
appendChild具有移动性: 每次执行完 fragment.appendChild(firstChild); 后,#app中的元素就会 少一个,
再次用node.firstChild实际拿到的是原来#app中的第二个元素,以此类推,
最后#app中的元素就被拿没有了。都在文档碎片(fragment)中了。
*/
while(firstChild = node.firstChild){
// appendChild具有移动性
fragment.appendChild(firstChild);
}
return fragment;
}
isElementNode(node){ //是不是元素节点
return node.nodeType === 1;
}
isTextNode(node){ //是不是文本节点
return node.nodeType === 3;
}
}
说完编译模板,我们再说下数据拦截:
// 数据拦截
class Observal{
constructor(vm){
this.vm = vm;
this.data = vm.$data;
this.setData(this.data);
}
setData(data){ // 给vm中的data遍历。增加get和set
let me = this;
if(data && typeof data == 'object'){ // data如果是对象或者是数组
for(let key in data){
dep = new Dep(); // 给每一个属性 都加上一个发布订阅功能(每个变量都有自己的dep)
let val = data[key];
Object.defineProperty(data, key, {
set(newVal){
// 当set的值得时候
if(val != newVal){
val = newVal;
me.setData(val);
dep.updateWatcher('$data'); // 属性值发生变化,就触发订阅dep中的方法
}
},
get(){
// 当得到值得时候,将属于变量的watch对象添加到dep中
if(Dep.target){
dep.sub.push(Dep.target);
}
return val;
}
});
this.setData(val);
}
}
}
}
说数据拦截后,会发现编译模板的时候的会有new Watch(), 数据拦截中的get中有dep.sub.push(xx); 这个xx其实就是new Watch()这个就是把订阅者追加到发布订阅中。监听模板上涉及到的属性。只要值发生变化就触发dep中的updateWatcher函数
完整代码:
/*
发布订阅 -- 观察者模式
当监听的值发生变化,观察模式就触发开关。让模板自动编译
*/
let dep = null;
// 发布者
class Dep{
constructor(){
this.sub = [];
}
// 订阅
addSub(watch){
this.sub.push(watch);
}
// 发布
// 值发生改变就要触发这个函数,让所有的watch都触发
updateWatcher(type){
this.sub.forEach(watch =>{
watch.nodify(type);
})
}
}
// 订阅者
class Watch{
constructor(vm, modelValue, callBack){
this.vm = vm;
this.modelValue = modelValue;
this.oldValue = this.getValue('$data');
this.fn = callBack;
}
getValue(type){
Dep.target = this;
let data = null;
if(type == '$data'){
data = this.vm.$data;
}else{
data = this.vm;
}
let val = this.modelValue.split('.').reduce((lastObj, current) =>{
debugger;
return lastObj[current];
}, data);
Dep.target = null;
return val;
}
nodify(type){
let newVal = this.getValue(type);
if(newVal != this.oldValue){
this.fn(newVal);
}
}
}
// 数据拦截
class Observal{
constructor(vm){
this.vm = vm;
this.data = vm.$data;
this.setData(this.data);
}
setData(data){ // 给vm中的data遍历。增加get和set
let me = this;
if(data && typeof data == 'object'){ // data如果是对象或者是数组
for(let key in data){
dep = new Dep(); // 给每一个属性 都加上一个发布订阅功能(每个变量都有自己的dep)
let val = data[key];
Object.defineProperty(data, key, {
set(newVal){
// 当set的值得时候
if(val != newVal){
val = newVal;
me.setData(val);
dep.updateWatcher('$data'); // 属性值发生变化,就触发订阅dep中的方法
}
},
get(){
// 当得到值得时候,将属于变量的watch对象添加到dep中
debugger;
if(Dep.target){
dep.sub.push(Dep.target);
}
return val;
}
});
this.setData(val);
}
}
}
}
// 编译模板(模板编译)
class Compiler{
constructor(el,vm){ // el 值 #app vm 就是 new Mvvm()
this.vm = vm; // 将vm和el 绑定到Compiler实例上是为了方便调用没有其他的意思
// 判断el属性 是不是一个元素 如果不是元素 那就获取它 如果是元素 则直接用
this.el = this.isElementNode(el) ? el : document.querySelector(el);
// 把当前节点中的元素 获取到 放到内存中 (避免每次回流和重绘)
let fragment =this.node2fragment(this.el);
/*
fragment中的存放的就是原来#app中的内容
编译模板 -- 把节点中的内容进行替换
*/
this.compile(fragment);
// 把内容再塞到#app中,这用#app中都是替换后的内容了
this.el.appendChild(fragment);
}
// Compiler中的静态变量
static CompilerTypeUtil = {
getValue(vm, value){ // data vue中的data集合 value 为 v-model中的值 v-model="school.aa"
let data = vm.$data;
let val = value.split('.').reduce((lastObj,currentKey) =>{
return lastObj[currentKey];
},data);
return val;
},
// 给$data中数据和vm中的数据赋值 modelValue:school.a.b value:更新的值 obj: this.$data或vm
setValueSecond(obj, modelValue, value){
let arr = modelValue.split('.');
// index当前索引 arr为遍历的数组 currentKey当前元素 obj上一次返回的值
arr.forEach((currentKey,index) =>{
// school.a.b -- > 证明遍历到b了。这时候直接赋值
if( index >= arr.length -1 ){
obj[currentKey] = value;
}else{
obj = obj[currentKey];
}
})
},
// 用来更新$data中数据和vm中的数据
setValue(modelValue, vm, value){
let obj = vm.$data
this.setValueSecond(obj, modelValue, value);
this.setValueSecond(vm, modelValue, value);
},
// 给输入框的value赋值
// compileObj -- Compiler实例, value -- v-model值, node -- 指定节点
// modelValue:"school.aa" node: <input v-model="school.aa"/>
model(vm, modelValue, node){ // value 值 school.aa
// 这里一会重点说下 // 给school.aa增加观察者,如果稍后数据更新会触发回调方法,会拿新的输入框赋值
new Watch(vm, modelValue, (newVal) => {
// newVal -- 从 $data.school.aa 的值(更改后的值)
this.setInput(node, newVal); // setInput 就是 给input的value属性 赋值
/*
这的setValue是什么意思呢? 是既给$data.school.aa更新值。又给vm.school.aa更新值。
这么写主要是为了vm.xx的值改了可以更新到$data.xx上、反之$data.xx的值改了可以更新到vm.xx上
*/
this.setValue(modelValue, vm, newVal);
});
// 在v-model的input框中 绑定Inputs事件。用来更新$data中数据 input绑定input事件。每次改变以后,动态更新$data中的数据
node.addEventListener('input', (e)=>{
let value = e.target.value;
this.setValue(modelValue, vm, value);
})
// 第一次编译的时候走这里,之后数据更新会走到watch中的回调中。就不会走到这里了
let val = this.getValue(vm, modelValue);
this.setInput(node, val);
},
// 给v-html赋值
html(){ // node.html
},
// 给文本赋值
text(vm, value){
let textVal = this.getValue(vm, value);
return textVal;
},
setInput(node,value){ // 给 文本框赋值 node -- input元素 value--input值
node.value = value;
},
setText(node, text){
node.textContent = text;
}
}
// 判断是否是指令
isDirective(name){
// startsWith --》 es6新属性,判断是否以v-开头。
return name.startsWith('v-');
}
compileEleNode(node){ // 替换v-model中内容 属性中带v-xx 的元素 例如:<input v-model="school.aa"/>
/* var reg = /v-model\s*=\s*\"(.+?)\"/g;
var ss = 'v-model="2222" ssss v-model="2222" '
ss.match(reg); */
// node.attributes 获取节点的属性
// Array.from或[...xx]方式可以将 伪数组转化成数组
Array.from(node.attributes).forEach(item =>{
let {name,value} = item; // item 对象, item中有name和value属性。在这只我只需要name和value的属性。
// 所以用了解构赋值。name:"v-model" value:"school.name"
if(this.isDirective(name)){ // 判断是否以v-开头,如果以v-开头,证明是自定义的属性(不是html自己的属性)
let [direct, funName] = name.split('-'); // 拆分 v-model, split拆分 拆分成 direct 为 v funName 为 model
// model/html Compiler.CompilerTypeUtil.model(this.vm, value, node);
Compiler.CompilerTypeUtil[funName](this.vm, value, node);
}
})
}
compileEleTextNode(node){ // 替换{{}}中内容
/* var reg = /\{\{(.+?)\}\}/g;
var ss = '{{11}}dddd{{6666}}'
ss.match(reg); */
var reg = /\{\{(.+?)\}\}/g;
let str = node.textContent; // 获取文本节点中的内容
if(reg.test(str)){
// 每返回一个值就会替换{{}}的位置
let textVal = str.replace(reg,(...args)=>{
/* args[1] -- school.aa 或 school.bb
str 为 {{school.aa}}{{school.bb}}
reg -- /\{\{(.+?)\}\}/g
node div(文本元素)
*/
// 因为repace是相当于循环替换,文本内容 为 {{school.aa}}{{school.bb}} replace中就会循环两次,
// 分别将 school.aa 和 school.bb 加入观察者模式,当值发生变化时触发
new Watch(this.vm, args[1], () =>{
this.regroupReg(this.vm, str, reg, node);
});
// 分别获取school.aa 和 school.bb 对应的值
let textValue = Compiler.CompilerTypeUtil.text(this.vm, args[1]);
// 返回 并替换正则匹配到的内容 -- 这是replace函数的特性
return textValue;
})
// 给文本赋值
Compiler.CompilerTypeUtil.setText(node, textVal);
}
}
/*
一个div中有可能会存在两个{{}}, 例如{{a}}{{b}}当a发生变化。
需要把a和b重新都替换一下。然后重新用node.textContent是改变整个div文本中的值。
*/
regroupReg(vm, str, reg, node){
let textVal = str.replace(reg,(...args)=>{
let reqChild = args[1]; // school.aa 或 school.bb
let textValue = Compiler.CompilerTypeUtil.text(vm, reqChild);
Compiler.CompilerTypeUtil.setValue(reqChild, vm, textValue);
return textValue;
})
// 给文本赋值
Compiler.CompilerTypeUtil.setText(node, textVal);
}
compile(node){ // 用来编译内存中的dom节点 -- node 是 fragment
let childNodes = node.childNodes //获取node的儿子元素
Array.from(childNodes).forEach(ele =>{ // Array.from(childNodes)或者[...childNodes]把类数组转化成数组 遍历
if(ele.childNodes && ele.childNodes.length > 0){ // 查看有没有儿子元素。如果有继续递归
this.compile(ele);
}
if(this.isElementNode(ele)){ // 验证是否元素节点
// 编译元素节点
this.compileEleNode(ele); // 递归编译元素节点
}else if(this.isTextNode(ele)){ // 验证是不是文本节点
// 编译文本节点
this.compileEleTextNode(ele); // 递归编译文本节点
}
});
}
// 把节点移动到内存中
node2fragment(node){
/*
创建一个文档碎片 可以对它做添加,删除,更改 节点的操作。
一般就是在文档碎片上添加完了 以后,在添加到html中这样可避免回流之类。
*/
let fragment = document.createDocumentFragment();
let firstChild;
// node 就是 document.querySelect('#app')
/*
while(firstChild = node.firstChild) 每次获取#app第一个元素(儿子节点)把这个节点放到文档碎片中
appendChild具有移动性: 每次执行完 fragment.appendChild(firstChild); 后,#app中的元素就会 少一个,
再次用node.firstChild实际拿到的是原来#app中的第二个元素,以此类推,
最后#app中的元素就被拿没有了。都在文档碎片(fragment)中了。
*/
while(firstChild = node.firstChild){
// appendChild具有移动性
fragment.appendChild(firstChild);
}
return fragment;
}
isElementNode(node){ //是不是元素节点
return node.nodeType === 1;
}
isTextNode(node){ //是不是文本节点
return node.nodeType === 3;
}
}
// 基础类
export class Mvvm{
constructor(option){
this.$el = option.el;
this.$data = option.data;
// 这个根元素 存在
if(this.$el){
// 数据劫持,给所有的增加defineProperty -- set,get
new Observal(this);
// 将$data中的数据绑定到vm实例上一份
this.proxVM(this, this.$data);
// 编译模板
new Compiler(this.$el, this);
}
}
proxVM(scope, data){
if(data && typeof data == "object"){
for(let key in data){
let val = data[key];
Object.defineProperty(scope, key, {
get(){
return val;
},
set(newVal){
if(val != newVal){
val = newVal;
dep.updateWatcher('vm');
}
}
});
}
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="school.name">
<input type="text" v-model="a">
{{a}}
<div>
{{school.aa}}{{school.bb}}
<span>
{{school.bb}}
</span>
</div>
</div>
<script type="module">
import {Mvvm} from './mvvm.js'
window.vm = new Mvvm({
el:'#app',
data:{
school:{
name:'zz',
aa: 22,
bb: 33
},
a:22
},
computed:{
getName(){
return this.school.name + '呵呵';
}
}
})
</script>
</html>