上一篇咱們分析了Vue的響應式原理(juejin.im/post/5e0dd4…),今天咱們來搞一下,Vue的計算屬性和監聽屬性的實現原理,以讓咱們更清楚何時該使用computed,何時該使用watch,以及爲何官方不建議使用watch?數組
還記得咱們在data渲染視圖(juejin.im/post/5e06b4…)中講的,New Vue()會發生什麼麼?這其中有一段源代碼:緩存
/* 初始化狀態 */
export function initState (vm: Component) {
// ...
/*初始化computed*/
if (opts.computed) initComputed(vm, opts.computed)
/*初始化watchers*/
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
複製代碼
能夠看出咱們在new Vue()以後,會執行initState方法,該方法去初始化initComputed(計算)和initWatch(監聽),咱們首先看計算屬性;bash
先看initComputed
源碼:src/core/instance/state.js函數
/* 爲了在屬性值不變的狀況下get()只執行一次而設置的標誌位,下邊會講的 */
const computedWatcherOptions = { lazy: true }
/* 初始化computed */
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
/* 循環計算屬性,給每一個屬性添加Watcher監聽,不知道Watcher幹什麼的能夠去看https://juejin.im/post/5e0dd467e51d45410f1232f5#heading-13 */
for (const key in computed) {
const userDef = computed[key]
/* 拿get方法 */
const getter = typeof userDef === 'function' ? userDef : userDef.get
/* 添加watcher監聽 */
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
/* 若是定義的計算屬性在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)
}
}
}
}
複製代碼
那麼咱們重點看一下defineComputed的實現工具
/**
* 定義計算屬性
* @param {Object | Function} userDef 計算屬性的值
*/
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
/* 非服務端渲染,執行createComputedGetter */
const shouldCache = !isServerRendering()
/* 計算屬性是函數時 */
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
/* noop是一個空函數,Vue中定義的工具函數 */
sharedPropertyDefinition.set = noop
} else {
/* 計算屬性是對象時 */
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
// ...
Object.defineProperty(target, key, sharedPropertyDefinition)
}
複製代碼
能夠看出,本質上就是利用Object.defineProperty去給屬性添加setter和getter,而且不管計算屬性是函數仍是對象,都會去執行createComputedGetter方法,並傳入屬性鍵。oop
function createComputedGetter (key) {
/* 返回一個函數,即對應的getter */
return function computedGetter () {
/* this._computedWatchers是在initComputed方法中定義的一個空對象 */
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
/*****
Watcher中有evaluate這麼一個方法,當取到get()值之後,將dirty置爲false,那麼下次再去取這個計算屬性值的時候由於dirty已經變爲false了,就不會再去執行get()方法了,而是用的以前的取的值,這就是computed的緩存機制
evaluate () {
this.value = this.get()
this.dirty = false
}
******/
if (watcher.dirty) {
watcher.evaluate()
}
/* 爲了不從新渲染的時候,計算屬性渲染的部分不被從新渲染,所以進行依賴收集 */
if (Dep.target) {
watcher.depend()
}
/* 返回屬性值 */
return watcher.value
}
}
}
複製代碼
createComputedGetter方法返回一個函數,即對應的是getter方法,該方法主要是返回watcher的值,也就是getter的值,看Watcher的源碼咱們能夠發現dirty的值就是lazy, 而上邊說的const computedWatcherOptions = { lazy: true },lazy初始值爲true,並在上邊initComputed方法中合併給Watcher了,所以計算屬性在屬性值不變的狀況下,只會去執行一次get()方法取值,這也就是爲何Vue的計算屬性有緩存做用。post
咱們舉個例子看一下computed和watch的不一樣,咱們知道computed也會對數據盡心監聽,下邊咱們把計算屬性的監聽暫且叫作computed watcher性能
var vm = new Vue({
data: {
firstName: 'yang',
lastName: 'bo'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
複製代碼
當初始化fullName時,咱們會執行到Watcher
源碼:/src/core/observer/watcher.jsui
constructor () {
/* 這一步是給computed watcher設置的,計算屬性並不會去馬上求值 */
this.value = this.lazy
? undefined
: this.get()
}
複製代碼
而後當render函數訪問到this.fullName的時候,就會觸發計算屬性的getter,它會拿到計算屬性對應的watcher,而後執行watcher.depend()進行依賴收集。this
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
複製代碼
而後還執行了watcher.evaluate()
evaluate () {
this.value = this.get()
this.dirty = false
}
複製代碼
這個方法咱們上邊已經講了,就不囉嗦了。咱們在看Watcher中的get方法
get () {
/* 收集Watcher實例,也就是Dep.target */
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
}
return value
}
複製代碼
get執行了getter方法,也就是咱們例子中的
this.firstName + ' ' + this.lastName
複製代碼
而後拿到計算屬性最後的value值。
watch初始化也是在initState方法中,上邊已經講到了。
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
複製代碼
來看一下 initWatch 的實現
源碼:src/core/instance/state.js
/**
* 初始化偵聽
*/
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
複製代碼
這裏就是對 watch 對象作遍歷,拿到每個 handler,由於 Vue 是支持 watch 的同一個 key 對應多個 handler,因此若是 handler 是一個數組,則遍歷這個數組,調用 createWatcher 方法,不然直接調用 createWatcher
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
複製代碼
首先對 hanlder 的類型作判斷,拿到它最終的回調函數,最後調用 vm.$watch(keyOrFn, handler, options)
函數,$watch 是 Vue 原型上的方法,它是在執行 stateMixin 的時候定義的
export function stateMixin (Vue: Class<Component>) {
// ...
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
/* 用戶自定義watch */
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
}
}
/* 返回卸載watcher的方法 */
return function unwatchFn () {
watcher.teardown()
}
}
}
複製代碼
偵聽屬性 watch 最終會調用 $watch
方法,這個方法首先判斷 cb 若是是一個對象,則調用 createWatcher 方法,這是由於 $watch 方法是用戶能夠直接調用的,它能夠傳遞一個對象,也能夠傳遞函數。接着執行 const watcher = new Watcher(vm, expOrFn, cb, options) 實例化了一個 watcher,這裏須要注意一點這是一個 user watcher,由於 options.user = true。經過實例化 watcher 的方式,一旦咱們 watch 的數據發生變化,它最終會執行 watcher 的 run 方法,執行回調函數 cb,而且若是咱們設置了 immediate 爲 true,則直接會執行回調函數 cb。最後返回了一個 unwatchFn 方法,它會調用 teardown 方法去移除這個 watcher
因此本質上偵聽屬性也是基於 Watcher 實現的,它是一個 user watcher。其實 Watcher 支持了不一樣的類型,下面咱們梳理一下它有哪些類型以及它們的做用。
if (options) {
this.deep = !!options.deep // 深度監聽
this.user = !!options.user // 在對 watcher 求值以及在執行回調函數的時候,會處理一下錯誤
this.lazy = !!options.lazy // 惰性求值,賦值給this.dirty,計算屬性的時候用到的
this.sync = !!options.sync // 在當前 Tick 中同步執行 watcher 的回調函數,不然響應式數據發生變化以後,watcher回調會在nextTick後執行;
}
複製代碼
因此 watcher 總共有 4 種類型,咱們來一一分析它們,看看不一樣的類型執行的邏輯有哪些差異
也就是咱們一般說的深度監聽,看一下咱們若是將一個對象進行深度監聽會發生什麼:
get () {
if (this.deep) {
traverse(value)
}
return value
}
複製代碼
看一下traverse源碼:src/core/observer/traverse.js
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
複製代碼
很清晰,對傳入對watch對象進行遞歸遍歷,由於遞歸有必定對性能開銷,所以,咱們必定要在合適的場景去設置deep。
就是用戶手寫的watch監聽,前面講過了,略過。
爲計算屬性量身定製的監聽,具備「緩存」功效,前面講過了,略過。
在咱們以前對 setter 的分析過程知道,當響應式數據發送變化後,觸發了 watcher.update(),只是把這個 watcher 推送到一個隊列中,在 nextTick 後纔會真正執行 watcher 的回調函數。而一旦咱們設置了 sync,就能夠在當前 Tick 中同步執行 watcher 的回調函數。
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
/* 執行Watcher回調,觸發視圖更新 */
this.run()
} else {
queueWatcher(this)
}
}
複製代碼
所以只有當咱們須要 watch 的值的變化到執行 watcher 的回調函數是一個同步過程的時候纔會去設置該屬性爲 true。
計算屬性和監聽屬性都是經過Watcher這個類去實現當,自己都具備監聽數據的能力。
計算屬性:計算屬性本質上是 computed watcher,計算屬性適合用在模板渲染中,某個值是依賴了其它的響應式對象甚至是計算屬性計算而來,它具備緩存能力,當依賴的值沒有變化甚至是計算結果沒有發生變化,觸發更新的回調則不會執行;
監聽屬性:偵聽屬性本質上是 user watcher,適用於觀測某個值的變化去完成一段複雜的業務邏輯,當新老值相同,也不會去觸發更新回調。