Vue.js 实现响应式的核心是利用了 ES5 的Object.defineProperty。
这个方法在大多数浏览器下都是支持的,但是在ie8及以下浏览器是没有这个方法的,并且没有任何的补丁来兼容这个方法,这也是为什么Vue.js不能兼容ie8及以下浏览器的原因。
Object.defineProperty
Objet.defineProperty
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
MDN链接: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
语法
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
)。
手绘流程如如下:

评论(0)