Vue2 响应式原理
响应式原理
vue 2.0
中,是基于 Object.defineProperty
实现的响应式系统。
Object.defineProperty(obj, prop, descriptor)
descriptor: -configurable - enumerable - value - writable - get - set
主要涉及属性:
enumerable
,属性是否可枚举,默认false
。configurable
,属性是否可以被修改或者删除,默认false
。get
,获取属性的方法。set
,设置属性的方法。
响应式基本原理就是,在 Vue
的构造函数中,对 options
的 data
进行处理。即在初始化 vue
实例的时候,对 data
、props
等对象的每一个属性都通过 Object.defineProperty
定义一次,在数据被 set
的时候,做一些操作,改变相应的视图。
class Vue {
/* Vue构造类 */
constructor(options) {
this._data = options.data
observer(this._data)
}
}
function observer(value) {
if (!value || typeof value !== 'object') {
return
}
Object.keys(value).forEach((key) => {
defineReactive(value, key, value[key])
})
}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true /* 属性可枚举 */,
configurable: true /* 属性可被修改或删除 */,
get: function reactiveGetter() {
return val
},
set: function reactiveSetter(newVal) {
if (newVal === val) return
cb(newVal)
}
})
}
实际应用中,各种系统复杂无比。假设我们现在有一个全局的对象,我们可能会在多个 Vue
对象中用到它进行展示。又或者写在 data
中的数据并没有应用到视图中呢,这个时候去更新视图就是多余的了。这就需要依赖收集的过程。
依赖收集
所谓依赖收集,就是把一个数据用到的地方收集起来,在这个数据发生改变的时候,统一去通知各个地方做对应的操作。“发布者”在 Vue
中基本模式如下:
初始化时 Vue
去遍历 data
的key
,在defineReactive
函数中对每个key
进行get
和set
的劫持,Dep
是一个新的概念,它主要用来做上面所说的 dep.depend()
去收集当前正在运行的渲染函数和dep.notify()
触发渲染函数重新执行。
export default class Dep {
static target: ?Watcher
id: number
subs: Array<Watcher>
constructor() {
this.id = uid++
this.subs = []
}
addSub(sub: Watcher) {
this.subs.push(sub)
}
removeSub(sub: Watcher) {
remove(this.subs, sub)
} //依赖收集,有需要才添加订阅
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify() {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
有了订阅者,再来看看 Watcher
的实现。源码 Watcher
逻辑比较多,简化后的模型如下
每个组件实例都对应一个 watcher
实例,它会在组件渲染的过程中把获取过的数据记录为依赖。之后当依赖项的 setter
触发时,会通知 watcher
,从而使它关联的组件重新渲染。
class Watcher {
constructor(vm, expOrFn, cb, options) {
//传进来的对象 例如Vue
this.vm = vm
//在Vue中cb是更新视图的核心,调用diff并更新视图的过程
this.cb = cb
//收集Deps,用于移除监听
this.newDeps = []
this.getter = expOrFn
//设置Dep.target的值,依赖收集时的watcher对象
this.value = this.get()
}
get() {
//设置Dep.target值,用以依赖收集
pushTarget(this)
const vm = this.vm
let value = this.getter.call(vm, vm)
return value
}
//添加依赖
addDep(dep) {
// 这里简单处理,在Vue中做了重复筛选,即依赖只收集一次,不重复收集依赖
this.newDeps.push(dep)
dep.addSub(this)
}
//更新
update() {
this.run()
}
//更新视图
run() {
//这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图
console.log(`这里会去执行Vue的diff相关方法,进而更新数据`)
}
}
defineReactive 详细逻辑
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 初始渲染时页面显示会调用get方法
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter(newVal) {
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()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
所以响应式原理就是,我们通过递归遍历,把 vue
实例中data
里面定义的数据,用defineReactive
重新定义。每个数据内新建一个Dep
实例,闭包中包含了这个 Dep
类的实例,用来收集 Watcher
对象。在对象被读取的时候,会触发 reactiveGetter
函数把当前的 Watcher
对象(存放在 Dep.target
中)收集到 Dep
类中去。之后如果当该对象被写入的时候,则会触发 reactiveSetter
方法,通知 Dep
类调用 notify
来触发所有 Watcher
对象的 update
方法更新对应视图。
Watcher 的产生
在 vue
中,共有 4 种情况会产生Watcher
:
-
Vue
实例对象上的watcher
, 观测根数据,发生变化时重新渲染组件updateComponent = () => {
vm._update(vm._render(), hydrating)
}
vm._watcher = new Watcher(vm, updateComponent, noop) -
在
vue
对象内用watch
属性创建的watcher
-
在
vue
对象内创建的计算属性,本质上也是watcher
-
使用
vm.$watch
创建的watcher
Wathcer
会增减,也可能在 render
的时候新增。所以,必须有一个Schedule
来进行 Watcher
的调度。部分主要代码如下:
queue.sort((a, b) => a.id - b.id)
// 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]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
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
}
}
}
Schedule
调度的作用:
-
去重,每个
Watcher
有一个唯一的id
。首先,如果id
已经在队列里了,跳过,没必要重复执行,如果id
不在队列里,要看队列是否正在执行中。如果不在执行中,则在下一个时间片执行队列,因此队列永远是异步执行的。 -
排序,按解析渲染的先后顺序执行,即
Watcher
小的先执行。Watcher
里面的id
是自增的,先创建的id
比后创建的id
小。所以会有如下规律:-
组件是允许嵌套的,而且解析必然是先解析了父组件再到子组件。所以父组件的
id
比子组件小。 -
用户创建的
Watcher
会比render
时候创建的先解析。所以用户创建的Watcher
的id
比render
时候创建的小。
-
删除Watcher
,如果一个组件的 Watcher 在队列中,而他的父组件被删除了,这个时候也要删掉这个Watcher
。
队列执行过程中,会保存一个对象circular
,里面有每个 watcher
的执行次数,如果哪个watcher
执行超过MAX_UPDATE_COUNT
定义的次数就认为是死循环,不再执行,默认是 100 次。
总之,调度的作用就是管理 Watcher
。
总结
-
初始化
Vue
实例并将data
通过Object.defineProprty
后, 执行new Watcher(vm, update)
传入组件的实例和更新方法,并调用组件的渲染函数,同时设置Dep
的静态属性target
为当前的Watcher
实例 -
调用渲染函数时若有从
data
中取值,则调用属性的get
方法。该方法会判断Dep
的target
有没有值,有值则将该target
的值加入到Dep
的subs
中 -
当
data
中有属性值更新时。调用属性的set
方法。由于在将属性变为reactive
时,给每个属性都加了一个dep
的实例,而且还在读取属性值时即调用属性的get
方法时,给dep
的属性subs
加入了订阅者Watcher
的实例。所以调用set
方法时可以拿到subs
中的值并逐个调用其run
方法。run
方法会执行组件的更新函数
补充
数组的响应式
为什么我们直接修改数据中某项的时候,视图并没有响应式地变化呢。因为没有对数组的每一项进行数据劫持。为了解决这个问题 vue
重写了数组相关的方法。
const arrayProto = Array.prototype
exportconst arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case'push':
case'unshift':
inserted = args
break
case'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
Computed 属性
// 模拟
const data = reactive({
number: 1
})
const numberPlusOne = computed(() => data.number + 1)
// 渲染函数watcher
new Watcher(() => {
document.getElementById('app2').innerHTML = `
computed: 1 + number 是 ${numberPlusOne.value}
`
})
这段渲染函数执行过程中,读取到numberPlusOne
的值的时候,首先会把Dep.target
设置为numberPlusOne
所对应的computedWatcher
computedWatcher
的特殊之处在于
- 渲染
watcher
只能作为依赖被收集到其他的dep
里,而computedWatcher
实例上有属于自己的dep
,它可以收集别的watcher
作为自己的依赖。 - 惰性求值,初始化的时候先不去运行
getter
。
export default class Watcher {
constructor(getter, options = {}) {
const { computed } = options
this.getter = getter
this.computed = computed
if (computed) {
this.dep = new Dep()
} else {
this.get()
}
}
}