一、前言
这篇文章结合Vue2.7.16
的源码和一个Vue2
的项目,来详细讲解Vue2
实现响应式数据的核心代码
1.1 准备
-
安装
@vue/cli
npm install -g @vue/cli
-
创建vue项目
vue create vue2-test
-
修改Vue实例的配置对象
二、响应式处理的入口
-
通过
new Vue()
调用Vue
构造函数,然后会执行里面的this._init(options)
方法 -
_init(options)
方法定义在Vue.prototype
上,定义是通过执行initMixin(Vue)
实现的 -
在
_init(options)
方法中,做了一些初始化的操作。其中initState(vm)
用来初始化状态。 -
initState(vm)
,按照props、methods、data、computed、watch的顺序,对一些属性做了初始化操作。其中initData(vm)
就是对配置对象中的data做响应式处理的。 -
在
initData()
方法中,做了以下处理:- 获取到配置对象上面的data配置项,如果是用方法定义的就通过调用方法获取,如果是用对象配置的,就直接访问。然后将data绑定到
vm._data
上。 - 检查data中的属性名是否与methods、props中定义重复
- 通过
proxy()
,将vm._data
上的数据直接放在了vm
身上。内部就是使用Object.defineProperty()
实现通过vm.属性
去访问vm._data.属性
。这样访问要方便一点。 - 调用
observe(data)
来对数据进行观测,这也是响应式处理的入口
- 获取到配置对象上面的data配置项,如果是用方法定义的就通过调用方法获取,如果是用对象配置的,就直接访问。然后将data绑定到
三、入口函数observe
-
observe接收一个value(第一次传入的就是data),然后判断其身上是否有
_ob_
属性,该属性用来标识是否已经被观测。如果已经被观测过了,就直接返回value身上的ob对象。 -
判断数据类型等条件,条件满足,则创建一个
Observer
实例并返回。 -
在上面
initData()
方法中,会拿到返回的ob对象,并将ob对象身上的vmCount++
。它的作用就是用来区分我们操作的对象是根\(data还是其子属性。 在Vue中,应该避免直接在一个Vue实例或其根`\)data`对象上添加或删除响应式属性。
四、 Observer类
-
Observer
类有两个属性:dep
和vmCount
。vmCount
的作用已经说过了。dep
是一个Dep
实例,用来收集依赖和派发更新。 -
在
Observer
的构造器中对dep
和vmCount
进行了初始化,然后在def(value, '__ob__', this)
方法中,通过Object.defineProperty
为当前的value(第一次调用时是data)添加了一个_ob_
属性,属性值为当前的Observer
实例 -
根据当前的value是不是数组,来进行不同的操作。如果不是数组,则遍历当前value的所有属性,执行
defineReactive()
方法。是数组的情况,在后面单独说明。
五、defineReactive
-
defineReactive()
方法就是用来对当前的属性做响应式处理的,主要做了以下操作:- 创建一个Dep实例对象,用来收集当前属性的依赖和派发更新。
- 通过
Object.getOwnPropertyDescriptor(obj, key)
来获取当前属性的所有自有描述信息,比如:是否可写、可枚举、可配置、get()、set()等
。 - 如果当前属性不可配置,就直接返回不做处理。
- 获取当前属性的getter和setter,后面会用到。获取当前属性的属性值,后面会用到。
- 根据是否深度观测,来决定是否调用
observe(value)
方法。调用该方法,就可以实现递归地对所有属性进行响应式处理,同时,该方法还会为当前属性身上添加一个_ob_
属性,指向一个Observer
实例,然后将该实例对象返回,即childOb
。 - 使用
Object.defineProperty
为当前属性添加getter、setter、enumerable: true、configurable: true等属性,即在此做数据劫持。
-
在get方法中,首先会获取当前属性的属性值(有getter就通过getter,没有就使用前面获取到的
val
)。然后通过
dep.depend()
将Dep.target
(当前访问该属性的watcher)添加到当前属性的依赖列表中,同时也添加到childOb
的依赖列表中。这里之所以添加到childOb
的列表中,是为了在其它地方也能知道有哪些watcher依赖该属性。因为可以通过属性._ob_.dep
来进行派发更新。这也是$set
、$delete
实现数据响应式的前提。最后将数据返回。
-
在set方法中,首先获取到当前属性的属性值(有getter就通过getter,没有就使用前面获取到的
val
)。然后对比当前set方法接收到的值,如果没有变化,就不做处理。如果发生了变化,就修改当前的值(有setter就通过setter修改,没有就手动修改)。
考虑到set方法接收的新值可能也是一个对象,所以需要对这个新的值再次调用
observe(newValue)
进行观测。最后调用
dep.notify()
派发更新(通知依赖列表中的所有watcher执行update方法)。
六、收集依赖和派发更新
-
在get方法中,会收集依赖当前属性的所有watcher
-
Dep类的结构如下
根据Dep类的定义,可以知道每一个Dep实例都有一个id(唯一标识)和一个subs(用来保存watcher)。
此外还有两个最重要的方法,depend()(收集依赖)和notify()(派发更新)。
同时Dep还有一个静态属性target,指向的就是当前的watcher。
-
depend()
需要注意的是depend()方法并没有直接将
Dep.target
添加到subs
中,它反而是调用了Dep.target
的addDep(this)
方法,将当前的Dep实例传给了watcherwatcher的
addDep()
方法,将传过来的dep信息保存下来,并通过dep.addSub(this)
,将当前watcher添加到dep的subs中。这里保证了watcher不重复收集dep,dep不重复收集watcher。而且,当watcher被销毁的时候,就可以根据收集的dep信息,通知相应的dep将自己从subs中移除,以免后面进行派发更新的时候通知给一个已经不存在的watcher。
-
notify()
这里根据watcher的id进行了排序,因为watcher有三种:渲染watcher、计算属性watcher、用户watcher。这三者要保证计算属性watcher、用户watcher、渲染watcher的顺序执行。
遍历调用watcher的update方法实现更新操作。
七、数组的响应式处理
-
对于数组的响应式处理,这里首先做了一个判断,在
if (!mock)
里面,对数组的七个方法进行了重写,准确来说,是为响应式的数组修改了原型对象。重写的七个方法,在原有方法的功能之上,实现了派发更新,就是通过数组身上的
_ob_.dep
实现的。这里的
hasProto
是如何定义的?就是判断对象是否支持原型,那一般情况都是支持的。所以就先不管下面的分支处理了。再来看一下arrayMethods是什么
- 首先通过
Object.create(arrayProto)
,将新创建的arrayMethods对象的_proto_
属性指向arrayProto
,也就是arrayMethods._proto_ = = Array.prototype
- 对会改变数组的七个方法,进行了重写。具体实现是
- 获取数组原始的方法
original
- 通过
def()
方法,调用Object.defineProperty
给arrayMethods
身上添加新的方法。新方法在原始方法original
的基础上进行增强 - 对于push、unshift、splice等会插入新值的方法,需要获取到新的值,然后对这个值做观测。
- 获取数组原始的方法
最后,再将数组的原型属性指向新的对象
arrayMethods
,即array._proto_ == arrayMethods
arrayMethods._proto_ = = Array.prototype
- 首先通过
-
此外,还在
if (!shallow)
这个判断中,调用了observeArray(value)
方法。该方法遍历数组元素,对每一个元素执行observe(),就是为了将数组中那些对象元素身上的属性变成响应式的。而对于基本类型的元素不做任何处理。因为数组可能会有很多的元素,为每一个元素添加getter和setter是很耗费性能的。