vue采用数据劫持结合发布者订阅者模式的方式,通过es5中Object.defineproperty()来劫持各个属性的setter、getter,在数据变动时发布消息给依赖收集器,去通知观察者,触发响应回调,去更新视图。
将以上的描述用以下的图来展示:
实现分析,具体步骤:
第一步:需要Observer的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter,这样的话,给这个对象的某个值赋值,就会出发setter,那么就能监听到了数据变化
第二步:Complie解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
第三步:watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
1.在自身实例化时往属性订阅器(Dep)里面添加自己
2.自身必须有一个update()方法
3.待属性变动Dep.notice()通知时,能调用自身的Update()方法,并触发compile中绑定的回调
第四步:MVVM作为数据绑定的入口,整合Observer、Compile、Watcher三者,通过Observer来监听自己的model数据变化,通过Complie来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的桥梁通信,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果
1.Compile
class Compile{ constructor(el,vm){ this.el = this.isElementNode(el) ? el : document.querySelector(el); this.vm = vm; // 1. 获取文档碎片对象,放入内存中会减少页面的回流和重绘 const fragment = this.node2Fragment(this.el) // 2. 编译模板 this.compile(fragment) // 3. 追加子元素到根元素 this.el.appendChild(fragment) } // 判断是不是元素节点,元素节点nodeType为1 isElementNode(node){ return node.nodeType === 1; } // 获取文档碎片对象 node2Fragment(el){ // 创建文档碎片对象 const f = document.createDocumentFragment() let firstChild; while(firstChild = el.firstChild){ f.appendChild(firstChild); } return f } // 编译模板:渲染页面元素 compile(fragment){ const childNodes = fragment.childNodes; [...childNodes].forEach( child =>{ if(this.isElementNode(child)){ // 编译元素节点 this.compileElement(child); }else{ // 编译文本节点 this.compileText(child); } // 如果节点有孩子节点则再次编译 if(child.childNodes && child.childNodes.length){ this.compile(child); } }) } // 编译元素节点 compileElement(node){ const attributes = node.attributes; [...attributes].forEach(attr => { const {name, value} = attr; if(this.isDirective(name)){ const [,directive] = name.split('-'); const [dirName, eventName] = directive.split(':'); // 更新视图 compileUtil[dirName](node, value, this.vm, eventName) // 删除有指令的标签上的属性 node.removeAttribute('v-'+ directive); if(directive === 'model'){ node.setAttribute('value',compileUtil.getVal(value, this.vm)); } }else if(this.isEventName(name)){ // 处理@click事件 let [,eventName] = name.split('@') compileUtil['on'](node, value, this.vm, eventName) // 删除有指令的标签上的@事件 node.removeAttribute('@'+ eventName); }else if(this.isAttr(name)){ // 处理:属性 let [,attrName] = name.split(':') compileUtil['bind'](node, value, this.vm, attrName) // 删除有指令的标签上的:属性 node.removeAttribute(':'+ attrName); } }); } // 编译文本节点 compileText(node){ const content = node.textContent; if(/\{\{(.+?)\}\}/.test(content)){ compileUtil['text'](node,content,this.vm) } } // 判断是不是v-开头指令 isDirective(attrName){ return attrName.startsWith('v-'); } // 判断是不是事件绑定简写@ isEventName(attrName){ return attrName.startsWith('@'); } // 判断是不是属性绑定: isAttr(attrName){ return attrName.startsWith(':'); } }
2.Compile工具集和Watcher
// 编译指令的方法集 const compileUtil = { // 获取value的值 getVal(exp,vm){ return exp.split('.').reduce((data,currentVal) => { return data[currentVal] },vm.$data) }, getContentVal(exp,vm){ return exp.replace(/\{\{(.+?)\}\}/g, (...args) => { return this.getVal(args[1],vm) }) }, setVal(exp,vm,inputVal){ return exp.split('.').reduce((data,currentVal) => { data[currentVal] = inputVal },vm.$data) }, // 处理v-text 和插值表达式{{}} text(node,exp,vm){ let value; // 对v-text和插值表达式做区分处理 if(exp.indexOf('{{') !== -1){ // ???????????? value = exp.replace(/\{\{(.+?)\}\}/g, (...args) => { // 绑定观察者,将来数据发生变化 触发这里的回调 进行更新 new Watcher(vm,args[1], () =>{ this.updater.textUpdate(node,this.getContentVal(exp,vm)) }) return this.getVal(args[1],vm) }) }else{ value = this.getVal(exp, vm) } this.updater.textUpdate(node,value) }, // 处理v-html html(node,exp,vm){ const value = this.getVal(exp, vm) // 添加数据观察者更新数据 //1. Complie和updater关联,添加数据观察者 new Watcher(vm,exp,(newVal) =>{ this.updater.htmlUpdate(node,newVal) }) // 初始话更新视图 this.updater.htmlUpdate(node,value) }, // 处理v-model 文本框 model(node,exp,vm){ const value = this.getVal(exp, vm) // 绑定更新函数,数据=》视图 new Watcher(vm,exp,(newVal) =>{ this.updater.modelUpdate(node,newVal) }) // 视图 =》数据=》视图 node.addEventListener('input', (e) =>{ // 设置值 this.setVal(exp,vm,e.target.value) }) this.updater.modelUpdate(node,value) }, // 处理事件绑定:v-bind和简写@ on(node,exp,vm,eventName){ let fn = vm.$options.methods && vm.$options.methods[exp] node.addEventListener(eventName,fn.bind(vm),false) }, // 处理事件绑定: v-bind和简写: bind(node,exp,vm,attrName){ const value = this.getVal(exp, vm) this.updater.attrUpdate(node,attrName,value) }, // 更新视图(没有抽离出去) updater:{ // text视图更新,设置text textUpdate(node,value){ node.textContent = value }, // html视图更新,设置innerHTML htmlUpdate(node,value){ node.innerHTML = value }, // model视图更新,设置文本框的value值 modelUpdate(node,value){ node.value = value }, // 属性值视图更新,设置元素的属性值 attrUpdate(node,attrName,value){ node.setAttribute(attrName,value) } } }
3.Observer
class Observer{ constructor(data){ this.observer(data) } observer(data){ if(data && typeof data === 'object'){ Object.keys(data).forEach( key => { this.defineReactive(data,key,data[key]) }) } } defineReactive(obj,key,value){ // 递归遍历 this.observer(value); // 实例化Dep const dep = new Dep() // 劫持并监听所有的属性 Object.defineProperty(obj,key,{ enumerable:true, configurable:false, // 初始化时(编译之前) get(){ // 订阅数据变化时,往dep中添加观察者 // 获取数据的时候,将Dep 和 Observer关联 Dep.target && dep.addSub(Dep.target); return value }, set:(newVal) => { this.observer(newVal) if(newVal !== value){ value = newVal } // 告诉Dep通知变化 dep.notify() } }) } }
4.Dep
class Dep{ constructor(){ this.subs = [] } // 添加观察者 addSub(watcher){ this.subs.push(watcher) } // 通知观察者去更新视图 notify(){ console.log('通知观察者',this.subs) this.subs.forEach( w => { // console.log(w.update) w.update() }) } }
5.Watcher
class Watcher{ constructor(vm,exp,cb){ this.vm = vm; this.exp = exp; this.cb = cb; // 先把旧值保存起来 this.oldVal = this.getOldVal() } getOldVal(){ // 在拿到旧数据之前将Watcher和Dep关联起来 Dep.target = this const oldVal = compileUtil.getVal(this.exp,this.vm) // 在拿到旧数据之后将观察者和dep关联取消,避免生成无数的watcher Dep.target = null return oldVal } update(){ const newVal = compileUtil.getVal(this.exp,this.vm) if(newVal !== this.oldVal){ this.cb(newVal) } } }
6.Mvue
class MVue{ constructor(options){ this.$el = options.el; this.$data = options.data; this.$options = options; if(this.$el){ // 实现数据观察者 new Observer(this.$data) // 实现指令解析器 new Compile(this.$el, this) // 代理 this.proxyData(this.$data) } } proxyData(data){ for(const key in data){ Object.defineProperty(this, key,{ get(){ return data[key] }, set(){ data[key] = newVal } }) } } }
7.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> <script src="./Mvue.js"></script> <script src="./Observer.js"></script> </head> <body> <div id="app"> <h2>{{person.name}}</h2> <h2>{{person.name}} -- {{person.age}}</h2> <ul> <li>{{msg}}</li> <li v-text='person.name'></li> <li v-text='msg'></li> <li v-html='htmlStr'>2</li> <li v-html='person.htmlStr'></li> </ul> <img v-bind:src="img" alt=" "> <img :src="img" alt=" "> <div> <a :href="aLink">百度</a> <a v-bind:href="aLink">百度</a> </div> <input v-model='msg' @click='clickFun' /> <button v-on:click='clickFun'>点击事件</button> </div> <script> let vm = new MVue({ el: '#app', data:{ person:{ name: 'hzz', age: 18, fav: 'code', htmlStr: '<h2>这是个html片段person</h2>', }, msg: '这是消息', htmlStr: '<h2>这是个html片段</h2>', img:'https://dss3.baidu.com/-rVXeDTa2gU2pMbgoY3K/it/u=3139850293,1719705775&fm=202&src=608&crossm&mola=new&crop=v1', aLink:'https://www.baidu.com' }, methods:{ clickFun(){ console.log(this) } } }) </script> </body> </html>
页面应用展示组成
1.Mvue.js文件包含(编译指令方法集、complie、Mvue)
2.observer.js文件中包含 (Watcher、Dep)
3.index.html