简介
Vue 的核心响应式是通过Obeject.defineProperty
方法来实现的。 而Object.defineProperty是 ES5 中无法shim的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因。
Object.defineProperty
Object.defineProperty 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象,先来看一下它的语法:
1 | Object.defineProperty(obj, prop, descriptor); |
obj 是要在其上定义属性的对象;prop 是要定义或修改的属性的名称;descriptor 是将被定义或修改的属性描述符。
由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 data
对象上存在才能让 Vue 转换它,这样才能让它是响应的。
响应式原理大致流程如下图所示:
Vue 数据响应式变化主要涉及Observer、Watcher、Dep这三个主要的类。这里主要是响应式对象,后面分别会记录它的依赖收集、派发更新、三种 Watcher。
把普通对象改造为响应式对象在 Vue 中的大致流程为:
initState
(初始化数据)Observer(
劫持数据)defineReactive
(依赖收集、派发更新)
在 getter 对象中又会依赖收集,在 setter 中派发更新。
Vue-version(2.6.10)
initState(初始化数据)
那么我们从一个简单的 Vue 实例的代码来分析 Vue 的响应式原理:
1 | <template> |
在Vue的初始化阶段,_init
方法执行的时候,会执行 initState(vm)
方法,它的定义在 src/core/instance/state.js中。
1 | export function initState(vm: Component) { |
initState
方法主要是对 props
、methods
、data
、computed
和 wathcer
等属性做了初始化操作。主要看initData
。
initData
initData
方法如下:
1 | function initData(vm: Component) { |
在 initData 过程中主要做了两件事:
- 通过
proxy
把每一个值 vm._data.[key] 都代理到 vm.[key] 上; - 调用
observe
方法观测整个data
的变化,把data
也变成响应式(可观察),可以通过vm._data.[key]
访问到定义data
返回函数中对应的属性。
Observer(劫持数据)
observe 的功能就是用来监测数据的变化,它的定义在 src/core/observer/index.js 中:
1 | /** |
observe
方法的作用就是给非 VNode 的对象类型数据添加一个 Observer
,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer
对象实例。接下来我们来看一下 Observer
的作用。
Observer
observe 的功能就是用来监测数据的变化,它的定义在 src/core/observer/index.js 中:Observer
是一个类,它的作用是给对象的属性添加 getter
和 setter
,用于依赖收集和派发更新:
1 | /** |
上面这个方法做的事情如下:
- 实例化
Dep
对象 - 通过执行
def
函数把自身实例添加到数据对象value
的 ob 属性上 - 对
value
做判断,对于数组会调用observeArray
方法,否则对纯对象调用walk
方法。
可以看到
observeArray
是遍历数组再次调用observe
方法,而walk
方法是遍历对象的key
调用defineReactive
方法,那么我们来看一下这个方法是做什么的。
def 方法
def 的定义在 src/core/util/lang.js 中:
1 | /** |
开发中输出 data
上对象类型的数据,会发现该对象多了一个 __ob__
的属性。
defineReactive(依赖收集、派发更新)
defineReactive
的功能就是定义一个响应式对象,给对象动态添加 getter
和 setter
,它的定义在 src/core/observer/index.js
中:
1 | /** |
这里暂时不对依赖收集、派发更新、Dep讲述只记录数据劫持的过程记录,后面文章记录具体的依赖收集、派发更新、watcher、Dep、记录。defineReactive
函数最开始初始化 Dep
对象的实例,接着拿到 obj
的属性描述符,然后对子对象递归调用 observe
方法,这样就保证了无论 obj
的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj
中一个嵌套较深的属性,也能触发 getter
和 setter
。最后利用 Object.defineProperty
去给 obj 的属性 key 添加 getter 和 setter。
数据观测的特殊处理
访问对象属性,其取值与赋值操作,都能被Object.defineProperty()
成功拦截,但是Object.defineProperty()
在处理数组上却存在一些问题。通过调用数据原型上的push
、pop
、shift
、unshift
、splice
、sort
、 reverse
等方法不能被观测到兼容性问题。
在Vue中是对数组的原型上述方法做了一些增强操作。即保留原来操作的基础上,植入Vue的特定的操作代码。
代码在 src/core/observer/index.js 中定义:
1 | const arrayProto = Array.prototype; |
保留数组原来的操作 push
、unshift
、splice
这些方法,会带来新的数据元素,而新带来的数据元素,我们是有办法得知的(即为传入的参数)那么新增的元素也是需要被配置为可观测数据的,这样子后续数据的变更才能得以处理。所以要对新增的元素调用observer
实例上的observeArray
方法进行一遍观测处理由于数组变更了,那么就需要通知观察者,所以通过ob.dep.notify()
对数组的观察者watchers
进行通知。
总结
从初始化initData
,到核心就是利用 Object.defineProperty
给数据添加了 getter
和 setter
,目的就是为了在我们访问数据以及写数据的时候能自动执行一些逻辑:getter
做的事情是依赖收集,setter
做的事情是派发更新。