實現 VUE 中 MVVM - step11 - Extend

undefined_er發表於2019-04-16

元件的擴充套件

Vue 中有 extend 方法可以擴充套件 Vue 的例項,在上一步中,有一些實現是必須要通過子父元件才能實現,而子元件相當於一個特殊的 Vue 例項,所以這步,我們先把這個擴充套件例項的方法實現。

我們先來看看官網對於 extend 方法的介紹:

使用基礎 Vue 構造器,建立一個“子類”。引數是一個包含元件選項的物件。

從後面一句和具體的使用方法可以得出其實是我們建立例項時,對於傳入引數的擴充套件。對於這個參入引數我們就叫它 options

我們接著往下想,既然這個 options 是能擴充套件的,那麼原 Vue 類下,肯定儲存著一個預設 options ,而建立 Vue 例項時,會把傳入的 options 和預設的 options 進行合併。

所以 extend 方法,是對預設 options 進行擴充套件,從而實現擴充套件。

mergeOptions

ok 有了思路,我們來實現它:

首先是預設的 options ,同時我們假設一個方法(mergeOptions)用來合併 options

let uid = 0

export class Vue extends Event {
    ···

    _init(options) {
        let vm = this
        // 為了方便引用合併的 options 我們把它掛載在 Vue 例項下
        vm.$options = mergeOptions(
            this.constructor.options,
            options,
            vm
        )
        ···
    }
}
// 預設的 options
Vue.options = {
    // 元件列表
    components: {},
    // 基類
    _base: Vue
}
複製程式碼

接著我們來實現 mergeOptions

import R from 'ramda'

export function mergeOptions(parent, child) {
    // data/methods/watch/computed
    let options = {}

    // 合併 data 同名覆蓋
    options.data = mergeData(parent.data, child.data)

    // 合併 methods 同名覆蓋
    options.methods = R.merge(parent.methods, child.methods)

    // 合併 watcher 同名合併成一個陣列
    options.watch = mergeWatch(parent.watch, child.watch)

    // 合併 computed 同名覆蓋
    options.computed = R.merge(parent.computed, child.computed)

    return options
}

function mergeData(parentValue, childValue) {
    if (!parentValue) {
        return childValue
    }
    if (!childValue) {
        return parentValue
    }
    return function mergeFnc() {
        return R.merge(parentValue.call(this), childValue.call(this))
    }
}

// 由於 watcher 的特殊性,我們不覆蓋同名屬性,而是都儲存在一個陣列中
function mergeWatch(parentVal, childVal) {
    if (!childVal) return R.clone(parentVal || {})
    let ret = R.merge({}, parentVal)
    for (let key in childVal) {
        let parent = ret[key]
        let child = childVal[key]
        if (parent && !Array.isArray(parent)) {
            parent = [parent]
        }
        ret[key] = parent
            ? parent.concat(child)
            : Array.isArray(child) ? child : [child]
    }
    return ret
}
複製程式碼

目前我們僅僅實現了 data/methods/watch/computed4options 中的內容,所以我們先合併這 4 項,由於 data/methods/computed3 項是具有唯一性(比如 this.a 應該是一個確定的值),所以採用同名屬性覆蓋的方式,而 watch 是當發生變化時候執行方法,所以所有註冊過的方法都應該執行,因而採用同名屬性的內容合併成一個陣列。

這裡我用了 ramda 這個庫提供的合併方法,用來合併兩個物件,並不會修改原物件的內容。

extend

ok 合併 options 的方法寫好了,我們接著來實現 extend 同過上面的分析,extend 函式僅僅是對預設 options 的擴充套件

Vue.extend = function (extendOptions) {
    const Super = this

    class Sub extends Super {
        constructor(options) {
            super(options)
        }
    }

    Sub.options = mergeOptions(
        Super.options,
        extendOptions
    )

    Sub.super = Super
    Sub.extend = Super.extend

    return Sub
}
複製程式碼

同樣的我們使用 mergeOptions 來合併一下 options 即可,同時將 super 指向父類、獲取 extend 方法。

測試

import {Vue} from './Vue.mjs'

let subVue = Vue.extend({
    data() {
        return {
            dataTest: 1
        }
    },
    methods: {
        methodTest() {
            console.log('methodTest')
        }
    },
    watch: {
        'dataTest'(newValue, oldValue) {
            console.log('watchTest newValue = ' + newValue)
        }
    },
    computed: {
        'computedTest': {
            get() {
                return this.dataTest + 1
            }
        }
    }
})

let test = new subVue({
    data() {
        return {
            subData: 11
        }
    },
    methods: {
        subMethod() {
            console.log('subMethodTest')
        }
    },
    watch: {
        'subData'(newValue, oldValue) {
            console.log('subWatch newValue = ' + newValue)
        }
    },
    computed: {
        'subComputed': {
            get() {
                return this.subData + 1
            }
        }
    }
})

console.log(test.dataTest)
// 1
console.log(test.subData)
// 11

console.log(test.computedTest)
// 2
console.log(test.subComputed)
// 12

test.methodTest()
// methodTest
test.subMethod()
// subMethodTest

test.dataTest = 2
// watchTest newValue = 2
test.subData = 12
// subWatch newValue = 12

console.log(test.constructor === subVue)
// true
console.log(subVue.super === Vue)
// true
複製程式碼

ok 符合我們的預期,extend 方法也就實現了,下一步,實現父子元件。

相關文章