一.MVVM(数据驱动视图):Model-View-ViewModel
数据驱动的传统方式
利用DOM操作,来更新视图
更新后的方式:利用vm=new Vue()来进行数据的初始化和视图的更新,依旧利用了DOM操作,但是是封装在vm=new Vue()中的,不需要我们自己来应用,是其内部应用的
把数据劫持在一个模拟Vue对象中,一旦数据发生改变,不直接更改,先被拦截
利用Object.defineProperty(目标对象,属性,描述)函数来进行数据劫持
主要靠的是这个函数中封装的两个方法get()和set()
get() { console.log("get", data.msg); }
set(newValue) { console.log("set", newValue); if (newValue === data.msg) { return; } data.msg = newValue; }
通过封装一个数据劫持的函数,函数中包括遍历整个对象的属性值,以及对其进行的数据劫持、数据更新,以及数据驱动视图操作
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <div id="app"> </div> </head> <body> <script src="js/vue.js"></script> <script> const data={ name:"aa", age:18 }; let vm={} function handleData(data){ Object.keys(data).forEach((key)=> { Object.defineProperty(vm,key,{ get(){ console.log('get',data[key]); }, set(newValue){ console.log('set',newValue); if(newValue===data[key]) { return } data[key]=newValue;//数据更新 //数据驱动视图 document.querySelector("#app").textContent=document.querySelector("#app").textContent+" "+data[key]; } }) }); } handleData(data) console.log(vm.name);//get vm.name="bb";//set console.log(vm.age);//get vm.age=100; </script> </body> </html>
例如:
const data = { name: "aa", age: 18, friend: { gender: 0, name1: "bb", }, };//friend是对象
方法:定义响应式数据,对每个属性进行数据劫持,利用递归,遍历来实现数据劫持的操作
Vue3.0版本进一步优化了数据劫持的方案:
不用去思考内部的属性,直接代理整个对象
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <div id="app">{{msg}}</div> </head> <body> <script src="js/vue.js"></script> <script> const data = { name:'aaa', age:18, friend:{ gender:"女", name:'bbb' } }; let vm = new Proxy(data,{ get(target,key){ //target代表目标对象,不能是具体的对象 console.log('get',target[key]); return target[key] }, set(target,key,value){ console.log('set',value); if(value===target[key]) { return } target[key]=value; } }) console.log(vm.name); vm.age=100; </script> </body> </html>
利用 o n 和 on和 on和emit来分别注册事件和触发事件。
e m i t 用 来 触 发 , 而 emit用来触发,而 emit用来触发,而on用来订阅
let vm=new Vue();//都是同一个,无法拆分成三个角色 //注册事件 //参数:事件类型,事件处理函数 vm.$on('change',()=>{ console.log('event1'); }) vm.$on('change',()=>{ console.log('event2'); }) vm.$emit('change')//事件类型,事件处理
思路:
发布( e m i t ) 者 调 用 事 件 中 心 ( 触 发 事 件 ) , 然 后 订 阅 ( emit)者调用事件中心(触发事件),然后订阅( emit)者调用事件中心(触发事件),然后订阅(on)者把任务注册到事件中心,进行订阅(注册事件)[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rVHNMUnD-1627296233557)(Vue2.x Vue3.0 源码解析.assets/image-20210716182850218.png)]
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <div id="app"></div> </head> <body> <script src="js/vue.js"></script> <script> //同一事件类型,可以有多个处理函数 class Vue1{ constructor(){ this.subs=Object.create(null)//比this.subs={}要好,代表没有属性,为空 //容器就是来存储eventType和handler的关系的 //{eventType1:[fun1,fun2..],eventType2:[],} } $on(eventType,handler){//注册事件,订阅,将数据注册到handler this.subs[eventType]=this.subs[eventType]||[] this.subs[eventType].push(handler) } $emit(eventType){//触发事件,不是注册事件,不能像上面一样,进行初始化,而是应该进行判断 if(this.subs[eventType]) { this.subs[eventType].forEach(fun => { fun()//每个eventType对应的不只有一个函数,当触发这个事件时,它包括的所有函数都会同时触发 }); } } } let vm=new Vue1(); //注册事件 //参数:事件类型,事件处理函数 vm.$on('change',()=>{ console.log('event1'); }) vm.$on('change',()=>{ console.log('event2'); }) vm.$emit('change')//事件类型,事件处理 </script> </body> </html>
与发布订阅模式相比,发布者与订阅者(观察者)联系更加紧密,不再单独有事件中心。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X6plTuJN-1627296233559)(Vue2.x Vue3.0 源码解析.assets/image-20210716183445822.png)]
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script src="js/vue.js"></script> <script> class Publisher{ constructor(){ this.subs=[] } //添加订阅者 addSub(sub){ if(sub&&sub.update()){ this.subs.push(sub) } } //通知订阅者 notify(){ this.subs.forEach(w=>{ w.update() }) } } class Watcher{ //获取发布者的通知信息 update(){ console.log('update'); } } const p=new Publisher() const w1=new Watcher() const w2=new Watcher() p.addSub(w1) p.addSub(w2) </script> </body> </html>
通过数据劫持、模板编译等方式进行Vue源码的编译
//compiler.js // 模板相关的操作 // dom :el // data class Compiler{ constructor(vm){ this.el = vm.$el this.data = vm.$data this.compile(this.el) } // 编译 compile(el){ let childNodes = el.childNodes // 遍历 和 递归 Array.from(childNodes).forEach(node=>{ // 代码分割 console.log('this'); if(this.isTextNode(node)){ // 文本处理 console.log(this); compileText(node) }else if(this.isElementNode(node)){ //元素处理 this.compileElement(node) } if(node.childNodes){ this.compile(node) // 递归 } }) } // 编译文本 插值类型处理{{msg}} : 比如 hello world {{msg}} => hello world vue compileText(node){ console.log(this); let value = node.textContent //内容 let reg = /\{\{(.+?)\}\}/ // 正则规则 if(reg.test(value)){ // 获取 : 插值表达式的变量名 let k = RegExp.$1.trim() console.log(k); // 替换 node.textContent = value.replace(reg,this.data[k]) //数据变化 - 通知订阅 -update - cb -compiler dom操作 new Watcher(this.data,k,(newVlaue)=>{ console.log('compiler--callback'); node.textContent = newVlaue console.log('view update'); }) } } // 编译元素 compileElement(node){ // 获取属性 let attributes = node.attributes // 遍历所有属性 Array.from(attributes).forEach(attr=>{ // v- : v-text -> text 获取vue指令 console.log(attr); let attrName = attr.name // 属性名 // 是否是指令 v- if(this.isDirective(attrName)){ // 获取属性名里面 v-后面部分 --指令名称 attrName = attrName.substr(2) console.log(attrName); // 属性值 - data (key) let key = attr.value // text - 映射方法 ,不同指令 不同处理方式 就是不同函数 this.update(node,attrName,key) } }) } // 调用指令函数 更新dom 内容 update(node,attrname,key){ // if(attrname=='text'){ // this.textUpdate(node,attrname,key) // } // if(attrname=='model'){ // this.modelUpdate(node,attrname,key) // } let fn = this[attrname + 'Update'] fn && fn.call(this,node,attrname,key) } // 代码分割 textUpdate(node,attrname,key){ node.textContent = this.data[key] //数据变化 - 通知订阅 -update - cb -compiler dom操作 new Watcher(this.data,key,(newVlaue)=>{ node.textContent = newVlaue }) } modelUpdate(node,attrname,key){ // 注意 value node.value =this.data[key] new Watcher(this.data,key,(newVlaue)=>{ node.value = newVlaue }) node.addEventListener('input',()=>{ this.data[key] =node.value }) } // 节点判断相关 isTextNode(node){ return node.nodeType === 3 } isElementNode(node){ return node.nodeType === 1 } isAttrNode(node){ return node.nodeType === 2 } isDirective(attrName){ return attrName.startsWith('v-') } // js dom // jquery dom // template view data 没有状态跟踪 // mvvm : view data 状态同步 数据驱动 效率极高; 虚拟DOM js {1} {2} }
//Vue.js class Vue{ constructor(options){ this.$options = options this.$data = options.data this.$el = typeof options.el ==='string'? document.querySelector(options.el) : options.el // 注入到Vue实例上; this._proxyData(this.$data) // vm.$data => 响应式 new Observer(this.$data) // 模板编译 : 解析指定 插值表达式.. new Compiler(this) } // data => 数据劫持 =>注入到Vue实例上; _proxyData(data){ // 遍历key Object.keys(data).forEach(key=>{ Object.defineProperty(this,key,{ get(){ return data[key] }, set(nValue){ if( data[key]===nValue){ return } data[key] = nValue } }) }) } }
//Observer.js class Observer{ constructor(data){ this.walk(data) } // 核心遍历 walk(data){ if(!data || typeof data !=='object'){ return } Object.keys(data).forEach(key=>{ this.defineReactive(data,key,data[key]) }) } // 定义响应式数据 defineReactive(data,key,value){ let publisher = new Publisher() let that = this // console.log(data[key]); // Maximum call stack size exceeded at Object // this.walk(data[key]) this.walk(value) Object.defineProperty(data,key,{ get(){ // 收集依赖 添加观察者 Publisher.target&&publisher.addSub(Publisher.target) return value }, set(nValue){ if( value===nValue){ return } // this 指向data // console.log('this',this); value = nValue // 属性赋值是一个对象 that.walk(nValue) console.log('set--notify()'); // 通知依赖 // 发送通知:数据变化 => 观察者 update() =>template dom({{ss}}=>value) =>views publisher.notify() } }) } }
//index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> hello world {{msg}} <h2 > sss{{msg}} <h3>hello {{msg}}</h3> </h2> <h2 v-text="msg"></h2> <input type="text" v-model='msg'> </div> <script src="./js/publisher.js"></script> <script src="./js/watcher.js"></script> <script src="./js/compiler.js"></script> <script src="./js/observer.js"></script> <script src="./js/vue.js"></script> <script> let vm = new Vue({ el:'#app', data:{ msg:'hello vue1', friend:{ name:'zhangsan', age:28 } } }) </script> </body> </html>
//publisher.js class Publisher{ constructor(){ this.subs = [] } // 添加订阅者 addSub(sub){ if(sub && sub.update){ this.subs.push(sub) } } // 通知订阅者 notify(){ console.log('publisher---notify()'); this.subs.forEach(w=>{ w.update() // 约定 }) } } // class Watcher{ // // 获取发布者的通知信息 // update(){ // console.log('update'); // } // }
//watcher.js class Watcher{ constructor(data,key,cb){ this.data = data this.key = key this.cb = cb Publisher.target = this this.oldValue = data[key] } // 获取发布者的通知信息 update(){ console.log('watcher --- update'); let newValue=this.data[this.key] if(this.oldValue===newValue){ return } // 调用模板更新DOM this.cb(newValue) } }