在上一篇咱們講到了數據劫持,和數據觀測。那麼怎麼將數據和相關的DOM關聯起來呢?本篇咱們將解開這個過程。html
上一篇講解中咱們知道Watcher
是實際執行數據變動以後操做的主要對象,咱們先找到它的實例化路徑,發現它是在mount
的時候進行的操做。vue
Vue -> this._init -> initLifecycle -> mountComponent
複製代碼
咱們在這個方法中找到了關於Watcher
的實例化代碼node
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
複製代碼
結合以前的Watcher
構造函數:bash
class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
}
}
複製代碼
咱們先解釋下參數:首先傳入了組件實例vm
,而後是expOrFn
傳入的是updateComponent
,cb
是一個空函數noop
,options
中定義了一個鉤子before
,最後傳入了isRenderWatcher
爲true
,代表這是一個RenderWatcher
,就會將該Wathcer
掛載到組件上。dom
而這裏關鍵的地方就是updateComponent
。咱們在上一篇分析中提到,當數據變動,依賴會通知全部訂閱者Watcher
作出相應更新,也就是watcher.update
,而watcher.update
不論是同步仍是異步,其核心是調用wathcer.run
去執行相關操做。異步
run () {
if (this.active) {
const value = this.get()
// ...
this.cb.call(this.vm, value, oldValue)
}
}
複製代碼
這個函數有兩個關鍵的地方,一個是獲取值,調用了get
方法,而另外一個就是執行回調函數cb
,在上一篇咱們一樣提到,咱們自定義的watch
就是經過傳入exp
和cb
來實現觀測具體某個屬性的。ide
好比:函數
new Vue({
data: {
msg: ''
},
watch: {
msg: function() {}
}
})
複製代碼
這裏的watch
就是經過new Watcher(vm, 'msg', fn)
相似這樣的方式定義的,這和咱們如今看見的徹底不同。oop
這也是困惑的一點,咱們如今看到的RenderWatcher
傳入了一個空函數做爲cb
,也就是說執行cb
是沒有任何做用的,那麼在數據更新時是怎麼通知到視圖層的呢?咱們發如今run
方法中,除了執行cb
外,還執行了get
方法。這就是關鍵!性能
在上一篇中咱們提到get
方法其實調用的就是getter
,在傳入的第二個參數expOrFn
類型爲function
時,getter = expOrFn
。那還記得傳了什麼進去嗎?updateComponent
!
咱們理一下思路,並暫時移除掉無關代碼:
// function mountComponent
new Watcher(vm, updateComponent, noop)
// class Watcher
class Watcher {
constructor(vm, fn, cb) {
this.vm = vm
this.getter = fn
this.value = this.get()
}
get() {
this.getter.call(this.vm, this.vm)
}
update() {
this.run()
}
run() {
const value = this.get()
// ...
this.cb.call(this.vm, value, this.value)
}
}
複製代碼
這裏有幾個要點。第一,在初始化Watcher
的時候就調用過一次get
;第二,在數據更改觸發更新時又會調用get
。再根據實際執行的函數名updateComponent
,我想你也猜到了,這個函數就是用來渲染DOM的,而且在每次觀測到數據變動時都會從新渲染DOM。
再來看這張圖,至此,Watcher
到Render
的路徑咱們也清晰了。
咱們猜想該函數是用來更新DOM的,但咱們仍是得實際看一下它是如何實現的,由於這裏面其實涉及到了更多技術,十分值得學習。
那咱們仍是一步一步的來,看完相關代碼,能夠總結出:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
複製代碼
它最主要就是調用了兩個函數,_render
和_update
。
咱們先來看看_render
,它是經過renderMixin
加在原型上的,因此相關定義會在不一樣的地方。
Vue.prototype._render = function (): VNode {}
複製代碼
咱們先看下這個函數聲明,其返回值是一個VNode
類型,若是你有仔細讀過官方文檔,你就會對這個詞有點印象。
在建立一個Vue組件的時候咱們能夠不使用template
選項來寫DOM模板,而使用render
選項。而render
函數返回值的類型就是VNode
。很顯然,從函數名上來看,內部的_render
是對傳入render
的二次包裝。
看一看源碼歸納:
Vue.prototype._render = function (): VNode {
const { render, _parentVnode } = vm.$options
// ...
vnode = render.call(vm._renderProxy, vm.$createElement)
// ...
return vnode
}
複製代碼
該函數調用了render
並返回了VNode
。 這裏發什麼什麼?僅僅是調用render
函數這麼簡單嗎? 咱們來看看好比下面這個render
函數:
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
複製代碼
他用到了this.blogTitle
,很明顯這裏是訪問屬性,也就是會調用到該屬性的get
方法,上一篇咱們再講Observer
時講過,屬性的get
裏面會進行依賴收集。此時,blogTitle
有了新的訂閱者subs.push(Watcher)
,而該Watcher
的依賴deps
也增長了blogTitle
,在blogTitle
更新時,就會調用該Watcher
的update
方法。
因此上面那張圖中的render
到data
這條線也清晰了吧,這也就是官方文檔上說的接觸
(touched
)!
OK,其實到這裏,整個數據劫持,依賴收集過程都已經很明瞭了,咱們已經能夠實現一個簡單而且優雅的數據單向綁定了。接下來就是Vue怎麼優化DOM渲染,提高性能的操做了。
咱們如今知道_render
是建立虛擬DOM的,那麼建立完虛擬DOM以後幹嗎?固然是渲染成真實DOM啊!這也就是_update
的做用,那爲何它叫作update
而不是create
或者transform
呢?這也是有知識點在裏面的。
// Vue.prototype._update
const prevVnode = vm._vnode
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
複製代碼
能看見清晰的註釋,initial render
/updates
,也就是該方法處理了新建和更新兩種操做。新建的時候會將VNode
掛載在vm
上表示已經建立過了,以後只須要更新就好了,減小消耗。
而這裏又用到了另外一個方法__patch__
:
// runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
// runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })
// vdom/patch.js
export function createPatchFunction (backend) {
return function patch (oldVnode, vnode, hydrating, removeOnly) {
//...
}
}
複製代碼
介於這裏內容比較複雜,暫時就不講了,咱們留着下一篇再見。