从官方描述中,我们可以看到有这几个词:defineProperty、watcher、getter/setter,但是是否只有这几个呢?他们之间的关联以及运作是怎样的呢?
init.js
首先我们去查看源码中实例初始化文件init.js,文件中核心代码有:
- 选项合并:将用户的选项options与通用的默认选项合并
- 初始化,实现众多方法,包括我们熟悉的事件监听、beforeCreate生命周期钩子、初始化状态、created生命周期钩子。
- 初始化完成了之后,执行挂载:
vm.$mount(vm.$options.el)
其中我们要查看的核心就是初始化状态,那么在initState方法中实现了什么呢?
state.js
我们看到initData就是我们响应式的入口,跟踪查看。
首先是一系列容错,如果data传入的是对象(不建议使用,因为推荐纯函数思想)还是函数,如果是函数就调用一个方法得到对象,然后再判断是否和方法名、prop名冲突,最后调用observe方法,将data响应式处理。
我们去查看observe方法
observer/index.js
observe
我们看到,observe方法中做了一些判断,但是核心还是
ob = new Observer(value),实例化了一个对象,那么我们再看看Observer类:
Observer
其实Observer就是判断当前数据是数组还是对象,调用不同的方法来实现响应式。
需要注意:
- 一个对象一个observer。
- 每次实例化observer得到ob,都会产生一个dep(负责对象新增、删除属性的更新通知)。
- 调用
defineReactive方法劫持数据。
defineReactive
我们可以看到,核心还是调用
Object.defineProperty定义对象属性的getter/setter,getter负责添加依赖,setter负责通知更新。但是在内部,还有几个重要的点:
- 每次调用defineReactive方法,都会产生一个dep(也就是说一个key一个dep,负责对应key的值变化的更新通知)。
- 在get方法上,首先判断类Dep上是否有target属性,如果有,就调用实例dep上的depend方法
- 在set方法上,核心:
dep.notify()。
那么现在就有了几个疑问:
- Dep是什么
- Dep上面的target属性是什么,从哪儿来的
- depend方法做了什么操作?
- notify方法做了什么操作?
再去看看Dep。
observer/dep.js
Dep
对于之前的问题,我们找到了答案:
- Dep其实可以理解为管家,管理多个Watcher,包括watcher实例的增删及通知更新,在数据改变的时候通知所有相关Watcher执行更新函数。
- Dep上面的target属性其实是Watcher,那么从哪儿来的呢?我们看到有个pushTarget方法,是添加watcher到target属性,那么在哪儿调用的呢?我们后面来解释。
- depend方法做了什么操作呢?我们可以看到,它是调用了watcher的addDep方法,将自身传递了过去,那么这是为何呢?
- notify方法做了什么操作?是获取所有的watcher,遍历并且调用他们的update方法。
牵扯watcher较多,我们来看看watcher。
observer/watcher.js
Watcher
首先我们看到了调用了get方法,get方法中首先调用了pushTarget传递了自身,使得类Dep上target属性为当前watcher。
难点:addDep方法,首先拿取dep的id,判断是否存在,如果不存在,就添加到数组中去。但是请注意,在if里面还有个if,里面调用dep的addSub方法,传递当前watcher过去。我们查看dep.js,查看Dep中的addSub方法,接受watcher,添加到数组subs里面去。那么这个怎么理解呢?为什么最开始先调用depend方法,在depend里面,调用watcher的addDep方法,将dep添加到watcher中去,然后在addDep方法中,又调用Dep的addSub方法,将watcher传递过去呢?
解释:首先,先来说说vue 1.x和2.x。1.x是细粒度的数据变化侦测,没有vNode,一个状态的使用(也就是一个表达式)一个watcher,所以一个key可能对应多个watcher,但是细粒度造成了大量开销,这对于大型项目来说是不可接受的。所以在2.x中选择了中等粒度的解决方案,每一个组件一个watcher,这样状态变化时只能通知到组件,再通过引入VNode去进行比对和渲染。但是呢,这个时候就有个疑问了,既然一个组件一个watcher,那么我只需要把dep添加到watcher中就可以了啊,为什么还需要将watcher添加到dep中去呢?
原来,watcher也有分类,首先就是根组件或者子组件产生的watcher我们称为渲染watcher,一个组件一个watcher,这个时候watcher和dep对应的关系为1对多。但是当我们在使用watch、computed、$watch()选项时,同样也会产生watcher,这个watcher我们可以称为用户watcher,这个时候watcher和dep对应的关系为多对1。所以,watcher和dep对应关系实际是多对多,因此需要相互保存引用关系。
至此,响应式数据核心概念基本都完毕,但是还存在这些问题:
- 触发set,调用了dep的notify方法,我们看到是获取所有的watcher,然后调用每个watcher的update方法。在watcher的update方法中,我们看到就是执行的
queueWatcher(this)方法,而这个方法里面,又牵扯了异步的批量更新策略,所以在这不做解释。 - 从整个流程上来看,我们发现,到defineReactive时,劫持数据时只是得到get和set,其实没有东西串联watcher、dep以及ob,那么watcher什么时候产生,怎么建立的联系呢?
补充
mountComponent
在init.js中,初始化实现了众多方法之后,还干了一件事,什么事呢?执行挂载$mount方法。其中的核心就是执行了mountComponent方法。
$set、$delete方法
官方描述:
Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。还可以使用 vm.$set 实例方法:this.$set(object, propertyName, value)。
那么vue内部是怎么做的呢?
其实也就是一些一些判断容错,然后增加/删除数据,最后都是调用dep的notify来通知更新。
数组更新检测
官方描述:
Vue 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue - 当你修改数组的长度时,例如:
vm.items.length = newLength
Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:
push()pop()shift()unshift()splice()sort()reverse()
我们从源码中来看一下: