从源码看vue响应式原理
vue响应式原理
前言
众所周知,Vue 是一个 MVVM 框架,它最基本的特征就是数据的双向绑定,在改变数据模型的时候更新视图,视图改变更新数据模型。Vue 上手快速、简单好用,再加上文档丰富全面,Vue 现在已经成为了市面上最流行前端框架之一。但是我们对 Vue 的了解不能仅仅只停留在应用层面上,我们还要了解它的内部原理,为什么这样设计,这样设计的优缺点是什么。我们去了解 Vue 源码,一方面是为了在我们遇到一些比较复杂的问题的时候,我们可以从源码的角度去思考问题;另一方面,了解了很多技术原理之后,或许某一天,你也能创造出一款同样优秀的框架,也说不定呢。
响应式原理的入口
initState
首先我们来看一下 /core/instance/state.js 下面的 initState 函数
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) // 如果有 props ,初始化 props
if (opts.methods) initMethods(vm, opts.methods) // 如果有 methods ,初始化 methods 里面的方法
if (opts.data) { // 如果有 data 的话,初始化,data;否则响应一个空对象
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed) // 如果有 computed ,初始化 computed
if (opts.watch && opts.watch !== nativeWatch) { // 如果有 watch ,初始化 watch
initWatch(vm, opts.watch)
}
}
在 initState 函数中,初始化了一些 Vue 的属性,我们现在只需要关注 data 就行,我们一个看到,如果在我们的 Vue 文件中有 data 这个属性的话,那么就会直接初始化 data,否则的话,就会相应一个空的对象。
initData
下面我们再来看看 initData 这个方法
function initData (vm: Component) {
let data = vm.$options.data
// 初始化 _data,组件中 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
)
}
// proxy data on instance
// 获取 data 中的所有属性
const keys = Object.keys(data)
// 获取 props / methods
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
// 判断 data 上的成员是否和 props/methods 重名
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 */)
}
从 initData 这个函数中我们能看出,如果 data 是一个函数的话,那么就执行 data 方法返回里面的对象,如果是一个对象的话,就直接返回。然后会遍历 data 里面的 key,看看是否和 props 和 methods 重名,如果重名就报出已经定义过相同数据的错误;如果不重名的话,就调用 observe 函数,observe 就是响应式的入口,它的第一个参数就是需要响应式处理的数据,第二个参数是标识这个数据是否为根数据。
observe
observe 是响应式的入口,下面我们来看看这个函数中都做了什么
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 判断 value 是否是对象
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 如果 value 有 __ob__(observer对象) 属性 结束
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 创建一个 Observer 对象
ob = new Observer(value)
}
/*如果是根数据则计数,后面Observer中的observe的asRootData非true*/
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
observe 函数中首先会判断传进来的数据不是一个对象或者是 Vnode 的实例,满足两者中的一个就直接返回,不需要响应式。如果这个两个条件都不满足的话,它会创建一个 ob,ob 一般就是 Observer 的实例,然后判断传进来的对象是否有 ob 这个属性或者是否为 Observer 的一个实例,是的话就赋值给 ob;不是的话,还会有一些判断条件,比较核心的就是判断它是否是数据或者是 Vue 的实例,如果不是的话,就创建一个 Observer 对象。
Observer
我们来看看新建的 Observer 中都做了什么
export class Observer {
// 观测对象
value: any;
// 依赖对象
dep: Dep;
// 实例计数器
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
// 初始化实例的 vmCount 为0
this.vmCount = 0
// 将实例挂载到观察对象的 __ob__ 属性
def(value, '__ob__', this)
// 数组的响应式处理
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 为数组中的每一个对象创建一个 observer 实例
this.observeArray(value)
} else {
// 遍历对象中的每一个属性,转换成 setter/getter
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
// 获取观察对象的每一个属性
const keys = Object.keys(obj)
// 遍历每一个属性,设置为响应式数据
for (let i = 0; i < keys.length; i++) {
defineReactive(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])
}
}
}
// /util/lang.js
// def 函数是对 Object.defineProperty 的封装
// enumerable 双取反,是为了防止 __ob__ 可被遍历
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
Observer 这个构造函数中做的事很简单,就是要将数据进行响应式处理,只不过将对象和数据这两种数据类型做了区分处理,如果是数组的话,就调用 observeArray 去响应式数组,当然了数组情况比较特殊,需要做的事情不仅仅是这些,这个我们稍后会单独看一下;如果不是数组的话,那么就调用 walk 函数,通过 defineReactive 去代理数据。
如何监听数组的变化
const arrayProto = Array.prototype
// 使用数组的原型创建一个新的对象
export const arrayMethods = Object.create(arrayProto)
// 修改数组元素的方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
// 保存数组原方法
const original = arrayProto[method]
// 调用 Object.defineProperty() 重新定义修改数组的方法
def(arrayMethods, method, function mutator (...args) {
// 执行数组的原始方法
const result = original.apply(this, args)
// 获取数组对象的 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)
// notify change
// 调用了修改数组的方法,调用数组的ob对象发送通知
ob.dep.notify()
return result
})
})
由于 Object.defineProperty 不能监听数组的变化,所以你会发现,直接改变数组的长度或者给数组的 index 赋值的时候,是不能响应式的。但是我们会发现当我们在使用 push 等方法的时候,数组的响应式还是能生效的,那是因为 Vue 对 push、pop、shift、unshift、splice、sort、reverse 这七个能改变数组的方法进行了重写。
具体的做法就是:从数组的原型新建一个对象,这样能保证在修改数据的时候不会污染数组原型。他会判断当前环境有没有 proto 这个属性,如果有的话,直接将重写的数组方法赋值给当前数组的原型;如果没有的话,那么就需要遍历所有数组方法,通过 Object.defineProperty 重新代理到当前数组对象上。最后通过调用 Dep 类上的notify() 去通知 Watcher 去更新视图。
defineReactive
// 为一个对象定义一个响应式的属性
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 创建依赖对象实例
const dep = new Dep()
// 获取 obj 的属性描述符对象
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 提供预定义的存取器函数
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 判断是否递归观察子对象,并将子对象属性都转换成 getter/setter,返回子观察对象
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 如果预定义的 getter 存在则 value 等于getter 调用的返回值
// 否则直接赋予属性值
const value = getter ? getter.call(obj) : val
// 如果存在当前依赖目标,即 watcher 对象,则建立依赖
if (Dep.target) {
dep.depend()
// 如果子观察目标存在,建立子对象的依赖关系
if (childOb) {
childOb.dep.depend()
// 如果属性是数组,则特殊处理收集数组对象依赖
if (Array.isArray(value)) {
dependArray(value)
}
}
}
// 返回属性值
return value
},
set: function reactiveSetter (newVal) {
// 如果预定义的 getter 存在则 value 等于getter 调用的返回值
// 否则直接赋予属性值
const value = getter ? getter.call(obj) : val
// 如果新值等于旧值或者新值旧值为NaN则不执行
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// 如果没有 setter 直接返回
// #7981: for accessor properties without setter
if (getter && !setter) return
// 如果预定义setter存在则调用,否则直接更新新值
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 如果新值是对象,观察子对象并返回 子的 observer 对象
childOb = !shallow && observe(newVal)
// 派发更新(发布更改通知)
dep.notify()
}
})
}
defineReactive 函数可以说是响应式原理的核心函数了
它有五个参数:
- obj --- 响应的对象
- key --- 对象的 key
- val --- 对象的值
- customSetter --- 自定义 setter
- shallow --- 是否递归响应数据
defineReactive 内部是对传进的数据进行依赖收集,如果 shallow 没有传的话,就会对数据进行递归响应,所以当 data 中的数据层级比较深的时候,Vue 运行的就会比较慢,这也是影响 Vue 性能的关键之处所在(Vue3.0 用 Proxy 代理解决了这个问题,有兴趣的可以去看看源码)。get 中将 Watcher 放入 Dep 函数中进行数据依赖收集,在 set 中通过 Dep 中的 notify 函数去通知 Watcher 去更新。
Dep
let uid = 0
// dep 是个可观察对象,可以有多个指令订阅它
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
// 静态属性,watcher 对象
static target: ?Watcher;
// dep 实例 Id
id: number;
// dep 实例对应的 watcher 对象/订阅者数组
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
// 添加新的订阅者 watcher 对象
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移除订阅者
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 将观察对象和 watcher 建立依赖
depend () {
if (Dep.target) {
// 如果 target 存在,把 dep 对象添加到 watcher 的依赖中
Dep.target.addDep(this)
}
}
// 发布通知
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
// 调用每个订阅者的update方法实现更新
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// Dep.target 用来存放目前正在使用的watcher
// 全局唯一,并且一次也只能有一个watcher被使用
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
// 入栈并将当前 watcher 赋值给 Dep.target
// 父子组件嵌套的时候先把父组件对应的 watcher 入栈,
// 再去处理子组件的 watcher,子组件的处理完毕后,再把父组件对应的 watcher 出栈,继续操作
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
// 出栈操作
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
Dep 这个类是基于发布订阅模式的,这个类中最和核心的两个方法 addSub,是将 watcher 添加订阅,而 notify 函数则是发布通知,然后调用 Watcher 中的 update 函数去更新页面模板,这之后就是 diff 算法和 模板更新了。
Watcher
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// expOrFn 是字符串的时候,例如 watch: { 'person.name': function... }
// parsePath('person.name') 返回一个函数获取 person.name 的值
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
/*获得getter的值并且重新进行依赖收集*/
get () {
/*将自身watcher观察者实例设置给Dep.target,用以依赖收集。*/
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
/*
执行了getter操作,看似执行了渲染操作,其实是执行了依赖收集。
在将Dep.target设置为自身观察者实例以后,执行getter操作。
譬如说现在的的data中可能有a、b、c三个数据,getter渲染需要依赖a跟c,
那么在执行getter的时候就会触发a跟c两个数据的getter函数,
在getter函数中即可判断Dep.target是否存在然后完成依赖收集,
将该观察者对象放入闭包中的Dep的subs中去。
*/
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
/*如果存在deep,则触发每个深层对象的依赖,追踪其变化*/
if (this.deep) {
/*递归每一个对象或者数组,触发它们的getter,使得对象或数组的每一个成员都被依赖收集,形成一个“深(deep)”依赖关系*/
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
/**
* Add a dependency to this directive.
*/
/*添加一个依赖关系到Deps集合中*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
/**
* Clean up for dependency collection.
*/
/*清理依赖收集*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
/*同步则执行run直接渲染视图*/
this.run()
} else {
/*异步推送到观察者队列中,由调度者调用。*/
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
/*触发回调渲染视图*/
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
/*获取观察者的值*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
/*收集该watcher的所有deps依赖*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
/*将自身从所有依赖收集订阅列表删除*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
/*从vm实例的观察者列表中将自身移除,由于该操作比较耗费资源,所以如果vm实例正在被销毁则跳过该步骤。*/
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
Watcher 中的代码虽然比较多,但是实际就是一个观察者对象,这个对象在依赖收集之后会被放在 Deps 中,如果数据变动的时候,会通知 Watcher 去调用 update 函数更新视图。
mountComponent
其实很多人看到这可能会有一个疑问,那么就是怎么去触发 Watcher?其实在 Vue1.0 中每一个被代理的对象都有一个 Watcher,你可以理解成没有个 data 中的数据都有一个私人保姆,每一个数据发生变化的话,对应的 Watcher 就会通知视图去变化,这样实现肯定会简单一些,但是无疑会造成网页性能会大大降低,这也是 Vue1.0不能做大型项目的制约因素之一。而 Vue2.0 是使用一个组件对应一个 Watcher,也就相当于将 data 中的数组集中监听,组件变化了,才会让 Watcher 去通知改变视图,大大的提升了页面的响应速度。
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 创建 Watcher
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}