前言
vue中computed和watch是vue.js開發者的利器,也是面試必問的題目之一,問題的答案也是可深可淺,可以反應回答者對個這個問題的認識程度(類似於輸入url到頁面渲染髮生了哪些事情)
分析
1 用法上的區別:
我的理解是,用到computed往往是我們需要使用他的值(vm[computedKey]
),這個值是多個值求值的結果,相當於是我們儲存了計算過程,計算過程中使用過的值發生變化時,會觸發重新執行computed[key]函式(或者computed[key].get),例如:
vm = new Vue({
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
複製程式碼
fullName
就是我們需要的值,fullName
依賴this.firstName
和this.lastName
,這兩個依賴值變化時會觸發computed函式重新執行求值。如果該需求使用watch就是這樣子的:
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
},
watch: {
firstName: function (val) {
this.fullName = val + ' ' + this.lastName
},
lastName: function (val) {
this.fullName = this.firstName + ' ' + val
}
}
})
複製程式碼
與computed相比,watch實現這種需求顯得很繁瑣。 watch的使用場景如他的名字一樣: 觀察。webpack中可以在config中或者命令列模式中使用watch欄位: webpack.config.js
module.exports = {
//...
watch: true
};
複製程式碼
命令列
webpack --watch
複製程式碼
達到的效果是:當前執行目錄(process.cwd()
)裡面檔案發生改變時,webpack能檢測到他變化了,重新打包和熱更新。
vue.js中也是如此,我們觀察某個值變化,這個值變化了,我們來做相應的事情。
例如:
元件的v-model語法糖:
{
props: {
value: {
type: String,
required: true
}
},
data () {
return {
text: ''
}
},
watch: {
// 父元件中可能改變value繫結的值
value (val) {
this.text = val
},
text (val) {
this.$emit('input', val)
}
}
}
複製程式碼
一句話概括就是: computed[key]這個值我們需要用到它,依賴變化運算過程自動執行,watch[key]這個值我們不需要用到它,它變化了我們想做一些事情。 當然,理論上來說computed能實現上面的需求:
computed: {
// 這裡xxx我們還需要使用到,不然無法觸發求值
xxx () {
this.value // 這裡啥都不做就是想做個依賴收集
this.text // 同上
// this.text和this.value舊值都需要快取起來
if (this.value !== this.value的舊值) {
this.text = this.value
}
if (this.text !== this.text的舊值) {
this.$emit('input', this.text)
}
}
}
複製程式碼
這樣實現太繁瑣,所以合適的場景使用合適的api的,這樣才符合設計的初衷。有些場景兩者使用沒有多大區別。
2 原始碼分析:
看過原始碼的同學都清楚,watch和computed的每一項最終都會執行new Watcher
生成一個watcher例項,執行上面會有一些差異。下面開始從原始碼分析一下:
注意:vue.js每個版本可能會更改一些邏輯,當前分析版本: v2.6.11 web版,下文中提到的vm[key]相當於我們在vue中使用的this.xxx屬性值
當執行 new Vue({})的時候或者生成元件例項,(元件會類似Copm extend Vue 派生出元件構造類在Vue上),最終都會執行_init()邏輯,如下(這裡其他邏輯省略):
_init () {
...
initState(vm)
...
}
initState () {
...
initComputed(vm, opts.computed)
initWatch(vm, opts.watch)
...
}
複製程式碼
1. watch:
function initWatch (vm, watch) {
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)
}
}
}
function createWatcher (vm,expOrFn,handler,options) {
if (isPlainObject(handler)) { // handler 是否為物件
// watch[key]可以是函式或者物件
options = handler
handler = handler.handler
}
//
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
Vue.prototype.$watch = function (expOrFn, cb, options) {
options = options || {}
options.user = true
const watcher = new Wacter(vm, expOrFn,cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
}
class Watcher {
constructor (vm, expOrFn, cb, options) {
// 保留關鍵程式碼
if (options) {
this.user = !!options.user
this.lazy = !!options.lazy
}
this.cb = cb
this.active = true
this.id = ++uid // uid for batching
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 使用者watch邏輯下 expOrFn 為watch[key]的key,型別為 string
this.getter = parsePath(expOrFn)
}
this.value = this.lazy
? undefined
: this.get()
}
get () {
// 這裡做的事情是 Dep.target = this
pushTarget(this)
let value
const vm = this.vm
// 防止使用者(你)做傻事讓js報錯終止執行
try {
// 訪問了 vm.obj.a.b
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
// deep 對obj的每個obj[key]訪問 觸發依賴收集
traverse(value)
}
// Dep.target = 上一個watcher 例項
popTarget()
}
return value
}
addDep (dep) {
// 一個dep 在一個watcher上只新增一次
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)
}
}
}
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
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) {
try {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
}
}
function parsePath (path) {
// 這個函式的目的是返回我們需要觀察的那個值的求值函式
/*
我們的定義watch可能是 watch: {
'obj.a.b.c': {
handler () {}
}
}
*/
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
// 響應式核心程式碼
// 一個值有一個dep例項
const dep = new Dep()
Object.defineProperty(obj, key, {
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : 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 = getter ? getter.call(obj) : val
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
class Dep {
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// 先建立的先執行 使用者watcher computed watcher 在渲染watcher之前
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
複製程式碼
流程大概是這樣的:
-
_init -> initWath -> createWatcher(vm, key, handler) -> vm.$watch -> new Watch() -> this.get
-
this.get()
的時候,會執行const value = getter ? getter.call(obj) : val
和pushTarget(this)
(做的事情:Dep.target =this),getter就是對我們要觀測到的值訪問值(比如:'obj.a.b' => obj.a.b),會觸發obj.a.b的get劫持。針對deep的情況會進一步的遞迴訪問值,觸發get劫持。 -
執行: dep.depend() -> Dep.target.addDep(dep例項) ->dep.addSub(當前watcher例項),依賴收集完成。
-
派發更新邏輯開始, 當obj.a.b的值發生改變時,會觸發set函式,執行dep.notify -> subs[i].update() -> watcher例項.update() -> queueWatcher(push到watcher佇列,排序watcher)->nextTick(flushSchedulerQueue)(下個tick執行watcher佇列的)->watcher.run() (後面的程式碼分支有點多,就不一一貼上了)
-
執行this.get,相當於執行了第二步的,邏輯,比較新舊值是否相等(value基礎型別,引用型別或者deep直接執行接下來的邏輯),執行
this.cb.call(this.vm, value, oldValue)
,this.cb就是使用者定義的watch[key]的函式。所以我們在定義watch函式的時候第一個引數是newValue 第二個引數是oldValue
總結:我們在Vue.js中使用的watch是userWatch,我們觀測某個屬性的變化,監測邏輯和渲染時的依賴收集一樣:dep新增watch,這個值變化了通知所有的watch, 最後會執行我們的定義的watch[key]的handler函式。
提示:渲染watcher類似與上面的watcher,監測的是template中用的值,只要有一個值發生變化,watcher就會觸發,重新渲染。
2. computed:
const computedWatcherOptions = { lazy: true }
function initComputed (vm, computed) {
for (const key in computed) {
// 不考慮設定了computed get set
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
}
class Watcher {
constructor (vm,expOrFn,cb,options) {
this.vm = vm
vm._watchers.push(this)
if (options) {
this.lazy = !!options.lazy
}
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.getter = expOrFn // 使用者computed[key]的值
// computed 不執行this.get
this.value = undefined
}
get () {
// Dep.target = 當前watcher
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
}
// Dep.target設定為上一個watcher 渲染watcher
popTarget()
this.cleanupDeps()
return value
}
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()
}
}
notify () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// 先建立的先執行 使用者watcher computed watcher 在渲染watcher之前
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = noop // noop 為空函式
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
// 確保執行一次
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
複製程式碼
大概的流程是:
- initComputed遍歷options.computed物件執行new Watcher和defineComputed
- new Watcher: new Watcher -> 只會執行Watcher類的建構函式
注意:元件這個邏輯會在Vue.extend()(Vue派生的元件構造類)過程中執行,這裡分析根節點的
- defineComputed: sharedPropertyDefinition.get = createComputedGetter(key)(生成vm[key] computed[key] 的get劫持函式),通過object.defineProperty設定vm[key]的get和set,所以能通過this[key]的方式訪問computed的值
- 當我們元件中使用到computed[key]即是vm[key]或this[key] (如:前面提到的fullName),觸發createComputedGetter(key)生成的get函式:
- watcher.evaluate() -> this.get() -> Dep.target = 當前的watcher例項(computed[key]生成的) -> 執行this.getter(使用者定義的computed[key]) -> 觸發函式裡面使用this.xxx(如:fullName () {return this.firstName + this.lastName})的get劫持函式和上面的watcher一樣: firstName和lastName都會收集該watcher -> this.dirty = false(專案中多個地方用到了該computed[key],watcher.evaluate()只需要執行一次)
- watcher.depend() -> watcher.dep[i].depend() computed[key]使用到的一個值(firstName lastName)就擁有一個dep,(deps包含了firstName和lastName的dep)-> Dep.target.addDep(this) 上一步的時候Dep.target已經設定為上一個watcher了,即是渲染watcher -> 這些dep也會收集渲染watcher
- return watcher.value computed[key] (vm[key]或this[key])就是watcher.value
- 當computed 依賴的這些值(fistName或者lastName發生變化)發生變化時,觸發set邏輯 -> dep.notify 通過id排序 確保computed watcher先執行,dep訂閱的watcher遍歷執行update -> this.dirty = true -> 執行渲染watcher.update -> 元件template重新渲染 -> 再執行第2步保持值為最新值。
注意:這個版本好像computed沒有之前所謂的快取,newVal oldVal不會比較了,依賴的值發生改變,重新求值。
而且vue的官方文件中也提到:
不同的是計算屬性是基於它們的響應式依賴進行快取的。只在相關響應式依賴發生改變時它們才會重新求值。這就意味著只要 message 還沒有發生改變,多次訪問 reversedMessage 計算屬性會立即返回之前的計算結果,而不必再次執行函式。
之前的版本我記得是這樣的, c () {return this.a + this.b},a = 1, b =2 -> a =2, b=1,最終的值不變就會快取,現在不再說computed會比較新舊值了,而是說明依賴發生改變,computed就重新求值。
總結:
- watcher和dep相互收集,我們定義的data中的一個屬性(基礎型別,引用型別遞迴建立dep, 確保一個基礎型別一個dep)擁有一個dep,dep會收集所有的watcher(渲染watcher 、computed watcher 、使用者定義的watcher), watcher也會記錄被哪些dep收集了,當然這個過程中會有一個去重處理, data.xxx發生變化會通知所有的watcher, dep是obj.xxx和watcher的一個橋樑。
- watch(使用者watcher)和computed的異同點:
-
相同點:computed[key]和user watcher為了監測其他值都會生成一個watcher例項。
-
不同點:
- dep數量不同:watch監聽的是vm[key]的變化,vm[key]的dep, vm[key]變化觸發watch.get求值,觸發watcher回撥函式,dep數量為1。computed中的dep是computed[key]執行過程中訪問的dep,即用到了哪些值,dep數量為1+。
- dep收集的物件不同: 執行使用者watcher.get的時候,watch的[key] (vm[key])的dep只會收集當然watcher,computed watcher中的dep會收集渲染watcher和computed watcher
- 執行時機不同: watcher immediate除外,computed[key]在我們使用到時會觸發getter,觸發watcher.get()執行computed[key],computed必須在專案中使用到(需要觸發getter), watch則只會在vm[key]改變觸發this.cb即watch中定義的handler。
最後:
- 如果有錯誤歡迎指出
- 如有幫助歡迎點贊^_^。
- 最後附上思維導圖: