Vue原始碼學習(十七):實現computed計算屬性

2023-12-02 06:00:31

好傢伙,本章我們嘗試實現computed屬性

 

0.完整程式碼已開源

https://github.com/Fattiger4399/analytic-vue.git

 

1.分析

1.1computed的常見使用方法

1. 計算依賴資料:當某個資料發生變化時,computed屬性可以自動更新,並返回計算結果。例如:
<template>
  <div>
    <p>使用者姓名:{{ userName }}</p>
    <p>使用者年齡:{{ age }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      userName: '張三',
      age: 25,
    };
  },
  computed: {
    // 計算使用者年齡顯示格式
    formattedAge() {
      return this.age.toString().padStart(2, '0');
    },
  },
};
</script>

2. 資料過濾:利用computed屬性對資料進行過濾處理,例如:
<template>
  <div>
    <p>列表資料:{{ filteredList }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      list: [1, 2, 3, 4, 5],
      filterValue: 3,
    };
  },
  computed: {
    // 計算過濾後的列表資料
    filteredList() {
      return this.list.filter((item) => item === this.filterValue);
    },
  },
};
</script>



而在computed定義中又分為兩種寫法:函數和屬性
computed: {
                //1.函數
                fullName() {
                    console.log('執行')
                    return this.firstName + this.lastName
                },
                //2.屬性
                goodName: {
                    get() {
                        return this.firstName + this.lastName
                    },
                    set(value) {
                        // ....
                    }
                }
            }

 

1.2.Vue實現computed大概步驟

1. 初始化階段:當建立 Vue 範例時,Vue 會遍歷 data 中的每個屬性,並使用 Object.defineProperty 方法將它們轉化為響應式。
這個過程會在 data 物件上建立一層 Watcher 物件,用於監聽資料的變化。 2. 建立 Computed:當建立一個 Computed 屬性時,Vue 會呼叫 initComputed 函數,該函數負責註冊這個 Computed 屬性。
註冊過程中,會建立一個 Watcher 物件,並將其掛載到 Computed 屬性的表示式上
這樣,當表示式依賴的資料發生變化時,Watcher 物件可以監聽到這些變化,並更新 Computed 屬性的值。 3. 更新 Computed 值:當 data 中的資料發生變化時,對應的 Watcher 物件會收到通知。

Watcher 物件會執行 Computed 屬性的 get 方法,計算出新的 Computed 值。然後,Watcher 物件會通知檢視層更新,使用新的 Computed 值。 4. 快取 Computed 值:為了提高效能,Vue 會快取 Computed 屬性的計算結果。
當 Computed 屬性的依賴資料發生變化時,Vue 會先檢查依賴資料的變化是否導致 Computed 值需要重新計算。
 如果需要重新計算,Vue 會清除快取,並呼叫 Computed 屬性的 get 方法計算新值。如果不需要重新計算,Vue 會直接使用快取的舊值。

 

 

2、程式碼實現

//initState.js

 
 import Dep from "./observe/dep.js";
export function initState(vm) {
    // console.log(vm)
    //
    const opts = vm.$options
    if (opts.data) {
        initData(vm);
    }
    if (opts.watch) {
        initWatch(vm);
    }
    if (opts.props) {
        initProps(vm);
    }


    if (opts.computed) {
        initComputed(vm);
    }
    if (opts.methods) {
        initMethod(vm);
    }
}

function initComputed(vm) {
    let computed = vm.$options.computed
    console.log(computed)
    let watcher = vm.computedWatchers = {}

    for (let key in computed) {
        let userDef = computed[key]

        let getter = typeof userDef == 'function' ? userDef : userDef.get
        watcher[key] = new Watcher(vm, getter, () => {}, {
            //標記此為computed的watcher
            lazy: true
        })
        defineComputed(vm, key, userDef)
    }
}
let sharedPropDefinition = {}

function defineComputed(target, key, userDef, ) {
    sharedPropDefinition = {
        enumerable: true,
        configurable: true,
        get: () => {},
        set: () => {}
    }
    if (typeof userDef == 'function') {
        sharedPropDefinition.get = createComputedGetter(key)
    } else {
        sharedPropDefinition.get = createComputedGetter(key)
        sharedPropDefinition.set = userDef.set
    }
    Object.defineProperty(target, key, sharedPropDefinition)
}
//高階函數
function createComputedGetter(key) {
    return function () {
        // if (dirty) {
        // }
        let watcher = this.computedWatchers[key]
        if (watcher) {
            if (watcher.dirty) {
                //執行 求值 
                watcher.evaluate() //
            }
            if(Dep.targer){ //說明

                watcher.depend()
            }
            return watcher.value
        }
    }
}

1.sharedPropDefinition: 這是一個空物件,用於定義計算屬性的屬性描述符(property descriptor)。屬性設定
它包含了enumerable、configurable、get和set等屬性設定,這些設定決定了計算屬性在物件上的可列舉性、可設定性以及獲取和設定屬性時的行為。

 

2.computedWatchers: 該物件用於儲存計算屬性的觀察者(watcher)
每一個計算屬性都會被建立一個對應的觀察者物件,並將其儲存在computedWatchers物件中。
觀察者物件負責偵聽計算屬性的依賴變化,並在需要時更新計算結果。

 

