vue数据响应式
vue数据响应化的代码都在src/core/observer里面。具体实现是在Vue初始化时,会调用initState,它会初始化data,props等,这里着重关注data初始化,initData核心代码是将data数据响应化。
initState
props、methods、computed、watch初始化,data响应化
//文件位置:/src/core/instance/state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initData
function initData (vm: Component) {
let data = vm.$options.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
)
}
// proxy data on instance
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)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
observe
observe 方法首先判断 value 是否已经添加了 ob 属性,它是一个 Observer 对象的实例。如果是就直接用,否则在 value 满足一些条件(数组或对象、可扩展、非 vue 组件等)的情况下创建一个 Observer 对象, 并最终返回一个Observer实例。
//core/observer/index.js
export function observe (value, vm) {
if (!value || typeof value !== 'object') {
return
}
var ob
if (
hasOwn(value, '__ob__') &&
value.__ob__ instanceof Observer
) {
ob = value.__ob__
} else if (
shouldConvert &&
(isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (ob && vm) {
ob.addVm(vm)
}
return ob
}
Observer
遍历data的属性,并根据属性的类型做不同的处理。 Observer 类的构造函数主要做了这么几件事:首先创建了一个 Dep 对象实例(关于 Dep 对象我们稍后作介绍);然后把自身 this 添加到 value 的 ob 属性上;最后对 value 的类型进行判断,如果是数组则观察数组,否则观察单个元素。其实 observeArray 方法就是对数组进行遍历,递归调用 observe 方法,最终都会调用 walk 方法观察单个元素。
walk 方法是对 obj 的 key 进行遍历,通过Object.defineProperty
方法让它们拥有 getter、setter 方法,来定义响应式数据,这样一旦属性被访问或者更新,我们就可以追踪到这些变化。
export function Observer (value) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment
augment(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], obj[keys[i]])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
defineReactive (obj, key, val)
defineReactive 方法最核心的部分就是通过调用 Object.defineProperty 给 data 的每个属性添加 getter 和setter 方法。当 data 的某个属性被访问时,则会调用 getter 方法,当 Dep.target 不为空时调用 dep.depend 和 childObj.dep.depend 方法做依赖收集。如果访问的属性是一个数组,则会遍历这个数组收集数组元素的依赖。当改变 data 的属性时,则会调用 setter 方法,这时调用 dep.notify 方法进行通知。这里我们提到了 dep,它是 Dep 对象的实例。
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter ? : ? Function,
shallow ? : boolean
) {
const dep = new Dep() // 一个key一个Dep实例
if (arguments.length === 2) {
val = obj[key]
}
// 递归执行子对象响应化
let childOb = !shallow && observe(val)
// 定义当前对象getter/setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// getter被调用时若存在依赖则追加
if (Dep.target) {
dep.depend()
// 若存在子observer,则依赖也追加到子ob
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value) // 数组需特殊处理
}
}
}
return value
},
set: function reactiveSetter(newVal) {
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal // 更新值
childOb = !shallow && observe(newVal) // 递归更新子对象
dep.notify() // 通知更新
}
})
}
Dep ()
Dep 类是一个简单的观察者模式的实现。它的构造函数非常简单,初始化了 id 和 subs。其中 subs 用来存储所有订阅它的 Watcher,Dep.target 表示当前正在计算的 Watcher,它是全局唯一的,因为在同一时间只能有一个 Watcher 被计算。
export default class Dep {
static target: ? Watcher; // 依赖收集时的wacher引用
subs: Array < Watcher > ; // watcher数组
constructor() {
this.subs = []
}
//添加watcher实例
addSub(sub: Watcher) {
this.subs.push(sub)
}
//删除watcher实例
removeSub(sub: Watcher) {
remove(this.subs, sub)
}
//watcher和dep相互保存引用
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify() {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
前面提到了在 getter 和 setter 方法调用时会分别调用 dep.depend 方法和 dep.notify 方法,接下来依次介绍这两个方法。
dep.depend()
depend 方法很简单,它通过 Dep.target.addDep(this) 方法把当前 Dep 的实例添加到当前正在计算的Watcher 的依赖中。
Dep.prototype.depend = function () {
Dep.target.addDep(this)
}
dep.notify()
notify 方法也很简单,它遍历了所有的订阅 Watcher,调用它们的 update 方法。
至此,vm 实例中给 data 对象添加 Observer 的过程就结束了。
Dep.prototype.notify = function () {
// stablize the subscriber list first
var subs = toArray(this.subs)
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
Vue数据响应化原理
一开始数据通过observe进行响应式,然后会得到一个Observer,Observer根据数据类型的不同,分别调用walk、observeArray方法对数据进行遍历,通过Object.defineProperty
方法让它们拥有 getter、setter 方法有了getter和setter以后,会尝试着做依赖收集,把Dep与Watcher串联起来。在初始化组件过程中,会去访问数据,就会调用getter方法,调用addDep方法把Dep与Watcher产生关联(将Dep实例添加到当前计算Watcher依赖中)。当有数据发生更新的时候,会去调用setter方法,通知dep(dep.notify),让Watcher去调用update方法,从而去更新视图。
vue更新操作
vue在做数据更新时候,尤其对DOM进行操作的时候,批量的异步的操作。怎么实现的呢?首先我们有一个异步队列,我们把所有的观察者所有的Watcher全部push队列中,然后等到每一个循环周期(event loop)到的时候,统一的去执行队列中所有Watcher(每一个Watcher都有一个update函数,它会比较preVnode与vnode diff操作,如果有变化则做DOM操作)的更新,这样就提高了执行效率。
vue在更新DOM时是异步执行的。只要侦听到数据的变化,vue将开启一个队列,并缓冲在同一个时间循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被push到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。然后,在下一个事件循环tick中,vue刷新队列并执行实际(已去重)工作。vue在内部对异步队列尝试使用原生的Promise.then、MutationObserver和setImmediate,如果执行环境不支持,则会采用setTimeout代替。
queueWatcher(this)入队操作,更新时不是立刻去执行DOM操作,而是让当前的Watcher进入队列中 ,异步批量更新