前言
上一篇文章 Vue 原始碼解讀(2)—— Vue 初始化過程 詳細講解了 Vue 的初始化過程,明白了 new Vue(options)
都做了什麼,其中關於 資料響應式
的實現用一句話簡單的帶過,而這篇文章則會詳細講解 Vue 資料響應式的實現原理。
目標
-
深入理解 Vue 資料響應式原理。
-
methods、computed 和 watch 有什麼區別?
原始碼解讀
經過上一篇文章的學習,相信關於 響應式原理
原始碼閱讀的入口位置大家都已經知道了,就是初始化過程中處理資料響應式這一步,即呼叫 initState
方法,在 /src/core/instance/init.js
檔案中。
initState
/src/core/instance/state.js
/**
* 兩件事:
* 資料響應式的入口:分別處理 props、methods、data、computed、watch
* 優先順序:props、methods、data、computed 物件中的屬性不能出現重複,優先順序和列出順序一致
* 其中 computed 中的 key 不能和 props、data 中的 key 重複,methods 不影響
*/
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 處理 props 物件,為 props 物件的每個屬性設定響應式,並將其代理到 vm 例項上
if (opts.props) initProps(vm, opts.props)
// 處理 methos 物件,校驗每個屬性的值是否為函式、和 props 屬性比對進行判重處理,最後得到 vm[key] = methods[key]
if (opts.methods) initMethods(vm, opts.methods)
/**
* 做了三件事
* 1、判重處理,data 物件上的屬性不能和 props、methods 物件上的屬性相同
* 2、代理 data 物件上的屬性到 vm 例項
* 3、為 data 物件的上資料設定響應式
*/
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
/**
* 三件事:
* 1、為 computed[key] 建立 watcher 例項,預設是懶執行
* 2、代理 computed[key] 到 vm 例項
* 3、判重,computed 中的 key 不能和 data、props 中的屬性重複
*/
if (opts.computed) initComputed(vm, opts.computed)
/**
* 三件事:
* 1、處理 watch 物件
* 2、為 每個 watch.key 建立 watcher 例項,key 和 watcher 例項可能是 一對多 的關係
* 3、如果設定了 immediate,則立即執行 回撥函式
*/
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
/**
* 其實到這裡也能看出,computed 和 watch 在本質是沒有區別的,都是通過 watcher 去實現的響應式
* 非要說有區別,那也只是在使用方式上的區別,簡單來說:
* 1、watch:適用於當資料變化時執行非同步或者開銷較大的操作時使用,即需要長時間等待的操作可以放在 watch 中
* 2、computed:其中可以使用非同步方法,但是沒有任何意義。所以 computed 更適合做一些同步計算
*/
}
initProps
src/core/instance/state.js
// 處理 props 物件,為 props 物件的每個屬性設定響應式,並將其代理到 vm 例項上
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// 快取 props 的每個 key,效能優化
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
// 遍歷 props 物件
for (const key in propsOptions) {
// 快取 key
keys.push(key)
// 獲取 props[key] 的預設值
const value = validateProp(key, propsOptions, propsData, vm)
// 為 props 的每個 key 是設定資料響應式
defineReactive(props, key, value)
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
// 代理 key 到 vm 物件上
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
proxy
/src/core/instance/state.js
// 設定代理,將 key 代理到 target 上
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
initMethods
/src/core/instance/state.js
/**
* 做了以下三件事,其實最關鍵的就是第三件事情
* 1、校驗 methoss[key],必須是一個函式
* 2、判重
* methods 中的 key 不能和 props 中的 key 相同
* methos 中的 key 與 Vue 例項上已有的方法重疊,一般是一些內建方法,比如以 $ 和 _ 開頭的方法
* 3、將 methods[key] 放到 vm 例項上,得到 vm[key] = methods[key]
*/
function initMethods (vm: Component, methods: Object) {
// 獲取 props 配置項
const props = vm.$options.props
// 遍歷 methods 物件
for (const key in methods) {
if (process.env.NODE_ENV !== 'production') {
if (typeof methods[key] !== 'function') {
warn(
`Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
`Did you reference the function correctly?`,
vm
)
}
if (props && hasOwn(props, key)) {
warn(
`Method "${key}" has already been defined as a prop.`,
vm
)
}
if ((key in vm) && isReserved(key)) {
warn(
`Method "${key}" conflicts with an existing Vue instance method. ` +
`Avoid defining component methods that start with _ or $.`
)
}
}
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
initData
src/core/instance/state.js
/**
* 做了三件事
* 1、判重處理,data 物件上的屬性不能和 props、methods 物件上的屬性相同
* 2、代理 data 物件上的屬性到 vm 例項
* 3、為 data 物件的上資料設定響應式
*/
function initData (vm: Component) {
// 得到 data 物件
let data = vm.$options.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
)
}
/**
* 兩件事
* 1、判重處理,data 物件上的屬性不能和 props、methods 物件上的屬性相同
* 2、代理 data 物件上的屬性到 vm 例項
*/
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)
}
}
// 為 data 物件上的資料設定響應式
observe(data, true /* asRootData */)
}
export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
initComputed
/src/core/instance/state.js
const computedWatcherOptions = { lazy: true }
/**
* 三件事:
* 1、為 computed[key] 建立 watcher 例項,預設是懶執行
* 2、代理 computed[key] 到 vm 例項
* 3、判重,computed 中的 key 不能和 data、props 中的屬性重複
* @param {*} computed = {
* key1: function() { return xx },
* key2: {
* get: function() { return xx },
* set: function(val) {}
* }
* }
*/
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
// 遍歷 computed 物件
for (const key in computed) {
// 獲取 key 對應的值,即 getter 函式
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// 為 computed 屬性建立 watcher 例項
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
// 配置項,computed 預設是懶執行
computedWatcherOptions
)
}
if (!(key in vm)) {
// 代理 computed 物件中的屬性到 vm 例項
// 這樣就可以使用 vm.computedKey 訪問計算屬性了
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
// 非生產環境有一個判重處理,computed 物件中的屬性不能和 data、props 中的屬性相同
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
/**
* 代理 computed 物件中的 key 到 target(vm)上
*/
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
// 構造屬性描述符(get、set)
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
// 攔截對 target.key 的訪問和設定
Object.defineProperty(target, key, sharedPropertyDefinition)
}
/**
* @returns 返回一個函式,這個函式在訪問 vm.computedProperty 時會被執行,然後返回執行結果
*/
function createComputedGetter (key) {
// computed 屬性值會快取的原理也是在這裡結合 watcher.dirty、watcher.evalaute、watcher.update 實現的
return function computedGetter () {
// 得到當前 key 對應的 watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 計算 key 對應的值,通過執行 computed.key 的回撥函式來得到
// watcher.dirty 屬性就是大家常說的 computed 計算結果會快取的原理
// <template>
// <div>{{ computedProperty }}</div>
// <div>{{ computedProperty }}</div>
// </template>
// 像這種情況下,在頁面的一次渲染中,兩個 dom 中的 computedProperty 只有第一個
// 會執行 computed.computedProperty 的回撥函式計算實際的值,
// 即執行 watcher.evalaute,而第二個就不走計算過程了,
// 因為上一次執行 watcher.evalute 時把 watcher.dirty 置為了 false,
// 待頁面更新後,wathcer.update 方法會將 watcher.dirty 重新置為 true,
// 供下次頁面更新時重新計算 computed.key 的結果
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
/**
* 功能同 createComputedGetter 一樣
*/
function createGetterInvoker(fn) {
return function computedGetter () {
return fn.call(this, this)
}
}
initWatch
/src/core/instance/state.js
/**
* 處理 watch 物件的入口,做了兩件事:
* 1、遍歷 watch 物件
* 2、呼叫 createWatcher 函式
* @param {*} watch = {
* 'key1': function(val, oldVal) {},
* 'key2': 'this.methodName',
* 'key3': {
* handler: function(val, oldVal) {},
* deep: true
* },
* 'key4': [
* 'this.methodNanme',
* function handler1() {},
* {
* handler: function() {},
* immediate: true
* }
* ],
* 'key.key5' { ... }
* }
*/
function initWatch (vm: Component, watch: Object) {
// 遍歷 watch 物件
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
// handler 為陣列,遍歷陣列,獲取其中的每一項,然後呼叫 createWatcher
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
/**
* 兩件事:
* 1、相容性處理,保證 handler 肯定是一個函式
* 2、呼叫 $watch
* @returns
*/
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
// 如果 handler 為物件,則獲取其中的 handler 選項的值
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// 如果 hander 為字串,則說明是一個 methods 方法,獲取 vm[handler]
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
/**
* 建立 watcher,返回 unwatch,共完成如下 5 件事:
* 1、相容性處理,保證最後 new Watcher 時的 cb 為函式
* 2、標示使用者 watcher
* 3、建立 watcher 例項
* 4、如果設定了 immediate,則立即執行一次 cb
* 5、返回 unwatch
* @param {*} expOrFn key
* @param {*} cb 回撥函式
* @param {*} options 配置項,使用者直接呼叫 this.$watch 時可能會傳遞一個 配置項
* @returns 返回 unwatch 函式,用於取消 watch 監聽
*/
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
// 相容性處理,因為使用者呼叫 vm.$watch 時設定的 cb 可能是物件
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
// options.user 表示使用者 watcher,還有渲染 watcher,即 updateComponent 方法中例項化的 watcher
options = options || {}
options.user = true
// 建立 watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
// 如果使用者設定了 immediate 為 true,則立即執行一次回撥函式
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
// 返回一個 unwatch 函式,用於解除監聽
return function unwatchFn () {
watcher.teardown()
}
}
observe
/src/core/observer/index.js
/**
* 響應式處理的真正入口
* 為物件建立觀察者例項,如果物件已經被觀察過,則返回已有的觀察者例項,否則建立新的觀察者例項
* @param {*} value 物件 => {}
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 非物件和 VNode 例項不做響應式處理
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 如果 value 物件上存在 __ob__ 屬性,則表示已經做過觀察了,直接返回 __ob__ 屬性
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 建立觀察者例項
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
Observer
/src/core/observer/index.js
/**
* 觀察者類,會被附加到每個被觀察的物件上,value.__ob__ = this
* 而物件的各個屬性則會被轉換成 getter/setter,並收集依賴和通知更新
*/
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
// 例項話一個 dep
this.dep = new Dep()
this.vmCount = 0
// 在 value 物件上設定 __ob__ 屬性
def(value, '__ob__', this)
if (Array.isArray(value)) {
/**
* value 為陣列
* hasProto = '__proto__' in {}
* 用於判斷物件是否存在 __proto__ 屬性,通過 obj.__proto__ 可以訪問物件的原型鏈
* 但由於 __proto__ 不是標準屬性,所以有些瀏覽器不支援,比如 IE6-10,Opera10.1
* 為什麼要判斷,是因為一會兒要通過 __proto__ 運算元據的原型鏈
* 覆蓋陣列預設的七個原型方法,以實現陣列響應式
*/
if (hasProto) {
// 有 __proto__
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
// value 為物件,為物件的每個屬性(包括巢狀物件)設定響應式
this.walk(value)
}
}
/**
* 遍歷物件上的每個 key,為每個 key 設定響應式
* 僅當值為物件時才會走這裡
*/
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])
}
}
}
defineReactive
/src/core/observer/index.js
/**
* 攔截 obj[key] 的讀取和設定操作:
* 1、在第一次讀取時收集依賴,比如執行 render 函式生成虛擬 DOM 時會有讀取操作
* 2、在更新時設定新值並通知依賴更新
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 例項化 dep,一個 key 一個 dep
const dep = new Dep()
// 獲取 obj[key] 的屬性描述符,發現它是不可配置物件的話直接 return
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 記錄 getter 和 setter,獲取 val 值
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 遞迴呼叫,處理 val 即 obj[key] 的值為物件的情況,保證物件中的所有 key 都被觀察
let childOb = !shallow && observe(val)
// 響應式核心
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// get 攔截對 obj[key] 的讀取操作
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
/**
* Dep.target 為 Dep 類的一個靜態屬性,值為 watcher,在例項化 Watcher 時會被設定
* 例項化 Watcher 時會執行 new Watcher 時傳遞的回撥函式(computed 除外,因為它懶執行)
* 而回撥函式中如果有 vm.key 的讀取行為,則會觸發這裡的 讀取 攔截,進行依賴收集
* 回撥函式執行完以後又會將 Dep.target 設定為 null,避免這裡重複收集依賴
*/
if (Dep.target) {
// 依賴收集,在 dep 中新增 watcher,也在 watcher 中新增 dep
dep.depend()
// childOb 表示物件中巢狀物件的觀察者物件,如果存在也對其進行依賴收集
if (childOb) {
// 這就是 this.key.chidlKey 被更新時能觸發響應式更新的原因
childOb.dep.depend()
// 如果是 obj[key] 是 陣列,則觸發陣列響應式
if (Array.isArray(value)) {
// 為陣列項為物件的項新增依賴
dependArray(value)
}
}
}
return value
},
// set 攔截對 obj[key] 的設定操作
set: function reactiveSetter (newVal) {
// 舊的 obj[key]
const value = getter ? getter.call(obj) : val
// 如果新老值一樣,則直接 return,不跟新更不觸發響應式更新過程
/* 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 不存在說明該屬性是一個只讀屬性,直接 return
// #7981: for accessor properties without setter
if (getter && !setter) return
// 設定新值
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 對新值進行觀察,讓新值也是響應式的
childOb = !shallow && observe(newVal)
// 依賴通知更新
dep.notify()
}
})
}
dependArray
/src/core/observer/index.js
/**
* 遍歷每個陣列元素,遞迴處理陣列項為物件的情況,為其新增依賴
* 因為前面的遞迴階段無法為陣列中的物件元素新增依賴
*/
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
陣列響應式
src/core/observer/array.js
/**
* 定義 arrayMethods 物件,用於增強 Array.prototype
* 當訪問 arrayMethods 物件上的那七個方法時會被攔截,以實現陣列響應式
*/
import { def } from '../util/index'
// 備份 陣列 原型物件
const arrayProto = Array.prototype
// 通過繼承的方式建立新的 arrayMethods
export const arrayMethods = Object.create(arrayProto)
// 運算元組的七個方法,這七個方法可以改變陣列自身
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* 攔截變異方法並觸發事件
*/
methodsToPatch.forEach(function (method) {
// cache original method
// 快取原生方法,比如 push
const original = arrayProto[method]
// def 就是 Object.defineProperty,攔截 arrayMethods.method 的訪問
def(arrayMethods, method, function mutator (...args) {
// 先執行原生方法,比如 push.apply(this, args)
const result = original.apply(this, args)
const ob = this.__ob__
// 如果 method 是以下三個之一,說明是新插入了元素
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 對新插入的元素做響應式處理
if (inserted) ob.observeArray(inserted)
// 通知更新
ob.dep.notify()
return result
})
})
def
/src/core/util/lang.js
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
protoAugment
/src/core/observer/index.js
/**
* 設定 target.__proto__ 的原型物件為 src
* 比如 陣列物件,arr.__proto__ = arrayMethods
*/
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
copyAugment
/src/core/observer/index.js
/**
* 在目標物件上定義指定屬性
* 比如陣列:為陣列物件定義那七個方法
*/
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
Dep
/src/core/observer/dep.js
import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'
let uid = 0
/**
* 一個 dep 對應一個 obj.key
* 在讀取響應式資料時,負責收集依賴,每個 dep(或者說 obj.key)依賴的 watcher 有哪些
* 在響應式資料更新時,負責通知 dep 中那些 watcher 去執行 update 方法
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
// 在 dep 中新增 watcher
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 像 watcher 中新增 dep
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
/**
* 通知 dep 中的所有 watcher,執行 watcher.update() 方法
*/
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)
}
// 遍歷 dep 中儲存的 watcher,執行 watcher.update()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
/**
* 當前正在執行的 watcher,同一時間只會有一個 watcher 在執行
* Dep.target = 當前正在執行的 watcher
* 通過呼叫 pushTarget 方法完成賦值,呼叫 popTarget 方法完成重置(null)
*/
Dep.target = null
const targetStack = []
// 在需要進行依賴收集的時候呼叫,設定 Dep.target = watcher
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
// 依賴收集結束呼叫,設定 Dep.target = null
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
Watcher
/src/core/observer/watcher.js
/**
* 一個元件一個 watcher(渲染 watcher)或者一個表示式一個 watcher(使用者watcher)
* 當資料更新時 watcher 會被觸發,訪問 this.computedProperty 時也會觸發 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 {
// this.getter = function() { return this.xx }
// 在 this.get 中執行 this.getter 時會觸發依賴收集
// 待後續 this.xx 更新時就會觸發響應式
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()
}
/**
* 執行 this.getter,並重新收集依賴
* this.getter 是例項化 watcher 時傳遞的第二個引數,一個函式或者字串,比如:updateComponent 或者 parsePath 返回的讀取 this.xx 屬性值的函式
* 為什麼要重新收集依賴?
* 因為觸發更新說明有響應式資料被更新了,但是被更新的資料雖然已經經過 observe 觀察了,但是卻沒有進行依賴收集,
* 所以,在更新頁面時,會重新執行一次 render 函式,執行期間會觸發讀取操作,這時候進行依賴收集
*/
get () {
// 開啟 Dep.target,Dep.target = this
pushTarget(this)
// value 為回撥函式執行的結果
let value
const vm = this.vm
try {
// 執行回撥函式,比如 updateComponent,進入 patch 階段
value = this.getter.call(vm, vm)
} catch (e) {
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
if (this.deep) {
traverse(value)
}
// 關閉 Dep.target,Dep.target = null
popTarget()
this.cleanupDeps()
}
return value
}
/**
* Add a dependency to this directive.
* 兩件事:
* 1、新增 dep 給自己(watcher)
* 2、新增自己(watcher)到 dep
*/
addDep (dep: Dep) {
// 判重,如果 dep 已經存在則不重複新增
const id = dep.id
if (!this.newDepIds.has(id)) {
// 快取 dep.id,用於判重
this.newDepIds.add(id)
// 新增 dep
this.newDeps.push(dep)
// 避免在 dep 中重複新增 watcher,this.depIds 的設定在 cleanupDeps 方法中
if (!this.depIds.has(id)) {
// 新增 watcher 自己到 dep
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
}
/**
* 根據 watcher 配置項,決定接下來怎麼走,一般是 queueWatcher
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
// 懶執行時走這裡,比如 computed
// 將 dirty 置為 true,可以讓 computedGetter 執行時重新計算 computed 回撥函式的執行結果
this.dirty = true
} else if (this.sync) {
// 同步執行,在使用 vm.$watch 或者 watch 選項時可以傳一個 sync 選項,
// 當為 true 時在資料更新時該 watcher 就不走非同步更新佇列,直接執行 this.run
// 方法進行更新
// 這個屬性在官方文件中沒有出現
this.run()
} else {
// 更新時一般都這裡,將 watcher 放入 watcher 佇列
queueWatcher(this)
}
}
/**
* 由 重新整理佇列函式 flushSchedulerQueue 呼叫,完成如下幾件事:
* 1、執行例項化 watcher 傳遞的第二個引數,updateComponent 或者 獲取 this.xx 的一個函式(parsePath 返回的函式)
* 2、更新舊值為新值
* 3、執行例項化 watcher 時傳遞的第三個引數,比如使用者 watcher 的回撥函式
*/
run () {
if (this.active) {
// 呼叫 this.get 方法
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
) {
// 更新舊值為新值
const oldValue = this.value
this.value = value
if (this.user) {
// 如果是使用者 watcher,則執行使用者傳遞的第三個引數 —— 回撥函式,引數為 val 和 oldVal
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
// 渲染 watcher,this.cb = noop,一個空函式
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* 懶執行的 watcher 會呼叫該方法
* 比如:computed,在獲取 vm.computedProperty 的值時會呼叫該方法
* 然後執行 this.get,即 watcher 的回撥函式,得到返回值
* this.dirty 被置為 false,作用是頁面在本次渲染中只會一次 computed.key 的回撥函式,
* 這也是大家常說的 computed 和 methods 區別之一是 computed 有快取的原理所在
* 而頁面更新後會 this.dirty 會被重新置為 true,這一步是在 this.update 方法中完成的
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
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.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
總結
面試官 問:Vue 響應式原理是怎麼實現的?
答:
-
響應式的核心是通過
Object.defineProperty
攔截對資料的訪問和設定 -
響應式的資料分為兩類:
-
物件,迴圈遍歷物件的所有屬性,為每個屬性設定 getter、setter,以達到攔截訪問和設定的目的,如果屬性值依舊為物件,則遞迴為屬性值上的每個 key 設定 getter、setter
-
訪問資料時(obj.key)進行依賴收集,在 dep 中儲存相關的 watcher
-
設定資料時由 dep 通知相關的 watcher 去更新
-
-
陣列,增強陣列的那 7 個可以更改自身的原型方法,然後攔截對這些方法的操作
-
新增新資料時進行響應式處理,然後由 dep 通知 watcher 去更新
-
刪除資料時,也要由 dep 通知 watcher 去更新
-
-
面試官 問:methods、computed 和 watch 有什麼區別?
答:
<!DOCTYPE html>
<html lang="en">
<head>
<title>methods、computed、watch 有什麼區別</title>
</head>
<body>
<div id="app">
<!-- methods -->
<div>{{ returnMsg() }}</div>
<div>{{ returnMsg() }}</div>
<!-- computed -->
<div>{{ getMsg }}</div>
<div>{{ getMsg }}</div>
</div>
<script src="../../dist/vue.js"></script>
<script>
new Vue({
el: '#app',
data: {
msg: 'test'
},
mounted() {
setTimeout(() => {
this.msg = 'msg is changed'
}, 1000)
},
methods: {
returnMsg() {
console.log('methods: returnMsg')
return this.msg
}
},
computed: {
getMsg() {
console.log('computed: getMsg')
return this.msg + ' hello computed'
}
},
watch: {
msg: function(val, oldVal) {
console.log('watch: msg')
new Promise(resolve => {
setTimeout(() => {
this.msg = 'msg is changed by watch'
}, 1000)
})
}
}
})
</script>
</body>
</html>
點選檢視動圖演示,動圖地址:https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9c957654bb484ae7ba4ace1b912cff03~tplv-k3u1fbpfcp-watermark.awebp
示例其實就是答案了
-
使用場景
-
methods 一般用於封裝一些較為複雜的處理邏輯(同步、非同步)
-
computed 一般用於封裝一些簡單的同步邏輯,將經過處理的資料返回,然後顯示在模版中,以減輕模版的重量
-
watch 一般用於當需要在資料變化時執行非同步或開銷較大的操作
-
-
區別
-
methods VS computed
通過示例會發現,如果在一次渲染中,有多個地方使用了同一個 methods 或 computed 屬性,methods 會被執行多次,而 computed 的回撥函式則只會被執行一次。
通過閱讀原始碼我們知道,在一次渲染中,多次訪問 computedProperty,只會在第一次執行 computed 屬性的回撥函式,後續的其它訪問,則直接使用第一次的執行結果(watcher.value),而這一切的實現原理則是通過對 watcher.dirty 屬性的控制實現的。而 methods,每一次的訪問則是簡單的方法呼叫(this.xxMethods)。
-
computed VS watch
通過閱讀原始碼我們知道,computed 和 watch 的本質是一樣的,內部都是通過 Watcher 來實現的,其實沒什麼區別,非要說區別的化就兩點:1、使用場景上的區別,2、computed 預設是懶執行的,切不可更改。
-
methods VS watch
methods 和 watch 之間其實沒什麼可比的,完全是兩個東西,不過在使用上可以把 watch 中一些邏輯抽到 methods 中,提高程式碼的可讀性。
-
連結
- 配套視訊,微信公眾號回覆:"精通 Vue 技術棧原始碼原理視訊版" 獲取
- 精通 Vue 技術棧原始碼原理 專欄
- github 倉庫 liyongning/Vue 歡迎 Star
感謝各位的:點贊、收藏和評論,我們下期見。
當學習成為了習慣,知識也就變成了常識。 感謝各位的 點贊、收藏和評論。
新視訊和文章會第一時間在微信公眾號傳送,歡迎關注:李永寧lyn
文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。