- Vue2.x 使用了Object.defineProperty,对 数据的所有属性添加getter和setter方法
- Vue3.x 使用了Proxy
vue源码解析:深入vue源码,了解vue的双向数据绑定原理
1、 Vue2.x响应式原理
所谓的双向绑定,就是view的变化能反映到ViewModel上,ViewModel的变化能同步到view上
原理:用Object.defineproperty() 重新定义属性的setter方法和getter方法来实现的
vue的数据双向绑定是通过数据劫持和发布-订阅者功能来实现的
(1)原理(主要掌握这一条):
- 当你把一个普通的 JavaScript 对象传入 Vue 实例作为
data
选项,Vue 将遍历此对象所有的 属性,并使用 Object.defineProperty 重新定义属性的setter方法和getter方法。(Object.defineProperty
是 ES5 中的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因)。 - 这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更。
- 每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖(getter触发,进行依赖收集)。
- 之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染re-render(即重新渲染 修改属性所关联的组件),生成虚拟DOM树。
注意:
- watcher中存放的是组件实例,每个组件实例对应一个watcher;
- 依赖:组件渲染过程中,所接触过的数据属性。
- 当依赖中的属性变化时,会通知watcher,重新渲染该变化属性所关联的组件
- Watcher是订阅类
vue的数据双向绑定是通过数据劫持和发布-订阅者功能来实现的,实现步骤:
1.实现一个监听者Oberver来劫持并监听所有的属性,一旦有属性发生变化就通知订阅者
2.实现一个订阅者watcher来接受属性变化的通知并执行相应的方法,从而更新视图
3.实现一个解析器compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相对应的订阅者
(2)举例:
- 如下图所示,new Vue一个实例对象a,其中有一个属性a.b;
- 在实例化的过程中,通过Object.defineProperty()会对a.b添加getter和setter;
- 同时Vue.js会对模板做编译(即组件),解析生成一个指令对象(这里是v-text指令),每个指令对象(即组件实例)都会关联一个Watcher;
- 当对a.b求值的时候,就会触发它的getter;
- 当修改a.b的值的时候,就会触发它的setter,同时会通知被关联的Watcher;
- 然后Watcher就会通知到指令,调用指令的update()方法;
- 由于指令是对DOM的封装,所以就会调用DOM的原生方法去更新视图,这样就完成了数据改变到视图更新的一个自动过程;
(3)源码解析:
1)Vue在初始化时调用方法initData,该方法会拿到用户传入的data数据;
2)通过new Observer对数据进行观测,如果是数组,则改写数组的原型方法,解决不能直接利用数组索引 设置或修改 一个数组项的问题;如果数据是对象类型,在Observer中调用this.walk(value)进行对象的处理;
3)循环遍历data的所有属性,在walk中调用defineReactive对属性重新定义;
4)采用Object.defineProperty为属性添加getter和setter方法
5)对依赖中的属性执行setter方法时,通过notify方法,通知watcher,重新渲染watcher关联的组件。
总结:拦截属性的获取,进行依赖收集(组件渲染的过程中把“接触”过的数据)。拦截属性的更新操作,对相关依赖进行通知。
(4)参照代码解析
1)开发者通过选项定义普通js对象
2) 将每个成员转化为getter/setter形式
getter:访问数据时,自动执行该方法。例如:访问 obj.message
setter:修改数据时,自动执行该方法。例如:修改 obj.message = "hahaha"
3)模板最终被编译为一个render函数
4)getter触发,进行依赖收集
5)后续message一旦被修改,就通知之前收集的依赖进行更新
2、Object.defineProperty()方法
Object.defineProperty(obj, prop, descriptor) 有三个参数,分别为
- obj :要定义属性的对象,如obj
- prop :要定义或修改的属性,如 obj中的mesg属性
- descriptor :具体的改变方法,如getter和setter方法
data中有多个属性,使用forEach进行遍历
3、 vue3.x为什么要重写响应式(即vue2.x存在缺陷)?
(1)vue2.x 中obj.defineproperty的问题:
1)针对 对象obj中的每一个属性都调用了defineReactive,为每个属性重写getter和setter方法,影响性能。
2)单独对数组进行了处理,不能直接利用数组索引 设置或修改 一个数组项,即不能使arr[已有元素的数组下标] = val 变成响应式
例如:通过数组下标进行修改数据项时,vue2.x 设置失败而且不报错, 在vue3.x 中设置成功。如下所示,代码一模一样:
4、 vue3.x 使用proxy:避免了循环,解决了数组问题
- 当我们从一个组件的
data
函数中返回一个普通的 JavaScript 对象时,Vue 会将该对象包裹在一个带有get
和set
处理程序的 Proxy 中。 - Proxy对 对象中的所有元素进行监听和劫持。
- Proxy 是 ES6 仅有的特性,因此在Vue3.x版本中也使用了Object.defineProperty来支持IE浏览器。
(1)模拟vue3.x的proxy
访问和修改数组,都能成功操作
5、Vue 3.0 Composition API
(1)vue3中的ref(点击按钮时,实现nu++)
(2)vue3中的reactive
补充:
dep 实例在执行getter时,收集该 key 所有 watcher 实例;watcher 实例用来监听某个 key ,如果该 key 产生变化,便会执行 watcher 实例自身的回调
export class Dep {
constructor() {
this.subs = [];
}
// 将 watcher 实例置入队列
addSub(sub) {
this.subs.push(sub);
}
// 通知队列里的所有 watcher 实例,告知该 key 的 对应的 val 被改变
notify() {
this.subs.forEach((sub, index, arr) => sub.update());
}
}
// Dep 类的的某个静态属性,用于指向某个特定的 watcher 实例.
Dep.target = null
observer.js
import {Dep} from './dep'
function makeItReactive(obj, key, val) {
var dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
// 收集依赖! 如果该 key 被某个 watcher 实例依赖,则将该 watcher 实例置入该 key 对应的 dep 实例里
if(Dep.target){
dep.addSub(Dep.target)
}
return val
},
set: (newVal) => {
if (newVal === val) {
return;
}
else if (isObject(newVal)) {
new Observer(newVal);
val = newVal;
// 通知 dep 实例, 该 key 被 set,让 dep 实例向所有收集到的该 key 的 watcher 实例发送通知
dep.notify()
}
else if (!isObject(newVal)) {
val = newVal;
// 通知 dep 实例, 该 key 被 set,让 dep 实例向所有收集到的该 key 的 watcher 发送通知
dep.notify()
}
}
})
}