3.createComputedGetter(): 該函數用於建立計算屬性的 getter
getter 函數會在存取計算屬性時被呼叫,它首先會檢查觀察者物件是否存在,如果存在則判斷觀察者物件是否需要重新計算計算屬性的值,
如果需要則執行求值操作,並最終返回計算屬性的值。
此外,通過Dep.target的判斷,可以將計算屬性的依賴新增到依賴收集器中,以便在依賴變化時及時更新計算屬性的值。

 

 

watcher.js

class Watcher {
    //vm 範例
    //exprOrfn vm._updata(vm._render()) 
    constructor(vm, exprOrfn, cb, options) {
        // 1.建立類第一步將選項放在範例上
        this.vm = vm;
        this.exprOrfn = exprOrfn;
        this.cb = cb;
        this.options = options;
        //for conputed
        this.lazy = options.lazy
        this.dirty = this.lazy
        // 2. 每一元件只有一個watcher 他是為標識
        this.id = id++
        this.user = !!options.user
        // 3.判斷表示式是不是一個函數
        this.deps = [] //watcher 記錄有多少dep 依賴
        this.depsId = new Set()
        if (typeof exprOrfn === 'function') {
            this.getter = exprOrfn
        } else { //{a,b,c}  字串 變成函數 
            this.getter = function () { //屬性 c.c.c
                let path = exprOrfn.split('.')
                let obj = vm
                for (let i = 0; i < path.length; i++) {
                    obj = obj[path[i]]
                }
                return obj //
            }
        }
        // 4.執行渲染頁面
        // this.value =  this.get() //儲存watch 初始值
        this.value = this.lazy ? void 0 : this.get()


    }
    addDep(dep) {
        //去重  判斷一下 如果dep 相同我們是不用去處理的
        let id = dep.id
        //  console.log(dep.id)
        if (!this.depsId.has(id)) {
            this.deps.push(dep)
            this.depsId.add(id)
            //同時將watcher 放到 dep中
            // console.log(666)
            dep.addSub(this)

        }
        // 現在只需要記住  一個watcher 有多個dep,一個dep 有多個watcher
        //為後面的 component 
    }
    run() { //old new
        let value = this.get() //new
        let oldValue = this.value //old
        this.value = value
        //執行 hendler (cb) 這個使用者wathcer
        if (this.user) {
            this.cb.call(this.vm, value, oldValue)
        }
    }
    get() {
        // Dep.target = watcher

        pushTarget(this) //當前的範例新增
        const value = this.getter.call(this.vm) // 渲染頁面  render()   with(wm){_v(msg,_s(name))} ,取值(執行get這個方法) 走劫持方法
        popTarget(); //刪除當前的範例 這兩個方法放在 dep 中
        return value
    }
    //問題:要把屬性和watcher 繫結在一起   去html頁面
    // (1)是不是頁面中呼叫的屬性要和watcher 關聯起來
    //方法
    //(1)建立一個dep 模組
    updata() { //三次
        //注意:不要資料更新後每次都呼叫 get 方法 ,get 方法回重新渲染
        //快取
        // this.get() //重新
        
        // 渲染
        if(this.lazy){
            this.dirty = true
        }else{
            queueWatcher(this)
        }
    }
    evaluate() {
        this.value = this.get()
        this.dirty = false
    }
    depend(){
        let i = this.deps.length
        while(i--){
            this.deps[i].depend()
        }
    }
}

1.evaluate(): 該方法用於求值計算屬性的結果。
它會呼叫計算屬性的 getter 方法(也就是sharedPropDefinition.get或createComputedGetter()函數中返回的函數),
獲取計算屬性的最新值,並將該值儲存在觀察者物件的value屬性中
同時,將觀察者物件的dirty屬性設定為false,表示計算屬性的值已經是最新的了。

 

2.depend():遍歷所有的依賴物件,並呼叫它們的depend()方法,

 

dep.js

class Dep {
    constructor() {
        this.subs = []
        this.id = id++
    }
    //收集watcher 
    depend() {
      
        //我們希望water 可以存放 dep
        //實現雙休記憶的,讓watcher 記住
        //dep同時,讓dep也記住了我們的watcher
        Dep.targer.addDep(this)
        // this.subs.push(Dep.targer) // id:1 記住他的dep
    }
    addSub(watcher){
        this.subs.push(watcher)
    }
    //更新
    notify() {
        // console.log(Dep.targer)
        this.subs.forEach(watcher => {
            watcher.updata()
        })
    }
}

 

2.depend(): 該方法用於將計算屬性的觀察者物件新增到依賴收集器(Dependency)中。
在計算屬性的 getter 方法執行過程中,如果存取了其他響應式屬性(依賴),

那麼這些響應式屬性對應的觀察者物件會將當前的計算屬性的觀察者物件新增到它們的依賴列表中。
這樣,在依賴變化時,觀察者物件會被通知到,並重新執行計算屬性的 getter 方法來更新計算屬性的值。(在compoted的watcher執行完畢後,還有其他元素的wathcer等待執行)

 

3.預覽效果