先问自己两个问题: 1.app.message修改数据的时候,Vue内部是如何监听message数据发生改变的 使用Object.defineProperty ->监听对象属性的改变 2.当数据发生改变后,Vue是如何知道 通知哪些 '人',界面发生刷新呢 (张三,李四,王五 发布订阅者模式 <div id='app'> {{ message }} //张三 {{ message }} //李四 {{ message }} //王五 这里假设他们分别对应这三个message {{ name }} </div> <script src='./node_modules/vue/dist/vue.js'></script> <script> const app = new Vue({ el:'#app', data:{//注意!这个是对象 message:'哈哈哈', name:'kobe ' } }) </script>
第一步肯定是创建Vue实例。 注意这里的data是 !对象
可以理解为我们把 这个obj对象传入Vue,Vue内部拿到的就是一个
const obj = { message:'哈哈哈', name:'kobe ' }
拿到这个对象后,Vue先用Object.keys(obj)拿到一个包含obj对象的所有属性(message,name)的数组,然后进行forEach遍历,拿到每一个属性对应的value值
Object.keys(obj).forEach(key => { let value = obj[key]
再进行‘数据劫持’ Object.defineProperty字面意思就是 给 obj对象重新 定义 属性。因为obj对象内的属性不容易监听
1 Object.defineProperty(obj, key, { 2 set(newValue){ 3 console.log('监听' + key + '改变' ) !!!注意这里的监听 4 value = newValue 7 }, 8 get(){ 9 console.log('获取'+ key +'对应的值') 14 return value 15 } 16 }) 17 }) 此时我们在控制台给message重新赋值 app.message = '老詹' 就会触发set方法,打印出: '监听message改变' '老詹' 直接app.message则触发get
当我们设置或者访问对象的属性的时候,都会触发相应的函数,然后在这个函数里进行打印/返回/或者设置属性的值
既然如此,我们当然可以在触发函数的时候动一些手脚做点我们自己想做的事情,这也就是“劫持”操作。
------------------------------------------------------------------------------------------------------------
既然set内监听到数据发生了变化
set(newValue){ console.log('监听' + key + '改变' ) !!!注意这里的监听 value = newValue },
那么监听到值改变后,告诉谁?谁在用呢?(记不记得一开始的 张三,李四,王五。让我们进行‘拟人’,更好理解)
谁在用其实是解析HTML代码,获取到哪些人有用到我们的属性
{{ message }} //张三 {{ message }} //李四 {{ message }} //王五 哎!!获取!获取!那么它肯定会调用一次message的get,那我就知道是张三,李四,王五你们在用这个message属性那么到时候!一旦message属性的值发生变化set,那我再去通知你们三个。 ---即发布者订阅者模式
class Dep{ //Dep 即 Depdency 依赖 存储所有对我这个属性有依赖的 constructor(){ this.subs = [] //用来记录现在是谁要订阅我们的属性的 subs 即subscribe订阅 } }const dep = new Dep() //这个Dep对象就可以用subs这个数组去记录所有的订阅者 (就是刚刚的张三,李四,王五啊)
那我怎么知道所有的订阅者在哪里呢 ,定义一个addSub方法,之后往里面传入sub形参,代表即将要传入进来的订阅者
class Dep { constructor(){ // 这个数组是用来记录现在是谁要订阅我们的属性的 this.subs = [] } addSub(){ //定义一个addSub方法,之后往里面传入sub形参,代表即将要传入进来的订阅者 为了拿到订阅者,我们得再创建一个类 class Watcher } }
为了拿到订阅者,我们得再创建一个类 class Watcher
class Dep { constructor(){ // 这个数组是用来记录现在是谁要订阅我们的属性的 this.subs = [] } addSub(){ } } // 监听观察 class Watcher{//订阅者 constructor(name){ this.name = name; } update(){ console.log(this.name + '发送update') //update 你细想,通知到张三,李四的时候,是不是需要他们自己更新一下,把界面更新一下 } }到时候我们创建一个watcher 实例,就可以实例化出来张三 w1对象 李四w2 对象, !!就可以把这些w1,w2实例对象放进dep实例的addSub内
addSub(watcher){ !形参
this.subs.push(watcher)
}
之后,谁用message属性了,我们就赶紧创建一个w1shiliduix
const w1 = new Watcher('张三') //意味着张三使用了一次
就把w1传进 dep.addSub(w1)
const obj = { message:'哈哈哈', name:'kobe ' } Object.keys(obj).forEach(key => { let value = obj[key] Object.defineProperty(obj, key, { set(newValue){ console.log('监听' + key + '改变' ) // 监听到值改变后告诉谁?谁在用呢? // 解析HTML代码,获取到哪些人有用我们的属性 (获取一次--谁用-谁就调用一次get) value = newValue // dep.notify()//通知 !!!如果有一天,值发生改变了,我们拿到这个dep实例对象,调用notify }, get(){ console.log('获取'+ key +'对应的值') // 张三 get ->通知到就需要自己 update一下 // 李四 get -> update // 王五 get -> update return value } }) }) // 发布订阅者模式 Dependency subscribe订阅 class Dep {//发布者 // 存储所有对我这个属性有依赖的 constructor(){ // 这个数组是用来记录现在是谁要订阅我们的属性的 this.subs = [] } addSub(watcher){ this.subs.push(watche r) } 再定义一个notify方法 notify(){ this.subs.forEach( item => { item.update() //拿到我们的subs,遍历找到里面所有的订阅者,让他去调用自己的update }) } } // 监听观察 class Watcher{//订阅者 constructor(name){ this.name = name; } update(){ console.log(this.name + '发送update') } } const dep = new Dep() const w1 = new Watcher('张三') dep.addSub(w1) const w2 = new Watcher('李四') dep.addSub(w2) const w3 = new Watcher('王五') dep.addSub(w3)
-------
最终代码一览
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <!-- 1.app.message修改数据,vue内部是如何监听message数据发生改变 Object.defineProperty ->监听对象属性的改变 2.当数据发生改变,Vue是如何知道通知哪些人,界面发生刷新呢 发布订阅者模式 --> <div id='app'> {{ mess age }} //张三 {{ message }} //李四 {{ message }} //王五 {{ name }} </div> <script> const obj = { message:'哈哈哈', name:'kobe ' } Object.keys(obj).forEach(key => { let value = obj[key] Object.defineProperty(obj, key, { set(newValue){ console.log('监听' + key + '改变' ) // 监听到值改变后告诉谁?谁在用呢? // 解析HTML代码,获取到哪些人有用我们的属性 (获取一次--谁用-谁就调用一次get) value = newValue // dep.notify()//通知 }, get(){ console.log('获取'+ key +'对应的值') // 张三 get ->通知到就需要自己 update一下 // 李四 get -> update // 王五 get -> update return value } }) }) // 发布订阅者模式 Dependency subscribe订阅 class Dep {//发布者 // 存储所有对我这个属性有依赖的 constructor(){ // 这个数组是用来记录现在是谁要订阅我们的属性的 this.subs = [] } addSub(watcher){ this.subs.push(watche r) } notify(){ this.subs.forEach( item => { item.update() }) } } // 监听观察 class Watcher{//订阅者 constructor(name){ this.name = name; } update(){ console.log(this.name + '发送update') } } const dep = new Dep() const w1 = new Watcher('张三') dep.addSub(w1) const w2 = new Watcher('李四') dep.addSub(w2) const w3 = new Watcher('王五') dep.addSub(w3) dep.notify() </script> <script src='./node_modules/vue/dist/vue.js'></script> <script> const app = new Vue({ el:'#app', data:{ message:'哈哈哈', name:'kobe ' } }) </script> </body> </html>
以下是copy来的,更为干练
数据双向绑定作为 Vue 核心功能之一,Vue 则采用的是数据劫持与发布订阅相结合的方式实现双向绑定。
其中数据劫持是利用了 Object.defineProperty() 方法重新定义了对象获取属性值get和设置属性值set的操作来实现的;
劫持了数据之后,我们就需要一个监听器 Observer 来监听属性的变化。得知属性发生变化之后我们需要一个 Watcher 订阅者来更新视图,我们还需要一个 compile 指令解析器,用于解析我们的节点元素的指令与初始化视图。
- Observer 监听器:用来监听属性的变化通知订阅者
- Watcher 订阅者:收到属性的变化,然后更新视图(这个过程中我们可能会有很多个订阅者 Watcher 所以我们要创建一个容器 Dep 去做一个统一的管理)
- Compile 解析器:解析指令,初始化模版,绑定订阅者
当我们访问或设置对象的属性的时候,都会触发相对应的函数,然后在这个函数里返回或设置属性的值。既然如此,我们当然可以在触发函数的时候动一些手脚做点我们自己想做的事情,这也就是“劫持”操作。
在Vue中其实就是通过Object.defineProperty
来劫持对象属性的setter
和getter
操作,并“种下”一个监听器,当数据发生变化的时候发出通知。
注意: 该方法每次只能设置一个属性,那么就需要遍历对象来完成其属性的配置:
Object.keys(student).forEach(key => defineReactive(student, key))
另外还必须是一个具体的属性,这也非常的致命。假如后续需要扩展该对象,那么就必须手动为新属性设置 setter 和 getter 方法,这就是为什么不在 data 中声明的属性无法自动拥有双向绑定效果的原因 。这时需要调用 Vue.set()
手动设置。
针对 Array 类型的劫持
数组是一种特殊的对象,其下标实际上就是对象的属性,所以理论上是可以采用 Object.defineProperty()
方法处理数组对象。
但是 Vue 并没有采用上述方法劫持数组对象,原因分析:1、特殊的 length 属性,相比较对象的属性,数组下标变化地相对频繁,并且改变数组长度的方法也比较灵活,一旦数组的长度发生变化,那么在无法自动感知的情况下,开发者只能手动更新新增的数组下标,这可是一个很繁琐的工作。2、数组主要的操作场景还是遍历,而对于每一个元素都挂载一个 get 和 set 方法,恐怕也是不小的性能负担。
数组方法的劫持:最终 Vue 选择劫持一些常用的数组操作方法,从而知晓数组的变化情况:push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice'
。数组方法的劫持涉及到原型相关的知识,首先数组实例大部分方法都是来源于 Array.prototype
对象。顺便提一下,采用 Vue.set() 方法设置数组元素时,Vue 内部实际上是调用劫持后的 splice() 方法来触发更新。
总结:由上述内容可知,Vue 中的数据劫持分为两大部分:
针对 Object 类型,采用 Object.defineProperty()
方法劫持属性的读取和设置方法;
针对 Array 类型,采用原型相关的知识劫持常用的函数,从而知晓当前数组发生变化。
并且 Object.defineProperty()
方法存在以下缺陷:每次只能设置一个具体的属性,导致需要遍历对象来设置属性,同时也导致了无法探测新增属性;属性描述符 configurable 对其的影响是致命的。
*Object.keys(obj)
参数:要返回其枚举自身属性的对象
返回值:一个表示给定对象的所有可枚举属性的字符串数组