这几天在B站大学上跟着尚硅谷的老师学习Vue2的源码解析,在今天终于是把数据的响应式看完了,就做一个总结吧!
老师的视频连接:https://www.bilibili.com/video/BV1G54y1s7xV?p=6&vd_source=a81826692f4afea80764f4048dc1ae0a
我的代码地址:Vue2的数据响应式: 在B站大学跟着视频一点点手敲的
这个文章还参考了某金上叫争霸爱好者的作者的文章
-
Vue的数据响应式是什么?
当我们在使用Vue2时,对数据进行修改之后,视图就会相应的更新,这就是数据的响应式
-
那么应该怎么做才可以将数据变为响应式的呢
通过Object.defineProperty(),这个方法是干什么用的呢?在MDN中是这么定义的
语法:
obj:要定义属性的对象。
prop:要定义或修改的属性的名称或 Symbol 。
descriptor:要定义或修改的属性描述符。
尝试一下:
const obj = {}
Object.defineProperty(obj, 'a', {
value: 66
})
console.log(obj.a);// 打印出来66
此方法中有两个属性,用过这两个属性的函数我们可以做到对数据进行劫持:
-
什么是数据劫持?
举个栗子:
const obj = {}
let value = 1
Object.defineProperty(obj, 'a', {
get() {
console.log('你要访问a属性')
return value
},
set(newVal) {
if (newVal === value) return
console.log('你要更改a属性');
value = newVal
}
})
obj.a = 2
console.log(obj.a);
当我们修改obj.a时,就会触发set,当我们打印obj.a时,就会触发get,这样我们就可以侦测到对属性a访问和修改的操作,也叫数据劫持
由于上面的方法需要定义一个全局变量,所以可以将这个方法进行改进,封装成一个闭包:
const obj = {}
function defineReactive(data, key, value) {
// 进行改进,如果没有value,令value等于原值
if (arguments.length <= 2) {
value = data[key]
}
Object.defineProperty(data, key, {
// 可枚举
enumerable: true,
// 可被删除
configurable: true,
get() {
return value
},
set(newValue) {
if (newValue === value) return
value = newValue
}
})
}
defineReactive(obj, 'a',1)
console.log(obj);
每当我们从data数据中的key读取数据的时候,get()函数就会触发,对data数据的内容进行修改的时候,set()就会触发,他们会对value进行操作,因此形成了一个闭包
-
Observer类
功能:将每一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
const obj = {
a: 10,
b: 20,
c: 30
}
如果obj有多个属性的话,我们可以创建一个Observer类对obj进行遍历:
class Observer {
constructor(value) {
this.walk(value)
}
// 遍历
walk(value) {
for (let k in value) {
defineReactive(value, k)
}
}
}
const obj = {
a: 10,
b: 20,
c: 30
}
new Observer(obj)
现在我们的obj有多层属性的嵌套 :
const obj = {
a: {
m: {
n: 10
}
},
b: 20,
c: 30
}
所以我们需要对Observer类进行一些改进:
// 入口函数observe
// 现在的__ob__就是给遍历过的对象添加的标识(后面会用来存储东西)
function observe(value) {
// 如果value不是对象,就什么都不做
if (typeof value != 'object') return
var ob
if (typeof value.__ob__ !== 'undefined') {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
// Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
class Observer {
constructor(value) {
// 给实例(构造器中的this不是表示类本身,而是表示实例)添加__ob__属性,值是这次new的实例
this.def(value, '__ob__', this)
this.walk(value)
}
// 遍历
walk(value) {
for (let k in value) {
defineReactive(value, k)
}
}
// 定义obj对象key的属性
def(obj, key, value) {
Object.defineProperty(obj, key, {
value,
// 添加一些配置项
writable: true,
configurable: true
})
}
}
function defineReactive(data, key, value) {
// 进行改进,如果没有value,令value等于原值
if (arguments.length == 2) {
value = data[key]
}
// 子元素要进行observe,至此形成递归
let childOb = observe(value)
Object.defineProperty(data, key, {
// 可枚举
enumerable: true,
// 可被删除
configurable: true,
get() {
return value
},
set(newValue) {
if (newValue === value) return
value = newValue
// 当我们向obj中添加新的属性时,这个属性也有可能是一个对象
childOb = observe(newValue)
}
})
}
const obj = {
a: {
m: {
n: 10
}
},
b: 20,
c: 30
}
observe(obj)
我们通过三个函数的循环调用形成了一个递归,这样我们就将obj中的每一个属性都转换成了响应式的数据,并且为他们打上了__ob__的标记,可能有的同学看到递归会有些懵,可以通过下面的的流程图来帮助理解:
图片来自尚硅谷的老师!!!
-
数组的响应式处理
当我们通过数组的方法对属性进行操作时,上面的代码是不起作用的,在Vue中,尤雨溪对数组的七个方法进行了改写:
在Vue中,尤雨溪通过原型拦截的方法对着其中数组进行了改写:
- 将Observer类中的def方法抽离出来封装成utils.JS文件,避免Observer过于臃肿
// 遍历工具函数
export const def = function (obj, key, value) {
// defineProperty即给对象定义属性
Object.defineProperty(obj, key, {
value,
writable: true,
configurable: true
})
}
2. 对Observer类进行修改
// 引入def
import { def } from './utils'
// 引入数组方法
import { arrayMethods } from './array'
// Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
class Observer {
constructor(value) {
// 给实例(构造器中的this不是表示类本身,而是表示实例)添加__ob__属性,值是这次new的实例
def(value, '__ob__', this)
// 检查他是数组还是对象
if (Array.isArray(value)) {
// 如果是数组:将这个数组的原型指向arrayMethods
Object.setPrototypeOf(value, arrayMethods)
// 遍历数组
this.observeArray(value)
} else {
this.walk(value)
}
}
// 数组的特殊遍历
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
// 逐项进行递归
observe(arr[i])
}
}
// 遍历
walk(value) {
for (let k in value) {
defineReactive(value, k)
}
}
}
3. 封装转换数组方法的JS文件
// 引入def
import { def } from './utils'
// 得到Array.prototype
const arrayPrototype = Array.prototype
// 以Array.prototype为原型创建arrayMethods对象,将他导出
export const arrayMethods = Object.create(arrayPrototype)
// 要被改写的七个数组方法
const methodsNeedChange = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
// 遍历上述数组方法名
methodsNeedChange.forEach(methodsName => {
// 备份原来的方法(因为基本功能还是需要使用原来的数组方法,只是我们需要添加响应式的功能)
const original = arrayPrototype[methodsName]
// 将重写后的方法定义到arrayMethods对象上,函数就是重写后的方法
def(
arrayMethods,
methodsName,
function () {
// 要有数组原来方法的功能
const result = original.apply(this, arguments)
let args = Array.from(arguments)
// 在这个数组中,__ob__已经被添加过,可以将__ob__取出来
const ob = this.__ob__
// 由于push,unshift,splice可以往数组中添加项,所以也需要对这些项进行遍历(防止插入的项是对象)
let inserted = []
switch (methodsName) {
case 'push':
case 'unshift':
inserted = arguments
break
case 'splice':
// slice(2)从下标为二的项开始
// 因为splice格式(下标,数量,插入新的项)
inserted = args.slice(2)
break
}
// 判断有没有要插入的新项,如果有就让新项也变为响应式的
if (inserted.length > 0) {
ob.observeArray(inserted)
}
return result
}
)
})
至此,数组也被变成了响应式的数据
我们通过defineReactive
方法将数据进行响应式后,虽然可以监听到数据的变化了,那我们怎么处理通知视图更新呢?这就需要用到依赖
-
依赖
需要用到数据的地方,称为依赖,在Vue中,依赖就是Watcher,他的只要作用就是观察Vue中的属性,当属性更新时做出相应操作,也就是在实例化Watcher时传入的回调函数 :
在实例化Watcher类时我们传入三个参数(要监听的数据,表达式,回调函数),Watcher类我们放到下面定义,现在只需要知道回调函数是什么就好了
-
收集依赖与派发更新
在Vue中,依赖的收集与派发采用了发布-订阅模式 :
发布订阅模式是对象间的一种一对多的关系,由三部分组成 :
发布者(observe) : 增加了订阅者,并且在改变时通知了订阅者
订阅者(watcher) : 每一个使用到数据的地方,所以每一个使用到数据的地方都有一个Watcher
事件中心(Dep) : 存储Weacher类的地方
当一个对象发生改变时,所有依赖于它的对象都将得到通知。
对数据的操作都会在getter和setter中被拦截,所以在getter中收集依赖,在setter中派发更新
我们先定义Dep类用于存储Watcher
var uid = 0
export default class Dep {
constructor() {
// 在Vue中,每一个组件都已一个自己的ID
this.id = uid++
// 用数组存储自己的订阅者.
// 这个数组存放Watcher的实例
this.subs = []
}
// 添加订阅
addSub(sub) {
this.subs.push(sub)
}
// 添加依赖
depend() {
// Dep.target就是Watcher
if (Dep.target) {
this.addSub(Dep.target)
}
}
// 通知更新
notify() {
// 浅克隆,防止改变原始subs
const subs = [...this.subs]
// 遍历
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // 用于同时Weather更新,Weather会放到后面定义
}
}
}
为了方便理解,我们把Watcher类也先定义出来:
import Dep from './Dep'
var uid = 0
export default class Watcher {
constructor(target, expression, callback) {
// target: 你要监听的数据对象
// expression:表达式,如a.m.n
// callback:依赖变化时触发的回调
this.id = uid++
// this指向window
this.target = target
// 对表达式进行拆分
this.getter = parsePath(expression)
this.callback = callback
// get方法会读取数据的值,从而触发了数据的getter
this.value = this.get()
}
// 当收到数据变化的消息时执行该方法
update() {
this.run()
}
// get方法的作用就是获取自己依赖的数据
get() {
// 进入依赖收集阶段,将全局的Dep.target设置为Watcher本身
// this就是Watcher
Dep.target = this
const obj = this.target
var value
// 只要能找到,就一直找
try {
value = this.getter(obj)
} finally {
Dep.target = null
}
return value
}
// 当数据更新时会调用run
run() {
// 得到并且唤起
this.getAndInvoke(this.callback)
}
getAndInvoke(cb) {
const value = this.get()
if (value !== this.value || typeof value == 'object') {
const oldValue = this.value
this.value = value
cb.call(this.target, value, oldValue)
}
}
}
function parsePath(str) {
// 将表达式按.进行拆分
var segments = str.split('.')
return obj => {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
在Observer中引入Dep类并创建实例
import Dep from './Dep'
class Observer {
constructor(value) {
this.dep = new Dep()
// 其他代码省略
}
}
为什么要在Observer中创建Dep的实例 :
由于数据在转换的过程中,每一层属性都会经过Observe类,所以每一个响应式的对象都会有一个自己的依赖存储器,并且__ob__指向的是这个Observer类的实例所以__ob__上会有Dep属性,相当于将Dep这个存储桶放到了一开始为每个响应式对象创建的标识上
由于要在Object.defineProperty()中收集和触发依赖,所以我们在闭包里面也创建一个Dep的实例并且改造getter和setter:
function defineReactive(data, key, value) {
const dep = new Dep()
// 其他代码省略
Object.defineProperty(data, key, {
// 可枚举
enumerable: true,
// 可以被配置,比如可以被delete
configurable: true,
get() {
// 如果现在处于依赖的收集阶段
if (Dep.target) {
// 把当前的watcher添加到dep数组中
// console.log(1213213)
dep.depend()
// 如果有子元素,还要让子元素添加依赖
if (childOb) {
childOb.dep.depend()
}
}
return value
},
set(newValue) {
if (value === newValue) {
return
}
value = newValue
childOb = observe(newValue)
// 修改数据是触发notify
dep.notify()
}
})
}
在get()中Dep.target就是触发数据响应的Watcher
当数组被更改,也需要通知Dep:
// 遍历上述数组方法名
methodsNeedChange.forEach(methodsName => {
// 备份原来的方法(因为基本功能还是需要使用原来的数组方法,只是我们需要添加响应式的功能)
const original = arrayPrototype[methodsName]
// 将重写后的方法定义到arrayMethods对象上,函数就是重写后的方法
def(arrayMethods, methodsName, function () {
// 要有数组原来方法的功能
const result = original.apply(this, arguments)
let args = Array.from(arguments)
// 在这个数组中,__ob__已经被添加过,可以将__ob__取出来
const ob = this.__ob__
// 由于push,unshift,splice可以往数组中添加项,所以也需要对这些项进行遍历(防止插入的项是对象)
let inserted = []
switch (methodsName) {
case 'push':
case 'unshift':
inserted = arguments
break
case 'splice':
// slice(2)从下标为二的项开始
// 因为splice格式(下标,数量,插入新的项)
inserted = args.slice(2)
break
}
// 判断有没有要插入的新项,如果有就让新项也变为响应式的
if (inserted.length > 0) {
ob.observeArray(inserted)
}
ob.dep.notify() // 修改:通知Dep
return result
})
})
-
总结
现在代码写完了,让我们总结一下 :
我们在转化响应式的过程中,通过循环递归使每一个Observer的实例中都有一个Dep的实例,当模板引擎使用数据的时候,我们就会在getter中通过dep.depend()方法收集依赖,当修改数据的时候,就会触发setter,通过dep.notify()通知Watcher进行更新,最后通过Watcher触发视图的更新