Vue2.0响应式原理剖析
Vue.js 实现响应式的核心是利用了 ES5 的 Object.defineProperty。这个方法在大多数浏览器下都是支持的,但是在ie8及以下浏览器是没有这个方法的,并且没有任何的补丁来兼容这个方法,这也是为什么Vue.js不能兼容ie8及以下浏览器的原因。
Object.defineProperty
Objet.defineProperty方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。 MDN链接
语法
Object.defineProperty(obj, prop, descriptor)
属性描述符
configurable
enumerable
value
writable
get
set
我们最关心的是 get 和 set,get 是一个给属性提供的 getter 方法,当我们访问了该属性的时候会触发 getter 方法;set 是一个给属性提供的 setter 方法,当我们对该属性做修改的时候会触发 setter 方法。
Vue内部实现流程
当我们 new Vue后,框架内部会进行_init操作。而数据的初始化是在initState函数中,它会根据用户传入的不同类型的数据进行相应的初始化。像props、methods、data、computed、watch的初始化。本篇文章主要分析data的初始化,像computed、watch的初始化会在另外章节再来分析。
注: 以下代码均为简化后的,去掉了一些非核心的流程。
function initState (vm: Component) {
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) initData(vm)
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch) initWatch(vm, opts.watch)
}
回到initData函数中 ,主要做了下面两件事:
-
遍历用户传入的
data数据。拿到data里的每一个键值,拿健值去判断一下props、methods是否已经定义过了,因为最终会把键值定义到vm实例上,所以是不能重复定义的。 -
调用**
observe**函数对数据进行观测。
function initData (vm: Component) {
let data = vm.$options.data
// 定义_data
data = vm._data = typeof data === 'function' // 定义data时尽量用定义函数返回数据的方式,避免数据之间污染
? getData(data, 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)) {
proxy(vm, `_data`, key) // 做了代理,用户访问vm.key实际是访问了vm._data.key,_data在👆🏻定义
}
}
observe(data, true) // observe data
}
observe函数会对传入的数据做校验,只有是**对象类型**才会观测。接着会实例化Observer并将其返回。
export function observe (value: any): Observer | void {
if (!isObject(value)) {
return
}
let ob: Observer | void
ob = new Observer(value)
return ob
}
Observer是一个类,接收传入的data,然后判断是对象的话,会执行walk方法,walk方法会遍历该对象,拿到对象的每个key值,如果key对应的值还是一个对象,会递归调用observe函数,最后调用Object.defineProperty为每一个key添加get和set函数。如果是数组的话,会修改data的__proto__指向,然后遍历该数组,拿到数组每一项后,递归调用ovserve方法。
class Observer {
value: any;
dep: Dep;
constructor (value: any) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this) // value.__ob__ 指向 observer实例
if (Array.isArray(value)) {
protoAugment(value, arrayMethods) // value.__proto__ = arrayMethods
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])
}
}
}
function defineReactive ( obj: Object, key: string) {
const dep = new Dep()
if (arguments.length === 2) {
val = obj[key]
}
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend() // 如果一个数据嵌套多层,让每一层都触发依赖收集
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = val
if (newVal === value) {
return
}
val = newVal
childOb = observe(newVal) // 观测用户修改后的数据,将其变为响应式
dep.notify()
}
})
}
依赖收集
大家可以看到,在defineReactive函数和实例化Observe类的时候,都会实例化一个Dep。这个dep是用来收集当前key对应的watcher。当我们执行渲染流程的时候,首先会实例化一个渲染watcher,然后执行其内部的get方法(会用Dep.target标识当前watcher的类型),然后会执行_render方法去访问定义在模板中的数据,访问数据就会触发该数据对应的getter方法,该数据的dep实例就会把当前正在渲染的Dep.target对应的watcher收集起来(添加到subs数组中)。
Dep类
class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
subs.sort((a, b) => a.id - b.id)
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
watcher类:
class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
this.cb = cb
this.id = ++uid
this.active = true
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this) // targetStack.push(target) Dep.target = target
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // 让传入的getter函数执行
} catch (e) {
} finally {
popTarget() // targetStack.pop() Dep.target = targetStack[targetStack.length - 1]
}
return value
}
addDep (dep: Dep) {
dep.addSub(this)
}
}
让我们用一个简单的demo来跑一个上述流程:
new Vue({
el: '#app',
render(h) {
return h('div', this.msg)
},
data: { msg: 'Hello Vue!' }
})
手绘流程图如下:

如果Dep.target存在,会调用dep.depend。depend函数会调用watcher.addDep(当前dep)方法,并把当前dep传入,最终会执行该dep上的addSub方法,把当前的watcher添加到dep对应的subs数组中。
派发更新
当我们修改data中定义的数据时,会触发该数据的setter方法。setter方法主要做了2件事:
1、给数据赋值,并把新赋值的数据变为响应式。
2、找到当前数据对应的dep,触发dep.notify。
代码如下
Object.defineProperty(obj, key, {
...,
set: function reactiveSetter (newVal) {
const value = val
if (newVal === value) {
return
}
val = newVal
childOb = observe(newVal)
dep.notify()
}
}
nofity会遍历subs数组中的watcher,依次调用watcher的update方法:
notify () {
const subs = this.subs.slice()
subs.sort((a, b) => a.id - b.id)
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // watcher.update()
}
}
回到Watcher类中,我们找到update方法,因为计算属性和监听属性也是基于watcher实现的,只不过传入的配置项和更新的方式不同,所以update函数内部针对不同的watcher做了不同的操作,代码如下:
update () {
if (this.lazy) { // computed watcher
this.dirty = true
} else if (this.sync) { // 同步watcher
this.run()
} else {
queueWatcher(this) // 普通watcher
}
}
lazy为计算属性watcher使用,sync为同步watcher(同步更新,不需要经过nextTick),我们这里的逻辑会执行queueWatcher,并把当前watcher做为参数传入,我们来看一下queueWatcher核心流程:
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
let index = 0
function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
}
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
function flushSchedulerQueue () {
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
}
...
}
queueWatcher函数内部主要做了一下工作: 定义全局变量queue,用来存放当前需要重新渲染的watcher,相同的watcher只能存放一次(id相同)。用waiting变量来控制nextTick函数只会只执行一次。
flushSchedulerQueue函数会遍历queue队列,拿到每一个watcher,调用watcher的run方法。我们去来看一下run函数做了那些事情:
run () {
if (this.active) {
const value = this.get()
if (value !== this.value || isObject(value) || this.deep) {
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
可以看到,run函数内部会重新执行get方法,针对渲染watcher而言,我们分析一下是如何实例化的:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true)
class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
this.cb = cb
this.active = true
...
if (typeof expOrFn === 'function') {
this.getter = expOrFn
}
this.value = this.get()
}
get() {
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // 让updateComponent函数执行
}
return value
}
实例化会执行一次get函数(内部会执行传入的updateComponent,依次执行_render(执行render函数,触发依赖收集),_update(patch为真实dom))。当我们对数据修改后,会执行dep.notify()-> wacther.update()-> queueWatcher(watcher) -> flushSchedulerQueue -> watcher.run() -> 执行get方法(重新执行_render,因为数据已经改变了,此时拿到的是最新值,重新_update patch成真实dom)。
手绘流程如如下:
至此派发更新的流程也分析完了。
转载自:https://juejin.cn/post/7123575720973959182