我们都知道 Vue 的一个核心特点是数据驱动,Vue 的内部实现了一个机制,该机制能监听到数据的变化然后触发更新,本文主要是对 Vue 的数据响应式进行一个介绍,同时也是笔者学习源码的一个笔记,如有错误的地方,欢迎评论区进行指正。
我们都知道 Vue2 是采用 Object.defineProperty
来实现数据的监听,具体怎么实现我们可以往下看;
一、入口文件
我们可以从源码中得知实例化一个 Vue 时,需要传入一个 options
,这个 options
就是我们 new Vue()
时传入的一个对象,实例化时会调用 _init
方法;
// src/core/instance/index.js
function Vue (options) {
// Vue 原型挂载的初始化方法,具体实现是在 initMixin 方法里面
this._init(options)
}
initMixin:初始化一些配置,包括初始化生命周期、初始化数据等;
// src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// ...
// 省略其他代码,只关注重要部分
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
// beforeCreate 生命周期函数,未初始化 data,所以在 beforeCreate 钩子函数访问不到 data 数据
callHook(vm, 'beforeCreate')
initInjections(vm)
// 初始化数据,包括 props、data、methods 等属性
initState(vm)
initProvide(vm)
// created 生命周期函数,已经初始化 data,所以在 created 钩子函数可以对 data 进行赋值
callHook(vm, 'created')
// ...
// 其他代码
}
}
二、初始化 initState
initState 主要做的事情是初始化 options 里的一些属性,顺序是 props
、methods
、data
、computed
、watch
,这里只介绍 data 的初始化;
initData
主要做的事情包括:
- 判断传入的 data 是函数还是对象;
- 代理 data 到 vm,方便我们通过 this.xxx 访问 this._data.xxx;
- 对 data 中的变量进行响应式处理;
// src/core/instance/state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 初始化 props
if (opts.props) initProps(vm, opts.props)
// 初始化 methods
if (opts.methods) initMethods(vm, opts.methods)
// 初始化 data
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 初始化 computed
if (opts.computed) initComputed(vm, opts.computed)
// 初始化 watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
function initData (vm: Component) {
let data = vm.$options.data
// 判断传入的 data 是函数还是对象
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// 获取 data 中变量的集合
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
// 判断是否有变量重名
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
// 代理 data 到 vm,方便我们通过 this.xxx 访问 this._data.xxx
proxy(vm, `_data`, key)
}
}
// 对 data 中的变量进行响应式处理
observe(data, true /* asRootData */)
}
三、数据响应式处理
Vue 的响应式处理主要是 Observer
类,Observer
类主要做的事情包括:
- 在响应式数据上添加
__ob__
属性,指向当前实例,代表该数据已经经过响应式处理; - 判断响应式数据是否是数组,如果是数组,遍历数组挨个进行响应式处理,否则调用
defineReactive
进行处理; defineReactive
会对数据进行get
和set
处理,也就是常说的数据劫持;- get:get 处理会有
依赖收集
的过程,后续会介绍; - set:set 处理会有
依赖更新
的过程,后续会介绍;
- get:get 处理会有
注意:Vue 中对数组的响应式处理是通过改写七种原型方法来实现,因为对数组的下标进行拦截,相对来说会比较耗费性能,这七种方法包括 push
,pop
,shift
,unshift
,splice
,sort
,reverse
;
// src/core/observer/index.js
export class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
// 判断需要响应式处理的变量是不是数组,Vue 对数组进行了特殊处理
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 对每个对象进行响应式处理
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
// 对数组的每一项进行响应式处理
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 依赖收集器
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
// 响应式处理的重点
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// 依赖收集
dep.depend()
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
childOb = !shallow && observe(newVal)
// 通知依赖进行更新
dep.notify()
}
})
}
数组改写:
- 获取数组的原型,赋值给一个新对象;
- 定义七种数组方法,进行遍历;
- 对七种方法进行
def
,再通过__ob__
对改变后的数组进行响应式处理,然后进行依赖更新;
// src/core/observer/array.js
// 获取数组的原型
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 数组的七种方法改写
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
// Vue 数据进行响应式处理时会在原型上添加 __ob__
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
ob.dep.notify()
return result
})
})
总结:
Vue 中的数据响应式是通过 Object.defineProperty
来对数据进行 get
和set
,从而达到数据监听的效果,另外对数组的原型进行改写,实现数组的响应式;