Vue0.11版本源碼閲讀系列七:補充

語言: CN / TW / HK

第一篇留了兩個問題:

1.計算屬性依賴的屬性變化了是如何觸發計算屬性更新的

2.watch選項或$watch方法的原理是怎樣的

本篇來分析一下這兩個問題,另外簡單看一下自定義元素是怎麼渲染的。

計算屬性

<p v-text="showMessage + '我是不重要的字符串'"></p>
{
    data: {
        message: 'Hello Vue.js!'
    },
    computed: {
        showMessage() { 
            return this.message.toUpperCase()
        }
    }
}

以這個簡單的例子來説,首先計算屬性也是會掛載到vue實例上成為實例的一個屬性:

for (var key in computed) {
    var userDef = computed[key]
    var def = {
        enumerable: true,
        configurable: true
    }
    if (typeof userDef === 'function') {
        def.get = _.bind(userDef, this)
        def.set = noop
    } else {
        def.get = userDef.get
            ? _.bind(userDef.get, this)
        : noop
        def.set = userDef.set
            ? _.bind(userDef.set, this)
        : noop
    }
    Object.defineProperty(this, key, def)
}

通過this.xxx訪問計算屬性時會調用我們定義的computed選項裏面的函數。

其次在模板編譯指令解析的階段計算屬性和普通屬性並沒有區別,這個v-text指令會創建一個Directive實例,這個Directive實例初始化時會以showMessage + '我是不重要的字符串'為唯一的標誌創建一個Watcher實例,v-text指令的update方法會被這個Watcher實例所收集,添加到它的cbs數組裏,Watcher實例化時會把自身賦值給Observer.target,隨後對showMessage + '我是不重要的字符串'這個表達式求值,也就會調用到計算屬性的函數showMessage(),這個函數調用後會引用所依賴的所有屬性,這裏也就是message,這會觸發messagegetter,這樣這個Watcher實例就被添加到message的依賴收集對象dep裏了,後續當message的值變化觸發其setter後會遍歷其dep裏收集的Watcher實例,觸發Watcherupdate方法,最後會遍歷cbs裏添加的指令的update方法,這樣這個依賴計算屬性的指令就得到了更新。

值得注意的是在這個版本里,計算屬性是沒有緩存的,即使所依賴的值沒有變化,重複引用計算屬性的值也會重新執行我們定義的計算屬性函數。

偵聽器

watch選項聲明的偵聽器最後調用的也是$watch方法,在第一篇已經知道了$watch方法裏主要就是創建了一個Watcher實例:

// exp就是我們要偵聽的數據,如:a、a.b
exports.$watch = function (exp, cb, deep, immediate) {
  var vm = this
  var key = deep ? exp + '**deep**' : exp
  var watcher = vm._userWatchers[key]
  var wrappedCb = function (val, oldVal) {
    cb.call(vm, val, oldVal)
  }
  if (!watcher) {
    watcher = vm._userWatchers[key] =
      new Watcher(vm, exp, wrappedCb, {
        deep: deep,
        user: true
      })
  } else {
    watcher.addCb(wrappedCb)
  }
}

對於Watcher我們現在已經很熟悉了,實例化的時候會把自己賦值給Observer.target,然後觸發表達式的求值,也就是我們要偵聽的屬性,觸發其gettter然後把該Watcher收集到它的依賴收集對象dep裏,只要被收集就好辦了,後續屬性值變化後就會觸發這個Watcher的更新,也就會觸發上面的回調。

自定義組件的渲染

<my-component></my-component>
new Vue({
    el: '#app',
    components: {
        'my-component': {
            template: '<div>{{msg}}</div>',
            data() {
                return {
                    msg: 'hello world!'
                }
            }
        }
    }
})

在第一篇裏我們提到了每個組件選項最後都會被創建成一個繼承了vue的構造函數:

image-20210114201622204

然後到模板編譯階段遍歷到這個自定義元素會給它添加一個v-component屬性:

tag = el.tagName.toLowerCase()
component =
    tag.indexOf('-') > 0 &&
    options.components[tag]
if (component) {
    el.setAttribute(config.prefix + 'component', tag)
}

image-20210115100403284

所以後續也是通過指令來處理這個自定義組件,接下來會生成鏈接函數,component屬於terminal指令的一種:

image-20210115100542982

接下來就回到了正常的指令編譯過程了,_bindDir方法會給v-component指令創建一個Directive實例,然後會調用component指令的bind方法:

{
    bind: function () {
        // el就是我們的自定義元素my-component
        if (!this.el.__vue__) {
            // 創建一個註釋元素替換掉該自定義元素
            this.ref = document.createComment('v-component')
            _.replace(this.el, this.ref)
            // 檢查是否存在keep-alive選項
            this.keepAlive = this._checkParam('keep-alive') != null
            // 檢查是否存在ref來引用該組件
            this.refID = _.attr(this.el, 'ref')
            if (this.keepAlive) {
                this.cache = {}
            }
            // 解析構造函數,也就是返回初始化時選項合併階段生成的構造函數,expression這裏是指令值my-component
            this.resolveCtor(this.expression)
            // 創建子實例
            var child = this.build()
            // 插入該子實例
            child.$before(this.ref)
            // 設置ref
            this.setCurrent(child)
        }
    }
} 

