响应式原理
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()
}
}
}