冲vue2底层,数据响应式原理
一. 数据响应式的一些简单介绍
vue的官方的数据响应式图:

vue的MVVM模式
数据变化,视图也会变化

数据模型 《通过viewmodel桥梁来进行相连》 视图
笼统的说是通过Object.defineProperty()来进行数据劫持/数据代理。
二. Object.defineProperty()方法
Object.defineProperty()方法可以设置一些额外隐藏的属性。
let obj = {}
//为obj对象设置a属性的值
Object.defineProperty(obj, "a", {
value: 3,
writable: false //不可重写
})
obj.a++
console.log(obj.a); //3
getter和setter需要用一个全局的临时变量来周转才能工作
let obj = {}
// Object.defineProperty(obj, "a", {
// // value: 3,
// // writable: false //不可重写
// })
// obj.a++
let temp //getter和setter需要用一个全局的临时变量来周转才能工作
Object.defineProperty(obj, "a", {
// getter,在获取obj.a时会触发get
get() {
return temp
console.log("你正在获取obj对象的a属性");
// return newValue
},
// setter,设置obj.a的值时会触发set
set(newValue) {
temp = newValue
console.log("你正在试图改变a属性");
}
})
obj.a = 0
console.log(obj.a);
封装一个具有数据拦截的reactive响应式函数
// 封装一个响应式函数,使用闭包环境的val来代替设置一个全局临时变量temp
function defineReactive(data, key, val) {
if (arguments.length == 2) {
val = data[key]
}
// data为数据对象,key-value为键值对
Object.defineProperty(data, key, {
enumerable: true, //可枚举
configurable: true, //可配置
// getter,在获取obj.a时会触发get
get() {
console.log(`你正在获取obj对象的${key}属性`);
return val
},
// setter,设置obj.a的值时会触发set
set(newValue) {
if (newValue == val) {
return
}
val = newValue
console.log(`你正在试图改变${key}属性`);
}
})
}
let obj = {}
defineReactive(obj, "a", 10) //设置obj对象的a属性,且属性a初始值为10
obj.a = 1
console.log(obj.a); //1
三. 封装一个Observer类
Observer观察者类的作用就是将一个正常的object对象转换为每个层级的属性都是响应式的对象。

写一个observe函数用于观察传递来的data对象。
首先上来就observe(obj),看obj上是否有__ob__,第一次肯定没有啊,所以就直接new Oberserver创建实例,然后会将实例添加到__ob__上,然后会在Observer类里面遍历每个属性,然后逐个defineReactive,然后当设置某个属性值的时候,会触发set,set里面的newValue也会被Observe一下,这样就能够实现递归监听对象数据了。
先说下要实现响应式所需要编写的几个函数和类以及它们的关系。
observe.js : 其作用就是在里面创建Observer实例来实现data数据响应式。
Observer.js:此类的作用就是将一个正常的object对象进行遍历(使用walk进行遍历value的每一个key),转化为每个层级的属性都是响应式(能够被侦测)的对象
defineReactive.js: 其是一个封装好的响应式函数,使用闭包环境的val来代替设置一个全局临时变量。其作用就是通过Object.defineProperty来进行拦截数据,并且在set中,会进行observe观测新的newValue即observe(newValue)
所以它们的关系依赖就是:observe.js => Observe.js => defineReactive.js => observe.js,是个循环。

