响应式原理
阅读之前需了解的知识
- MVVM模式
- 观察者模式和发布者-订阅者模式
- Object.defineProperty与ES6中的Proxy
- 数据劫持
- 原型对象和原型链
变化侦测
变化侦测就是侦测数据的变化。从Vue2.0开始,引入了虚拟DOM,将更新粒度调整为中等程度,也就是一个状态所绑定的依赖不再是具体的dom节点,而是一个组件。当状态变化之后,会通知到组件,组件内部再使用虚拟dom进行比对。这样可以大大降低依赖的数量,从而降低依赖追踪所消耗的内存。
图:每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
下面将具体介绍vue中是如何侦测数据的变化的,从而实现数据驱动视图变化。
核心实现类
-
Observer
利用 Object.defineProperty 给对象的属性添加getter和setter,用于依赖收集和派发更新。使数据的变化可以被观察到!
-
Dep
收集当前响应式对象的依赖关系,每个响应式对象包括其子对象都拥有一个Dep实例(Dep.subs数组是watcher实例数组),当数据发生变更的时候,通过dep.notify()通知数组里面的所有watcher让其触发更新。
-
Watcher
观察者对象,就是依赖。实例分为render(渲染) watcher、computed(计算) watcher、user(侦测器) watcher。
依赖中记录了所有数据属性以及一些对响应式数据的操作的包装,可以响应数据的变化。
依赖收集
- initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集。
- initState 时,对侦听属性初始化时,触发 user watcher 依赖收集。
- render()的过程,触发 render watcher 依赖收集。
- re-render 时,vm.render()再次执行,会移除所有 subs 中的 watcher 的订阅,重新对dep.subs赋值,进行新一轮依赖的收集。
派发更新
- 组件中响应的数据发生变化,触发 setter 的逻辑。
- 调用 dep.notify()通知 subs数组中所有的Watcher 实例进行更新操作。
- 每一个 watcher 调用 update 方法触发视图更新或用户的某个回调函数。
实现原理
-
Data通过Observer转换成了getter/setter的形式(响应式数据)来追踪变化。
-
当外界通过watcher读取数据的时候,会触发getter从而将watcher添加到依赖中。
-
数据发生变化后,会触发setter,从而向Dep中的依赖(wathcer)发送通知。
-
wathcer接收到通知之后,会向外界发送通知,之后可能会触发视图更新,也可能会触发用户的某个回调函数等。
总结
vue.js采用数据劫持结合发布-订阅模式,通过Object.defineproperty来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发响应的监听回调。
注意事项——当前实现存在的不足
由于js的限制,vue不能检测数组和对象的变化。
- 对象
Vue 无法检测 property 的添加或移除。
由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
解决方法:
使用 Vue.set(object, propertyName, value) 方法 / vm.$set向嵌套对象添加响应式 property。
Vue.set(vm.someObject, 'b', 2)
this.$set(this.someObject,'b',2)
- 数组
Vue 不能检测以下数组的变动:
1、当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
2、当你修改数组的长度时,例如:vm.items.length = newLength
解决方法:
用Vue.set / vm.$set / Array.prototype.splice 实现和 vm.items[indexOfItem] = newValue 相同的效果。
Vue.set(vm.items, indexOfItem, newValue)
vm.$set(vm.items, indexOfItem, newValue)
vm.items.splice(indexOfItem, 1, newValue)
用 Array.prototype.splice 实现和 vm.items.length = newLength 相同的效果。
vm.items.splice(newLength)
代码实现
Observer类的实现
/*
* 主文件 index.js
*/
// 方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作
// 为名称的属性)组成的数组,只包括实例化的属性和方法,不包括原型上的。
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
/**
* Observer类
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number; // 根$数据的vm数
constructor (value: any) {
this.value = value
// dep实例
this.dep = new Dep()
this.vmCount = 0
// dep中添加__ob__属性,指向自身,循环引用
def(value, '__ob__', this)
// 如果是数组,先判断浏览器是否支持__proto__属性,最后遍历数组的所有元素进行响应式处理
// 如果支持,就使用protoAugment将arrayMethods覆盖原型
// 如果不支持,就直接在数组上挂载一些方法,当用户使用这些方法时,不是使用浏览器原生提供的Array.prototype上的方法,而是拦截器提供的方法。
if (Array.isArray(value)) {
if (hasProto) {
// 覆盖响应式数据的原型
protoAugment(value, arrayMethods)
} else {
// 在数据上挂载方法,不去原型上找方法
copyAugment(value, arrayMethods, arrayKeys)
}
// 遍历数组的所有元素进行响应式处理
this.observeArray(value)
} else {
// 如果是对象,执行walk将值进行响应式处理
this.walk(value)
}
}
/**
* 遍历所有的属性,将其转换为getter/setters。
* 给对象的所有key进行响应化,也就是逐一调用defineReactive
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* 遍历数组中的所有元素进行observe,实现深度响应
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
/**
* index.js中一些具体方法的实现
*/
/**
* 通过拦截来扩充目标对象或数组原型链使用__proto__
*/
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
/**
* 将arrayMethods中的值(7个处理过的方法)直接赋在数组的同名属性上
* 当响应式数据使用这些方法时直接在数组上找,而不是去原型上找
*/
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
/**
* 导入Object.defineProperty ,定义一个属性
*/
function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
/**
* 给对象创建observer实例
* 返回一个新的Observer实例或者是已经存在的Observer实例
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 如果不是对象或者是实例化的vnode,就直接返回
if (!isObject(value) || value instanceof VNode) {
return
}
// 创建一个Observer
let ob: Observer | void
// 如果有缓存ob,就直接拿来用
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve && // 当前状态可以添加观察者
!isServerRendering() && // 不是ssr
(Array.isArray(value) || isPlainObject(value)) && // 是对象或者是数组
Object.isExtensible(value) && // 可以在它上面添加新的属性
!value._isVue // 不是vue实例
) {
// 没有缓存ob,把value响应化并返回ob实例
ob = new Observer(value)
}
// 如果是根data,并且当前ob有值,根$数据的vm数加一
if (asRootData && ob) {
ob.vmCount++
}
// 最后把observer实例返回
// 返回的实例添加了__ob__属性,并且其对象和数组都进行了响应化
return ob
}
/**
* 在对象上定义一个响应式属性
*/
export function defineReactive (
obj: Object,
key: string,
val: any, // 监听的数据,通过闭包来保存值
customSetter?: ?Function, // 日志函数
shallow?: boolean // 是否要添加__ob__属性
) {
// 实例化一个Dep对象
const dep = new Dep()
// Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
const property = Object.getOwnPropertyDescriptor(obj, key)
// 如果key中没有描述符或者是不可配置,直接返回
if (property && property.configurable === false) {
return
}
// 满足预定义的getter/setters
const getter = property && property.get
const setter = property && property.set
// 如果对象不可获取值或者可以设置值,并且只传递了两个参数
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 递归响应式处理,给每一层的属性附加一个Observer实例
// shallow不存在代表没有__ob__属性,这时候对val进行observe返回一个ob实例
// walk函数中调用defineReactive时没有传递shallow参数,所以此时该参数为undefined,也就是默认是深度观测
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中有Watcher实例化对象(触发了watcher的get),就将watcher添加到dep中
if (Dep.target) {
dep.depend()
// 如果存在子对象,也将子对象的dep中添加watcher
if (childOb) {
childOb.dep.depend()
// 如果值是数组,处理方式有所差异,要对数组中每一项都做depend操作
if (Array.isArray(value)) {
dependArray(value)
}
}
}
// 触发get最终都要把值返回出去
return value
},
set: function reactiveSetter (newVal) {
// 获取值,触发依赖收集
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
// 如果新旧值相同就没必要执行操作,直接返回
// newVal !== newVal && value !== value 属于这种情况:NaN === NaN
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
// 不再生产环境下
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter() // 执行日志函数
}
// 对于没有setter的访问器属性,直接返回
if (getter && !setter) return
// 有setter就设置新值,没有就直接给新值
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 对新来的值也进行observe响应式处理,返回ob实例
childOb = !shallow && observe(newVal)
// 触发set说明该响应式数据发生了变化,这时候应该通知依赖进行派发更新
dep.notify()
}
})
}
/**
* 在接触到数组时收集对数组每个元素的依赖关系
* 因为我们不能像属性getter那样拦截数组元素访问
*/
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
// 判断当前元素是否存在__ob__实例,调用depend添加watcher
e && e.__ob__ && e.__ob__.dep.depend()
// 递归调用直到不是数组
if (Array.isArray(e)) {
dependArray(e)
}
}
}
数组的变化侦测方法
/*
* 数组变化侦测 array.js
*/
// 复制一份原型方法
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 需要拦截的7个方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* 拦截转换方法并发出事件
*/
methodsToPatch.forEach(function (method) {
// 取出原始方法
const original = arrayProto[method]
// def相当于Object.defineProperty,给arrayMehods的method方法定义一个函数mutator
def(arrayMethods, method, function mutator (...args) {
// 执行数组原本应该执行的方法
const result = original.apply(this, args)
// 获取数组的Object实例
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)
// 派发更新
ob.dep.notify()
// 返回数组原本操作的结果
return result
})
})
/**
* Observe a list of Array items.
* 数组中新操作的对象进行响应式处理
*/
Observer.prototype.observeArray = function observeArray(items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
dep类的实现
/*
* Dep类 dep.js
*/
let uid = 0
/**
* dep可以被观测到,也可以有多个订阅它的指令
*/
export default class Dep {
// 全局唯一watcher,静态属性
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
// Dep实例id
this.id = uid++
// 存放watcher的数组
this.subs = []
}
// 给subs数组添加watcher对象
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 删除watcher对象
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 如果dep中存在watcher,就添加当前watcher
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 派发更新,通知所有依赖watcher执行update操作
notify () {
// 浅拷贝一份subs数组
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// 如果不运行async,就不会在调度程序中对sub进行排序
// 需要对watcher进行排序以确保watcher执行update顺序正确
subs.sort((a, b) => a.id - b.id)
}
// 通知subs中所有依赖watcher执行update操作
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
//全局正在被评估的watcher观察者,当前目标观察程序
Dep.target = null
// Dep.target用targetStack栈结构来管理
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
watcher类的实现
/*
* Watcher类 watcher.js
*/
let uid = 0
/**
* 观察者分析表达式,收集依赖关系,并在表达式值更改时触发回调
* 同时用于$watch() api和指令
*/
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean; // 用来告诉当前观察者实例对象是否是深度观测
user: boolean; // 用来标识当前观察者实例对象是 开发者定义的 还是 内部定义的
lazy: boolean;
sync: boolean; // 用来告诉观察者当数据变化时是否同步求值并执行回调
dirty: boolean; // 只有计算属性的观察者实例对象的dirty值为true,因为计算属性是惰性求值,用来缓存计算属性的值
active: boolean; // 当前实例对象是否是激活状态
// 用来实现避免收集重复依赖,且移除无用依赖
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function; // 可以理解为 Watcher 实例的钩子,当数据变化之后,触发更新之前,调用在创建渲染函数的观察者实例对象时传递的 before 选项。
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function, // 回调,获取值或者是更新视图的函数
cb: Function, // 回调函数
options?: ?Object, // 参数
isRenderWatcher?: boolean // 是否是渲染过的watcher
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// 有参数的话先获取参数
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
// 没有参数的话默认赋值为false
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// expOrFn是getter的解析表达式,如果是函数,直接赋给getter,作为表达式的get拦截器函数
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 如果是keepAlive组件会走这里
// parsePath:解析路径获取值
this.getter = parsePath(expOrFn)
if (!this.getter) {
// 没有值的话就执行空函数
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
)
}
}
// 如果惰性求值的话值为undefined,如果lazy不存在,计算getter,重新收集依赖
this.value = this.lazy
? undefined
: this.get()
}
/**
* 计算getter,并重新收集依赖项,返回观察目标的值
*/
get () {
// 将当前watcher添加到dep.target
pushTarget(this)
let value
const vm = this.vm
try {
// this.getter.call就是触发当前watcher的get方法,判断dep.target是否存在,存在就收集依赖并且获取值返回
// 每个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 {
// 遍历每一个属性,以便它们都被跟踪为深度侦测的依赖项
if (this.deep) {
traverse(value)
}
// 当前dep.target出栈
popTarget()
// 每次求值完毕后清空 newDepIds 和 newDeps 这两个属性的值,并且在被清空之前把值分别赋给了 depIds 属性和 deps 属性
this.cleanupDeps()
}
// 返回值
return value
}
/**
* 添加依赖项,新旧deps同步,并且dep和watcher互相保存各自的引用
* 这是真正执行收集依赖进subs数组的操作
*/
addDep (dep: Dep) {
const id = dep.id
// id不存在的时候再进行收集,防止重复收集新的依赖项
if (!this.newDepIds.has(id)) {
// 当前watcher的新deps数组中添加当前依赖项dep
this.newDepIds.add(id)
this.newDeps.push(dep)
// 如果旧的deps中不存在该依赖,再往旧的subs中添加当前watcher,避免重复求值时收集重复的观察者
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
/**
* 清空依赖集合
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
// 移除废弃的观察者
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
// 将旧id高新的depid,最后清空新的depid
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
// 新旧deps数组进行互换,清空新deps数组
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* 观察者接口,响应式数据发生变化的时候触发
*/
update () {
// lazy为true,
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
// 同步的话直接运行更新
this.run()
} else {
// 异步的话数据不会立即更新,会将需要重新求值并执行回调的观察者放到一个异步队列中,当所有数据变化结束之后统一求值并执行回调
queueWatcher(this)
}
}
/**
* 调度程序作业接口,实质上是执行更新操作
*/
run () {
// 如果是活跃的
if (this.active) {
// 获取值
// 对于渲染函数的观察者来讲,重新求值等价于重新执行渲染函数,最终结果就是重新生成了虚拟DOM并更新真实DOM,此时value返回值为undefined
const value = this.get()
// if语句内的代码是为非渲染函数类型的观察者准备的
// 对比新值旧值是否相等,如果是对象,需要用isObject函数来判断,因为对象虽然引用地址不变,但是值可能改变了
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// 保存旧值,将新值赋给value
const oldValue = this.value
this.value = value
if (this.user) {
// 如果是开发者定义的观察者,需要放进try catch,因为开发者的回调函数在执行过程中行为不可预知
try {
// 更新value的值
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
// 更新value的值
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* 评估观察者的值,用于懒惰观察者
*/
evaluate () {
this.value = this.get()
// 取过一次值之后,dirty重置为false
this.dirty = false
}
/**
* 当newDeps数据被清空后重新收集依赖,当属性是计算属性的时候才会执行这个函数
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* 取消观察数据,从所有的依赖项订阅服务中删除自身watcher实例
*/
teardown () {
// 如果是激活状态才执行
if (this.active) {
// 如果组件实例还没有被销毁,就删除当前实例watcher数组中的当前观察者
if (!this.vm._isBeingDestroyed) {
// 由于这个参数的性能开销比较大,所以仅在组件没有被销毁的情况下才会执行该操作
remove(this.vm._watchers, this)
}
// 遍历从所有订阅服务dep中删除自身
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
// 变为不活跃
this.active = false
}
}
}
深层遍历转换成响应式
/*
* 深层遍历转换成响应式 traverse.js
* 递归遍历对象以唤起所有已转换的getter
* 使对象内的每个嵌套属性作为深层依赖关系收集
*/
const seenObjects = new Set()
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
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
}
// !!这一步解决了循环引用导致死循环的问题,如果该对象被遍历过就跳过
// set结构的作用也在此体现了
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
// 如果是数组就递归遍历它的每一项
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
// 如果不是数组,是对象,就递归遍历它的每个键值
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
相关API实现原理
vm.$watch
deep
watch: {
a () {
console.log('a 改变了')
}
}
上面的代码使用 watch 选项观测了数据对象的a属性,此时会创建Watcher实例对象会读取a的值从而触发属性a的get拦截器函数,最终将依赖收集。但是当属性a的值是一个对象,如下所示,修改a.b的值是触发不了响应的。
data () {
return {
a: {
b: 1
}
}
},
watch: {
a () {
console.log('a 改变了')
}
}
深度观测就是用来解决这个问题的,使用深度观测时要把deep选项参数设置为true。
原理: 调用traverse()递归地读取观察属性的所有子属性的值,这样被观察属性的所有子属性都会收集到观察者,从而达到深度观测。
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
unwatch
实际上是执行了watcher.teardown()来观察数据,其本质是把watcher实例从正在被观察的状态的所有依赖列表中移除。
具体代码解析可参考上面代码实现中的teardown()函数实现。
vm.$set
/**
* Vue.set方法
*
*/
export function set (target: Array<any> | Object, key: any, val: any): any {
// 检查target的值,如果是undefinded,null或者原始类型值,在非生产环境下打印警告信息
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 如果是数组以及数组索引有效
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 将数组长度设置为两者较大值,否则当要设置的元素索引大于数组的长度时splice无效
target.length = Math.max(target.length, key)
// 将指定位置元素的值转换为新值,本身splice方法能触发响应
target.splice(key, 1, val)
return val
}
// 如果不是数组,那就是纯对象
// key在target中并且不在Object的原型上
if (key in target && !(key in Object.prototype)) {
// 设置对象的值
target[key] = val
return val
}
// 引用observer对象
const ob = (target: any).__ob__
// 第一个条件成立时,说明你正在使用 Vue.set/$set 函数为 Vue 实例对象添加属性,为了避免属性覆盖的情况出现,在非生产环境下会打印警告信息。
// 第二个条件成立时,说明当前正在根数据添加属性,这也是不允许的,因为根数据对象ob实例收集不到依赖
if (target._isVue || (ob && ob.vmCount)) {
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, key, val)
// 同时触发响应,派发更新
ob.dep.notify()
// 返回值
return val
}
/**
* Vue.delete方法
*/
export function del (target: Array<any> | Object, key: any) {
// 检测 target 是否是 undefined 或 null 或者是原始类型值,如果是的话那么在非生产环境下会打印警告信息。
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 用splice方法移除数组,触发响应
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
// 第一个条件成立时,说明你正在为 Vue 实例对象删除属性,为了避免删除vue中定义的属性情况出现,出于安全因素考虑,在非生产环境下会打印警告信息。
// 第二个条件成立时,说明当前正在根数据删除属性,这也是不允许的,因为根数据对象ob实例收集不到依赖
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
// 如果将要删除的属性原本就不在对象上,直接返回
if (!hasOwn(target, key)) {
return
}
// 删除属性
delete target[key]
// 如果不是响应式数据,直接返回
if (!ob) {
return
}
// 是响应式数据就派发更新
ob.dep.notify()
}
思考
Watcher 和 Dep 的关系
watcher中(响应式数据被读取时第一次触发getter时当前watcher也会记录自己被收集进当前的dep)实例化了dep,并向dep.subs中添加了订阅者,dep通过notify遍历了dep.subs通知每个watcher进行更新。
vue中是如何检测数组变化的?
函数劫持。
Vue通过原型拦截的方式重写了数据的7个方法,首先获取数组的Observer对象,如果有加入新的值,就调用observeArray对新的值进行响应式处理,然后手动调用notify,派发更新,渲染页面。
为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?
Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。
Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。
Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
Vue为什么不把所有的数据都放到data?
data用来存放绑定的数据。data中的数据都会增加getter、setter,会收集对应的watcher。
如果data里的数据是属于纯展示的数据,根本不需要对这个数据进行监听,特别是一些复杂的列表/对象,放进data中会浪费性能。
可以选择放进computed,因为如果computed是直接返回一个没有引用其他实例属性的值,即没有任何访问响应式数据(如data/props上的属性/其他依赖过的computed等)的操作,根据Vue的依赖收集机制,只有在computed中引用了实例属性,触发了属性的getter,getter会把依赖收集起来,等到setter调用后,更新相关的依赖项。
计算属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算,所以使用computed会更加节约内存。
Vue 为什么不允许动态添加根级响应式 property?
一方面消除了依赖项追踪系统中的一类边界情况,也使Vue实例能更好地配合类型检查系统工作。因为在data对象上才能让Vue将它转换为响应式的数据。
另一方面是在data中提前声明所有的响应式property,会使组件状态的结构更加清晰,便于维护。
Vue Dep.target为什么需要targetStack栈结构来管理?
从Vue2.0开始,一个状态所绑定的依赖不再是具体的dom节点,而是一个组件。视图被抽象为一个 render 函数,一个 render 函数只会生成一个 watcher,其处理机制可以简化理解为:
renderRoot () {
...
renderMy ()
...
}
在 Vue2 中组件数的结构在视图渲染时就映射为 render 函数的嵌套调用,有嵌套调用就会有调用栈。当 evaluate root 时,调用到 my 的 render 函数,此时就需要中断 root 而进行 my 的 evaluate,当 my 的 evaluate 结束后 root 将会继续进行,这就是 target stack 的意义。
computed 和 watch 有什么区别及运用场景?
区别
computed:依赖于其他属性值,并且它的值是有缓存的。
只有它依赖的属性值发生了变化,下一次获取computed属性值的时候才会重新计算这个属性的值。
watch 监听器:更多的是观察的作用,无缓存性。
当监听的数据发生变化后才会执行回调进行后续操作。
应用场景
computed:
- 需要进行数值计算
- 这个计算依赖于其他的数据
因为computed属性值具有缓存特性,可以避免每次获取值时重新计算。
watch:
- 在数据变化时执行异步
- 在数据变化时执行开销较大的操作
因为watch允许我们执行异步操作(发起请求),限制我们执行该操作的频率,并在得到最终结果之前设置中间状态,而计算属性无法做到。