写文章不容易,点个赞呗兄弟
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue版本 【2.5.17】
如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧
【Vue原理】依赖收集 - 源码版之引用数据类型
上一篇,我们已经分析过了 基础数据类型的 依赖收集
【Vue原理】依赖收集 - 源码版之基本数据类型
这一篇内容是针对 引用数据类型的数据的 依赖收集分析,因为引用类型数据要复杂些,必须分开写
文章很长,高能预警,做好准备耐下心好,肯定还是有点收获的
但是两个类型的数据的处理,又有很多重复的地方,所以打算只写一些差异性的地方就好了,否则显得废话很多
两个步骤,都有不同的地方
1、数据初始化
2、依赖收集
数据初始化流程
如果数据类型是引用类型,需要对数据进行额外的处理。
处理又分了 对象 和 数组 两种,会分开来讲
1对象
1、遍历对象的每个属性,同样设置响应式,假设属性都是基本类型,处理流程跟上一篇一样
2、每个数据对象会增加一个 ob 属性
比如设置一个 child 的数据对象
下图,你可以看到 child 对象处理之后添加了一个 ob 属性
ob_ 属性有什么用啊?
你可以观察到,__ob__ 有一个 dep 属性,这个 dep 是不是有点属性,是的,在上一篇基础数据类型中讲过
那么这个 ob 属性有什么用啊?
你可以观察到,__ob__ 有一个 dep 属性,这个 dep 是不是有点属性,是的,在上一篇基础数据类型中讲过
dep 正是存储依赖的地方
比如 页面引用了 数据child,watch 引用了数据child,那么child 就会把这个两个保存在 dep.subs 中
dep.subs = [ 页面-watcher,watch-watcher ]
但是,在上一篇基础类型种, dep 是作为闭包存在的啊,并不是保存在什么【__ob__.dep】 中啊
没错,这就是 引用类型 和 基础类型的区别了
基础数据类型,只使用 【闭包dep】 来存储依赖
引用数据类型,使用 【闭包dep】 和 【 __ob__.dep】 两种来存储依赖
什么?你说闭包dep 在哪里?好吧,在 defineReactive 的源码中,你去看看这个方法的源码,下面有
那么,为什么,引用类型需要 使用__ob__.dep 存储依赖呢?
首先,明确一点,存储依赖,是为了数据变化时通知依赖,所以 __ob__.dep 也是为了变化后的通知
闭包 dep 只存在 defineReactive 中,其他地方无法使用到,所以需要保存另外一个在其他地方使用
在其他什么地方会使用呢?
在Vue挂载原型上的方法 set 和 del 中,源码如下
function set(target, key, val) {
var ob = (target).__ob__;
// 通知依赖更新
ob.dep.notify();
}
Vue.prototype.$set = set;
function del(target, key) {
var ob = (target).__ob__;
delete target[key];
if (!ob) return
// 通知依赖更新
ob.dep.notify();
}
Vue.prototype.$delete = del;
这两个方法,大家应该都用过,为了给对象动态 添加属性和 删除属性
但是如果直接添加属性或者删除属性,Vue 是监听不到的,比如下面这样
child.xxxx=1
delete child.xxxx
所以必须要通过 Vue 包装过的方法 set 和 del 来操作
在 set 和 del 执行完,是需要通知依赖更新的,但是我怎么通知?
此时,【__ob__.dep】 就发挥作用了!就因为依赖多收集了一份在 __ob__.dep 中
使用就是上面一句话,通知更新
ob.dep.notify();
2、数组
1、需要遍历数组,可能数组是对象数组,如下面
[{name:1},{name:888}]
遍历时,如果遇到子项是对象的,会跟上面解析对象一样操作
2、给数组保存一个 ob 属性
比如设置一个 arr 数组
看到 arr数组 加多了一个 ob 属性
其实这个 ob 属性 和 上一段讲对象 的作用是差不多的,这里也只是说 __ob__.dep
数组中的 __ob__.dep 存储的也是依赖,给谁用呢?
给 Vue 封装的数组方法使用,要知道要想数组变化也被监听到,是必须使用Vue封装的数组方法的,否则无法实时更新
这里举重写方法之一 push,其他的还有 splice 等,Vue 官方文档已经有过说明
var original = Array.prototype.push;
Array.prototype.push = function() {
var args = [],
len = arguments.length;
// 复制 传给 push 等方法的参数
while (len--) args[len] = arguments[len];
// 执行 原方法
var result = original.apply(this, args);
var ob = this.__ob__;
// notify change
ob.dep.notify();
return resul
}
看到在执行完 数组方法之后,同样需要通知依赖更新,也就是通知 __ob__.dep 中收集的依赖去更新
现在,我们知道了,响应式数据对 引用类型做了什么额外的处理,主要是加了一个 ob 属性
我们已经知道了 ob 有什么用,现在看看源码是怎么添加 ob 的
// 初始化Vue组件的数据
function initData(vm) {
var data = vm.$options.data;
data = vm._data =
typeof data === 'function' ?
data.call(vm, vm) : data || {};
....遍历 data 数据对象的key ,重名检测,合规检测
observe(data, true);
}
function observe(value) {
if (Array.isArray(value) || typeof value == "object") {
ob = new Observer(value);
}
return ob
}
function Observer(value) {
// 给对象生成依赖保存器
this.dep = new Dep();
// 给 每一个对象 添加一个 __ob__ 属性,值为 Observer 实例
value.__ob__ = this
if (Array.isArray(value)) {
// 遍历数组,每一项都需要通过 observe 处理,如果是对象就添加 __ob__
for (var i = 0, l =value.length; i < l; i++) {
observe(value[i]);
}
} else {
var keys = Object.keys(value);
// 给对象的每一个属性设置响应式
for (var i = 0; i < keys.length; i++) {
defineReactive(value, keys[i]);
}
}
};
源码的流程跟上一篇差不多,只是处理引用数据类型会增加多几行源码的额外处理
我们之前只说了一种对象数据类型,比如下面这样
如果会嵌套多层对象呢?比如这样,会怎么处理
没错,Vue 会递归处理,当遍历属性,使用 defineReactive 处理时,递归调用 observe 处理(源码标红加粗)
如果值是对象,那么同样给 值加多一个 ob
如果不是,那么正常往下走,设置响应式
源码如下
function defineReactive(obj, key, value) {
// dep 用于中收集所有 依赖我的 东西
var dep = new Dep();
var val = obj[key]
// 返回的 childOb 是一个 Observer 实例
// 如果值是一个对象,需要递归遍历对象
var childOb = observe(val);
Object.defineProperty(obj, key, {
get() {...依赖收集跟初始化无关,下面会讲},
set() { .... }
});
}
画一个流程图,仅供参考
哈哈哈,上面写得好长啊,是有点,但是没办法,想说详细点啊,好吧,还有一段,但是比较短一些哈哈哈,反正看完的人,我jio 得很厉害了,答应我,如果你仔细看完了,评论一下好吗,让我知道有人仔细看了
依赖收集流程
收集流程,就是重点关注 Object.defineProperty 设置的 get 方法了
跟 基础类型数据 对比,引用类型的 收集方法也只是多了几行处理,差异在两行代码
childOb.dep.depend,被我 简单化为 childOb.dep.addSub(Dep.target)
dependArray(value)
可以先看下源码,如下
function defineReactive(obj, key, value) {
var dep = new Dep();
var val = obj[key]
var childOb = observe(val);
Object.defineProperty(obj, key, {
get() {
var value = val
if (Dep.target) {
// 收集依赖进 dep.subs
dep.addSub(Dep.target);
// 如果值是一个对象,Observer 实例的 dep 也收集一遍依赖
if (childOb) {
childOb.dep.addSub(Dep.target)
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
}
});
}
上面的源码,混杂了 对象和 数组的处理,我们分开说
1、对象
在数据初始化的流程中,我们已经知道值是对象的话,会存储多一份依赖在 __ob__.dep 中
就只有一句话
childOb.dep.depend();
数组还有另外一个处理,就是
dependArray(value);
看下源码,如下
function dependArray(value) {
for (var i = 0, l = value.length; i < l; i++) {
var e = value[i];
// 只有子项是对象的时候,收集依赖进 dep.subs
e && e.__ob__ && e.__ob__.dep.addSub(Dep.target);
// 如果子项还是 数组,那就继续递归遍历
if (Array.isArray(e)) {
dependArray(e);
}
}
}
显然,是为了防止数组里面有对象,从而需要给 数组子项对象也保存一份
你肯定会问,为什么子项对象也要保存一份依赖?
1、页面依赖了数组,数组子项变化了,是不是页面也需要更新?但是子项内部变化怎么通知页面更新?所以需要给子项对象也保存一份依赖?
2、数组子项数组变化,就是对象增删属性,必须用到Vue封装方法 set 和 del,set 和 del 会通知依赖更新,所以子项对象也要保存
看个栗子
页面模板
看到数组的数据,就存在两个 ob
总结
到这里,就可以很清楚,引用类型和 基础类型的处理差异了
1、引用类型会多添加一个 __ob__属性,其中包含 dep,用于存储 收集到的依赖
2、对象使用 __ob__.dep,作用在 Vue 自定义的方法 set 和 del 中
3、数组使用 __ob__.dep,作用在 Vue 重写的数组方法 push 等中
终于写完了,真的好长,但是我觉得值得了