observe.js:
// 创建并导出一个observe的函数,其作用就是起到观察data数据
import Observer from "./Observer"
export default function observe(val) {
if (typeof val != "object") { //如果value不是对象,那么什么也不用做
return
}
// 下面开始递归监听了
let ob
if (typeof val.__ob__ != "undefined") {
ob = val.__ob__
} else {
ob = new Observer(val)
}
return ob
}
Observe.js
import { def } from "./util";
import { defineReactive } from "./defineReactive";
// 其作用就是将一个正常的object对象转化为每个层级的属性都是响应式(能够被侦测)的对象
export default class Observer {
constructor(value) {
// 为传进来的value对象配置__ob__属性,值为当前new的实例this,并且__ob__不可枚举
def(value, "__ob__", this, false)
// 其实这个__ob__就是一个当前传入value的Observer实例
console.log("我是Observer构造器", value);
// 不要忘记初心!Observer类的目的就是:将一个正常的object对象转化为每个层级的属性都是响应式(能够被侦测)的对象
this.walk(value)
}
// walk的作用就是遍历value对象的每一个key,把每一个key都设置为reactive响应式数据
walk(value) {
for (let key in value) {
defineReactive(value, key)
}
}
}
// 封装一个响应式函数,使用闭包环境的val来代替设置一个全局临时变量
import observe from "./observe";
export function defineReactive(data, key, val) {
console.log("我是defineReactive", key);
if (arguments.length == 2) {
val = data[key]
}
// 子元素要进行observe,至此形成了递归。这个函数不是函数自己调用自己,而是多个函数、类循环调用
let childOb = observe(val)
// data为数据对象,key-value为键值对
Object.defineProperty(data, key, {
enumerable: true, //可枚举
configurable: true, //可配置
// getter,在获取obj.a时会触发get
get() {
console.log(`你正在获取obj对象的${key}属性`, val);
return val
},
// setter,设置obj.a的值时会触发set
set(newValue) {
if (newValue == val) {
return
}
val = newValue
// 当设置了新值,这个新值被observe
childOb = observe(newValue)
console.log(`你正在试图改变${key}属性`, val);
}
})
}
// let obj = {}
// Object.defineProperty(obj, "a", {
// // value: 3,
// // writable: false //不可重写
// })
// obj.a++
// let temp //getter和setter需要用一个全局的临时变量来周转才能工作
// Object.defineProperty(data, "a", {
// // getter,在获取obj.a时会触发get
// get() {
// return temp
// console.log("你正在获取obj对象的a属性");
// // return newValue
// },
// // setter,设置obj.a的值时会触发set
// set(newValue) {
// temp = newValue
// console.log("你正在试图改变a属性");
// }
// })
import { defineReactive } from "./defineReactive" //引入响应式函数
import Observer from "./Observer" //引入观察者类
import observe from "./observe" //引入观察函数,在此函数中实例化Observer类,将传入的Observer的参数val变为reactive响应式数据
// defineReactive(obj, "a", 10) //设置obj对象的a属性,且属性a初始值为10
// obj.a = 1
// console.log(obj.a); //1
let obj1 = {
a: {
m: {
c: 1
}
},
b: 3
}
observe(obj1)
obj1.b = 1
console.log(obj1.a.m.c);
四. 重写数组的7个方法对改变的数组元素进行侦听
vue2为什么要重写这几个方法:Vue2的响应式是通过Object.defineProperty()来实现的,但是这个api的一大缺点就是不能够监听到数组长度的变化,也就没办法监听数组的新增。
另外一点,Vue无法侦听通过数组索引来修改数组元素的操作,这一点不是Object.defineProperty的原因,而是尤大认为性能消耗与带来的用户体验不成正比。事实上源码中当判断此元素为数组时是单独对数组进行处理了即observeArray(arr),如果同样对数组执行walk(arr)遍历数组的每一项是它称为reactive响应式数据,这样就可以监听到数组通过索引来修改数组数据了。但是对数组中每一项进行监听非常耗性能,因为数组项可能很大,比如1000000条。所以尤大就没这样做。
ok,回到正题。这个7个方法分别是push、pop、unshift、shift、splice、sort、reverse,这就是说你执行了数组的七个方法之后,除了得到方法执行的结果外,还需对改变的元素进行监听,基本实现思路就是对Array数组原型上重新修改这几个方法的功能,除了正常执行这几个方法的操作外,还要对改变的数组元素进行侦听。

