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
)。
手绘流程如如下:
至此派发更新的流程也分析完了,后续我们再来分析nextTick核心流程。