【深入浅出Vue.js篇】之变化侦测相关API实现原理
文章目录
一、watch 的内部原理
回顾一下
/* 参数
- { string | Function} expOrFn
- { Function | Object} callback
- { Object } [options]
- *{boolean} deep 发现对象内部值的变化,监听数组变化时不需要设置
- *{boolean} immediate 立即以表达式的当前值触发回调
*/vm.$watch(expOrFn, callback, [options])
用法: 用于观察一个表达式活computed函数在Vue.js实例上的变化。回调函数调用时,会从参数得到新数据(new Value) //和旧数据(old Value).表达式只接受以点分隔的路径,如a.b.c.如果是一个比较复杂的表达式,可以用函数代替表达式。
vm.$watch(‘a.b.c’, function (newVal, oldVal) {
// 做点什么
})
1.watch 的内部原理
代码如下(示例):
Vue.prototype.$watch = function (expOrFn, cb, options) {
const vm = this
options = options || {}
const watcher = new Watcher(vm, expOrFn, cb, options)
// 判断用户是否使用了immediate 参数,如果使用了,则立即执行一次cb
if (options.immediate) {
cb.call(vm, watcher.value)
}
// 取消观察数据
return function unwatchFn () {
// 执行了watcher.teardown() 来取消观察数据
watcher.teardown()
}
}
//Watcher 想监听某个数据,就会触发某个数据收集依赖的逻辑,将自己收集进去,然后当它发生变化时,就会通知Watcher
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm
this.deps = []
this.depIds = new Set()
this.getter = parsePath(expOrFn)
// // expOrFn参数支持函数
// if (typeof expOrFn === 'function') {
// this.getter = expOrFn
// } else {
// this.getter = parsePath(expOrFn)
// }
this.cb = cb
this.value = this.get()
}
get () {
window.target = this
let value = this.getter.call(this.vm, this.vm)
window.target = undefined
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
// 记录了自己会被哪些Dep 通知
addDep (dep) {
const id = dep.id
// 如果当前Watcher 已经订阅了该Dep ,则不会重复订阅
if (!this.depIds.has(id)) {
// 记录当前Watcher 已经订阅了这个Dep 。
this.depIds.add(id)
// 记录自己都订阅了哪些Dep
this.deps.push(dep)
// 将自己订阅到Dep 中
dep.addSub(this)
}
}
/**
* 从所有依赖项的Dep列表中将自己一处
*/
teardown () {
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
}
}
let uid = 0
// Dep 会记录数据发生变化时,需要通知哪些Watcher
class Dep{
constructor() {
this.id = uid++
this.subs = []
}
//...这里省略了部分代码 可以去 Object侦测原理篇 查看完成代码
depend () {
if (window.target) {
window.target.addDep(this)
}
}
// 把Watcher 从sub 中删除掉,数据发生变化时,将不再通知这个已经删除的Watcher
removeSub (sub) {
const index = this.subs.indexOf(sub)
if (index > -1) {
return this.subs.splice(index,1)
}
}
}
2.deep参数的实现原理
要想实现deep 的功能,其实就是除了要触发当前这个被监听数据的收集依赖的逻辑之外,还要把当前监听的这个值在内的所有子值都触发一遍收集依赖逻辑。这就可以实现当前这个依赖的所有子数据发生变化时,通知当前Watcher 了。
代码如下(示例):
class Watcher{
constructor(vm, expOrFn, ob, options) {
this.vm = vm
if (options) {
this.deep = !!options.deep
} else {
this.deep = false
}
this.deps = []
this.depIds = new Set()
this.getter = parsePath(expOrFn)
this.cb = this.cb
this.value = this.get()
}
get () {
window.target = this
let value = this.getter.call(vm, vm)
if (this.deep) {
// 处理deep的逻辑
traverse(value)
}
window.target = undefined
return value
}
// 省略部分代码
}
// 递归value的所有子集来触发他们收集依赖的功能
const seenObjects = new Set()
function traverse (val) {
_traverse(val,seenObjects)
seenObjects.clear()
}
function _traverse (val, seen) {
let i, keys
const isA = Array.isArray(val)
// 如果不是Array 和Object ,或者已经被冻结,那么直接返回,什么都不干。
if ((!isA && !isObject(val)) || Object.isFrozen(val)) {
return
}
if (val.__ob__) {
// 拿到val的dep.id
const depId = val.__ob__.dep.id
// 用id来保证不会重复收集依赖
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
// 递归调用_traverse
while (i--) {
_traverse(val[i],seen)
}
} else {
keys = Object.keys(val)
i = keys.length
while (i--) {
// 循环Object中的左右key,然后执行一次读取操作
//val[keys[i]]会触发收集依赖的操作
_traverse(val[keys[i]],seen)
}
}
}
二、vm.$set 的内部原理
回顾一下
/**
* 参数
* {objdct | Array} target
* {string | number} key
* {any} value
*/
vm.set(target, key, values)
- 用法
- 在objet上设置一个属性,如果object是响应式的,Vue.js会保证属性被创建后也是响应式的
- 并且触发试图更新
vm.$set的具体实现其实就是在observer中抛出set方法
代码如下(示例):
function set (target, key, val) {
// 如果target是数组且key是一个有效的索引值
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
// 通过splice方法把val设置到target中的指定位置
//当使用splice方法把val设置到target中的时候,数组拦截器会侦测到target发生了变化
//接下来就会将新增的val转换成响应式的
target.splice(key, 1, val)
return val
}
// key已经存在于target中 说明这个key已经被侦测了变化
//直接用key和val修改数据即可
//修改数据的动作会被Vue.js侦测到
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 获取target的__ ob __属性
const ob = target.__ob__
// target不能是Vue.js实例或Vue.js实例的根数据对象
// 使用target.__isVue判断是否为Vue.js实例,使用ob.vmCount判断是否为根数据对象(this.$data就是根函数)
if (target.__isVue || (ob && ob.vmCount)) {
process.env.NODE_EVN !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance pr root $data' +
'at runtime - declare it upfornt in this data option.'
)
return val
}
// target不是响应式
if (!ob) {
target[key] = val
return val
}
// 新增属性 需追踪该属性的变化
//使用defineReactive将新增属性转换成getter/setter形式
defineReactive(ob.value, key, val)
// 触发变化通知
ob.dep.notify()
return val
}
三、vm.$delete 的内部原理
回顾一下
- 参数
- {Object | Array} target
- {string | number} key/index
vm.set(target, key, values)
- 用法
- 删除对象的属性。如果对象是响应式的,确保删除能触发视图更新
代码如下(示例):
function del (target, key) {
// 如果target是数组且key是一个有效的索引值
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 使用splice 方法 触发拦截器向依赖发送通知
target.splice(key, 1)
return
}
// 获取target的__ ob __属性
const ob = target.__ob__
// target不能是Vue.js实例或Vue.js实例的根数据对象
// 使用target.__isVue判断是否为Vue.js实例,ob.vmCount 数量大于1
if (target.__isVue || (ob && ob.vmCount)) {
process.env.NODE_EVN !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
// 如果key不是target自身的属性,则终止程序继续执行
if (!hasOwn(target, key)) {
return
}
delete target[key]
// 如果数据是非响应式的 终止程序继续执行
if (!ob) {
return
}
ob.dep.notify()
}
总结
文章中,我们详细介绍了变化侦测相关API 的内部实现原理。
先介绍了vm.$watch 的内部实现及其相关参数的实现原理,包括deep 、immediate 和unwatch 。
随后介绍了vm.$set 的内部实现。这里介绍了几种情况,分别为Array 的处理逻辑,key 已经存在的处理逻辑,以及最重要的新增属性的处理逻辑。
最后,介绍了vm.$delete 的内部实现原理。