前面的話
前端日問,鞏固基礎,不打烊!!!
解答
如有錯誤歡迎指出,感謝!!!
提出問題
提出幾個問題:
- computed 是如何初始化的?
- 爲何data值的變化computed會重新計算?
- 爲什麼computed值是緩存的呢?
想了解computed的原理,你需要了解Vue的響應式原理,瞭解computed其實就是一個惰性watcher
。
下面一一解答這幾個問題。
Watcher 的實現
給出watcher的實現,方便下文好看。
//去重 防止重複收集
let uid = 0
class Watcher{
constructor(vm,expOrFn,cb,options){
//傳進來的對象 例如Vue
this.vm = vm
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
}else{
this.deep = this.user = this.lazy = false
}
this.dirty = this.lazy
//在Vue中cb是更新視圖的核心,調用diff並更新視圖的過程
this.cb = cb
this.id = ++uid
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
if (typeof expOrFn === 'function') {
//data依賴收集走此處
this.getter = expOrFn
} else {
//watch依賴走此處
this.getter = this.parsePath(expOrFn)
}
//設置Dep.target的值,依賴收集時的watcher對象
this.value = this.lazy ? undefined : this.get()
}
get(){
//設置Dep.target值,用以依賴收集
pushTarget(this)
const vm = this.vm
//此處會進行依賴收集 會調用data數據的 get
let value = this.getter.call(vm, vm)
popTarget()
return value
}
//添加依賴
addDep (dep) {
//去重
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
//收集watcher 每次data數據 set
//時會遍歷收集的watcher依賴進行相應視圖更新或執行watch監聽函數等操作
dep.addSub(this)
}
}
}
//更新
update () {
if (this.lazy) {
this.dirty = true
}else{
this.run()
}
}
//更新視圖
run(){
console.log(`這裏會去執行Vue的diff相關方法,進而更新數據`)
const value = this.get()
const oldValue = this.value
this.value = value
if (this.user) {
//watch 監聽走此處
this.cb.call(this.vm, value, oldValue)
}else{
//data 監聽走此處
//這裏只做簡單的console.log 處理,在Vue中會調用diff過程從而更新視圖
this.cb.call(this.vm, value, oldValue)
}
}
//如果計算熟悉依賴的data值發生變化時會調用
//案例中 當data.name值發生變化時會執行此方法
evaluate () {
this.value = this.get()
this.dirty = false
}
//收集依賴
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
// 此方法獲得每個watch中key在data中對應的value值
//使用split('.')是爲了得到 像'a.b.c' 這樣的監聽值
parsePath (path){
const bailRE = /[^w.$]/
if (bailRE.test(path)) return
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
//此處爲了兼容我的代碼做了一點修改
//此處使用新獲得的值覆蓋傳入的值 因此能夠處理 'a.b.c'這樣的監聽方式
if(i==0){
obj = obj.data[segments[i]]
}else{
obj = obj[segments[i]]
}
}
return obj
}
}
}
computed 的實現
在Vue響應式原理這篇文章已經重點提了data數據的初始化(即initDate),computed的初始化是在其後面定義的:(爲什麼一定要在data數據的後面初始化,這也是有原因的,下面這接着說)
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// computed初始化
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
接着看一看initComputed
函數的定義:
//空函數
const noop = ()=>{}
// computed初始化的Watcher傳入lazy: true就會觸發Watcher中的dirty值爲true
const computedWatcherOptions = { lazy: true }
//Object.defineProperty 默認value參數
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
// 初始化computed
class initComputed {
constructor(vm, computed){
//新建存儲watcher對象,掛載在vm對象執行
const watchers = vm._computedWatchers = Object.create(null)
//遍歷computed
for (const key in computed) {
const userDef = computed[key]
//getter值爲computed中key的監聽函數或對象的get值
let getter = typeof userDef === 'function' ? userDef : userDef.get
//新建computed的 watcher
watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
if (!(key in vm)) {
/*定義計算屬性*/
this.defineComputed(vm, key, userDef)
}
}
}
//把計算屬性的key掛載到vm對象下,並使用Object.defineProperty進行處理
//因此調用vm.somecomputed 就會觸發get函數
defineComputed (target, key, userDef) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = this.createComputedGetter(key)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? userDef.cache !== false
? this.createComputedGetter(key)
: userDef.get
: noop
//如果有設置set方法則直接使用,否則賦值空函數
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
//計算屬性的getter 獲取計算屬性的值時會調用
createComputedGetter (key) {
return function computedGetter () {
//獲取到相應的watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
//watcher.dirty 參數決定了計算屬性值是否需要重新計算,默認值爲true,即第一次時會調用一次
if (watcher.dirty) {
/*每次執行之後watcher.dirty會設置爲false,只要依賴的data值改變時纔會觸發
watcher.dirty爲true,從而獲取值時從新計算*/
watcher.evaluate()
}
//獲取依賴
if (Dep.target) {
watcher.depend()
}
//返回計算屬性的值
return watcher.value
}
}
}
}
開頭的參數很重要。
初始化大致流程:
- 新建存儲watcher對象的數組,並掛載在
vm
的_computedWatchers
屬性上。 - 遍歷整個computed上的屬性key
-
獲取key上的監聽函數(賦給getter)
-
創建watcher (即每一個computed屬性都是一個watcher)
-
調用
defineComputed
函數 :將每一個計算屬性key
掛載到vm
上,並使用Object.defineProperty
進行處理。具體看一看defineComputed函數做了什麼:
既然要是用
Object.defineProperty
函數將每個計算屬性key掛載到vm
上,需要三個值:掛載目標 vm
、掛載對象 key
、設置key的value對象
。前面兩個已經具備,只差value對象了
。還記得開頭的參數麼?這個value對象就是開頭設置的sharedPropertyDefinition對象
。sharedPropertyDefinition
對象的get屬性剛開始是空函數,在這裏我們調用createComputedGetter
函數來設置get屬性。具體看一看createComputedGetter 函數做了什麼:
它返回了一個函數給
sharedPropertyDefinition.get
. 其內部:根據計算屬性key找到對應的watcher。(因爲計算屬性watcher創建時,會傳入computedWatcherOptions 對象
,這個對象裏面定義了lazy: true
,導致了計算屬性watcher
的lazy爲true,dirty值初始時爲lazy的值。)找到對應的watcher後,會執行這段代碼:
if (watcher.dirty) { /*每次執行之後watcher.dirty會設置爲false, 只要依賴的data值改變時纔會觸發 watcher.dirty爲true,從而獲取值時從新計算*/ watcher.evaluate() }
watcher.evaluate()就是本文的核心: 初始時,就會執行這個函數
//如果計算熟悉依賴的data值發生變化時會調用 //案例中 當data.name值發生變化時會執行此方法 evaluate () { this.value = this.get() this.dirty = false }
瞭解響應式原理的應該知道
wacther.get()
函數的用途:就是綁定Dep.target 爲當前的watcher。
並且進行data數據的依賴收集。看代碼: 調用
watcher.get
會觸發data數據的getter,進行依賴收集,收集這個watcher
。前面講過data裏面的數據會比computed
裏面的數據先初始化,就是這個原因。get(){ //設置Dep.target值,用以依賴收集 pushTarget(this) const vm = this.vm //此處會進行依賴收集 會調用data數據的 get let value = this.getter.call(vm, vm) popTarget() return value }
-
到此爲止,上面的問題應該有答案了吧! 計算屬性的值,是依賴於其他data屬性的,而計算屬性本質是計算watcher
, 它所依賴的data屬性的dep會將這個watcher加入到subs數組中,當其變化時,就會通知這個watcher改變,所以說計算屬性時緩存的。
總結
-
初始化一個computed,會爲每個屬性key,創建相應的watcher(將key的監聽函數傳入,作爲watcher的getter)。
-
掛載屬性key到vm上,並用
Object.defineProperty
爲其添加getter。 -
第一次調用屬性key時,觸發
watcher.evaluate
--> 觸發watcher.get
-->watcher.getter(就是key的監聽函數)
--> 觸發屬性key所依賴的data屬性的getter
,進而收集這個watcher。 -
當data屬性變化時,觸發其setter,通知watcher更新,執行
watcher.run
,進而觸發diff,更新視圖。
通過一個computed實例來走一下上面的流程
computed:{
sayHello() {
return this.hello + ' ' + this.world;
}
},
sayHello
計算屬性依賴兩個屬性:this.hello
與this.world
-
在初始化時會創建一個sayHello對應的
計算屬性watcher
,watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions) -
將sayHello計算屬性掛載到vm上,用
Object.defineProperty
定義其get屬性。初次獲取sayHello屬性,觸發監聽函數 :
-
獲取this.hello -->觸發hello的getter --> hello的dep.depend收集收集sayHello的watcher.
-
獲取this.world時,觸發world的getter,dep.depend收集sayHello的watcher.
-
-
所以sayhello的watcher最後,會有兩個dep收集它。hello與world其中任意一個更新,都會觸發更新sayhello watcher的run函數
-
最後觸發diff算法,更新虛擬dom,進而更新界面
computed 與watch的區別
- computed屬性不是data中的屬性值,是一個新值,初始化時使用
Object.defineProperty
方法掛載到vm上;而watch是監聽已經存在於data中的屬性 - computed本質是一個
惰性的觀察者
,具有緩存性
,之後依賴的data值變化時,纔會變化;watch沒有緩存性,數據變化就更新 - computed適合與一個數據被多個數據影響;而watch適用於一個數據影響多個數據。
參考