Vue源码解析系列——响应式原理篇:依赖收集

准备

vue版本号2.6.12,为方便分析,选择了runtime+compiler版本。

回顾

如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:《Vue源码分析系列:目录》

依赖收集

所谓依赖收集,就是利用Vue定义的Dep类去收集当前正在执行中的watcher
依赖收集的操作在dataget过程中,所以我们进入defineReactive方法,去看看在get的过程中做了什么操作。

defineReactive

const dep = new Dep();
...
//访问该值时,依赖收集
    get: function reactiveGetter() {
      //如果有原始的getter
      //value就是调用原始的getter后的结果
      //如果没有,value就直接是val
      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;
    },

一开始先实例化了一个Dep,之后在get过程中有一个判断,如果存在Dep.target就执行dep实例的depend方法。在这里我提前透露下,其实这个Dep.target就是当前正在运行的watcher。现在先进入depend方法一起看看:

dep.depend

depend () {
   if (Dep.target) {
     Dep.target.addDep(this)
   }
}

先判断当前是否有正在运行的watcher,如果存在就调用这个watcheraddDep方法,进入addDep方法。

watcher.addDep

addDep(dep: Dep) {
    const id = dep.id;
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        dep.addSub(this);
      }
    }
  }

addDep方法主要进行了两个操作:

  • 将刚刚的Dep实例保存到实例属性this.newDeps
  • 调用dep.addSub,然后传入当前watcher的实例。进入dep.addSub

dep.addSub

addSub (sub: Watcher) {
    this.subs.push(sub)
}

由这里可以看到,其实dep将收集到的依赖(watcher)全部放入了this.subs这个数组中。
到这里,其实依赖收集已经讲完了,接下来该讲的应该就是派发更新了,但是我刚刚说Dep.target就是当前正在运行的watcher,这又是从哪里得知的呢?

Dep.target

Dep类的定义中可以看到,target是一个静态属性:

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  ...
}

Dep.target = null

静态属性的特点就是全局唯一,因为静态属性是随着类的加载而加载的。
那这个target是在哪里被赋值的呢?先不急看在哪赋值,我们先看看watcher是在什么时候实例化的,因为当我们知道watcher是在什么时候实例化,那就知道了当前正在运行的watcher是怎么赋值到Dep.target身上的了。

mountComponent

我们可以在Vue挂载的阶段,mountComponent函数中看到这么一段定义:

updateComponent = () => {
	vm._update(vm._render(), hydrating);
};

new Watcher(
   vm,
   updateComponent,
   noop,
   {
     before() {
       if (vm._isMounted && !vm._isDestroyed) {
         callHook(vm, "beforeUpdate");
       }
     },
   },
   true /* isRenderWatcher */
 );

在这里我们看到了一个watcher的实例化过程,传入了updateComponent这个函数,updateComponent会调用_render,这些逻辑我们前面都已经分析过了,在这不多赘述。
直接进入watcher的构造器:

class Watcher

constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
  ....
   this.value = this.lazy ? undefined : this.get();
}

在构造器的最后一行我们看到这里调用了一个get方法,进入get方法

watcher.get

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 {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      //清除无用的watcher
      this.cleanupDeps();
    }
    return value;
  }

这里的逻辑的大概意思是先执行pushTarget传入当前watcher实例,之后调用this.getter,这个this.getter在上面mountComponent的过程中就是updateComponent函数,于是这个时候就会执行vm._update然后就是__patch__,最后更新DOM,也就是说在new Watcher的过程中就会执行一次渲染操作。那么,那个Dep.target是在哪里赋值的呢?回过来看看这个get方法的第一行,不是有个pushTarget吗?

pushTarget

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

pushTarget做了两个操作:

  • 将当前的正在运行的watcher实例收集到了一个栈结构targetStack
  • 将当前的正在运行的watcher实例赋值给了Dep.target

targetStack栈结构,就是将当前的watcher保存起来,毕竟Dep.target只能保存一个watcher实例,如果存在组件嵌套,当前watcher还未执行完,就被子组件的watcher给覆盖了,所以需要存储watcher

defineReactive

回到defineReactive函数,我们继续往下看:

if(childOb) {
  childOb.dep.depend();
  if (Array.isArray(value)) {
    dependArray(value);
  }
}

这里其实是比较难理解的一个点。
如果存在childOb,就调用childOb实例属性dep上的depend()方法。
childOb其实是$data中的对象添加响应式后的结果,我们来举个例子:

data:() => ({
	a:{
		b:"1"
	}
})

这是一个Vue实例的data选项,其中有一个对象aa中有一个键bb的值为"1"。在这里childOb就是指a对象的__ob__属性。源码中调用了childOb.dep.depend(),意思是将a对象整个对象的结构也加入了依赖中,为的是监听对象属性的增加和删除,而不是为了监听对象某个属性的值是否发生改变。

继续向下看,如果发现value是一个数组,就会调用dependArray

function dependArray(value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i];
    e && e.__ob__ && e.__ob__.dep.depend();
    if (Array.isArray(e)) {
      dependArray(e);
    }
  }
}

有了上面childOb的理解的基础上,这个函数的理解就比较简单了。
如果数组中有对象,也需要监听数组中的对象结构是否发生改变,然后再派发更新。

总结

其实依赖收集是Vue中比较难的部分,因为这里的源码非常的绕,一会是Dep,一会是Watcher,一会又是defineReactive甚至还有mountComponent,逻辑顺序比较复杂,需要好好梳理后才能彻底理解,在这里我推荐一个老师的课程,很短,没几集,但是将ObserverDepWatcherobservedefineReactive梳理得非常清楚、浅显易懂:点击查看,重点是免费的哦。
好了,接下来我们要进入一个更加复杂的逻辑了,派发更新,准备好了吗?我们下一章见~

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱学习的前端小黄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值