Vue.js是当前比较火的JavaScript MVVM(Model View ViewModel),数据,视图,视图模型)库,它跟以往的不同之处在于,之前更多的是手动操作Dom,现在是以数据为驱动,本篇文章通过对数据驱动
、数据响应式的核心原理
、发布订阅模式
、观察者模式
这几个知识点的学习,然后模拟vue响应式原理,实现一个简单小版本的Vue。
数据驱动 说到数据驱动,首先对数据响应式
、双向绑定
、数据驱动
这三个模块进行一个简单介绍
数据响应式 数据响应式中的数据指的是数据模型,数据模型仅仅是普通的JavaScript对象,数据响应式的核心指的是当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率。
双向绑定 数据改变,视图改变;视图改变,数据也随之改变;我们可以使用v-model在表单元素上创建双向数据绑定。
数据驱动 这个是Vue最独特的特性之一,让我们在开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图的。数据响应式的核心原理 Vue2.x Vue2.x的核心是通过Object.defineProperty
把数据转成getter/setter
让Vue能够追踪依赖,在数据被访问和修改时,可以进行进行数据劫持通知变更。
Vue2.x响应式原理
MDN-Object.defineProperty
Object.defineProperty
是 ES5 中一个无法 shim(在一个旧的环境中模拟出一个新 API ,而且仅靠旧环境中已有的手段实现,以便所有的浏览器具有相同的行为)
的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
Vue 官方对于 ie 浏览器版本兼容情况的描述是 ie9+,即是 ie9 及更高的版本。经过测试,Vue 的核心框架 vuejs本身,以及生态的官方核心插件(VueRouter、Vuex等)均可以在 ie9 上正常使用。解决方案是使用 babel-polyfill,它可以将 es6 的代码翻译成低版本浏览器可以识别的 es5 代码.
Object.defineProperty
基本使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > defineProperty</title > </head > <body > <div id ="app" > hello </div > <script > let data = { msg : 'Hello' } let vm = {} Object .defineProperty (vm, 'msg' , { enumerable : true , configurable : true , get ( ){ console .log ('get:' ,data.msg ) }, set (newValue ){ console .log ('set:' ,newValue) if (newValue == data.msg ){ return } data.msg = newValue document .querySelector ('#app' ).textContent = data.msg } }) </script > </body > </html >
在控制台通过vm.msg
获取值的时候,触发了get
的方法,通过vm.msg='xxx'
,触发了set
的方法,效果如下: 以上代码模拟的是对一个对象中一个属性msg
的转换getter/setter
,那么如果一个对象中多个属性需要转换getter/setter
如何处理呢? 思路如下:通过 Object.keys()
遍历,然后再转换成vm的setter/getter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > defineProperty</title > </head > <body > <div id ="app" > hello </div > <script > let data = { msg : 'Hello' , count : 100 } let vm = {} proxyData (data) function proxyData (data ){ Object .keys (data).forEach (key => { Object .defineProperty (vm, key, { enumerable : true , configurable : true , get ( ){ console .log ('get:' , key, data[key]) return data[key] }, set (newValue ){ console .log ('set:' , key, newValue) if (newValue === data[key]){ return } data[key] = newValue document .querySelector ('#app' ).textContent = data[key] } }) }) } </script > </body > </html >
Vue3.x Vue3.x核心的是通过Proxy
把数据转成getter/setter
让Vue能够追踪依赖。
MDN-Proxy
直接监听对象,并非属性
ES6 中新增,IE不支持,性能由浏览器优化(比Object.defineProperty好)
使用如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <div id ="app" > hello </div > <script > let data = { msg : 'hello' , count : 10 } let vm = new Proxy (data, { get (target, key ){ console .log ('set, key:' , key, target[key]) return target[key] }, set (target,key,newValue ){ console .log ('set,key:' , key, newValue) if (target[key] === newValue){ return } target[key] = newValue document .querySelector ("#app" ).textContent = newValue } }) </script > </body > </html >
发布订阅模式 & 观察者模式 发布订阅模式 通俗理解,假设存在一个“信号中心”
,某个任务执行完毕,就向信号中心“发布”
一个信号,其他任务可以向信号中心“订阅”
这个信号,从而知道什么时候自己可以开始执行,这就叫发布/订阅模式
。
在Vue中的事件机制和Node中的事件就是给予发布订阅模式的,下面兄弟组件通信过程为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <script> let eventHub = new Vue() ; addTodo: function () { eventHub.$emit('add -todo ', { text : this .newTodoText }) this.newTodoText = '' } created: function () { eventHub.$on('add -todo ',this .addTodo ) } </script>
Vue自定义事件的实现 页面中事件使用写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <script> // vue 自定义事件 let vm = new Vue() // vm内部的变量是一个对象的形式,对象的属性是事件的名称,对象的值是事件处理函数 // vm内部:{'click' :[fn1,fn2], 'change' :['fn3' ]} // 注册事件(订阅消息) vm.$on ('dataChange' , ()=>{ console.log('dataChange' ) }) vm.$on ('dataChange' , ()=>{ console.log('dataChange1' ) }) // 触发事件(发布消息) vm.$emit ('dataChange' ) // 当通过vm.$emit 触发事件的时候,就会去内部去找到对应的属性,然后去执行对应属性后面的函数 </script>
实现方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <script > class EventEmitter { constructor ( ){ this .subs = Object .create (null ) } $on(eventType, handler) { this .subs [eventType] = this .subs [eventType] || [] this .subs [eventType].push (handler) } $emit(eventType) { if (this .subs [eventType]){ this .subs [eventType].forEach (handler => { handler () }); } } } let em = new EventEmitter () em.$on('click' , ()=> { console .log ('click1' ) }) em.$on('click' , ()=> { console .log ('click2' ) }) em.$emit('click' ) </script >
打开浏览器在控制台中查看,打印出了click1
、click2
观察者模式
观察者模式(订阅者)–Watcher
目标(发布者)–Dep
subs数组:存储所有的观察者
addsub(): 添加观察者
notify(): 当事件发生,调用所有观察者的update方法
没有事件中心
简单实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <script > class Dep { constructor (){ this .subs = [] } addSub (sub ){ if (sub && sub.update ){ this .subs .push (sub) } } notify ( ){ this .subs .forEach (sub => { sub.update () }) } } class Watcher { update ( ){ consolel.log ('update' ) } } let dep = new Dep () let watcher = new Watcher () dep.addSub (watcher) dep.notify () </script >
总结:
观察者模式
是由具体目标调度,比如当前事件触发,Dep就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间存在依赖的
发布/订阅模式
由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在
模拟Vue响应式原理 前面的 数据响应式原理
、发布/订阅模式
、观察者模式
都了解之后,将前面的这些方法结合起来,然后模拟一个简单的Vue,在准备模拟开发一个Vue前,首页要对vue的整体结构有个认识,结构如下: 主要实现 Vue
、Observer
、Dep
、Watcher
、Compiler
五种,接着具体分析
Vue
负责接收初始化的参数(选项),把data中的成员注入到Vue实例
负责把data中的属性注入到Vue实例,转换成getter/setter
负责调用observer监听data中所有属性变化
负责调用compiler解析指令/差值表达式
创建一个minVue/index.html
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > mini Vue</title > </head > <body > <div id ="app" > <h1 > 差值表达式</h1 > <h3 > {{ msg }} </h3 > <h3 > {{ count }} </h3 > <h1 > v-text</h1 > <div v-text ="msg" > </div > <h1 > v-model</h1 > <input type ="text" v-model ="msg" > <input type ="text" v-model ="count" > </div > <script src ="./js/vue.js" > </script > <script > let vm = new Vue ({ el : "#app" , data :{ msg : 'hello' , count : 1000 } }) </script > </body > </html >
创建一个minVue/js/vue.js
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Vue { constructor (options){ this .$options = options || {} this .$data = options.data || {} this .$el = typeof options.el === 'string' ?document.querySelector(options.el):options.el this ._proxyData(this .$data ) } _proxyData(data ){ Object.keys(data ).forEach(key => { Object.defineProperty(this , key, { enumerable: true , configurable: true , get (){ return data [key] }, set (newValue){ if (newValue === data [key]){ return } data [key] = newValue } }) }); } }
Observer
负责把data选项中的属性转换成响应式数据进行监听
如果data中的某个属性是对象,把该属性转换成响应式数据
如有变化可拿到最新值并通知Dep
创建minVue/js/observer.js
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 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(obj, key, val ){ let that = this this .walk(val ) Object.defineProperty(obj, key, { enumerable: true , configurable: true , get (){ return val }, set (newValue){ if (newValue === val ){ return } val = newValue that.walk(newValue) } }) } }
在vue.js
文件中调用Observer
对象,监听数据变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Vue { constructor (options){ this .$options = options || {} this .$data = options.data || {} this .$el = typeof options.el === 'string' ?document.querySelector(options.el):options.el this ._proxyData(this .$data ) new Observer(this .$data ) } _proxyData(data ){ Object.keys(data ).forEach(key => { Object.defineProperty(this , key, { enumerable: true , configurable: true , get (){ return data [key] }, set (newValue){ if (newValue === data [key]){ return } data [key] = newValue } }) }); } }
Compiler
负责编译模版,解析指令/差值表达式
负责页面的首次渲染
当数据变化后重新渲染试图
创建一个minVue/js/compiler.js
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 class Compiler { constructor (vm ){ this .el = vm.$el this .vm = vm this .compile (this .el ) } compile (el ){ let childNodes = el.childNodes Array .from (childNodes).forEach (node => { if (this .isTextNode (node)){ this .compileText (node) }else if (this .isElementNode (node)){ this .compileElement (node) } if (node.childNodes && node.childNodes .length ){ this .compile (node) } }) } compileElement (node ){ console .log (node.attributes ) Array .from (node.attributes ).forEach (attr => { let attrName = attr.name if (this .isDirective (attrName)){ attrName = attrName.substr (2 ) let key = attr.value this .update (node, key, attrName) } }) } update (node, key, attrName ){ let updateFn = this [attrName + 'Updater' ] updateFn && updateFn (node, this .vm [key]) } textUpdater (node, value ){ node.textContent = value } modelUpdater (node, value ){ node.value = value } compileText (node ){ console .dir (node) let reg = /\{\{(.+?)\}\}/ let value = node.textContent if (reg.test (value)){ let key = RegExp .$1 .trim () node.textContent = value.replace (reg, this .vm [key]) } } isDirective (attrName ){ return attrName.startsWith ('v-' ) } isTextNode (node ){ return node.nodeType === 3 } isElementNode (node ){ return node.nodeType === 1 } }
在vue.js
文件中,第四步需要调用compiler
对象,解析指令和差值表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Vue { constructor (options){ this .$options = options || {} this .$data = options.data || {} this .$el = typeof options.el === 'string' ?document.querySelector(options.el):options.el this ._proxyData(this .$data ) new Observer(this .$data ) new Compiler(this ) } _proxyData(data ){ Object.keys(data ).forEach(key => { Object.defineProperty(this , key, { enumerable: true , configurable: true , get (){ return data [key] }, set (newValue){ if (newValue === data [key]){ return } data [key] = newValue } }) }); } }
Dep(Dependency)
收集依赖,添加观察者(watcher)
通知所有观察者 建minVue/js/dep.js
文件1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Dep { constructor () { // 存储所有的观察者 this.subs = [] } // 添加观察者 addSub(sub ) { if (sub && sub .update ) { this.subs.push(sub ) } } // 发送通知 notify (){ this.subs.forEach(sub=> { sub.update() }) } }
然后在js/observer.js
中收集依赖,发送通知1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 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(obj, key, val ){ let that = this let dep = new Dep() this .walk(val ) Object.defineProperty(obj, key, { enumerable: true , configurable: true , get (){ Dep.target && dep.addSub(Dep.target) return val }, set (newValue){ if (newValue === val ){ return } val = newValue that.walk(newValue) dep.notify() } }) } }
Watcher
当数据变化出发依赖,dep通知所有的Watcher实例更新视图
自身实例化的时候往dep对象中添加自己
建js/watcher.js
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Watcher { constructor (vm, key, cb ){ this .vm = vm this .key = key this .cb = cb Dep.target = this this .oldValue = vm[key] Dep.target = null } update(){ let newValue = this .vm[this .key] if (this .oldValue === newValue ){ return } this .cb(newValue) } }
oberser.js
文件中添加收集依赖,Dep.target && dep.addSub(Dep.target)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 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(obj, key, val ){ let that = this let dep = new Dep() this .walk(val ) Object.defineProperty(obj, key, { enumerable: true , configurable: true , get (){ Dep.target && dep.addSub(Dep.target) return val }, set (newValue){ if (newValue === val ){ return } val = newValue that.walk(newValue) dep.notify() } }) } }
回到Oberver.js
中,创建插值表达式时候创建watcher对象
,当数据改变更新视图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 compileText (node ){ console .dir (node) let reg = /\{\{(.+?)\}\}/ let value = node.textContent if (reg.test (value)){ let key = RegExp .$1 .trim () node.textContent = value.replace (reg, this .vm [key]) new Watcher (this .vm , key, (newValue )=> { node.textContent = newValue }) } }
在处理指令的方法compiler.js
文件中创建watcher对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 update (node, key, attrName) { let updateFn = this[attrName + 'Updater' ] updateFn && updateFn.call (this, node, this.vm [key] , key) } textUpdater (node, value, key) { node.textContent = value new Watcher (this.vm , key, (newValue)=>{ node.textContent = newValue }) } modelUpdater (node, value, key) { node.value = value new Watcher (this.vm , key, (newValue)=>{ node.value = newValue }) }
总结: