You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
exportfunctiondel(target,key){if(Array.isArray(target)&&isValidArrayIndex(key)){// 利用数组的splice变异方法触发响应式target.splice(key,1)return}// 获取Observer实例constob=(target: any).__ob__// 不允许删除 Vue.js 实例 或 Vue.js 实例的根数据对象(vm.$data) 的属性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}// 如果 key 不是 target 自身的属性,直接终止程序的执行if(!hasOwn(target,key)){return}deletetarget[key]// 如果ob不存在, 说明target本身不是响应式数据,直接终止程序的执行if(!ob){return}// 触发依赖通知ob.dep.notify()}
Vue2 变化侦测API实现原理
在《Vue2 设计系统之响应式》中,作者介绍到 Vue2 的发布订阅原理,Dep类、Watcher 、vm.$set、vm.$delete 的概念。好学的我,当然要再深究一下 Vue2 中
vm.$watch
vm.$set
vm.$delete
的实现原理啦!了解 Vue2 watch 的实现原理,对于理解 Vue3 响应式设计逻辑也有一定帮助。以下内容是本人阅读 Vue.js 官方文档 和 《深入浅出Vue.js》 书籍之后的学习总结,如有错误,欢迎指出~
vm.$watch
介绍
首先,简单回顾一下 Vue2 watch 对象:
{ [key: string]: string | Function | Object | Array }
官方文档介绍:watch 是一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用
$watch()
,遍历 watch 对象的每一个 property。接着,再介绍一下 Vue2 $watch 实例方法:
vm.$watch(expOrFn, callback, [options])
{string | Function} expOrFn
{Function | Object} callback
{Object} [options]
{boolean} deep
{boolean} immediate
{Function} unwatch
官方文档介绍:$watch 用于观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受简单的键路径。对于更复杂的表达式,用一个函数取代。
表达式仅支持以点分隔的路径,例如 a.b.c 。如果需要观察多个 data property 计算的总结果,则使用函数返回。举一些简单的例子:
实现原理
大概了解 vm.$watch 的使用方法后,再来剥开层层外壳,学习一下内部原理吧~
先看一段简化的代码:
在 $watch 中,会执行
new Watcher()
。进入 Watcher 的构造函数,先判断expOrFn
的类型,如果是function,直接将expOrFn
赋给getter
;否则再调用parsePath()
函数读取expOrFn
,也就是a.b.c
属性路径的值并赋给getter。我们假设某个 watch 是这样定义的:
那么,Watcher 将会同时订阅
this.a
和this.b
这两个Vue.js实例的响应式数据。在这之后,如果其中之一的值(假设this.a)有改变,this.a
会告知所有订阅了它的 watcher 实例,包括这里举例的 watcher 实例。执行完
new Watcher()
后,$watch 会判断是否传入了immediate
参数,如果是,则立即使用初始值执行一次cb。最后,$watch 会返回一个取消订阅的
unwatch
函数,调用它,实际上是调用 Watcher.teardown() 。那它又是怎么工作的呢?首先,取消订阅,肯定是要知道自己订阅了谁,也就是 watcher 实例需要通过 list 记录订阅的 Dep 。调用
unwatch
(即teardown
) 函数时,循环遍历这个 list ,告诉 Deps :我取消订阅咯,你们可以删掉我啦~上面这段代码,展示了 Watcher 类是如何记录自己订阅的 Dep 和 Dep.id。如果是已经订阅过的 Dep ,就不再重复记录。
在《Vue2 设计系统之响应式》文章,作者写到了 Dep 类的设计思路。在这个基础上,我们再改改代码,展示一下如何维护 Dep 的订阅者列表:
看完这两段代码,你看得出来 Watcher 和 Dep 是 多对多 关系了吗?如果还是不理解的话,也没关系,再看看下面的图,会更清晰明了~
我们假设
watcher1
观察的响应式数据是this.a + this.b
,watcher2
观察的响应式数据是this.a + this.c
,那么 Data 、Watcher 和 Dep 的(简化)关系将会是:options.deep
再说说
vm.$watch
options.deep
参数的原理。前面提到,当 data 的某个 property 被监听,就会触发
property.getter()
把当前的订阅者 watcher 收集到subscribers
。当 property 的值改变,也就是property.setter()
被调用,将会通知subscribers
中的所有订阅者更新数据,并触发 re-render 重渲染。当 watcher 中存在 deep 参数的时候,如果 property 是一个数组,则循环每一个元素并递归调用
_traverse()
函数;如果 property 是一个对象,则循环每一个 key 的 value 并递归调用_traverse()
函数。具体代码如下:
注:可能会有细心的同学注意到,当遍历Array元素时,如果该元素既不是数组,也不是对象,就会return,没有下一步操作。感兴趣的同学可以戳一下:Vue2.0为什么不能检查数组的变化?又该如何解决?
受 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。如何处理这个问题呢?答案是使用 vm.$set 和 vm.$delete 。
vm.$set
介绍
介绍一下 Vue2 $set 实例方法:
vm.$set(target, propertyName/index, value)
参数:
{Object | Array} target
{string | number} propertyName/index
{any} value
返回值:设置的值。
用法:
这是全局
Vue.set
的别名。可以通过这个方法实现 object 属性被添加后也是响应式的。实现原理
让我们来看一下,vm.$set 怎么处理数组和对象:
vm.$delete
vm.$delete(target, propertyName/index)
介绍
介绍一下 Vue2 $delete 实例方法:
参数:
{Object | Array} target
{string | number} propertyName/index
用法:
这是全局
Vue.delete
的别名。可以通过这个方法实现 object 属性被删除后,数据侦测可正常运行,并更新视图。实现原理
让我们来看一下,vm.$set 怎么处理数组和对象。思路和 vm.$set 类似:
Vue3 的响应式设计已从 Object.defineProperty 转为 Proxy ,Vue3 的官方文档仍有 $watch 的介绍,但是与 Vue2 $watch 有一定差异;而 vm.$set 和 vm.$delete 已经不在 Vue3 的文档中。不过现在仍有许多项目使用的是 Vue2,所以了解一下这几个 API 的实现原理也有一定的收获~
阅读完这篇文章,大家应该对 Vue2 的响应式原理有更深的理解啦。对 Vue3 响应式有学习兴趣的同学,可以看一下《Vue3 设计原理之响应式》哦。
以上是个人的学习与总结,感谢阅读。如有错误,欢迎指出~
内容参考:
[1] Vue.js 官方文档
[2]《深入浅出Vue.js》刘博文·著
The text was updated successfully, but these errors were encountered: