vue的原始碼學習之六——3.patch
1. 介紹
版本:2.5.17。
我們使用vue-vli建立基於Runtime+Compiler的vue腳手架。
學習文件:https://ustbhuangyi.github.io/vue-analysis/components/patch.html
2.patch
通過上一節的分析我們知道,當我們通過 createComponent
vm._update
,執行 vm.__patch__
去把 VNode 轉換成真正的 DOM 節點。這個過程我們在前一章已經分析過了,但是針對一個普通的 VNode 節點,接下來我們來看看元件的 VNode 會有哪些不一樣的地方。
patch 的過程會呼叫 createElm
建立元素節點,回顧一下 createElm
的實現,它的定義在 src/core/vdom/patch.js
中:
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { // ... if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } // ... }
3.createComponent
我們刪掉多餘的程式碼,只保留關鍵的邏輯,上面的程式碼會判斷 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值,如果為 true 則直接結束。
createComponent其實呼叫的就是給逐漸VNode新增的init方法
那麼接下來看一下 createComponent 方法的實現:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { const isReactivated = isDef(vnode.componentInstance) && i.keepAlive if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */) } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } }
3.1 對vnode.data 做了一些判斷
let i = vnode.data
if (isDef(i)) {
// ...
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
// ...
}
// ..
}
- vnode 是一個元件 VNode,那麼條件會滿足,並且得到 i 就是 init 鉤子函式
- 判斷vnode.data中是否有hook,並且有init方法(因為上一節講了會給元件VNode merage一些鉤子,其中就有init,所以這裡是true),就會呼叫init方法
3.2 init方法
init方法定義在 src/core/vdom/create-component.js 中:
其實就是和元件的data.hook鉤子合併的 componentVNodeHooks 鉤子物件的init方法
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
init
鉤子函式執行也很簡單,我們先不考慮 keepAlive
的情況,它是通過 createComponentInstanceForVnode
建立一個 Vue 的例項,然後呼叫 $mount
方法掛載子元件
3.3 createComponentInstanceForVnode
建立一個 vnode 的例項
在src/core/vdom/create-component.js中
export function createComponentInstanceForVnode (
vnode: any, // 元件VNode
parent: any, // 當前vue例項vm
): Component {
// 定義引數
const options: InternalComponentOptions = {
_isComponent: true,
// 父VNode,是一個佔位節點,Vue例項A呼叫B元件,B呼叫C元件,_parentVnode就是C元件佔位符
_parentVnode: vnode,
//表示當前啟用的子元件的父級例項,例如 app =new Vue,並呼叫子元件,parent就是app
parent
}
...
// 由上一節我們知道元件會生成子構造器,vnode.componentOptions.Ctor 對應的就是子元件的建構函式
return new vnode.componentOptions.Ctor(options)
}
createComponentInstanceForVnode 函式構造的一個內部元件的引數,然後執行 new
vnode.componentOptions.Ctor(options)。
由上一節我們知道元件會生成子構造器,vnode.componentOptions.Ctor 對應的就是子元件的建構函式,
我們上一節分析了子構造器實際上是繼承於 Vue 的一個構造器 Sub,相當於 new Sub(options)
這裡有幾個關鍵引數要注意幾個點,_isComponent 為 true 表示它是一個元件,parent 表示當前啟用的元件例項。
3.4 Sub定義
sub定義在 src/core/global-api/exten.js
const Sub = function VueComponent (options) {
this._init(options)cd
}
所以子元件的例項化實際上就是在這個時機執行的,並且它會執行例項的 _init 方法
3.5 普通VNode 節點和元件的 VNode 初始化的不同。
他們都是在src/core/instance/init.js中初始化的,只是有一些不同,我們主要來看不同的
程式碼在 src/core/instance/init.js 中:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
3.5.1. 合併 options 的過程有變化
合併 options 的過程有變化,_isComponent 為 true,所以走到了 initInternalComponent 過程,,而上一章普通的 VNode 節點則會走else。這個函式的實現也在當前頁面 src/core/instance/init.js :
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
這個過程我們重點記住以下幾個點即可:opts.parent = options.parent
、opts._parentVnode = parentVnode
,它們是把之前我們通過 createComponentInstanceForVnode
函式傳入的幾個引數合併到內部的選項 $options
裡了。
_init
函式最後執行的程式碼
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
$mount
相當於執行 child.$mount(undefined, false)
,它最終會呼叫 mountComponent
方法,進而執行 vm._render()
方法