Vue指令實現原理

前言

自定義指令是

vue

中使用頻率僅次於元件,其包含

bind

inserted

update

componentUpdated

unbind

五個生命週期鉤子。本文將對

vue

指令的工作原理進行相應介紹,從本文中,你將得到:

指令的工作原理

指令使用的注意事項

基本使用

官網案例:

指令工作原理

初始化

初始化全域性

API

時,在

platforms/web

下,呼叫

createPatchFunction

生成

VNode

轉換為真實

DOM

patch

方法,初始化中比較重要一步是定義了與

DOM

節點相對應的

hooks

方法,在

DOM

的建立(

create

)、啟用(

avtivate

)、更新(

update

)、移除(

remove

)、銷燬(

destroy

)過程中,分別會輪詢呼叫對應的

hooks

方法,這些

hooks

中一部分是指令宣告週期的入口。

// src/core/vdom/patch。jsconst hooks = [‘create’, ‘activate’, ‘update’, ‘remove’, ‘destroy’]export function createPatchFunction (backend) { let i, j const cbs = {} const { modules, nodeOps } = backend for (i = 0; i < hooks。length; ++i) { cbs[hooks[i]] = [] // modules對應vue中模組,具體有class, style, domListener, domProps, attrs, directive, ref, transition for (j = 0; j < modules。length; ++j) { if (isDef(modules[j][hooks[i]])) { // 最終將hooks轉換為{hookEvent: [cb1, cb2 。。。], 。。。}形式 cbs[hooks[i]]。push(modules[j][hooks[i]]) } } } // 。。。。 return function patch (oldVnode, vnode, hydrating, removeOnly) { // 。。。 }}

模板編譯

模板編譯就是解析指令引數,具體解構後的

ASTElement

如下所示:

