首先,瞭解下Vue中,computed的作用,主要用於對一個變量的惰性更新,當這個屬性所依賴的變量發生改變時,它將會更新。用官網的話講 “計算屬性是基於它們的響應式依賴進行緩存的”
1、創建 Vue 實例時會 initState
和 $mount
,前者用於初始化 data,computed,watch初始化函數。後者用於做頁面渲染
Vue.prototype._init = function (options) {
// vue中初始化 this.$options 表示的是vue中參數
let vm = this;
vm.$options = options;
// MVVM原理 需要數據重新初始化
// 攔截數組的方法 和 對象的屬性
initState(vm); // data computed watch
// ....
// 初始化工作 vue1.0 =>
if(vm.$options.el){
vm.$mount();
}
}
export function initState(vm){
//做不同的初始化工作
let opts = vm.$options;
if(opts.data){
initData(vm); // 初始化數據
}
if(opts.computed){
initComputed(vm,opts.computed); // 初始化計算屬性
}
if(opts.watch){
initWatch(vm); // 初始化watch
}
}
2、initComputed()
,初始化計算屬性函數
function initComputed(vm,computed){
// 將計算屬性的配置 放到vm上
let watchers = vm._watchersComputed = Object.create(null); // 創建存儲計算屬性的watcher的對象
for(let key in computed){ // {fullName:()=>this.firstName+this.lastName}
let userDef = computed[key];
// new Watcher此時什麼都不會做 配置了lazy dirty = true
watchers[key] = new Watcher(vm,userDef,()=>{},{lazy:true}); // 計算屬性watcher 默認剛開始這個方法不會執行
// vm.fullName
Object.defineProperty(vm,key,{
get:createComputedGetter(vm,key)
}) // 將這個屬性 定義到vm上
}
}
將 Vue 實例上 computed 屬性進行遍歷,並且設置 vm._watchersComputed 的屬性爲每一個 computed 的Watcher,這個 Watcher 類和 渲染 Watcher 使用的是同一個類,第二個傳入參數是一個屬性計算函數,然後將 computed 中的屬性掛載到 vm 實例上,並且做一個屬性劫持。
3、createComputedGetter()
,返回的是一個函數
function createComputedGetter(vm,key){
let watcher = vm._watchersComputed[key]; // 這個watcher 就是我們定義的計算屬性watcher
return function() { // 用戶取值是會執行此方法
if(watcher){
// 如果dirty 是false的話 不需要重新執行計算屬性中的方法
if(watcher.dirty){ // 如果頁面取值 ,而且dirty是true 就會去調用watcher的get方法
watcher.evaluate();
}
if(Dep.target){ // watcher 就是計算屬性watcher dep = [firstName.dep,lastName.Dep]
watcher.depend();
}
return watcher.value
}
}
}
4、computed如何new Watcher的
class Watcher{
constructor(vm,exprOrFn,cb=()=>{},opts={}){
this.vm = vm;
this.exprOrFn = exprOrFn;
if(typeof exprOrFn === 'function'){
this.getter = exprOrFn; // getter就是new Watcher傳入的第二個函數
}else {
this.getter = function () { // 如果調用此方法 會將vm上對應的表達式取出來
return util.getValue(vm,exprOrFn)
}
}
this.lazy = opts.lazy; // 如果這個值爲true 說明他是計算屬性
this.dirty = this.lazy;
....省略很多代碼
....
this.value = this.lazy? undefined : this.get(); // 默認創建一個watcher 會調用自身的get方法;
}
get(){
// Dep.target = 用戶的watcher
debugger;
pushTarget(this); // 渲染watcher Dep.target = watcher msg變化了 需要讓這個watcher重新執行
// 默認創建watcher 會執行此方法
// dep = [watcher] dep =[watcher]
// fullName(){return this.firstName + this.lastName}
// 這個函數調用時就會將當前計算屬性watcher 存起來
let value = this.getter.call(this.vm); // 讓這個當前傳入的函數執行
popTarget(); // Dep.target = undefined
return value;
}
evaluate(){
this.value = this.get();
this.dirty = false; // 值求過了 下次渲染的時候不用求了
}
depend(){
let i = this.deps.length;
while(i--){
this.deps[i].depend();
}
}
update(){ // 如果立即調用get 會導致頁面刷新 異步來更新
if(this.lazy){ // 如果是計算屬性
this.dirty = true; // 計算屬性依賴的值變化了 稍後取值時重新計算即可
}else {
queueWatcher(this);
}
}
}
可以看到,傳入 lazy:true
,在 Watcher中會在實例上掛載一個 dirty 屬性,watcher.dirty 爲true,則會調用 watcher.evaluate()
進行求值,dirty:false
則不會對computed中的屬性進行求值
evaluate() 可以看到該方法調用了 this.get()
,會將該屬性的計算函數執行,執行該屬性的計算函數是肯定會去訪問 該屬性所依賴的其他變量,所依賴的變量dep就會存放計算屬性watcher
watcher.depend() 則是讓上述所依賴變量的dep再添加一個渲染watcher,爲了後續 computed 計算屬性的更新。
我們設定如下示例,進行源碼分析:
<template>
<div>{{fullName}}</div>
</template>
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
整個過程如下:
- $mount 會編譯模板,stack 中存放【渲染 watcher】
- 編譯模板時讀取 fullName 屬性,讀取時進行 fullName 屬性劫持
Object.defineProperty
,createComputedGetter 函數中 watcher.evaluate(); - evaluate() 調用 this.get(),pushTarget(), Dep.target 目前爲計算Watcher, stack中爲 【渲染Watcher, 計算屬性Watcher】, this.getter() 可以看到是調用傳入的表達式函數,就會訪問 this.firstName, this.lastName,會導致 firstName.dep存入該 計算Watcher, lastName.dep存入該 計算Watcher,最後return 出計算出的結果,最後 計算屬性Watcher 從 stack 中出棧,Dep.target 爲 【渲染Watcher】
- 如果存在 Dep.target 則會 watcher.depend(), 該 watcher 還是計算屬性Watcher,watcher.depend()作用是:使得存在計算屬性的變量(firstName,lastName )的 計算屬性watcher 的 deps 中 dep 中(或者firstName,lastName每一個dep中)再存一個【渲染watcher】,用於在 fullName 所依賴的 firstName,lastName變化時,notify()遍歷自己的 watcher,執行update(),進而 fullName 在 watcher.dirty: true是重新計算求值,也再一次 進入步驟3