# 80%的人都会答错的vue题目!!!

1,384 阅读5分钟

先来三道题


以上三个问题的答案分别为

  1. 不会触发
  2. 会被触发
  3. 3s之后不会触发

如果以上三个问题都答对了,下面的就不用看了,说明你对vue响应式原理理解非常深刻。


赋予数据响应式能力

声明以下代码基于vue@2.6.10,对于源码有一定删减。那就从new Vue()开始说起

vue响应式数据初始化是在initData这个函数中,并且我们可以看出data实际是可以为对象的,只是最好是一个函数,保证多个组件不会共享同一份data,导致组角相互干扰。

从名字上也能看出,将普通对象变成响应式数据的核心在于 observe这个方法。

observe会先检查对象上是否有 __ob__ 这个属性,如果有说明已经是响应式数据了,不用在observe了。第二个分支比较啰嗦,如果服务端渲染或者是vue组件对象,就不用变成响应式数据了,因为没有必要。命中的话就将数据当场参数传入,创建 Observer 实例,从名字上看就是讲普通对象变成了可观察对象。

Observe 构造函数中 new Dep() 先简单理解为创建一个用来放依赖(dependency)的地方,然后把Observer 这个实例本身挂到 value__ob__ 上,避免之后被重复 observe。如果是Array类型,调用observeArray,这是因为vue用到了 Object.defineProperty提供的get和set访问器。而Array的index是索引属性,并不是访问器属性,所以没有办法触发get和set方法。

如果是对象,则会遍历对象中的可访问属性,对于每个属性,执行defineReactive,让每个属性变得reacttive起来。

如果说要举一段非常有js味道的代码,我觉得这里就再好不过了。先创建一个dep 实例,使用Object.defineProperty定义当前属性的get和set操作。getset分别是两个新定义的函数,并且引用了函数外部的变量dep,这里是闭包的典型应用场景。

get被调用时,如果取值的时候Dep.target不为空,调用 dep.depend(),可以先简单理解为该属性收集到了依赖。如果是深层次的观察,子对象也应该收集依赖。

set被调用时,会先检查 newVal与旧的值是否有变化,没变化什么都不做,直接结束函数。有变化就调用 dep.notify(),通知收集到的依赖。所以对于对象上的任意一个属性,都会有一个依赖容器,当属性发生变化时,通知依赖容器中的依赖。

举例可以看出 d.m.a 上有 __ob__ 属性,并且dep.subs不为空,这里就解释了第二题。因为只要属性被观察了,当属性改变的时候就会通知相关的依赖。

观察数据,收集依赖的过程

梳理一下整个 observe的过程就是如上所示,data通过Observer与Dep关联起来,至此数据赋予了响应式的能力。

但是如果数据从未用过,没有调用过触发过属性的 get 方法,没有收集到依赖,那么改变的时候也没有依赖需要通知,自然也不会产生额外的副作用了。

因为响应式的数据还需要一个watch操作才能完成任务,有两种watch使用姿势

  1. render 方法本质上就是调用了watch api
  2. vue 暴露出来的$watch方法

new Watch的第二参数是要观察的内容,通常是表达式或者函数,第三个参数是执行回调函数。

在render 的场景下,watch了需要render的函数,而我们知道render函数其实就是模板编译成了函数。在函数执行的过程中,会触发vm.$data的取值操作,也就是get函数。

如果是renderWatcher, 则会把vm.watcher 设为this。computed的模式暂时先不用关注,因为普通情况就是执行了 this.get()

在get执行之前做一次 pushTarget,之后结束做一个PopTarget,因为有多个依赖要收集,并且可能是递归的。因此使用栈来保存之前的target,并把当前的taget挂载到Dep上。执行结束之后,把之前的target重新设置回来。

getter函数也就是此时的渲染函数就会执行,渲染函数执行的时候涉及了属性的取值,此时触发了 get操作。

我们现在再回头看get中的依赖收集,其实就是把Watcher.addDep(dep),而addDep同时也会调用dep.addSub(this),使用Dep和Watch保持相互引用关系,并且是多对多的关系。

等Dep调用notify的时候,会调用所有 this.subs 的update方法,此时可以把Watcher理解为依赖,而前面也正是new Watcher 开启了观察。因此当调用 watcher.get 方法时,实质是观察者,触发了观察。

由于是多对多的关系,因为可以同时观察同一个属性,一个属性有多个观察者;也可以一次性观察多个属性,每个属性都有各自的依赖容器,watcher充当多个属性的依赖,任意一个属性改变时都会通知到watcher。

watcher.get 方法的finally 执行了cleanupDeps, 从字面意思就是清理依赖。

从上述代码中我们可以看出Watcher 会维护一个旧依赖和新依赖的表,并且有选择性的进行更新。

因为这里就可以回答第三题,不会触发,因为3s之后Watcher观察的属性有变化,对应在watcher 这里的deps会发生更新。

因此主要可以分为如上三大块儿,initData只是赋予了数据响应式的能力,但此时还没有建立起响应的关系。

只有当data touch 的时候才会收集到依赖,这里理解的依赖是指被通知去执行update的那个。因为第一题也有了答案。

通知依赖

如果有sync,则表示同步执行,直接执行this.run。默认情况下调用quueWatcher(this),将当前watcher当到队列中。

queueWatcher 会区分几种情况去消费队列中的任务,因为我们可以看出当数据改变后,会在下一个tick才清空任务队列。或者只有一个watcher回调触发的另一个watcher,才会立即同步加入队列中执行。因此在watcher的回调中触发另一个watcher,是有可能触发死循环的。同时也解释了为什么改变数据之后必须在nextTick中才能获取dom变化,这样也是出于性能考虑,一次性执行update。