前言
看过很多讲响应式的文章,大多都是告诉你们,有Observer,Dep,Wathcer类,Object.definePorperty,先会触发get中的dep.depend收集依赖,然后数据改变时,会触发set中的dep.notify去通知Wathcer执行它的update方法,这样响应式就完成了
这只能说是一个粗略的总结,如果我告诉你,Watcher的update方法其实只是一个调用其他函数的方法而已,它还会进行非常多的操作,其中还会涉及到异步更新的原由!这只是其中的一些小细节,Vue2.0x内部的响应式实现是包括整个observer文件夹的,其中大多细节上面都是没有提及的。
这次文章可能会非常的长,我是有分开写的打算的,但是想了又想,这种断点式分析感觉不能中断,所以我将用这一篇博客带大家理清楚Vue2.0x的响应式到底是怎么样的,包括其实现的内部细节!
Vue响应式
探索响应式之前,要先了解Object.defineProperty和发布订阅模式。Vue2.0x都是使用Object.defineProperty进行数据劫持并通过发布订阅完成的双向数据绑定(响应式)。Vue3将Object.defineProperty替换成了Proxy,这里还是只讨论Vue2.0x的响应式原理!
开始前的准备
思考
试想一下,如果自己用js实现响应式,该怎么做?
此时,有个需求:给两个变量a和b,要求b永远是a的10倍。
直接上代码,给了a,b两个变量,让b = a * 10,之后改变a的值,b并不会去改变
let a = 3
let b = a * 10
console.log(b) // 30
a = 4
console.log(b) // 30
// 当改变a时,b并没有变化,
但是,再执行一次b = a * 10后,就可以使b又是a的10倍了
let a = 3
let b = a * 10
console.log(b) // 30
a = 4
console.log(b) // 30
// 当改变a时,b并没有变化,
// 除非再执行一次 b = a * 10
b = a * 10
console.log(b) // 40
那么,将b = a * 10放进一个函数function alwaysTen中
function alwaysTen(a) {
return a * 10
}
然后将上面代码转换一下
let a = 3
let b = a * 10
console.log(b) // 30
a = 4
b = alawaysTen(a)
console.log(b) // 40
function alwaysTen(a) {
return a * 10
}
看,当改变a的值时,去主动调用alwaysTen这个函数,b就会永远是a的10倍了!
但还是有个问题,怎么知道a的值被改变了呢?上面的代码中是我们人为的知道它改变了,但是程序中,它的值会是经常变化的,总不可能在每一次变化后都手动执行alwaysTen函数吧。
因此,最主要的问题,就集中在怎么知道a的值改变了,并要在改变的时候执行相应的alwaysTen函数。
总结起来就是:
- 要能知道a的值何时改变
- 并且在a的值改变时调用alwaysTen函数
好,现在引入Object.defineProperty来解决这个问题
Object.defineProperty
Object.defineProperty()的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性
Object.defineProperty(obj, prop, desc)
- obj 需要定义属性的当前对象
- prop 当前需要定义的属性名
- desc 属性描述符
属性描述符如下图
这里面,要注意的其实是get与set存取描述符,是由一对 getter、setter 函数功能来描述的属性
- get:一个给属性提供getter的方法,如果没有getter则为undefined。该方法返回值被用作属性值。默认为undefined。
- set:一个给属性提供setter的方法,如果没有setter则为undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认值为undefined。
<div id="app">
<p>你好,<span id="name"></span></p>
</div>
let obj = {}
// 数据拦截
Object.defineProperty(obj, 'name', {
get () {
console.log('有人想获取name属性')
return document.getElementById('name').innerHTML
},
set (newValue) {
console.log('有人想要修改name属性')
document.getElementById('name').innerHTML = newValue
}
})
obj.name = 'jerry'
console.log(obj.name)
输出:
可以看到,当修改值时,会触发属性描述符内部的set函数,因此会打印’有人想要修改name属性’,然后读取obj.name时会触发属性描述符内部的get函数,因此会打印’有人想获取name属性’。
好!重点来了,修改值时,触发set,并可以在set函数内部添加任意操作!!
这不就成了吗?
将之前上面的代码拿下来
a = 3
b = a * 10
let temp = null
Object.defineProperty(window, 'a', {
get() {
console.log('有人想获取a的值')
return temp
},
set(newValue) {
console.log('有人改变了a的值')
b = alwaysTen(newValue)
temp = newValue
}
})
console.log(b) // 30
a = 4
console.log(b) // 40
function alwaysTen(a) {
return a * 10
}
输出:
好的!这不就完成了吗,在set中调用了alwaysTen函数,也就实现了在a的值被修改时,监听到这个修改,然后去执行相应的操作。
其实这个操作就叫做数据劫持/数据拦截
Vue2.0x内部就是通过Object.defineProperty来实现数据劫持的。但远远不够,Vue中还使用到了发布订阅模式,因为Vue涉及的不止是这样改变一个值的10倍这么简单了。他还要考虑编译解析渲染监听等。
所以,还需要了解什么是发布订阅模式
发布订阅模式
关于发布订阅,直接上图吧,好理解
而Vue要实现发布订阅,当然也需要三个主体:Observer,Dep,Watcher
Observer在这里就是Publisher,Dep是中间的Publish channel,而Wathcer则是Subscriber,当然现在还没说到,可以在看过后面之后再返回来品
响应式入口
在了解完Object.defineProperty和发布订阅模式后,终于可以开始深入了
现在就来揭开响应式的面纱!
首先要找到响应式是在源码中哪个文件夹里实现的
看过之前博客的应该都知道在initData中,最后一步会执行observe(data, true)将data响应化
所以这次,就通过initData这里作为入口来分析整个响应式!
// from src\core\instance\state.js initData
observe(data, true /* asRootData */)
ctrl+鼠标左键点击observe,进入到src\core\observer\index.js中
可以看到,现在已经离开了instance文件夹,来到了observer文件夹,这就是Vue2.0x响应式实现的源码所在地
响应式开始
observe
从initData中跳进来,就是这个observe函数了,先分析这个函数!
observe函数会返回一个Observer实例,可能从缓存中直接返回,或者直接new一个返回
不过在初始化时,是不会有缓存的Observer实例的,所以都会执行ob=new Observer()
// from src\core\observer\index.js
// 是否可以添加到观察者模式
export function toggleObserving (value: boolean) {
shouldObserve = value
}
/**
* from src\core\observer\index.js
*
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
* 尝试给一个value对象创建一个observer实例,
* 如果观察成功,返回一个新的observer实例
* 或者返回一个已经存在的observer 如果这个value对象早已拥有
*/
// observe作用就是为了拿到Observe实例并返回,从缓存中或者new一个
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 判断是否为对象 判断是否为VNode
if (!isObject(value) || value instanceof VNode) {
// 如果不是对象 或者 是实例化的Vnode 也就是vdom
return
}
// 观察者 创建一个ob
let ob: Observer | void
// 检测是否有缓存ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 直接将缓存的ob拿到
ob = value.__ob__
} else if (
// 如果没有缓存的ob
shouldObserve && // 当前状态是否能添加观察者
!isServerRendering() && // 不是ssr
(Array.isArray(value) || isPlainObject(value)) && // 是对象或数组
Object.isExtensible(value) && // 是否可以在它上面添加新的属性
!value._isVue // 是否是Vue实例
) {
// new 一个Observer实例 复制给ob
ob = new Observer(value)
}
// 如果作为根data 并且当前ob已有值
if (asRootData && ob) {
// ++
ob.vmCount++
}
// 最后返回ob,也就是一个Obesrver实例 有这个实例就有__ob__,然后其对象和数组都进行了数据劫持
return ob
}
observe会返回一个Observer实例,那么就要来看看class Observer了
Observer constructor
/**
* from src\core\observer\index.js class Observer
*
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
* 附加到每个被观察者的观察者类对象。一旦链接,观察者就会转换目标对象的属性键放入g
*/
export class Observer {
value: any;
// Dep类
dep: Dep;
vmCount: number; // number of vms that have this object as root $data 将此对象
constructor (value: any) {
this.value = value
// 这里会new一个Dep实例
this.dep = new Dep()
this.vmCount = 0
// def添加__ob__属性,value必须是对象
def(value, '__ob__', this)
// 判断当前value是不是数组
if (Array.isArray(value)) {
// 如果是数组
// 检测当前浏览器中有没有Array.prototype
// 当能使用__proto__时
// 这里完成了对数组方法的数据劫持,不使用这7个方法都不会触发响应式
if (hasProto) {
// 有原型时 将arrayMethods覆盖value.__proto__,也就是把增加了副作用的7个
protoAugment(value, arrayMethods)
} else {
// 复制增加了副作用的7个数组方法
copyAugment(value, arrayMethods, arrayKeys)
}
// 遍历将数组所有元素进行observe进行数据劫持
this.observeArray(value)
} else {
// 不是数组是对象,执行这里
// walk就是给对象的所有key进行数据劫持
this.walk(value)
}
}
}
功能:
- 首先new一个dep实例
- 通过def(内部也就是Object.defineProperty)添加__ob__属性,此属性是是否已添加数据劫持的一个标记
- 判断传入的value是否是数组:
- 如果是数组,将数组7个方法进行覆盖或重写,以实现数组方法的响应式,之后会通过observeArray遍历数组进行数据劫持式
- 如果不是数组,是对象,使用walk方法进行数据劫持
- 最后数组和对象都会被数据劫持
数组响应式分析
在Vue官方文档中,给出了7种可以响应式更改数组的方法:
- push
- pop
- shift
- unshift
- splice
- sort
- reverse
Vue 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:vm.items.length = newLength
但为什么是这样的呢?此处就开始分析数组的响应式方法了!
可以看到,在Observer构造函数内部,进行了数组的判断,如果是数组的话,又要进行hasProto判断,分别执行protoAugment和copyAugment,之后再执行observeArray。
// from Observer constructor
// 判断当前value是不是数组
if (Array.isArray(value)) {
// 如果是数组
// 检测当前浏览器中有没有Array.prototype
// 当能使用__proto__时
// 这里完成了数组的方法重写,不使用这7个方法都不会触发响应式
if (hasProto) {
// 有原型时 将arrayMethods覆盖value.__proto__,也就是把增加了副作用的7个数组方法放了进来
protoAugment(value, arrayMethods)
} else {
// 复制增加了副作用的7个数组方法
copyAugment(value, arrayMethods, arrayKeys)
}
// 遍历将数组所有元素进行observe数据劫持
this.observeArray(value)
} else {
// 不是数组是对象,执行这里
// walk就是给对象的所有key进行数据劫持
this.walk(value)
}
hasProto
// from src\core\util\env.js
// can we use __proto__?
export const hasProto = '__proto__' in {}
功能: 1. 判断是否能使用__proto__,返回一个布尔值,由此决定使用protoAugment或是copyAugment
arrayMethods
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
// 导入def 也就是 Object.defineProperty
import { def } from '../util/index'
// 复制一份 Array.prototype到arrayMethods
const arrayProto = Array.prototype
// arrarMethods是Array.proto的复制
export const arrayMethods = Object.create(arrayProto)
// 获取这7个数组方法,通过def拦截这7个方法,给它们增加副作用
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
* 拦截转换方法并发出事件
*/
// 将这7个方法遍历
methodsToPatch.forEach(function (method) {
// cache original method
// 从原型中把原始方法拿出,在后面会调用一次原始方法,
// 并在原始方法的上增加副作用
const original = arrayProto[method]
// 额外通知更新 def相当于Object.defineProperty
// 给arrayMehods的method方法定义一个函数mutator
// 就是在执行push pop等方法的基础上干一些额外的事
// 也就是下面的ob.dep.notify()通知改变
def(arrayMethods, method, function mutator (...args) {
// 执行数组方法原本应该做的事情
const result = original.apply(this, args)
// 获取到这个数组的__ob__实例
const ob = this.__ob__
let inserted
// 这三个方法特殊,因为会对数组进行增加操作,之前数组所有元素都是已经
// 做过响应式了,所以要对新增加的元素再进行响应式处理
// 所以要通过inserted是否有值,对新增值的三个数组方法进行再次遍历响应式
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 如果有新增的值,也就是使用了push unshift splice三个方法
// 调用ob.observeArray,也就是遍历将数组所有元素进行observe
// 也就是说增加和删除元素,都还是会响应式
if (inserted) ob.observeArray(inserted)
// notify change
// 通知更新
ob.dep.notify()
// 最后返回数组方法原本操作的结果
return result
})
})
protoAugment和copyAugment的调用都传入了arrayMethods这个参数,那就先来分析这个参数是什么,ctrl+左键点击arrayMethods,来到src\core\observer\array.js,其实这个文件就是对数组方法进行响应式的源码
// 复制一份 Array.prototype到arrayMethods
const arrayProto = Array.prototype
// arrarMethods可以通过原型链访问到 arrayProto也就是复制的Array.prototype
export const arrayMethods = Object.create(arrayProto)
将7个数组方法放入methodsToPatch数组
// 获取这7个数组方法,通过def拦截这7个方法,给它们增加副作用
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
拷贝一份Array.prototype给arrayMethods,可以通过其原型链访问到拷贝的Array.prototype的方法。所以不会对Array.prototype产生影响
/**
* Intercept mutating methods and emit events
* 拦截转换方法并发出事件
*/
// 将这7个方法遍历
methodsToPatch.forEach(function (method) {
// cache original method
// 从原型中把原始方法拿出,在后面会调用一次原始方法,
// 并在原始方法的上增加副作用
const original = arrayProto[method]
// 额外通知更新 def相当于Object.defineProperty
// 给arrayMehods的method方法定义一个函数mutator
// 就是在执行push pop等方法的基础上干一些额外的事
// 也就是下面的ob.dep.notify()通知改变
def(arrayMethods, method, function mutator (...args) {
// 执行数组方法原本应该做的事情
const result = original.apply(this, args)
// 获取到这个数组的__ob__实例
const ob = this.__ob__
let inserted
// 这三个方法特殊,因为会对数组进行增加操作,之前数组所有元素都是已经
// 做过响应式了,所以要对新增加的元素再进行响应式处理
// 所以要通过inserted是否有值,对新增值的三个数组方法进行再次遍历响应式
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 如果有新增的值,也就是使用了push unshift splice三个方法
// 调用ob.observeArray,也就是遍历将数组所有元素进行observe
// 也就是说增加和删除元素,都还是会响应式
if (inserted) ob.observeArray(inserted)
// notify change
// 通知更新
ob.dep.notify()
// 最后返回数组方法原本操作的结果
return result
})
遍历数组
现在分析这个遍历过程
// cache original method
// 从原型中把原始方法拿出保存,在后面会调用一次原始方法,
// 并在原始方法上增加副作用
const original = arrayProto[method]
首先保存一份原始数组方法
// 执行数组方法原本应该做的事情
const result = original.apply(this, args)
// 获取到这个数组的__ob__实例
const ob = this.__ob__
let inserted
// 这三个方法特殊,因为会对数组进行增加操作,之前数组所有元素都是已经
// 做过响应式了,所以要对新增加的元素再进行响应式处理
// 所以要通过inserted是否有值,对新增值的三个数组方法进行再次遍历响应式
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 如果有新增的值,也就是使用了push unshift splice三个方法
// 调用ob.observeArray,也就是遍历将数组所有元素进行observe
// 也就是说增加和删除元素,都还是会响应式
if (inserted) ob.observeArray(inserted)
// notify change
// 通知更新
ob.dep.notify()
// 最后返回数组方法原本操作的结果
return result
通过def给arrayMethods的method(methodsToPatch中的7个方法)设置副作用
总结:
- 首先执行之前保存的原始数组方法,用result保存执行结果
- 对push和unshift和splice三个可以给数组增加元素的方法进行额外判断和操作,如果是新增值的数组方法,insert变量不为空,所以会执行ob.observeArray 对数组元素遍历进行数据劫持
- 之后ob.dep.notify()通知更新,也就代表着这7个方法执行后,都会通知更新,也就成了响应式,方法的返回结果还是之前保存的原始方法执行的结果,所以只是单纯的添加了个响应式的副作用而已
- 所以通过下标或长度去改变数组值是不会触发响应式的,因为vue内部根本没有这么写,只是对这7个数组方法进行响应式而已
observeArray
/**
* Observe a list of Array items.
*/
// 遍历将数组所有元素进行observe
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
遍历将数组所有元素进行observe
protoAugment
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
* 通过拦截来扩充目标对象或数组原型链使用__proto__
*/
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
// 这里直接用劫持的7个数组覆盖
target.__proto__ = src
/* eslint-enable no-proto */
}
当能使用原型时,调用该方法
也就是protoAugment(value, arrayMethods),value是传入的数组
value.__proto__ = arrayMethods,将更改完的数组方法覆盖给value
copyAugment
// 方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作
// 为名称的属性)组成的数组,只包括实例化的属性和方法,不包括原型上的。
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
/**
* Augment a target Object or Array by defining
* hidden properties.
* 通过定义隐藏属性。
*/
/* istanbul ignore next */
// target: value数组 src arrayMethods keys arrayKeys
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
// 给target设置key属性 内容为src[key] 也就是arrayMethods的值
def(target, key, src[key])
}
}
当不能使用原型时,调用该方法
copyAugment(value, arrayMethods, arrayKeys)
复制arrarMethods上的方法,包括那7个重写的数组方法
对象响应式分析
现在分析完了数组方法的响应式,现在返回到对象
else {
// 不是数组是对象,执行这里
// walk就是给对象的所有key进行响应化
this.walk(value)
}
walk
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
* 遍历所有属性,将其转换为getter/setters。这个方法只应该在value的类型为对象时调用
*/
// walk就是给对象的所有key进行响应化
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 遍历对象的每个key,通过defineReactive进行响应化
defineReactive(obj, keys[i])
}
}
如果不是数组而是对象,调用walk方法
遍历对象所有key值,通过defineReactive进行数据劫持(设置getter,setter)
defineReactive
来到了一个至关重要的函数了!之前一直说通过defineReactive进行数据劫持,那么就来看看这个函数都做了些什么吧!
/**
* Define a reactive property on an Object.
* 在对象上定义一个响应式属性
*/
export function defineReactive (
obj: Object, // 对象
key: string, // 对象的key
val: any, // 监听的数据
customSetter?: ?Function, //日志函数
shallow?: boolean // 是否要添加__ob__属性
) {
// 实例化一个Dep对象, 其中有空的观察者列表
const dep = new Dep()
// 获取obj的key的描述符
const property = Object.getOwnPropertyDescriptor(obj, key)
// 检测key中是否有描述符 如果是不可配置 直接返回
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
// 满足预定义的getter/setters
// 获取key中的get
const getter = property && property.get
// 获取key中的set
const setter = property && property.set
// 如果getter不存在或setter存在 并且参数长度为2
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 递归响应式处理 给每一层属性附加一个Obeserver实例
// shallow不存在时代表没有__ob__属性 将val进行observe返回一个ob实例赋值给childO
// 如果是对象继续调用 observe(val) 函数观测该对象从而深度观测数据对象
// walk 函数中调用 defineReactive 函数时没有传递 shallow 参数,所以该参数是 und
// 默认就是深度观测
let childOb = !shallow && observe(val)
// 数据拦截
// 通过Object.defineProperty对obj的key进行数据拦截
Object.defineProperty(obj, key, {
// 枚举描述符
enumerable: true,
// 描述符
configurable: true,
get: function reactiveGetter () {
// 获取值
const value = getter ? getter.call(obj) : val
// 判断是否有Dep.target 如果有就代表Dep添加了Watcher实例化对象
if (Dep.target) {
// 加入到dep去管理watcher
dep.depend()
// 如果存在子对象
if (childOb) {
// 也加进去管理
childOb.dep.depend()
// 如果值是数组,要特殊处理
if (Array.isArray(value)) {
// 循环添加watcher
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// 获取value值 触发依赖收集
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
// 新旧值比较 如果是一样则不执行了
return
}
/* eslint-enable no-self-compare 不是生产环境的情况下*/
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
// 对于没有setter的访问器属性 返回
if (getter && !setter) return
// 如果setter存在
if (setter) {
// 设置新值
setter.call(obj, newVal)
} else {
// 如果没有setter ,直接给新值
val = newVal
}
// 递归,对新来的值 对新值进行observe 返回ob实例
childOb = !shallow && observe(newVal)
// 当set时触发通知
dep.notify()
}
})
}
// 实例化一个Dep对象, 其中有空的观察者列表
// 这个dep常量所引用的Dep实例对象其实就是一个闭包,可以引用着属于自己的dep常量
// 每次调用defineReactive定义访问器属性时,该属性的 setter/getter 都闭包引用了一个属于自己的dep常量
const dep = new Dep()
首先,会实例化一个Dep对象,这个dep其实是一个闭包引用,保持对其内部subs观察者数组的引用
// 获取obj的key的描述符
const property = Object.getOwnPropertyDescriptor(obj, key)
// 检测key中是否有描述符 如果是不可配置 直接返回
if (property && property.configurable === false) {
return
}
获取obj的key的属性描述符,并判断其configurable属性,如果是不可配置的,直接返回
// 保存了来自 property 对象的 get 和 set
// 避免原有的 set 和 get 方法被覆盖
const getter = property && property.get
// 获取key中的set
const setter = property && property.set
保存来自property的get和set方法,以免被覆盖
深度观察属性
// 递归响应式处理 给每一层属性附加一个Obeserver实例
// shallow不存在时代表没有__ob__属性 将val进行observe返回一个ob实例赋值给childOb
// 如果是对象继续调用 observe(val) 函数观测该对象从而深度观测数据对象
// walk 函数中调用 defineReactive 函数时没有传递 shallow 参数,所以该参数是undefined
// 所以默认就是深度观测
let childOb = !shallow && observe(val)
深度观察属性,递归响应式处理,给每一层属性附加一个Obeserver实例
data: {
a: {
b : {
c: 1
}
}
}
深度观察属性是什么意思呢?举个栗子:
data: {
// 属性 a 通过 setter/getter 通过闭包引用着 dep 和 childOb
a: {
// 属性 b 通过 setter/getter 通过闭包引用着 dep 和 childOb
b: {
c: 1
__ob__: {b, dep, vmCount}
}
__ob__: {a, dep, vmCount}
}
__ob__: {data, dep, vmCount}
}
当遇到这样嵌套属性的data时,深度观察会去递归每一层,给每一层属性都添加一个Observer实例__ob__,并且也会实例化一个属于那一层的dep实例
这样就可以实现,无论多复杂的obj对象,都可以访问其所有属性,即使它嵌套的很深。
接下来就要通过设置getter和setter实现数据劫持了!
// 数据拦截
// 通过Object.defineProperty对obj的key进行数据拦截
Object.defineProperty(obj, key, {
// 枚举描述符
enumerable: true,
// 描述符
configurable: true,
get: function reactiveGetter () {
// 获取值
const value = getter ? getter.call(obj) : val
// 判断是否有Dep.target 如果有就代表Dep添加了Watcher实例化对象
if (Dep.target) {
// 加入到dep去管理watcher
dep.depend()
// 如果存在子对象
if (childOb) {
// 也加进去管理
childOb.dep.depend()
// 如果值是数组,要特殊处理
if (Array.isArray(value)) {
// 循环添加watcher
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// 获取value值 触发依赖收集
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
// 新旧值比较 如果是一样则不执行了
return
}
/* eslint-enable no-self-compare 不是生产环境的情况下*/
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
// 对于没有setter的访问器属性 返回
if (getter && !setter) return
// 如果setter存在
if (setter) {
// 设置新值
setter.call(obj, newVal)
} else {
// 如果没有setter ,直接给新值
val = newVal
}
// 递归,对新来的值 对新值进行observe 返回ob实例
childOb = !shallow && observe(newVal)
// 当set时触发通知
dep.notify()
}
})
可以看到,defineReactive内部使用了Object.defineProperty对所有的key进行设置了getter和setter,就是在此时完成了数据劫持,并添加相应的副作用。
接下来看看getter内部添加了什么副作用呢?
getter
get: function reactiveGetter () {
// 获取值
const value = getter ? getter.call(obj) : val
// 判断是否有Dep.target 如果有就代表Dep添加了Watcher实例化对象
if (Dep.target) {
// 加入到dep去管理watcher
dep.depend()
// 如果存在子对象
if (childOb) {
// 也加进去管理
childOb.dep.depend()
// 如果值是数组,要特殊处理
if (Array.isArray(value)) {
// 循环添加watcher
dependArray(value)
}
}
}
return value
},
首先保存原始get返回的value值,因为只添加副作用,不改变值的获取
// 获取值
const value = getter ? getter.call(obj) : val
判断Dep.target,存在就执行dep.depend,再判断childOb,存在就执行childOb.dep.depend(),再判断是否是数组,是数组则执行dependArray
// 判断是否有Dep.target 如果有就代表Dep添加了Watcher实例化对象
if (Dep.target) {
// 加入到dep去管理watcher
dep.depend()
// 如果存在子对象
if (childOb) {
// 也加进去管理
childOb.dep.depend()
// 如果值是数组,要特殊处理
if (Array.isArray(value)) {
// 循环添加watcher
dependArray(value)
}
}
所以Dep.target,dep.depend和dependArray分别是什么呢?
现在来到src\core\observer\dep.js
Dep.target
Dep.target = null
const targetStack = []
// 压栈
export function pushTarget (target: ?Watcher) {
// 压栈
targetStack.push(target)
// target就是watcher dep是Dep对象
Dep.target = target
}
export function popTarget () {
// 出栈
targetStack.pop()
// 成为最后一个元素
Dep.target = targetStack[targetStack.length - 1]
}
在这里面找到Dep.target,可以看到此变量为全局变量,并且结合pushTarget的参数target:? Wathcer,所以Dep.target就是个Watcher实例
dep.depend
// 添加watcher
// 为Watcher.newDeps.push(dep) 一个dep对象
depend () {
// target就是Watcher dep就是dep对象,dep中是否有watcher对象
if (Dep.target) {
// 用当前的watcher调用addDep
// 为了多对多关系,得分析addDep
Dep.target.addDep(this)
}
}
dep.depend方法,判断Dep.target然后执行Dep.target.addDep。这里的Dep.target就是一个Watcher实例,其addDep方法,先留在这,之后依赖收集时一起收了它。所以dep.depend方法其实就是完成依赖收集
dependArray
/**
* Collect dependencies on array elements when the array is touched, since
* we cannot intercept array element access like property getters.
* 在接触数组时收集对数组元素的依赖关系,因为我们不能像属性getter那样拦截数组元素访问。
*/
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
// 判断是否存在__ob__实例,并且每个都调用depend添加wathcer管理
e && e.__ob__ && e.__ob__.dep.depend()
// 递归完数组所有内容,直到不是数组,跳出递归
if (Array.isArray(e)) {
dependArray(e)
}
}
}
接着回到src\core\observer\index.js,找到dependArray函数,其实就是遍历数组,对数组每个元素触发dep.depend收集依赖,因为不能直接对数组进行收集依赖。
分析完这三个方法,就可以返回去分析get内部的操作了
// 判断是否有Dep.target 如果有就代表Dep添加了Watcher实例化对象
if (Dep.target) {
// 加入到dep去管理watcher
dep.depend()
// 如果存在子对象
if (childOb) {
// 也加进去管理
childOb.dep.depend()
// 如果值是数组,要特殊处理
if (Array.isArray(value)) {
// 循环添加watcher
dependArray(value)
}
}
}
当Dep.target也就是Watcher实例存在时,执行dep.depend收集依赖,接着判断是否childOb是否存在,如果存在就执行childOb.dep.depend
额外为Vue.set和Vue.delete收集的依赖
而这个childOb.dep.depend就是我们需要分析的,首先要知道childOb是啥,之前分析过,当data被observe后,如下
data: {
a: {
b : {
c: 1
}
}
}
// 经过observe处理后,会添加__ob__属性
data: {
// 属性 a 通过 setter/getter 通过闭包引用着dep和childOb也就是内部的__ob__
a: {
// 属性 b 通过 setter/getter 通过闭包引用着dep和childOb也就是内部的__ob__
b: {
c: 1
__ob__: {b, dep, vmCount}
}
__ob__: {a, dep, vmCount}
}
__ob__: {data, dep, vmCount}
}
对于属性a来说,其通过getter/setter保持了对dep.subs观察者数组队列的引用,此数组用来收集依赖
另外属性a的setter/getter 还通过闭包引用着childOb,且childOb === data.a.ob 所以 childOb.dep === data.a.ob.dep。
也就是说 childOb.dep.depend() 这句话的执行说明除了要将依赖收集到属性a的dep.subs中,还要将同样的依赖收集到 data.a.ob.dep.subs中
为什么要将同样的依赖分别收集到这两个不同的subs数组中呢?其实答案就在于这两个数组收集的依赖的触发时机是不同的,即作用不同
两个数组如下:
- 第一个数组是dep.subs
- 第二个数组是childOb.subs 也就是data.a.ob.subs
第一个dep.subs中的收集的依赖是在值被修改时触发的,通过set方法内部的dep.notify
第二个childOb.subs中收集的依赖则是在Vue.set给数据对象添加新属性时触发
好,这里肯定都会发出疑问:怎么突然就是给Vue.set时触发了?
首先我们知道,在Vue2.0x中,使用的Object.definePorperty进行响应式,initData只会对事先写在data中的属性进行响应化
如果后面再给data添加属性,vue2是不能将新添加的属性进行响应化的(vue3可以,proxy)
所以vue提供了一个Vue.set方法,向响应式对象中添加一个property,并确保这个新property触发依赖收集,触发视图更新。
那么这个触发依赖收集怎么做到的呢?直接看看set方法吧!
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
* 给对象设置一个属性,添加新属性和添加触发更改通知(dep.notify),如果这个属性不是早已存在
* Vue.set
*/
export function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
// 判断数据 是否是undefined或者null
// 判断数据类型是否是string,number,symbol,boolean
(isUndef(target) || isPrimitive(target))
) {
// target必须是对象或者数组,否则发出警告
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 如果是数组 并且检查key是否是有效的数组索引
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 设置数组长度
target.length = Math.max(target.length, key)
// 像数组尾部添加一个新数据,相当于push
target.splice(key, 1, val)
// 返回val
return val
}
// 如果key在target上 并且不是通过原型链查找的
if (key in target && !(key in Object.prototype)) {
// 赋值
target[key] = val
return val
}
// 声明一个对象ob 值为该target对象中的原型上面的所有方法和属性,表明该数据加入过观察者中
const ob = (target: any).__ob__
// 如果是vue 或者 检测vue被实例化的次数 vmCount
if (target._isVue || (ob && ob.vmCount)) {
// 如果不是生产环境,发出警告
// 避免添加响应式属性给vue实例或者根$data
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 如果ob不存在,证明没有添加观察者,不是相应,直接赋值返回
if (!ob) {
target[key] = val
return val
}
// 通过defineReactive将ob.value加入的观察者
defineReactive(ob.value, key, val)
// 触发通知更新,通知订阅者obj.value更新数据
ob.dep.notify()
return val
}
为了容易理解,把set简化一下,并且直接使用Vue.set给一个对象添加属性
Vue.set = function set(target, key, val) {
// 声明一个对象ob 值为该target对象中的原型上面的所有方法和属性,表明该数据加入过观察者中
const ob = target.__ob__
// 通过defineReactive将ob.value加入的观察者
defineReactive(ob.value, key, val)
// 触发通知更新,通知订阅者obj.value更新数据
ob.dep.notify()
return val
}
Vue.set(data.a, 'lu', 1)
此时ob.dep.notify()其实是data.a.__ob__.dep.notify
而第二个数组是data.a.ob.sub,这是不是就是触发了第二个数组的收集的依赖?
所以ob属性以及ob.dep的主要作用是为了添加、删除属性时有能力触发依赖,而这就是Vue.set和Vue.delete的原理
执行完这些后,来到判断数组,执行dependArray,对数组每个元素进行收集依赖,因为数组不能直接进行收集。
// 如果值是数组,要特殊处理
if (Array.isArray(value)) {
// 循环添加watcher
dependArray(value)
}
好了,getter内部添加的副作用分析完毕了,总结如下
- 如果Dep.target存在,收集依赖
- 如果有childOb,给childOb也收集相同依赖用于Vue.set和Vue.del
- 如果是数组,执行dependArray,遍历数组元素收集依赖
所以这里分析的依赖收集都是要建立在触发了getter函数并且Dep.target存在的情况下!
以上已经差不多将Observer和Dep的关系讲解的很透彻了,每一个经过observe处理的属性都对应一个dep实例,dep内有一个收集依赖(也就是添加观察者watcher)的数组队列subs。
那么Watcher是怎么被添加的呢,也可以说依赖是怎么被收集的呢?
收集依赖是怎么触发的?
怎么收集依赖?上文说到的收集依赖其实就是将Watcher观察者实例加入到对应的dep.subs观察者数组中,所以就来看看Watcher
先忽略掉构造函数内部一大段代码,这涉及到一些属性,之后会带着updateComponent来讲,现在只需要注意最后一段代码
// from src\core\observer\watcher.js class Watcher
constructor (
vm: Component, // dom
expOrFn: string | Function, //获取值的函数,或是更新视图的函数
cb: Function, //回调函数
options?: ?Object, //参数
isRenderWatcher?: boolean //是否是渲染过的watcher
) {
// 上面先忽略
//
this.value = this.lazy
? undefined // 当lazy为真时
: this.get() // lazy不在时 计算getter,并重新收集依赖项。
}
这里通过this.lazy来判断,不过提一句,通常lazy都是不存在的(后面会说),所以都会执行this.get()方法
watcher.get
/**
* Evaluate the getter, and re-collect dependencies.
* 计算getter,并重新收集依赖项。
*/
get () {
// 添加dep.target dep.target = this
pushTarget(this)
let value
const vm = this.vm
try {
// 这时dep.target = this ,然后执行this.getter.call也就触发get方法,判断dep.target是否存在,存在则dep.depend()
// 获取值 触发get 也就触发Object.definePorperty的get中的dep.depend(),依赖收集
// 每个watcher第一次实例化的时候,都会作为订阅者订阅其相应的Dep。
value = this.getter.call(vm, vm)
} catch (e) {
// 如果报错
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
// “触摸”每个属性,以便它们都被跟踪为深度监视的依赖项
if (this.deep) {
// 为 seenObjects 深度收集val 中的key
traverse(value)
}
// 出栈一个dep.target
popTarget()
// 清理依赖项集合
this.cleanupDeps()
}
// 返回值
return value
}
pushTarget
好,get方法一进来,就执行了pushTarget,然后执行value = this.getter.call(vm, vm)
export function pushTarget (target: ?Watcher) {
// 压栈
targetStack.push(target)
// target就是watcher dep是Dep对象
Dep.target = target
}
pushTarget的作用其实就是将Dep.target赋值,而pushTarget(this)里的this指向当前实例化的Watcher对象,所以执行了pushTarget后,Dep.target此时保存的就是当前实例化的Wathcer对象,也就是要被收集的依赖。
watcher.getter(updateComponent)
之后继续执行this.getter
value = this.getter.call(vm, vm)
此时的getter其实是UpdateComponent,其内部会调用_update和_render函数,先不去考虑这些方法,只要记住这个函数的执行就意味着对被观察目标的求值,并将得到的值赋值给 value 变量,最后返回了value
而Watcher实例对象的value属性会保存这个返回值,也就是说Watcher实例对象保存着被观察的值
this.value = this.lazy
? undefined // 当lazy为真时
: this.get() // lazy不在时 计算getter,并重新收集依赖项。
触发属性的getter
当对观察目标进行求值的时候,肯定会触发数据的getter函数
get: function reactiveGetter () {
// 获取值
const value = getter ? getter.call(obj) : val
// 判断是否有Dep.target 如果有就代表Dep添加了Watcher实例化对象
if (Dep.target) {
// 加入到dep去管理watcher
dep.depend()
// 如果存在子对象
if (childOb) {
// 也加进去管理
childOb.dep.depend()
// 如果值是数组,要特殊处理
if (Array.isArray(value)) {
// 循环添加watcher
dependArray(value)
}
}
}
return value
},
进入get后,判断Dep.target是否存在,此时Dep.target是存在的,因为我们执行了this.get方法内部的pushTarget,此时Dep.target就是一个Watcher实例,因此会执行dep.depend()
// 添加watcher
// 为Watcher.newDeps.push(dep) 一个dep对象
depend () {
// target就是Watcher dep就是dep对象,dep中是否有watcher对象
if (Dep.target) {
// 用当前的watcher调用addDep
// :todo 为了多对多关系,得分析addDep
Dep.target.addDep(this)
}
}
watcher.addDep(避免重复收集依赖)
dep.depend一来也判断了Dep.target,同样的,也是存在的,所以执行Dep.target.addDep(this),其实就是执行Watcher.addDep(this),此时的this是dep这个实例对象因此来看addDep
/**
* Add a dependency to this directive.
* 向该指令添加依赖项
*/
addDep (dep: Dep) {
// dep.id 陆续自+
const id = dep.id
// 如果id不存在
if (!this.newDepIds.has(id)) {
// :todo
// 你保存我的引用
// 我也要保存你的引用
// newDepIds添加一个id
this.newDepIds.add(id)
// newDeps添加一个dep
this.newDeps.push(dep)
// 如果depIds中id不存在
if (!this.depIds.has(id)) {
// 给subs数组添加一个Watcher对象
dep.addSub(this)
}
}
}
addDep接收一个dep实例,也就是接收到了属性通过闭包保持引用dep实例对象
首先保存dep实例的唯一id
接下来这波操作啊,很复杂,是当遇到重复的依赖时,只会收集一个依赖
如果不进行避免重复收集依赖操作
当然,先给大家看看为什么要进行这波避免重复收集依赖的操作吧
假设不进行避免重复收集依赖的操作,也就是直接执行addSub
addDep (dep: Dep) {
dep.addSub(this)
}
而addSub其实也只是将Watcher实例添加进了subs数组
addSub (sub: Watcher) {
// 给subs数组添加一个Watcher对象
this.subs.push(sub)
}
现在给出这样式的模板
<div id="app">
<p>{{name}}</p>
<p>{{name}} + {{age}}</p>
</div>
这里,渲染函数会将其解析为如下样式(调试可得)
with (this) {
return _c('div',{attrs:{"id":"app"}},
[
_c('p',[_v(_s(name))]),
_v(" "),
_c('p',[_v(_s(name)+" + "+_s(age))])
]
)
}
可以看到,渲染函数会读取两次name进行求值,也就相应的触发了两次getter,接着触发两次addDep和两次addSub
因此这样会出现同一个Watcher实例被收集多次的问题,所以会在addDep中进行避免重复收集依赖操作
如何避免重复收集依赖
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
首先判断newDepIds中是否有相同的dep实例的id,如果有,代表存在重复的,就不会进行下面操作,如果没有,就将id添加进newDepIds数组中,并且把对应的dep实例添加进newDeps数组中。
所以此时无论一个属性重复出现多少次,都只会收集一个Wathcer实例(依赖),并且当其改变时,会通知所有相同的属性都改变(这里涉及到通知更新)
这样已经完成了避免重复收集依赖操作,但是代码还没完
一次求值和多次求值避免重复收集依赖
既然已经完成了避免处理,那这段代码又是做了什么呢
if (!this.depIds.has(id)) {
dep.addSub(this)
}
好,带着这个问题,继续回去分析watch的get方法
// from class watch
// get方法
finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
// “触摸”每个属性,以便它们都被跟踪为深度监视的依赖项
if (this.deep) {
// 为 seenObjects 深度收集val 中的key
traverse(value)
}
// 出栈一个dep.target
popTarget()
// 清理依赖项集合
this.cleanupDeps()
}
在get的finally中,会执行cleanupDeps
cleanupDeps
来分析cleanupDeps
/**
* Clean up for dependency collection.
* 清理依赖项集合。
*/
cleanupDeps () {
// 获取deps长度
let i = this.deps.length
// 遍历
while (i--) {
const dep = this.deps[i]
// 如果在newDepIds中不存在dep的id
if (!this.newDepIds.has(dep.id)) {
// 清楚依赖项
dep.removeSub(this)
}
}
// 互换
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
// 换完后
this.newDepIds.clear() // 清空对象
// 互换
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
// 换完后
this.newDeps.length = 0 // 清空数组
}
while内部进行移除废弃的观察者,得先看后面的关键再来理解这个while
看,这里最后面,两个经典的互换和清空,将newDepIds内容全部给了depIds,并清空newDepIds,将newDeps内容全部给了deps,并清空newDeps数组。
这也就代表着,每一次执行watcher.get方法时,最后都会将newDepids和newDeps用depIds和deps保存,并且清除掉newDepids和newDeps。
所以上一次求值的id和deps都会保存在depIds和deps中。
那么回到addDep中
addDep (dep: Dep) {
const id = dep.id
// 如果id在newDepids中不存在,直接不做任何操作
if (!this.newDepIds.has(id)) {
// newDepIds添加一个id
this.newDepIds.add(id)
// newDeps添加一个dep
this.newDeps.push(dep)
// 如果depIds中id不存在,也就是总共的depIds中没有这个id时,才添加
if (!this.depIds.has(id)) {
// 给subs数组添加一个Watcher对象
dep.addSub(this)
}
}
}
首先,判断newDepids中是否有相同id,如果没有,就给newDepIds和newDeps分别加入id和dep,然后判断这个id是否在总共的depIds中存在,如果不存在,才会进行addSub添加
一次求值和多次求值避免重复收集依赖总结
所以可以总结出:
- newDepIds是用来避免在一次求值中收集重复的依赖,因为每次执行完后,都会将其赋给depIds后清空
- depIds是用来避免在多次求值中收集重复的依赖
移除废弃依赖
这里再回去分析内部的while移除废弃观察者
// from cleanupDeps
// 获取deps长度
let i = this.deps.length
// 遍历
while (i--) {
const dep = this.deps[i]
// 如果在newDepIds中不存在dep的id
if (!this.newDepIds.has(dep.id)) {
// 清楚依赖项
dep.removeSub(this)
}
}
可以看到,while遍历的其实是上一次保存的deps,也就是上一次收集到的依赖数组,然后进行判断,上一次收集的依赖是否会在这一次收集的依赖中,如果不存在,代表这个依赖已经被废弃,所以调用dep.removeSub进行删除
removeSub (sub: Watcher) {
// 删除watcher对象
remove(this.subs, sub)
}
removeSub就是将Watcher观察者从当前subs数组中删除
收集依赖总结
到这里,依赖收集已经分析完毕了
总结一下:
- 首先会执行watcher.get方法,触发pustTarget
- pustTarget给Dep.target赋值当前Wathcer实例然后调用this.getter
- this.getter(updateComponent内部有_update和_render函数),render函数会对属性求值,也就触发了属性的get操作
- 属性的get操作会判断Dep.target是否存在
- 此时Dep.target是存在的,然后执行dep.depend
- dep.depend执行Wathcer.addDep
- Wathcer.addDep内部进行了避免重复收集依赖的操作,并且收集依赖
- 执行完addDep后,此次依赖收集就完成了
但是我们分析的依赖收集都是要建立在new Wathcer时,因为只有new Watcher时才会执行构造函数内部的get方法,才会进行那些依赖收集,那么在哪里new了Watcher呢?
跟着mountComponet来分析Watcher
通过在文件夹中寻找new Watcher
可以看到src\core\instance\lifecycle.js和src\core\instance\state.js中都有new Watcher
不过经过调试可得知:state.js中的watcher只有在有computed属性时才会执行到。
而lifecycle.js中new Watcher是在mountComponent中的,这是必定会发生的,因为无论是手动挂载或者是vue自动挂载,都会执行到mountComponent
因此我们通过这个mountComponet内部的new Wathcer来分析Watcher
// mountComponent :安装组件
export function mountComponent(
vm: Component, //vnode
el: ? Element, //dom
hydrating ? : boolean //ssr相关
): Component {
// 忽略
// 执行生命周期 beforeMount 钩子函数
callHook(vm, 'beforeMount')
// 更新组件
let updateComponent
/* istanbul ignore if */
// 忽略 如果开发环境
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
// 忽略 看性能用的
}
} else {
// updateComponet函数 直接更新view视图
updateComponent = () => {
vm._update(
/*
render 是 虚拟dom,需要执行的编译函数 类似于这样的函数
(function anonymous( ) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('input',{directives:[{name:"info",rawName:"v-info"},{name:"data",rawName:"v-data"}],attrs:{"type":"text"}}),_v(" "),_m(0)])}
})
*/
vm._render(),
// ssr相关
hydrating
)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 我们在观察者的构造函数中设置vm._watcher
// 因为观察者的初始patch可能调用$foreceUpdate(例如 inside child 组件的挂载钩子)
// 它依赖于已经定义的vm._watcher
new Watcher(
vm, //vnode
updateComponent, //上面的更新视图函数
noop, //回调函数
{
before() {
// 如果已经挂载并且没有被销毁
if (vm._isMounted && !vm._isDestroyed) {
// 触发生命周期 beforeUpdate 钩子函数
callHook(vm, 'beforeUpdate')
}
}
},
true /* isRenderWatcher */ )
//忽略
}
我已经将mountComponent简化了,其他部分都忽略掉,只关注其updateComponent(作为new Wathcer时的第二个参数)和new Watcher
upadateComponent
所以先看updateComponent,if中的updateComponent可以省略,所以直接看else里
// updateComponet函数 直接更新view视图
updateComponent = () => {
vm._update(
/*
render 是 虚拟dom,需要执行的编译函数 类似于这样的函数
(function anonymous( ) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('input',{directives:[{name:"info",rawName:"v-info"},{name:"data",rawName:"v-data"}],attrs:{"type":"text"}}),_v(" "),_m(0)])}
})
*/
vm._render(),
// ssr相关
hydrating
)
}
updateComponent内部_update函数,_update函数将vue.prototype._render函数的返回值作为参数
upadateComponent内的_render和_update
因为这里涉及到render,所以直接给出其功能:
- _render函数返回一个虚拟DOM,其经过compiler的parse,optimize,generate之后返回一个虚拟DOM
- _update函数就是将这个虚拟DOM通过patch转换成真实DOM
所以updateComponent的功能就是将虚拟DOM转为真实DOM
new Watcher传参分析
然后结合Watcher构造函数的参数来分析new Watcher的传参
new Watcher(
vm, //vnode
updateComponent, //上面的更新视图函数
noop, //回调函数
{
before() {
// 如果已经挂载并且没有被销毁
if (vm._isMounted && !vm._isDestroyed) {
// 触发生命周期 beforeUpdate 钩子函数
callHook(vm, 'beforeUpdate')
}
}
},
true /* isRenderWatcher */ )
// 结合watcher constructor 来自watcher.js class Watcher
constructor (
vm: Component, // dom
expOrFn: string | Function, //获取值的函数,或是更新视图的函数
cb: Function, //回调函数
options?: ?Object, //参数
isRenderWatcher?: boolean //是否是渲染过的watcher
)
- 第一个参数:vm 传入vm 也就是当前vm实例
- 第二个参数:expOrFN 传入updateComponent函数
- 第三个参数:cb 传入noop 空函数
- 第四个参数:options 传入一个包含before函数的对象
- 第五个参数: isRenderWatcher 传入true
new Watcher传参结合Watcher constructor分析
传完参数之后,就带着这些参数来分析Watcher constructor吧
watcher.vm
// 获取到vm
this.vm = vm
获取组件实例,vm属性代表观察者所属哪个组件
watcher._watcher
// 如果是渲染函数的watcher
if (isRenderWatcher) {
// 把当前Watcher对象给_wathcer
vm._watcher = this
}
如果是渲染函数的watcher,把Wathcer实例赋给当前组件实例的_watcher属性
// 把观察者添加到_watchers数组中
vm._watchers.push(this)
将观察者添加到组件实例的_watchers数组中,也就是说所有这个组件中的观察者都会加入到这个数组中
watcher属性(deep,user,lazy,sync,before)声明
// 如果有options
if (options) {
// 获取参数
this.deep = !!options.deep // 是否深度观察
this.user = !!options.user //
this.lazy = !!options.lazy // 是否懒惰观察,也就是不观察
this.sync = !!options.sync // 是否同步求值
this.before = options.before // before 算是回调钩子函数 组件更新前触发
} else {
// 否则都为false
this.deep = this.user = this.lazy = this.sync = false
}
给vm设置deep,user,lazy,sync属性,因为传入的options是一个只包含before函数的对象,所以这四个属性都为false,before则为before函数
watcher属性(cb,active,dirty)
this.cb = cb // 回调函数
this.active = true // 激活 涉及到后面组件更新
this.dirty = this.lazy // for lazy watchers 用于懒惰的观察者
给组件实例添加cb属性为传入的noop空函数,添加active属性为true,添加dirty属性
watcher属性(id,deps,newDeps,depIds,newDepIds,避免重复收集依赖)
this.id = ++uid // uid for batching uid用于批处理
this.deps = [] // 观察者队列
this.newDeps = [] // 同样是观察者队列 但每一次求值最后都会赋给deps,并清空数组
this.depIds = new Set() // depId 不可重复
this.newDepIds = new Set() // 每一次求值最后都会赋给depIds 并clear set 不可重复
这里就是之前提到的避免重复收集依赖的数组和set了
newDeps和newDepIds在每一次求值后都会执行cleanupDeps,赋值给deps和depIds,并清空数组和clear set
watcher.getter(此时是updateComponent)
if (typeof expOrFn === 'function') {
// 获取值函数
this.getter = expOrFn
} else {
// 调用parsePath返回值 返回一个遍历所有属性的函数,也是触发get
this.getter = parsePath(expOrFn) //updateComponent
if (!this.getter) {
// 如果不存在
// 给一个noop空函数
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
expOrFn就是传入的updateComponent函数
首先进行判断,如果是函数,就直接把updateComponent赋给this.getter,这也印证了上面我说的this.getter就是updateComponent
如果不是函数,将调用parsePath,这个函数返回一个遍历路径分割成数组后每个元素的函数,也相当于触发get操作
如果getter不是函数,并且getter不存在,给getter一个noop空函数
/**
* Parse simple path.
* 解析简单路径
*/
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
// 匹配不是 数字字母下划线 $符号 开头的为true
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
//将对象中的一个key值 赋值给该对象 相当于 obj = obj[segments[segments.length-1]];
obj = obj[segments[i]]
}
return obj
}
}
首先判断,如果是数字字母下划线$符号开头的,直接返回
然后将路径通过.分割成数组,返回一个遍历这些数组元素的函数,也就触发了get操作,这个函数主要用于data.xiaolu.name这种的调用
执行watcher.get方法
//
this.value = this.lazy
? undefined // 当lazy为真时
: this.get() // lazy不在时 计算getter,并重新收集依赖项。
最后,判断lazy属性,可以从前面看出,lazy为false,所以会执行this.get方法,并将返回值赋给value属性
跟随着mountComponent传参分析watcher总结
好,调用get方法后,又回到了之前的收集依赖的开始过程,进行依赖收集了!
这样跟随着mountComponent传入的参数,我们把Watcher也分析的差不多了!
触发通知更新
现在就要开始分析当改变值的时候触发的set操作了
首先要知道,触发通知更新的前提是依赖收集完毕
<div id="data">
{{name}}
</div>
现在,给一个这样的模板,按照之前的说法
在mountComponent中,首先会new Watcher实例,因此触发watcher.get方法,相应执行updateComponent,也就是执行render函数,而我们知道template模板会通过compiler编译成render函数,而render函数会对这个name属性进行求值,因此会触发name属性的getter,从而收集依赖。此时依赖已经收集完成
setter
这样当我们修改name属性时,也就会触发name属性的setter,这时候就会调用dep.notify去通知更新了
set: function reactiveSetter (newVal) {
// 获取旧value值
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
// 新旧值比较 如果是一样则不执行了
return
}
// 忽略
// 当set时触发通知
dep.notify()
}
新旧值对比
首先会通过getter获取一波旧值,然后比较新旧值,如果一样,就直接返回了
// 获取value值
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
// 新旧值比较 如果是一样则不执行了
return
}
dep.notify
然后进行一些相应的更改值的操作,这里忽略掉,直接看dep.notify
// 通知所有watcher对象更新视图,也就是执行update
notify () {
// stabilize the subscriber list first
// 浅拷贝一份subs数组,也就是Watchers列表
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
// 如果不运行async,则不会在调度程序中对sub进行排序
// 我们现在需要对它们进行分类以确保它们发射正确秩序
subs.sort((a, b) => a.id - b.id)
}
// 所有subs中的wathcers执行update函数,也就是更新
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
浅拷贝dep.subs观察者数组
dep.notify()调用,因此this指向dep实例,所以刚开始,浅拷贝一份这个dep实例的观察者数组subs
const subs = this.subs.slice()
if语句块中的是处理同步调用的,现在不需要看
遍历执行wathcer.update
直接看最后的for循环,将浅拷贝的观察者数组subs进行遍历,取到里面的观察者Watcher实例,并让其执行update方法
// subs中的所有wathcers执行update函数,也就是更新
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
update
因此,我们又要跳到wathcer的update方法来分析了
update () {
/* istanbul ignore else */
// 如果是懒惰的lazy
if (this.lazy) {
//
this.dirty = true
} else if (this.sync) { //如果是同步
//
this.run()
} else {
// 异步队列
// 数据并不会立即更新,而是异步,批量排队执行
queueWatcher(this)
}
}
首先判断lazy属性,这是懒惰的观察者,关于计算属性的,在之前传参里我们可以知道这个lazy为false,因此不执行
再判断sync属性,此时sync属性也是false,因此跳到下面的else(这几个属性的声明,我留了目录)
执行queueWatcher方法
这时候执行queueWathcer,内部涉及太多函数和属性了,这些都是在src\core\observer\scheduler.js文件中声明的,所以要先来分析这个文件
scheduler.js分析
声明变量
export const MAX_UPDATE_COUNT = 100
const queue: Array<Watcher> = []
const activatedChildren: Array<Component> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0
首先,声明变量:
- MAX_UPDATE_COUNT: 最大循环次数,超过此次数,报错
- queue: 记录观察者队列的数组
- activatedChildren 记录活跃的子组件数组
- has: 记录观察者的id的对象
- circular: 持续循环更新的次数,如果超过100次 则判断已经进入了死循环,则会报错
- waiting: 观察者在更新数据时候 等待的标志
- flushing: 进入flushSchedulerQueue 函数等待标志
- index: queue观察者队列的索引
resetSchedulerState重置状态
/**
* Reset the scheduler's state.
* 重置计划程序的状态
* 也就是清空观察者watcher队列中所有数据
*/
function resetSchedulerState () {
// 观察队列长度和活跃子组件长度都变为0
index = queue.length = activatedChildren.length = 0
// 观察者记录的id
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
// 两个等待标志设为false
waiting = flushing = false
}
resetSchedulerState的声明
功能:清空观察者watcher队列中所有数据
- 将所有数组清空,对象重置为空对象,标志都设置为false
getNow(Date.now和performance.now的选择)
export let currentFlushTimestamp = 0
let getNow: () => number = Date.now
if (
inBrowser && //如果是浏览器
window.performance && //如果performance存在
typeof performance.now === 'function' && // 如果performance.now是函数
document.createEvent('Event').timeStamp <= performance.now() //如果时间戳小于现在
) {
getNow = () => performance.now()
}
这一段则是对getNow函数的声明
首先getNow其实就是Date.now,但是为了更精确,会去判断performace.now是否可用,如果可用就将getNow设置为performance.now
因为performance.now()是当前时间与performance.timing.navigationStart的时间差,
以微秒(百万分之一秒)为单位的时间,与 Date.now()-performance.timing.navigationStart
的区别是不受系统程序执行阻塞的影响,因此更加精准。
所以getNow的功能就是:获取当前时间戳,可能使用Date.now或者Performance.now
callUpdatedHooks
// 触发 updated生命周期钩子函数
function callUpdatedHooks (queue) {
// 获取观察者队列长度
let i = queue.length
// 遍历
while (i--) {
const watcher = queue[i]
// 获取到虚拟dom
const vm = watcher.vm
// 如果有watcher 并且 已经mounted并且没被Destroyed
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
// 触发updated生命周期钩子函数
callHook(vm, 'updated')
}
}
}
遍历queue观察者队列,获取到每个观察者上的vm属性,这是在new Watcher时记录的,记录当前的组件
然后判断vm._watcher,这在之前wathcer中也提到过,_watcher是代表渲染函数的watcher,因此这个判断是:如果是渲染函数的观察者,并且挂载了但没被销毁
在满足这个条件时,就会调用callHook(vm, ‘updated’),这个组件就会触发updated生命周期钩子函数
queueActivatedComponent
// 添加活跃的组件函数,把活跃的vm添加到activatedChildren中
export function queueActivatedComponent (vm: Component) {
vm._inactive = false
activatedChildren.push(vm)
将组件的_inactive设为false,然后加入到activatedChildren记录活跃子组件队列中
callActivatedHooks
// 调用组件激活的钩子
function callActivatedHooks (queue) {
// 遍历观察者队列
for (let i = 0; i < queue.length; i++) {
queue[i]._inactive = true
activateChildComponent(queue[i], true /* true */)
}
}
遍历queue观察者队列,将queue中所有的watcher的_inactive属性设为true,然后调用activateChildComponent函数,此函数来自于src\core\instance\lifecycle.js,功能是判断是否有不活跃的组件 禁用他 如果有活跃组件则触发钩子函数activated
queueWatcher
// 将观察者watcher推进到观察者队列中,过滤重复id,除非是刷新队列时推送
export function queueWatcher (watcher: Watcher) {
// 获取id
const id = watcher.id
if (has[id] == null) {
has[id] = true
// 如果进入flushSchedulerQueue 函数等待标志 为false
if (!flushing) {
// 把观察者添加到队列中
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
// 如果已经刷新,则根据其id拼接观察程序
// 如果已经超过了它的id,它将立即运行。
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
//根据id大小拼接插入在数组的哪个位置
queue.splice(i + 1, 0, watcher)
}
// queue the flush
// 观察者在更新数据时候 等待的标志 为false
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
// 刷新两个队列并运行观察程序
// 更新观察者,运行watcher.run(),并且调用组件更新和激活的钩子
flushSchedulerQueue()
return
}
// 更新观察者 运行观察者watcher.run() 函数 并且调用组件更新和激活的钩子
// 异步清空回调函数队列
nextTick(flushSchedulerQueue)
}
}
}
获取id(避免重复的观察者入列)
这里开始一步步分析了
// 获取id
const id = watcher.id
if (has[id] == null) {
has[id] = true
// 忽略
}
先获取到watcher的id属性,这是唯一的,然后通过has对象是否有id这个属性来避免重复的观察者入列的操作
因为入队列后的观察者,会将has[id]置为true,下一次重复id观察者再想进来时,就会被这个has[id]==null的判断给挡下
watcher添加进queue
// 如果进入flushSchedulerQueue 函数等待标志 为false
if (!flushing) {
// 把观察者添加到队列中
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
// 如果已经在更新了,则根据其id拼接观察程序
// 如果已经超过了它的id,它将立即运行。
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
//根据id大小拼接插入在数组的哪个位置
queue.splice(i + 1, 0, watcher)
}
判断flushing属性,当进入flushSchedulerQueue这个函数后,flushing会被设置为true,代表此时正在执行更新
所以这段代码意思其实就是:当我没在执行更新时,你可以把观察者直接添加入queue队列,如果是在更新时,则根据id拼接进queue,如果执行顺序id已经超过了其id,它将在下一个立即运行。(根据id排序执行顺序,flushSchedulerQueue这里面的操作)
执行nextTick(flushSchedulerQueue)
// queue the flush
// 观察者在更新数据时候 等待的标志 为false
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
判断waiting属性,在开头设置的waiting为false,所以会进入,并将waiting设为true,这意味着无论调用多少次 queueWatcher 函数,该 if 语句块的代码只会执行一次
然后if语句中if,这是我们不需要管的,因此直接看最后
nextTick(flushSchedulerQueue)
这里涉及到两个函数,我们先分析nextTick这个函数,而要分析清楚nextTick,也要把相应的文件分析清楚,src\core\util\next-tick.js
next-tick.js分析
export let isUsingMicroTask = false
// 回调函数队列
const callbacks = []
// pending状态
let pending = false
同样,声明变量:
- isUsingMicroTask: 是否使用了微任务
- callbacks: 回调函数队列
- pending: pending状态
flushCallbacks
// 执行所有Callback队列中的所有函数
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
将pending置为false,浅拷贝一份callbacks回调函数队列,将callbacks数组清空,然后遍历将拷贝的回调函数全部执行
功能:执行callbacks回调函数队列中所有函数,并将callbacks数组清空
timerFunc 异步更新的原由
// timerFunc函数
let timerFunc
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
// nextTick行为利用可以访问的微任务队列
// 通过自带的Promise.then。或是MutationObserver。
// MutationObserver有更广泛的支持,但是它被严重地窃听进来了
// 当触发in-touch事件处理程序时,iOS>=9.3.3中的UIWebView。它
// 触发几次后完全停止工作。。。所以,如果是有自带的
// Promise可用,我们将使用它:
// 默认使用Promise解决方法 关于宏任务微任务 优先度 Promise是微任务
/* istanbul ignore next, $flow-disable-line */
// 如果Promise存在并且native
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 使用Promise
const p = Promise.resolve()
timerFunc = () => {
// 通过Promise微任务清空回调队列
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
// 在有问题的uiwebview中,Promise.then不会完全崩溃,但是
// 它可能会陷入一种奇怪的状态,即回调被推送到
// 微任务队列,但队列不会被刷新,直到浏览器
// 需要做一些其他的工作,例如处理计时器。所以我们可以
// 通过添加空计时器“强制”刷新微任务队列。
// 添加空计时器 强制刷新微任务队列
if (isIOS) setTimeout(noop)
}
// 使用微任务 为true
isUsingMicroTask = true
// 如果Promise不能用,用MutationObserver
// 如果不是IE 并且MutationObserver存在并native
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
// 如果本地的Promise不可用,请使用MutationObserver,
// 例如PhantomJS,iOS7,Android 4.4
//(#6466 MutationObserver在IE11中不可靠)它会在指定的DOM发生变化时被调用。
// 通过MutationObserver的方式执行
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
// MutationObserver还是微任务
isUsingMicroTask = true
// 如果MutationObserver还不能用,判断setImmediate是否存在并native,用setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Techinically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
// 回退到setImmediate。
// 在技术上,它利用(宏)任务队列
// 但它仍然是比setTimeout更好的选择。
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
// 最后回退到使用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
timerFunc就比较有意思了,这就是vue异步处理任务的关键了
首先timerFunc会依次判断Promise,MutationObserver,setImmediate,setTimeout这四个异步调用,但是是有区别的,Promise和MutationObserver是MicaroTask微任务,而setImmediate和setTimeout是MacaroTask宏任务,关于微任务,宏任务,这里涉及到EventLoops了,不过记住微任务肯定是好的了,不然vue内部也不会这样按顺序去依次判断
所以timerFunc 最后都是通过异步调用flushCallbacks,也就是异步的执行清空回调函数队列,也就是异步执行回调函数
接下来看next-tick
next-tick
// 为callbacks 收集队列cb函数 并且根据 pending 状态是否要触发callbacks 队列函数
// 异步清空回调函数队列
export function nextTick (
cb?: Function, // 回调函数
ctx?: Object //this指向
) {
let _resolve
// 向callbacks回调函数队列添加一个函数
callbacks.push(() => {
// 如果cb存在
if (cb) {
try {
// 指向cb这个函数
cb.call(ctx)
} catch (e) {
// 如果不是函数,报错
handleError(e, ctx, 'nextTick')
}
// 如果_resolve存在
} else if (_resolve) {
// 执行_resolve
_resolve(ctx)
}
})
if (!pending) {
pending = true
// 通过异步 清空回调任务队列
timerFunc()
}
// $flow-disable-line
// 如果cb不存在 并且Promise存在
if (!cb && typeof Promise !== 'undefined') {
// 返回一个Promise
return new Promise(resolve => {
_resolve = resolve
})
}
}
一步一步分析
let _resolve
// 向callbacks回调函数队列添加一个函数
callbacks.push(() => {
// 如果cb存在
if (cb) {
try {
// 指向cb这个函数
cb.call(ctx)
} catch (e) {
// 如果不是函数,报错
handleError(e, ctx, 'nextTick')
}
// 如果_resolve存在
} else if (_resolve) {
// 执行_resolve
_resolve(ctx)
}
})
会声明一个_resolve变量,然后像callbacks回调函数队列添加一个函数
函数内部会判断cb,cb是nextTick的参数,之前我们是通过这样调用nextTick(flushSchedulerQueue),因此cb就是flushSchedulerQueue
所以cb存在,因此callbacks回调函数队列其实添加的就是一个包含flushSchedulerQueue执行的函数,也就是当清空回调函数队列时,调用回调函数里的函数,也就是相当于执行了flushSchedulerQueue
if (!pending) {
pending = true
// 通过异步 清空回调任务队列
timerFunc()
}
然后判断pending,此时pending是false,所以执行,pending置为true,然后执行timerFunc(),异步清空回调任务队列
而此时回调任务队列中,是flushSchedulerQueue函数,所以我们最后回到src\core\observer\scheduler.js文件中分析这最后的flushSchedulerQueue函数
flushSchedulerQueue
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
// 遍历观察者数组
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
// 执行run
watcher.run()
// in dev build, check and stop circular updates.
// 在dev build中,检查并停止循环更新。
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
一步一步分析
获取时间戳和排序
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
- 首先通过getNow获取当前时间戳,可能以Date.now或者performance.now获取
- 接着将flushing置为true,这在之前添加进queue观察者队列时提到过,为true代表正在执行更新
- 声明wathcer和id两个变量
- 刷新前对队列排序,这样可以确保:
-
- 组件从父级更新到子级。(因为父对象总是在子对象之前创建)
-
- 组件的用户观察程序在其渲染观察程序之前运行(因为用户观察程序是在渲染观察程序之前创建的)
-
- 如果某个组件在父组件的观察程序运行期间被破坏,则可以跳过它的观察程序。
执行watcher.run
// do not cache length because more watchers might be pushed
// as we run existing watchers
// 当我们运行现有的观察者时,不要缓存长度,因为可能会推送更多观察者
// 遍历观察者数组
for (index = 0; index < queue.length; index++) {
// 获取单个观察者
watcher = queue[index]
// 如果存在before
if (watcher.before) {
watcher.before()
}
// 获取id
id = watcher.id
has[id] = null
// 运行观察者
watcher.run()
}
- 首先遍历观察者数组,这里的数组是会动态增加的,之前提到过,在执行更新时,如果有观察者Watcher想入列,会根据id对应插入,如果id是在之前已经被执行过id的里面,它将在下一个立即运行。
- 获取每个观察者,看是否有before这个方法,我们这之前分析过,在mountComponent是new Wathcer时传入的options中就有before这个方法,并且保存在了watcher.before中,因此这会调用此方法,会判断当前组件是否已经挂载并且还没被销毁,满足这个条件,就会触发beforeUpdate生命周期钩子函数
before() {
// 如果已经挂载并且没有被销毁
if (vm._isMounted && !vm._isDestroyed) {
// 触发生命周期 beforeUpdate 钩子函数
callHook(vm, 'beforeUpdate')
}
}
- 获取id,并将has中id置为null
- 执行watcher.run() !!!这就是更新的关键了!!!
watcher.run
此时,先来分析run,这就是每次更新数据的关键了,看懂这个,以后就不用再说通知watcher执行update函数更新了
run () {
// 如果是活跃
if (this.active) {
// 获取值
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
// 深度观察者和对象/数组上的观察者应该是相同的
// 当值相同时,因为值可能变异了。
// 是否是对象
isObject(value) ||
// 获取deep 如果为true
this.deep
) {
// set new value
// 设置新值
const oldValue = this.value
// 赋值新值
this.value = value
// 如果是user
if (this.user) {
try {
// 更新回调函数
this.cb.call(this.vm, value, oldValue)
} catch (e) {
// 如果出错
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
// 如果不是user,更新回调函数 获取到新的值 和旧的值
this.cb.call(this.vm, value, oldValue)
}
}
}
}
- 首先判断这个wathcer实例的active属性,在wathcer的构造函数初始化时,active属性默认就是true,所以会执行if语句内部代码块
- 进入if后,执行了const value = this.get(),就是这里了,至关重要的一次执行。之前也说过,渲染函数的观察者执行get方法,会执行它的getter方法,这时getter方法就是updateComponent(内部_update和_render),之前也说了updateComponent作用就是将render渲染的虚拟DOM转为真实DOM,所以这里其实是完成了一次重新渲染
- 而在后面的if判断,渲染函数的观察者是不会执行这后面的,因为this.get()的返回值其实也就是updateComponent的返回值,这个返回值是undefined
- 而后面这个if判断其实是给非渲染函数的观察者值变更时使用的,更改新的值:比如计算属性computed或watch监听属性
//
{
// set new value
// 设置新值
const oldValue = this.value
// 赋值新值
this.value = value
// 如果是user
if (this.user) {
try {
// 更新回调函数
this.cb.call(this.vm, value, oldValue)
} catch (e) {
// 如果出错
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
// 如果不是user,更新回调函数 获取到新的值 和旧的值
this.cb.call(this.vm, value, oldValue)
}
对于执行到这里的,已经不是渲染函数的观察者了,所以会将老值保存,新值赋值,再通过是否是user执行回调,是user放在try/catch里是因为watch是用户自己写的监听回调函数,各种形式都有,所以要放在try/catch中,而不是用户写的就直接执行回调
执行完watcher.run后还需执行的代码
// flushSchedulerQueue内部执行完watcher.run方法后的还需执行的代码
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
// 调用组件更新并激活钩子函数
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
// 触发父层flush事件钩子函数
if (devtools && config.devtools) {
devtools.emit('flush')
}
在执行完watcher.run后,还要继续执行这些
- 浅拷贝activatedChildren和queue一份给activatedQueue和updatedQueue
- 执行resetSchedulerState,将activatedChildren和queue的数组都清空,一些状态都置为false,has对象也被清空
- callActivatedHooks queue中所有watcher的_inactive属性置为true,并判断是否有不活跃的组件 禁用他 如果有活跃组件则触发钩子函数activated
- callUpdatedHooks,触发updated生命周期钩子函数
当触发玩updated生命周期钩子函数时,这次触发通知更新,已经全部完成了!
触发通知更新总结
总结:
- 数据发生更改,触发setter
- set会执行dep.notify
- dep.notify会去遍历dep.subs观察者数组,遍历执行watcher.update方法
- watcher.update执行了queueWatcher
- queueWatcher执行了nextTick(flushSchedulerQueue)
- nextTick(flushSchedulerQueue)先把flushSchedulerQueue添加进callbacks回调函数数组
- 其次nextTick执行timerFunc
- timerFunc是异步的(通过Promise,MutationObserver,setImmediate,setTimeout这四种异步方法)执行flushCallbacks
- flushCallbacks是异步执行callbacks中所有回调函数,也就是异步执行添加进去的flushSchedulerQueue
- flushSchedulerQueue会获取时间戳,对id进行排序,最主要是遍历执行watcher.run方法
- watcher.run执行watcher.getter
- watcher.getter也就是执行updateComponent
- updateComponent将虚拟DOM转换成真实DOM,这里就完成了数据更改后的重新渲染
- 之后返回flushSchedulerQueue中,继续执行后续函数
- 先resetSchedulerState重置状态
- callActivatedHooks调用组件更新并激活钩子函数
- callUpdatedHooks触发updated生命周期钩子函数
- 至此!一个触发通知更新完成
响应式原理总结
这一切都是从initData和render函数的watcher开始分析的
- 从initData开始,initData函数最后会执行observe(data, true)
- observe会判断是否拥有__ob__属性,因为是第一次初始化,所以肯定是没有的,因此会执行new Observer
- new Observer一个实例时会在内部也new一个dep实例,并且添加__ob__属性,再对数组的元素和对象的属性添加数据劫持(defineReactive)
- 当挂载时,也就是执行vue.$mount这个函数时,会执行mountComponet
- mountComponet会声明updateComponet并将updateComponet作为第二个参数去执行new Watcher
- new Wathcer构造函数内部会声明一系列属性,最后会执行watcher.get方法
- 执行watcher.get方法,会触发pustTarget
- pustTarget给Dep.target赋值为当前Wathcer实例然后调用watcher.getter
- watcher.getter(updateComponent内部有_update和_render函数),_render函数会对属性求值,也就触发了属性的get操作
- 属性的get操作会判断Dep.target是否存在
- 此时Dep.target是存在的,然后执行dep.depend
- dep.depend执行Wathcer.addDep
- Wathcer.addDep内部进行了避免重复收集依赖的操作,并且收集依赖
- 执行完addDep后,此次依赖收集就完成了
- 数据发生更改,触发setter
- set会执行dep.notify
- dep.notify会去遍历dep.subs观察者数组,遍历执行watcher.update方法
- watcher.update执行了queueWatcher
- queueWatcher执行了nextTick(flushSchedulerQueue)
- nextTick(flushSchedulerQueue)先把flushSchedulerQueue添加进callbacks回调函数数组
- 其次nextTick执行timerFunc
- timerFunc是异步的(通过Promise,MutationObserver,setImmediate,setTimeout这四种异步方法)执行flushCallbacks
- flushCallbacks是异步执行callbacks中所有回调函数,也就是异步执行添加进去的flushSchedulerQueue
- flushSchedulerQueue会获取时间戳,对id进行排序,最主要是遍历执行watcher.run方法
- atcher.run执行watcher.getter
- watcher.getter也就是执行updateComponent
- updateComponent将虚拟DOM转换成真实DOM,这里就完成了数据更改后的重新渲染
- 之后返回flushSchedulerQueue中,继续执行后续函数
- 先resetSchedulerState重置状态
- callActivatedHooks调用组件更新并激活钩子函数
- callUpdatedHooks触发updated生命周期钩子函数
- 至此!一个响应式从头到尾正式完成!
彩蛋:traverse.js 深度监听
最后observer文件夹中我们只剩下了一个文件还没分析:traverse.js
其traverse的调用是在wathcer.get方法中
get () {
// 添加dep.target dep.target = this
pushTarget(this)
let value
const vm = this.vm
try {
// 这时dep.target = this ,然后执行this.getter.call也就触发get方法,判断dep.target是否存在,存在则dep.depend()
// 获取值 触发get 也就触发Object.definePorperty的get中的dep.depend(),依赖收集
// 每个watcher第一次实例化的时候,都会作为订阅者订阅其相应的Dep。
value = this.getter.call(vm, vm)
} catch (e) {
// 如果报错
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
// “触摸”每个属性,以便它们都被跟踪为深度监视的依赖项
if (this.deep) {
// 为 seenObjects 深度收集val 中的key
traverse(value)
}
// 出栈一个dep.target
popTarget()
// 清理依赖项集合
this.cleanupDeps()
}
// 返回值
return value
}
会根据其deep属性是否存在,去执行traverse,我们假设deep存在,去分析这个函数,来到rc\core\observer\traverse.js
const seenObjects = new Set()
export function traverse (val: any) {
// 为seenObjects深度收集val中的key
_traverse(val, seenObjects)
seenObjects.clear()
}
可以看到traverse其实是调用_traverse
// 为seenObjects深度收集val中的key
function _traverse (val: any, seen: SimpleSet) {
let i, keys
// 是否是数组
const isA = Array.isArray(val)
// 如果不是数组并且不是对象或被冻结 或是Vnode实例
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
// 返回
return
}
// 如果val存在__ob__属性
if (val.__ob__) {
// 获取__ob__的dep.id
const depId = val.__ob__.dep.id
// 如果seenObjects中有这个depId
if (seen.has(depId)) {
// 返回
return
}
// 如果没有这个depId,给seenObjects这个set添加一个depId
seen.add(depId)
}
// 如果是数组
if (isA) {
i = val.length
// 遍历所有值,进行递归检查添加
while (i--) _traverse(val[i], seen)
} else {
// 如果不是数组,获取所有key
keys = Object.keys(val)
i = keys.length
// 遍历对象的所有key进行循环递归检查添加
while (i--) _traverse(val[keys[i]], seen)
}
}
- 如果不是数组并且不是对象或被冻结 或是Vnode实例,返回
- 如果val存在__ob__属性,获取其depid,这波操作也是避免重复收集相同依赖,如果没有相同id,就添加进seenObjects
- 如果是数组,遍历数组,递归执行_traverse,每次都会获取值,也就触发getter进行深度依赖收集
- 如果是对象,遍历对象,递归执行_traverse,每次都会获取值,也就触发getter进行深度依赖收集
至此,深度依赖收集也完成了
很多人会问deep属性哪来的,其实是用户给的,在watch监听属性时,想进行深度监听,就要传deep:true这个options
想说的话
能坚持一步一步看到这里的人,我愿称你为最强(也给我点个赞吧!)
我也是刚看20天左右Vue源码的小白,如果发现我写的有什么问题,可以在评论区指出,大家可以一起探讨,毕竟这只是我对响应式的理解和分析
不过还是希望看完这篇能对你们理解响应式有所帮助吧!