{ tag: ‘input’, parent: ASTElement, directives: [ { arg: null, // 引數 end: 56, // 指令結束字元位置 isDynamicArg: false, // 動態引數,v-xxx[dynamicParams]=‘xxx’形式呼叫 modifiers: undefined, // 指令修飾符 name: “model”, rawName: “v-model”, // 指令名稱 start: 36, // 指令開始字元位置 value: “inputValue” // 模板 }, { arg: null, end: 67, isDynamicArg: false, modifiers: undefined, name: “focus”, rawName: “v-focus”, start: 57, value: “” } ], // 。。。}

生成渲染方法

vue

推薦採用指令的方式去操作

DOM

,由於自定義指令可能會修改

DOM

或者屬性,所以避免指令對模板解析的影響,在生成渲染方法時,首先處理的是指令,如

v-model

,本質是一個語法糖,在拼接渲染函式時,會給元素加上

value

屬性與

input

事件(以

input

為例,這個也可以使用者自定義)。

with (this) { return _c(‘div’, { attrs: { “id”: “app” } }, [_c(‘input’, { directives: [{ name: “model”, rawName: “v-model”, value: (inputValue), expression: “inputValue” }, { name: “focus”, rawName: “v-focus” }], attrs: { “type”: “text” }, domProps: { “value”: (inputValue) // 處理v-model指令時新增的屬性 }, on: { “input”: function($event) { // 處理v-model指令時新增的自定義事件 if ($event。target。composing) return; inputValue = $event。target。value } } })])}

生成VNode

vue

的指令設計是方便我們操作

DOM

,在生成

VNode

時,指令並沒有做額外處理。

生成真實DOM

vue

初始化過程中,我們需要記住兩點:

狀態的初始化是 父 -> 子,如

beforeCreate

created

beforeMount

,呼叫順序是 父 -> 子

真實

DOM

掛載順序是 子 -> 父,如

mounted

,這是因為在生成真實

DOM

過程中,如果遇到元件,會走元件建立的過程,真實

DOM

的生成是從子到父一級級拼接。

patch

過程中,每次呼叫

createElm

生成真實

DOM

時,都會檢測當前

VNode

是否存在

data

屬性,存在,則會呼叫

invokeCreateHooks

,走初建立的鉤子函式,核心程式碼如下:

// src/core/vdom/patch。jsfunction createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { // 。。。 // createComponent有返回值,是建立元件的方法,沒有返回值,則繼續走下面的方法 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } const data = vnode。data // 。。。。 if (isDef(data)) { // 真實節點建立之後,更新節點屬性,包括指令 // 指令首次會呼叫bind方法,然後會初始化指令後續hooks方法 invokeCreateHooks(vnode, insertedVnodeQueue) } // 從底向上,依次插入 insert(parentElm, vnode。elm, refElm) // 。。。 }

以上是指令鉤子方法的第一個入口,是時候揭露

directive。js

神秘的面紗了,核心程式碼如下:

// src/core/vdom/modules/directives。js// 預設丟擲的都是updateDirectives方法export default { create: updateDirectives, update: updateDirectives, destroy: function unbindDirectives (vnode: VNodeWithData) { // 銷燬時,vnode === emptyNode updateDirectives(vnode, emptyNode) }}function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) { if (oldVnode。data。directives || vnode。data。directives) { _update(oldVnode, vnode) }}function _update (oldVnode, vnode) { const isCreate = oldVnode === emptyNode const isDestroy = vnode === emptyNode const oldDirs = normalizeDirectives(oldVnode。data。directives, oldVnode。context) const newDirs = normalizeDirectives(vnode。data。directives, vnode。context) // 插入後的回撥 const dirsWithInsert = [ // 更新完成後回撥 const dirsWithPostpatch = [] let key, oldDir, dir for (key in newDirs) { oldDir = oldDirs[key] dir = newDirs[key] // 新元素指令,會執行一次inserted鉤子方法 if (!oldDir) { // new directive, bind callHook(dir, ‘bind’, vnode, oldVnode) if (dir。def && dir。def。inserted) { dirsWithInsert。push(dir) } } else { // existing directive, update // 已經存在元素,會執行一次componentUpdated鉤子方法 dir。oldValue = oldDir。value dir。oldArg = oldDir。arg callHook(dir, ‘update’, vnode, oldVnode) if (dir。def && dir。def。componentUpdated) { dirsWithPostpatch。push(dir) } } } if (dirsWithInsert。length) { // 真實DOM插入到頁面中,會呼叫此回撥方法 const callInsert = () => { for (let i = 0; i < dirsWithInsert。length; i++) { callHook(dirsWithInsert[i], ‘inserted’, vnode, oldVnode) } } // VNode合併insert hooks if (isCreate) { mergeVNodeHook(vnode, ‘insert’, callInsert) } else { callInsert() } } if (dirsWithPostpatch。length) { mergeVNodeHook(vnode, ‘postpatch’, () => { for (let i = 0; i < dirsWithPostpatch。length; i++) { callHook(dirsWithPostpatch[i], ‘componentUpdated’, vnode, oldVnode) } }) } if (!isCreate) { for (key in oldDirs) { if (!newDirs[key]) { // no longer present, unbind callHook(oldDirs[key], ‘unbind’, oldVnode, oldVnode, isDestroy) } } }}

對於首次建立,執行過程如下:

oldVnode === emptyNode

isCreate

true

,呼叫當前元素中所有

bind

鉤子方法。

檢測指令中是否存在

inserted

鉤子,如果存在,則將

insert

鉤子合併到

VNode。data。hooks

屬性中。

DOM

掛載結束後,會執行

invokeInsertHook

,所有已掛載節點,如果

VNode。data。hooks

中存在

insert

鉤子。則會呼叫,此時會觸發指令繫結的

inserted

方法。

一般首次建立只會走

bind

inserted

方法,而

update

componentUpdated

則與

bind

inserted

對應。在元件依賴狀態發生改變時,會用

VNode diff

演算法,對節點進行打補丁式更新,其呼叫流程:

響應式資料發生改變,呼叫

dep。notify

,通知資料更新。

呼叫

patchVNode

,對新舊

VNode

進行差異化更新,並全量更新當前

VNode

屬性(包括指令,就會進入

updateDirectives

方法)。

如果指令存在

update

鉤子方法,呼叫

update

鉤子方法,並初始化

componentUpdated

回撥,將

postpatch hooks

掛載到

VNode。data。hooks

中。

當前節點及子節點更新完畢後,會觸發

postpatch hooks

,即指令的

componentUpdated

方法

核心程式碼如下:

// src/core/vdom/patch。jsfunction patchVnode ( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { // 。。。 const oldCh = oldVnode。children const ch = vnode。children // 全量更新節點的屬性 if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs。update。length; ++i) cbs。update[i](oldVnode, vnode) if (isDef(i = data。hook) && isDef(i = i。update)) i(oldVnode, vnode) } // 。。。 if (isDef(data)) { // 呼叫postpatch鉤子 if (isDef(i = data。hook) && isDef(i = i。postpatch)) i(oldVnode, vnode) } }

unbind

方法是在節點銷燬時,呼叫

invokeDestroyHook

,這裡不做過多描述。

注意事項

使用自定義指令時,和普通模板資料繫結,

v-model

還是存在一定的差別,如雖然我傳遞引數(

v-xxx=‘param’

)是一個引用型別,資料變化時,並不能觸發指令的

bind

或者

inserted

,這是因為在指令的宣告週期內,

bind

inserted

只是在初始化時呼叫一次,後面只會走

update

componentUpdated

指令的宣告週期執行順序為

bind -> inserted -> update -> componentUpdated

,如果指令需要依賴於子元件的內容時,推薦在

componentUpdated

中寫相應業務邏輯。

vue

中,很多方法都是迴圈呼叫,如

hooks

方法,事件回撥等,一般呼叫都用

try catch

包裹,這樣做的目的是為了防止一個處理方法報錯,導致整個程式崩潰,這一點在我們開發過程中可以借鑑使用。

小結

開始看整個

vue

原始碼時,對很多細枝末節方法都不怎麼了解,透過梳理具體每個功能的實現時,漸漸能夠看到整個

vue

全貌,同時也能避免開發使用中的一些坑點。