工具工具util.js:
// 此函数是为obj对象下的某个属性配置一些属性
export const def = function (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: enumerable,//可枚举性
writable: true,
configurable: true
})
}
重写数组:arr.js:
// 此文件是实现重写Array数组的7个方法
import { def } from "./util"
// 得到Array.protoType
const arrPrototype = Array.prototype
// 以Array.prototype为原型创建arrMethods对象,并暴露出去
export const arrayMethods = Object.create(arrPrototype)
// 要被改写的7个数组方法
const methodsNeedChange = [
"push",
"pop",
"unshift",
"shift",
"splice",
"sort",
"reverse"
]
// rowArrayMethod.forEach(item => {
// item = Array.prototype[item]
// })
// alert(1)
// 将Array.prototype上的array的这7个方法备份,并分别赋给methodsNeedChange数组中的对应方法,在此基础上再拓展监听数组改变元素的功能
methodsNeedChange.forEach(methodName => {
// 备份原来的方法
const original = arrPrototype[methodName]
// 定义新的方法,这里是对数组的方法重写,在其完成原先的功能外,还需要对改变的数组元素进行监听
def(arrayMethods, methodName, function () {
// 相当于arr.original(arguments),先让其执行原先的功能
let res = original.apply(this, arguments) //需要修改此函数的执行上下文,当调用original时上下文中的this本应该是数组实例,而直接执行original(),其调用者为全局对象window
// 把这个数组身上的__ob__取出来,__ob__已经被添加了,
// 为什么被添加了?比如obj.g是一个数组,这是因为在遍历obj这个对象时,
// 已经给g属性添加了__ob__属性了
let ob = this.__ob__
// 有三种方法push、unshift、splice能够插入新的数据
// 则现在要把插入的新的数据也要observe一下
let inserted = [] //插入的数据
switch (methodName) {
case "push":
inserted = arguments
break;
case "unshift":
inserted = arguments
break
case "splice":
// splice(下标, 0, 插入的新项)
inserted = Array.from(arguments).slice(2) //要从第二项进行切割
break
}
// 判断有没有要插入的新项,让新的项也变为reactive响应式数据
if (inserted) {
ob.observeArray(inserted)
}
console.log(this);
console.log("我监听到数组变化了",); //再对其做其他的监听元素操作
return res //将数组执行某个方法的返回值返回,比如pop时会返回删除的数据
}, false)
})
Observer.js:
import { def } from "./util";
import { defineReactive } from "./defineReactive";
import { arrayMethods } from "./arr";
import observe from "./observe";
// 其作用就是将一个正常的object对象转化为每个层级的属性都是响应式(能够被侦测)的对象
export default class Observer {
constructor(value) {
// 为传进来的value对象配置__ob__属性,
// 值为当前new的实例this,并且__ob__不可枚举
def(value, "__ob__", this, false)
// 其实这个__ob__就是一个当前传入value的Observer实例
console.log("我是Observer构造器", value);
// 不要忘记初心!Observer类的目的就是:将一个正常的object对象转化为每个层级的属性都是响应式(能够被侦测)的对象
// 检查value是数组还是对象
if (Array.isArray(value)) {//value为数组时
//如果是数组,要非常强行的蛮干,让此数组的原型为我们指定的arrMethods
Object.setPrototypeOf(value, arrayMethods) //将value的原型指向arrayMethods
// value.__proto__ = arrayMethods //或者这样,等同于上面代码setPrototypeOf
this.observeArray(value)//遍历监听数组的所有元素
} else { //value为对象
this.walk(value)
}
}
// walk的作用就是遍历value对象的每一个key,把每一个key都设置为reactive响应式数据
walk(value) {
for (let key in value) {
defineReactive(value, key)
}
}
// 为遍历监听数组元素,专门写的方法
observeArray(arr) {
// 这里l=arr.length主要是为了防止数组长度在遍历中发生改变
for (let i = 0, l = arr.length; i < l; i++) {
// 逐项对他们进行observe
observe(arr[i])
}
}
}
五. 依赖收集
依赖是指用到数据的地方,称为依赖。
vue1.x中为细粒度依赖,也就是用到数据的dom都是依赖。
vue2.x中为中等粒度依赖,意思是用到数据的组件式依赖。
在getter中收集依赖,在setter中触发依赖。
Dep类和Watcher类
把依赖收集的代码封装成一个Dep类,它专门用来管理依赖,每个Observer的实例,成员中都有一个Dep的实例。
回忆一下,Observer的作用就是将data对象变成reactive响应式数据,这里的Observer其实就是为data对象包括data对象中的每一个属性的属性值为复杂数据类型的属性都会递归的创建一个Observer实例。
data: {
a: "a",
b: {
c: "c"
}
}
比如以上代码,就会创建2个Observer实例。分别是Observer(data)和Observer(b),因为a为简单数据类型,对a进行监听后就直接return返回了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ngb4oOxD-1649236127713)(C:/Users/wang_He/AppData/Roaming/Typora/typora-user-images/image-20220327211943896.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-grhyOeEg-1649236127713)(C:/Users/wang_He/AppData/Roaming/Typora/typora-user-images/image-20220327212026477.png)]
Watcher是一个中介,数据发生变化是通过Watcher中转,通知组件。
- 依赖就是Watcher,只有Watcher触发的getters才会收集依赖,那个Watcher触发了setter,就把所有的Watcher都通知一遍。
- Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。
- 代码实现的巧妙之处:Watcher把自己设置到全局的一个指定位置,然后读取数据,因为读取了数据,所以会触发这个数据的getter。在getter中就能得到当前正在读取数据的Watcher,并把这个Wacher收集导Dep中。
看完以上文字是不是已经蒙了,别慌,我们从代码中领悟它的意思。看懂代码后再回来看上面的文字总结就清晰一点了。
我理解的watcher就相当于是组件中的插值表达式{{data}},当该watcher读取了data,data中的每一个key都会有一个dep来收集自己的依赖,data的dep就会把这个watcher推入到订阅自己的订阅者数组subs中了;然后当修改了了data中的数据,此时该dep就会逐个通知订阅自己的watcher进行getAndInvoke执行自己的回调函数。
dep.js:
var uid = 0 //这里也是可以通过构造器的闭包拿到
// Dep用于记录每一个Observer的依赖
export default class Dep {
constructor() {
console.log("我是dep类的构造器");
this.id = uid++ //每一个Dep都有自己的一个id
// 这个数组里面放的是watcher的实例
this.subs = [] //subscribe订阅。用数组来存储自己的订阅者
}
// 添加订阅,向订阅数组中添加自己的订阅者
addSub(sub) {
this.subs.push(sub)
}
// 添加依赖
depend() {
// Dep.target就是我们指定的全局的位置,用window.target也行,
// 只要是全局唯一,没有歧义就行
if (Dep.target) {//Dep.target其实是一个订阅者
// getter函数当中就会在全局唯一的地方读取正在读取数据的watcher,并把这个watcher再收集到dep中
this.addSub(Dep.target)//也就是说当前哪个watcher触发的getter,然后再收集依赖
}
}
// 通知更新
notify() {
console.log("我是notify");
const subs = this.subs.slice() //浅克隆一份
// 提醒更新所有的订阅者
for (let i = 0; i < subs.length; i++) {
subs[i].update()
}
}
}
watcher.js:
// watcher是调用watch函数产生的,它会保存每一次要watch一个东西的回调函数
// 在数组中要保存所有watcher的Dep
import Dep from "./Dep";
var uid = 0 //身份证标识
export default class Watcher {
// 你要监听的target对象的expression表达式,然后观察到它时触发回调函数callback
constructor(target, expression, callback) {
console.log("我是watcher构造类");
this.id = uid++ //每一个watcher都有一个身份证标识
this.target = target
// 这里的getter获得器是用于解析对象最后的数据
this.getter = parsePath(expression)
this.callback = callback
this.value = this.get() //在这里收集依赖,也就是为全局的Dep.target赋值
}
// 通知更新
update() {
this.run() //数据一旦被set更新,就启动run方法跑起来启动更新
}
// 跑起来进行更新
run() {
// 得到并且唤起
this.getAndInvoke(this.callback)
}
getAndInvoke(cb) {
let value = this.get()
if (value != this.value || typeof value == "object") {
let oldValue = this.value
this.value = value
// 当监听到某个对象发生改变后会触发回调,并将新值老值作为参数传入
cb.call(this.target, value, oldValue)
}
}
get() {
// 进入依赖收集阶段
// 让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集阶段
Dep.target = this //此时Dep.target被赋值
const obj = this.target //接收传进来的要被监听的对象
let value
try {
// 这里就是为了获取对象obj中的expression的值
// 比如: obj: {a: {b:0}},expression为obj.a则返回值就是{b: 0}
value = this.getter(obj)
} finally {
//退出依赖收集阶段,当前watcher把依赖收集的资格让给其他watcher
// 哪个watcher在读getter,哪个watcher现在就是Dep.target
Dep.target = null
}
return value
}
}
// 此函数返回一个函数,返回的函数用于将一个层级.对象解析到最后一层返回数值
function parsePath(str) {
let segments = str.split(".")
console.log(segments);
return (obj) => {
for (let i = 0; i < segments.length; i++) {
obj = obj[segments[i]] //一层一层缩小
}
return obj
}
}
Observer.js:
import { def } from "./util";
import { defineReactive } from "./defineReactive";
import { arrayMethods } from "./arr";
import observe from "./observe";
import Dep from "./Dep";
// 其作用就是将一个正常的object对象转化为每个层级的属性都是响应式(能够被侦测)的对象
export default class Observer {
constructor(value) {
this.dep = new Dep() //每一个Observer的实例都有一个Dep
// 为传进来的value对象配置__ob__属性,
// 值为当前new的实例this,并且__ob__不可枚举
def(value, "__ob__", this, false)
// 其实这个__ob__就是一个当前传入value的Observer实例
console.log("我是Observer构造器", value);
// 不要忘记初心!Observer类的目的就是:将一个正常的object对象转化为每个层级的属性都是响应式(能够被侦测)的对象
// 检查value是数组还是对象
if (Array.isArray(value)) {//value为数组时
//如果是数组,要非常强行的蛮干,让此数组的原型为我们指定的arrMethods
Object.setPrototypeOf(value, arrayMethods) //将value的原型指向arrayMethods
// value.__proto__ = arrayMethods //或者这样,等同于上面代码setPrototypeOf
this.observeArray(value)//遍历监听数组的所有元素
} else { //value为对象
this.walk(value)
}
}
// walk的作用就是遍历value对象的每一个key,把每一个key都设置为reactive响应式数据
walk(value) {
for (let key in value) {
defineReactive(value, key)
}
}
// 为遍历监听数组元素,专门写的方法
observeArray(arr) {
// 这里l=arr.length主要是为了防止数组长度在遍历中发生改变
for (let i = 0, l = arr.length; i < l; i++) {
// 逐项对他们进行observe
observe(arr[i])
}
}
}
defineReactivec.js
// 封装一个响应式函数,使用闭包环境的val来代替设置一个全局临时变量
import observe from "./observe";
import Dep from "./Dep";
export function defineReactive(data, key, val) {
const dep = new Dep()//对象的每一个key都需要有一个自己的dep,来收集依赖
console.log("我是defineReactive", key);
if (arguments.length == 2) {
val = data[key]
}
// 子元素要进行observe,至此形成了递归。这个函数不是函数自己调用自己,而是多个函数、类循环调用
let childOb = observe(val)
// data为数据对象,key-value为键值对
Object.defineProperty(data, key, {
enumerable: true, //可枚举
configurable: true, //可配置
// getter,在获取obj.a时会触发get
get() { //在getter中收集依赖
console.log(`你正在获取obj对象的${key}属性`, val);
// 如果现在处于依赖收集阶段
console.log(Dep.target, dep);
if (Dep.target) {
dep.depend() //添加依赖
if (childOb) { //如果存在子元素,则让其子元素也添加依赖
childOb.dep.depend()
}
}
return val
},
// setter,设置obj.a的值时会触发set
set(newValue) {//在setter中触发依赖
if (newValue == val) {
return
}
val = newValue
// 当设置了新值,这个新值被observe
childOb = observe(newValue)
console.log(`你正在试图改变${key}属性`, val);
// 发布订阅模式,通知dep
dep.notify() //每一次set修改数据你就要通知dep
}
})
}
- 一个data属性对应一个Dep,一个Dep对应n个Watcher(属性多次在模板中被使用时n>1:{{a}}/v-text=‘a’)
- 一个表达式对应一个Watcher, 一个Watcher对应n个Dep(多层表达式时n>1:a.b.c)
六. 总结(便于理解!)
首先使用defineProperty可以拦截data对象中的数据,在组件中若使用到这些data数据时,会第一次调用此属性的getter,获取到它的值,同时此属性会创建一个dep来收集依赖watcher(相当于是一个组件),然后将数据第一次渲染在页面中。
那还为什么还需要在setter更改数据时notify通知他的依赖进行更新呢,这是因为此时组件中的数据已经被渲染出来了,他不会再次因为数据被setter更改而带来组件中页面上的数据更改,所以此时就需要通知所有使用此数据的watcher进行重新渲染视图(dep.notify()的过程)。

本文深入剖析Vue2中数据响应式的实现机制,包括利用Object.defineProperty进行数据劫持、自定义Observer类实现对象属性的监听、重写数组方法确保数组变动可被跟踪,以及依赖收集与更新通知机制。
373

被折叠的 条评论
为什么被折叠?