build方法:

{
    build: function () {
        // 如果有緩存直接返回
        if (this.keepAlive) {
            var cached = this.cache[this.ctorId]
            if (cached) {
                return cached
            }
        }
        var vm = this.vm
        if (this.Ctor) {
            var child = vm.$addChild({
                el: this.el,
                _asComponent: true,
                _host: this._host
            }, this.Ctor)// Ctor就是該組件的構造函數
            if (this.keepAlive) {
                this.cache[this.ctorId] = child
            }
            return child
        }
    }
}

這個方法用來創建子實例,調用了$addChild方法,簡化後如下:

exports.$addChild = function (opts, BaseCtor) {
    var parent = this
    // 父實例就是上述我們new Vue的實例
    opts._parent = parent
   	// 根組件也就是父實例的根組件
    opts._root = parent.$root
    // 創建一個該自定義組件的實例
    var child = new BaseCtor(opts)
    return child
}

上面兩個方法主要就是創建了一個該組件構造函數的實例,因為組件構造函數繼承了vue,所以之前的new Vue時做的初始化工作同樣也都會走一遍,什麼觀察數據、遍歷該自定義組件及其所有子元素進行模板編譯綁定指令等等,因為我們傳遞了template選項,所以在第一篇裏一帶而過的方法_compile裏在調用compile方法之前會先對這個進行處理:

// 這裏會把template模板字符串轉成dom,原理很簡單,創建一個文檔片段,再創建一個div,之後再把模板字符串設為div的innserHTML,最後再把div裏的元素都添加到文檔片段裏即可
el = transclude(el, options)
// 編譯並鏈接其餘的
compile(el, options)(this, el)

最後如果存在keep-alive則把該實例緩存一下,回到bind方法裏的child.$before(this.ref)

exports.$before = function (target, cb, withTransition) {
    return insert(
        this, target, cb, withTransition,
        before, transition.before
    )
}
function insert (vm, target, cb, withTransition, op1, op2) {
    // 獲取目標元素,這裏就是bind方法裏創建的註釋元素
    target = query(target)
    // 元素當前不在文檔中
    var targetIsDetached = !_.inDoc(target)
    // 判斷是否要使用過渡方式插入,如果元素不在文檔中則會使用帶過渡的方式插入
    var op = withTransition === false || targetIsDetached
    ? op1
    : op2
    // 如果目標元素當前已經插入文檔以及該該組件沒有掛載過就需要觸發attached生命週期
    var shouldCallHook =
        !targetIsDetached &&
        !vm._isAttached &&
        !_.inDoc(vm.$el)
    // 插入文檔
    op(vm.$el, target, vm, cb)
    if (shouldCallHook) {
        vm._callHook('attached')
    }
    return vm
}

op方法會調用transition.before方法把元素插入到文檔中,關於過渡插入的詳細分析請參考vue0.11版本源碼閲讀系列六:過渡原理

到這裏組件就已經渲染完成了,bind方法裏最後調用了setCurrent

{
    setCurrent: function (child) {
        this.childVM = child
        var refID = child._refID || this.refID
        if (refID) {
            this.vm.$[refID] = child
        }
    }
}

如果我們設置了引用比如:<my-component v-ref="myComponent"></my-component>,那麼就可以通過this.$.myComponent訪問到該子組件。

keep-alive的工作原理也很簡單,就是返回之前的實例而不是創建新實例,這樣所有的狀態都還保存着。

總結

本系列到這裏基本就結束了,我相信能看到這裏的人不多,因為第一次寫這種源碼閲讀的系列,總的來説有點亂,很多地方重點不是很突出,描述的可能也不是很詳細,可能不是很讓人看的下去,另外難免也會有錯誤,歡迎大家指出。

閲讀源碼是每個開發者都無法繞過去的必經之路,無論是為了提升自己還是為了面試,我們終歸是要對自己每時每刻在用的東西有個更深的瞭解,這樣對於使用來説也是有好處的,另外思考和學習別人優秀的編碼思維,也能讓自己變的更好。

不得不説閲讀源碼是挺枯燥和無聊的,也是有難度的,很容易讓人心生退意,很多地方你不是非常的瞭解其作用的話是基本看不懂的,當然我們也不必執着於這些地方,也不用把所有地方都看完看懂,更好的方式還是帶着問題去閲讀,比如説我想搞懂某一個地方原理,那麼你就去看這部分的代碼就可以了,當你沉浸在裏面也是別有一番意思的。

話不多説,白白~