Vue源码系列
前言
Object 和Array的变化侦测采用不同的处理方式(后面会解释原因),这一篇将介绍Object的变化侦测。
一、什么是变化侦测?
Vue.js会自动通过状态生成DOM,并将其输出到页面显示出来,这个过程叫渲染。
通常,在运行时应用内部的状态会不断发生变化,此时需要不停地重新渲染。那如何知道状态发生变化了呢,变化侦测就是用来追踪状态变化的,一旦变化,就重新更新视图。
变化侦测有两种类型,一种是“推”,一种是“拉”。Angular和React中是“拉”,即不知道哪个状态变了,只知道状态可能变了,然后发信号给框架,框架内部进行暴力比对找到节点进行渲染。Vue则是“推”,状态变了,立刻能侦测到并向改状态对应的依赖发送通知,让它们更新。
Vue2粒度更新是组件级别的,而不再是具体的节点。(因为粒度越细,绑定的依赖就越多,依赖追踪在内存中的开销就越大)
二、如何追踪变化
如何侦测一个对象的变化?
easy,用Object.defineProperty
和ES6的Proxy
。Vue2用的Object.defineProperty
,Vue3用Proxy
重写了这部分。
那还是从Vue2学起吧…
写Vue2时,会new一个Vue,然后进行初始化,调用init方法,这个init方法绑定到了Vue构造函数的原型上:
进入init方法,数据的变化侦测就看initState方法就可以:
进入initState方法,发现里面是初始化的逻辑:(源码被简化过):
我们看initData初始化数据的方法,通过observe方法对数据劫持,observe方法会去实例化一个Observe类,目的是为了循环对象,给每一个属性配成可侦测的。通过Object.defineproperty()
就可以追踪变化:
通过以上调试,我们知道,Object.defineProperty可以侦测到对象的变化,函数defineReactive用来对Object.defineProperty进行封装。defineReactive函数作用是定义一个响应式数据。每当data的key中读取数据时,get函数被触发;每当往data的key中设置数据时,set函数被触发。
defineReactive函数会对value进行递归侦测,因为value有可能也是对象。
三、如何收集依赖
我们已经知道如何侦测变化了,接下来要收集依赖…
侦测数据的目的是,属性发生变化时需要通知用到数据的地方。Vue2中会发送到组件,然后组件内部通过虚拟DOM重新渲染。
<template>
<h1>{{name}}</h1>
</template>
<!--该模板使用了数据那么,所以当它发生变化时,要向使用了它的地方发送通知-->
使用这个name数据的地方可能很多,既有可能是模板,也可能是用户写的一个watch。这时需要抽象出一个能集中处理这些情况的类,这个类就是Watcher(Watcher的实现在下文)。
依赖是Watcher,我们要收集的是Watcher。
在依赖收集阶段只收集这个封装好的类的实例,通知也只通知它一个。
如何收集依赖呢?把用到数据name的地方收集起来,然后等属性发生变化时,把之前收集的依赖循环触发一遍就好。即在getter中收集依赖,在setter中触发依赖。
四、依赖收集在哪里?
通过第三点,知道在getter中收集依赖,那把依赖放哪里呢?
思考一下,首先想到的是每个key都有一个数组,用来存储当前key的依赖。假设依赖是一个函数,保存在window.target上,那defineReactive函数可以写成这样:
export function defineReactive(target, key ,value) {
let dep = []; // 每一个属性都有一个dep
Object.defineProperty(target, key, {
get() { // 取值调用
dep.push(window.target); // 收集依赖
return value
},
set(newValue) { // 修改的时候,会执行set
if(newValue === value) return
for(let i = 0; i < dep.length;i++) {
dep[i](newVal,val); // 依次调用依赖方法,更新界面
}
val = newVal
}
})
}
数组dep,用来存储被收集的依赖;set被触发时,循环dep以触发收集到的依赖。
由于这样写有点耦合,所以封装成了Dep类,专门管理依赖,使用这个类,可以收集依赖、删除依赖或向依赖发送通知,代码如下:
let id = 0;
class Dep {
constructor() {
this.id = id++; // 属性dep手机watcher
this.sub = []; // 存放当前属性对应的watcher有哪些
}
depend() {
if(window.target){
this.addSub(window.target)
}
}
addSub(watcher) {
this.sub.push(watcher)
}
notify() {
this.sub.forEach(watcher => {
watcher.update(); // 告诉watcher更新
});
}
// 还有其他方法略
}
Dep.target = null;
export default Dep;
因此,要封装一个管理依赖的类Dep,defineReactive函数可改造成:
export function defineReactive(target, key ,value) {
let dep = new Dep(); // 修改
Object.defineProperty(target, key, {
get() {
dep.depend(); // 修改
return value
},
set(newValue) {
if(newValue === value) return
dep.notify() // 修改
val = newVal
}
})
}
所以,依赖收集到了Dep类中。
五、Watcher是什么?
通过上文,数据侦测时,getter把依赖收集到了Dep类中,setter触发时会通知依赖更新dom,而这个依赖就是Watcher。那究竟什么是Watcher呢?
初始化之后,会调用Vue原型上的mount方法vm.$mount('#app')
,mount方法里面有一个mountComponent方法,里面会创建一个渲染Watcher,数据变化时通知它,它再通知update更新方法重新渲染:
Watcher类实现代码:
export default class Watcher {
constructor (vm,expOrFn,cb) {
this.vm = vm; // 实例
this.cb = cb; // 用户定义的watch会调用这个cb,变化侦测可以不看这个
this.getter = exprOrFn // 将实例化渲染watcher时传入的update更新方法放到getter中
this.value = this.get() // 执行get方法
}
get () {
window.target = this;
const vm = this.vm
let value = this.getter.call(vm, vm) // 执行update方法,这里会去读取数据,会触发属性的getter方法,从而主动收集依赖
window.target = undefined;
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
Watcher可以自己主动添加到对应变化属性的Dep中,是不是很神奇?
因为在get方法中先把window.target设置成了this,也就是当前的watcher实例,然后再读属性的值,就肯定会触发属性的getter。触发了getter,就会触发收集依赖的逻辑。
依赖注入Dep后,属性变化时,就会让依赖列表中所有的依赖循环触发update方法,从而进行更新。
所以,读取属性时,将Watcher放在Dep类的sub数组中;修改属性时,遍历sub数组,依次通知所有的Watcher实例去更新视图。
一个属性对应一个Dep,一个Dep对应多个Watcher(渲染Watcher、用户定义的Watcher等),一个Watcher也可以对应多个Dep。
六、不足之处
const vm = new Vue({
data: {
obj:{}
},
methods: {
action() {
this.obj.name='lisa'
}
},
el:'#app', // 将数据解析到el元素上
})
在obj中增加了name属性,但是Vue.js无法侦测到这个变化,删除也不会被侦测到。这是因为Object.defineProperty的getter/setter只能追踪一个数据是否被修改,无法最终新增属性和删除属性。
总结
侦测就是侦测数据的变化。当数据发生变化时,要能侦测到并发出通知。
object可以通过object.defineProperty将属性转换成getter/setter的形式来追踪变化。
我们需要在getter中收集有哪些依赖使用了数据。setter触发时,通知getter中收集的依赖数据发生了变化。
收集依赖,我们创建了Dep类,管理收集、删除依赖等操作。
依赖即Watcher类。只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。
Observer类,作用是把Vue data中的object数据,通过object.defineProperty都转化为响应式的(包括子数据)。