從原始碼看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
}