Vue.js会自动通过状态生成DOM,并将其输出到页面上显示出来,这个过程叫做渲染。
那么,在运行时应用内部的状态会不断发生变化,此时需要不停的重新渲染,如何确定状态中发生了什么变化?
这里就用到了 “变化侦测”,它就是用来监听内部数据状态变化的。
变化侦测分为两种类型,一种是 “推”(push) ,一种是 “拉”(pull)。
目前我们的前端主流框架中,Angular 和 React 中的变化侦测都属于 “拉” 的模式,也就是说,当状态发生变化时,它们并不知道哪个状态变了,只知道有可能变了,然后会发出一个信号告诉框架,框架接收到信号后,会进行一个暴力比对来找出那些DOM节点需要重新渲染。
这在Angular中是脏检查的流程,在React中使用的是虚拟DOM。
在Vue.js中的变化侦测属于 “推” 的过程,当状态发生变化时,Vue.js立刻就知道了,而且在一定程度上知道那些状态变化了。因此,在Vue.js中可以进行更细粒度的更新。
在JS中,如果想侦测一个对象的变化,有两种办法:
注: 由于ES6在浏览器中的支持度并不理想,在Vue2.0源码中,还是使用了Object.defineProperty来实现的,但使用Object.defineProperty来侦测变化会有很多缺陷,比如:
所以,在已发布的测试版Vue3.0的源码里,作者使用Proxy重写了这部分的代码。
这里主要说说Vue2.0源码中的数据侦测的实现,同时也是对学习过程中的一个知识梳理。Proxy的方式实现数据侦测待Vue3.0源码正式版发布后,再和小伙伴们做讨论。
代码如下:
// 定义一个响应式数据,在此函数中追踪变化
function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
return val
},
set: function(newVal) {
if(val === newVal) {
return
}
val = newVal
}
})
}
通过上方defineReactive函数的封装,就能实现每当从data的key中读取数据时,get函数被触发,每当往data的key中设置数据时,set函数被触发。
我们之所以要观察数据,目的是当数据发生变化时,可以通知曾经使用了该数据的地方,进行更新、渲染,所以我们要先收集依赖。
收集依赖: 即把用到数据的地方收集起来,等属性发生变化时,把之前收集好的依赖循环触发一遍就好了。
在Vue2.0中,模板使用数据等同于组件使用数据,所以当数据发生变化时,会将通知发送到组件,然后组件内部再通过虚拟DOM重新渲染。
总结下来就是:在getter中收集依赖,在setter中触发依赖。
数据变化时,曾经使用了该数据的地方有很多,而且类型可能还不一样,既有可能是模板,也有可能是用户写的一个watch。所以,我们需要抽象出一个能集中处理这些情况的类。
然后,在收集依赖阶段,只收集这个封装好的类的实例进来,通知也只通知它一个,它再负责通知其他地方,这个抽象的东西(类),就叫做 Watcher .
现在已经明确了,在getter中收集依赖,假设依赖是一个函数,保存在window.target上,我们就可以把依赖收集的代码封装成一个Dep类,帮助我们管理依赖:
// 为了减少耦合,把收集依赖的代码封装成一个dep类,专门帮助我们管理依赖
export default class Dep {
constructor () {
this.subs = []
}
addSub(sub) {
// 收集依赖
this.subs.push(sub)
}
removeSub(sub) {
remove(this.subs, sub)
}
depend () {
if(window.target) {
this.addSub(window.target)
}
}
notify() {
//通知依赖更新
const subs = this.subs.slice()
for(let i = 0,l = subs.length;i < l;i++) {
subs[i].update()
}
}
function remove(arr, item) {
// 删除依赖
if(arr.length) {
const index = arr.indexOf(item)
if(index > -1) {
arr.splice(index, 1)
}
}
}
}
使用上方的Dep类,我们可以做收集依赖、删除依赖、向依赖发送通知等操作。
然后把上方的defineReactive函数进行改造:
// 改造defineReactive函数,使用dep类
function defineReactive(data, key, val) {
let dep = new Dep() // 实例化一个对象(类)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend() // 收集依赖(调用类中的收集函数)
},
set: function (newVal) {
if(val === newVal) {
return
}
dep.notify() // 通知依赖
val = newVal
}
})
}
到此,“依赖收集在哪里” 的疑问仿佛已经揭秘了,依赖收集到 Dep 中。
上方我们已经提到依赖是 Watcher,Watcher 在这里代表的是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。它的主要作用是:
比如我们的watch监听函数的使用:
//keypath
vm.$watch('a.b.c', function(newVal, oldVal)) {
// 监听数据变化,做具体操作
}
这段代码表示:当data.a.b.c属性发生变化时,触发第二个参数中的函数。
通过以上数据侦测的介绍,实现以上功能,接着我们就想到只要把watcher实例,添加到data.a.b.c属性的Dep中就行了,这个过程也叫做收集依赖的过程。然后,当data.a.b.c的值发生变化时,通知Watcher。接着,Watcher再执行参数中的这个回调函数。
实现以上功能,我们可以对Watcher抽象类做如下的封装:
// 什么是watcher?watcher抽象类
export default class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
// 执行this.getter(),就可以读取data.a.b.c的内容
this.getter = parsePath(expOrFn) // parsePath是一个读取一个字符串keypath的函数,这里不再列举
this.cb = cb
this.value = this.get()
}
get() {
window.target = this
let value = this.getter.call(this.vm, this.vm)
window.target = undefined
return value
}
update() {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
这段代码可以把自己主动添加到data.a.b.c的Dep中去。因为,在get方法中先把window.target设置成了this,也就是当前的watcher实例。然后读data.a.b.c的值,就会触发getter(收集依赖的逻辑),并把依赖添加到Dep中。
依赖注入到Dep中后,每当data.a.b.c的值发生变化时,就会让所有的依赖循环触发update方法,而update方法会执行参数中的回调函数,将value和oldValue传到参数中。
所以,不管是用户执行watch用到data数据,还是在模板中用到data数据,都是通过 Watcher 来通知自己是否需要发生变化。
通过上方赘述,我们已经实现了变化侦测功能了,但只能侦测数据中的某一个属性,而我们所期望的是把数据中的所有属性(包括子属性)都侦测到。所以,我们封装一个Observer类,这个类的作用是把数据内的所有属性都转换成getter/setter的形式,然后追踪它们的变化。
// 封装一个Observer类,递归检测所有属性key
export class Observer {
constructor (value) {
this.value = value
if(!Array.isArray(value)) {
this.walk(value)
}
}
// 将每个属性都转换成getter/setter的形式检测变化
// 这个方法只有在数据类型为object的时被调用
walk (obj) {
const keys = Object.keys(obj)
for(let i = 0;i < keys.length;i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
// 定义检测对象变化的函数
function defineReactive(data, key, val) {
// 判断某个属性值是否仍然为一个对象,如果是,则继续检测变化
if(typeof val === 'object') {
new Observer(val)
}
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
// 收集依赖
dep.depend()
return val
},
set: function (newVal) {
// 通知更新
if(val === newVal) {
return
}
val = newVal
dep.notify()
}
})
}
}
上方我们定义的Observer类,它用来将一个正常的object转换成被侦测的object(响应式的object)。也就是说,我们只要将一个对象传到Observer中,那么这个对象就会变成响应式的了。这就是我们Vue2.0源码中数据响应的实现方式。
在深究了 object 的变化侦测原理后,也会发现一些问题,当我们去给data新增、删除属性时,Vue.js是无法侦测到这个变化的。以为在ES6之前,JS没有提供元编程的能力,无法侦测到一个属性的新增和删除。为了解决此问题,Vue.js 提供了两个API :
Data、Observer、Dep、Watcher之前的关系:
总结:Vue.js中的的数据响应侦测原理就是:
以上就是对Vue2.0中对象变化侦测的原理分析,在此梳理知识和大家分享,后期还会对数组侦测原理、Vue3.0实现数据侦测原理做进一步的更新分享。
喜欢记得点赞,你的认可是我学习分享的最大动力 ~ ❤️