响应式数据
在开始分析之前,先了解一下Dep,Watcher,Observe 这三个类的关系和作用
Dep
它在整个数据响应式的过程当中充当了被观察者
的作用,每一个数据都有一个dep实例,dep里面有一个subs
数组,用来存放收集了它的Watcher
,当这一个数据发生变化时,遍历subs数组, 通知收集过它的Watcher重新渲染页面或重新求值
Dep.target
具有全局唯一性,指向的是当前使用的 Watcher实例
class Dep {
constructor() {
this.id = id++
// 如果多个组件都使用到了这个数据,则分别存放对应组件的Watcher
this.subs = [] // 用来存放watcher
}
depend() {
// Dep.target == 此时正在使用的Watcher
if (Dep.target) {
Dep.target.addDep(this)
}
}
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
Dep.target = null
Watcher
它在整个数据响应式的过程当中充当了观察者
的作用,一共有2种Watcher
- 每个组件都有一个对应的
渲染
用的Watcher - computed用于惰性求值的Watcher(不渲染页面)
每一个Watcher内部都有一个deps
数组,保存着这个Watcher观察的所有Dep。
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.id = id++ // 给watcher添加标识
this.getter = exprOrFn
this.deps = []
this.get()
}
get() {
// getter 其实就是调用了 vm.update(vm.render()) 渲染页面
this.getter()
}
update() {
// 更新视图时调用update,其本身依然调用的是 vm.update(vm.render())
this.get()
}
addDep(dep) {
// 收集需要被观察的dep,一个数据在多处被使用时,只要收集一次即可
// 把这个Watcher加入到dep的subs数组中
let id = dep.id
if (!this.depsId.has(id)) {
this.depsId.add(id)
this.deps.push(dep)
dep.addSub(this)
}
}
}
observe
观测数据的入口, 主要功能就是检测属性是否带有 __ob__确认是否已经被观测过,否则的话就调用Obserber
进一步处理
function observe (value, asRootData) {
// 非对象类型或者value为VNode 不做处理
if (!isObject(value) || value instanceof VNode) {
return
}
// 如果属性带有 __ob__ ,就说明该属性已经被观测过,就不再处理
let ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
// 不是服务端渲染,不是Vue实例
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 观测数据
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
Observer
在这里检测类型是数组还是对象,分别采用不同的处理方法处理数据
class Observer {
constructor (value) {
// 初始化一个Dep实例
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
// 在当前观测的对象上添加一个__ob__属性,值就是 Observer 实例
def(value, '__ob__', this);
// 数组类型的处理
if (Array.isArray(value)) {
if (hasProto) {
// 将已经重写过方法的数组原型设置为当前对象的原型
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
// 遍历数组的每个元素,依次调用 observe
this.observeArray(value);
} else {
// 对象类型的处理
this.walk(value);
}
}
walk (obj) {
// 遍历对象逐个添加劫持方法
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i]);
}
}
// 当为数组时,遍历每一项依次调用 observe ,重复上面的步骤
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
}
}
defineReactive$$1
在这里为数据设置 seter 、getter , 添加数据劫持
function defineReactive$$1 (obj,key,val,customSetter,shallow) {
var dep = new Dep();
// 读取当前属性的描述符
var property = Object.getOwnPropertyDescriptor(obj, key);
// 如果当前的属性还是对象,则继续调用observe重复上面的步骤
var childOb = !shallow && observe(val);
var getter = property && property.get;
var setter = property && property.set;
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 添加数据劫持 get
get: function reactiveGetter () {},
// 添加数据劫持 set
set: function reactiveSetter (newVal) {}
});
}
组件在调用render创建VNode的过程中会读取属性, 属性在 mergeOptions 阶段
已经被代理到实例上,所以可以通过vm.xx
读取属性, 由于在前面 defineReactive 阶段
为属性添加了劫持方法get,此时就会开始收集"被观测者"
// 例:
<TestComponent :name="fullName" />
// 该组件对应的render函数
return _c("TestComponent",{attrs:{name: _vm.fullName}})
get
读取属性时,在defineReactive
中设置的get函数就会被调用, 它通过 addDep 函数完成双向的数据收集。
// 每一个数据都有一个 dep
var dep = new Dep()
get: function(){
// 在 mergeOptions 合并选项时,data可能是一个闭包函数
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
// dep.depend(); 这是源码,为了便于理解把调用的代码改动一下
Dep.target.addDep(dep)
}
return value
}
Watcher.prototype.addDep 双向收集
- 每个dep实例都有一个id,检查是否已被该Watcher收集过,无需重复收集
- 将新的 dep 收集到
Watcher.newDeps
中 - 将当前的 Watcher 添加到
dep.subs
中
Watcher.prototype.addDep = function(dep){
var id = dep.id
if(!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if(!this.depIds.has(id)) {
// dep.addSub(this) 此为源码,为了便于理解,替换为实际调用的代码
dep.subs.push(this)
}
}
}
set
数据变更时,当新值和旧值不一致就调用dep.notify
通知之前收集的Watcher
var dep = new Dep()
set: function reactiveSetter (newVal) {
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
dep.notify()
}
dep.notify 更新通知
取出当前dep收集的所有Watcher,依次遍历调用update将自身推入更新队列,再通过nextTick下一次更新视图时,更新页面的数据
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // Watcher.update()
}
}
数组的响应式
以 Array.prototype 为原型创建一个对象,重写其中的 [push,pop,shift,unshift,splice,sort,reverse]
方法,这些方法均会触发页面更新。当在Observer中检测到当前的对象为数组类型时,把此对象设置为原型
const arrayProto = Array.prototype
// 创建一个新的对象,原型就是数组的prototype,后面重写的方法就添加到该对象上
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
// 遍历即将重写的方法
methodsToPatch.forEach(function (method) {
// 取出数组原型上对应的方法
const original = arrayProto[method]
// 这些方法逐个添加到 arrayMethods 上
def(arrayMethods, method, function mutator (...args) {
// 解构的 args 为一个数组
// 调用原型上的方法
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
// push , unshift 的时候属于新增元素
case 'push':
case 'unshift':
inserted = args
break
// splice 第三个参数也是新增
case 'splice':
inserted = args.slice(2)
break
}
// 遍历 args 为每一项新增的元素设置响应式
if (inserted) ob.observeArray(inserted)
// 通知页面更新
ob.dep.notify()
return result
})
})