前言
v-bind指令想必大家都不陌生,並且都知道他支援各種寫法,比如<div v-bind:title="title">
、<div :title="title">
、<div :title>
(vue3.4中引入的新的寫法)。這三種寫法的作用都是一樣的,將title
變數繫結到div標籤的title屬性上。本文將透過debug原始碼的方式帶你搞清楚,v-bind指令是如何實現這麼多種方式將title
變數繫結到div標籤的title屬性上的。注:本文中使用的vue版本為3.4.19
。
關注公眾號:【前端歐陽】,給自己一個進階vue的機會
看個demo
還是老套路,我們來寫個demo。程式碼如下:
<template>
<div v-bind:title="title">Hello Word</div>
<div :title="title">Hello Word</div>
<div :title>Hello Word</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const title = ref("Hello Word");
</script>
上面的程式碼很簡單,使用三種寫法將title變數繫結到div標籤的title屬性上。
我們從瀏覽器中來看看編譯後的程式碼,如下:
const _sfc_main = _defineComponent({
__name: "index",
setup(__props, { expose: __expose }) {
// ...省略
}
});
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock(
_Fragment,
null,
[
_createElementVNode("div", { title: $setup.title }, "Hello Word", 8, _hoisted_1),
_createElementVNode("div", { title: $setup.title }, "Hello Word", 8, _hoisted_2),
_createElementVNode("div", { title: $setup.title }, "Hello Word", 8, _hoisted_3)
],
64
/* STABLE_FRAGMENT */
);
}
_sfc_main.render = _sfc_render;
export default _sfc_main;
從上面的render函式中可以看到三種寫法生成的props物件都是一樣的: { title: $setup.title }
。props屬性的key為title
,值為$setup.title
變數。
再來看看瀏覽器渲染後的樣子,如下圖:
從上圖中可以看到三個div標籤上面都有title屬性,並且屬性值都是一樣的。
transformElement
函式
在之前的 面試官:來說說vue3是怎麼處理內建的v-for、v-model等指令?文章中我們講過了在編譯階段會執行一堆transform轉換函式,用於處理vue內建的v-for等指令。而v-bind指令就是在這一堆transform轉換函式中的transformElement
函式中處理的。
還是一樣的套路啟動一個debug終端。這裡以vscode
舉例,開啟終端然後點選終端中的+
號旁邊的下拉箭頭,在下拉中點選Javascript Debug Terminal
就可以啟動一個debug
終端。
給transformElement
函式打個斷點,transformElement
函式的程式碼位置在:node_modules/@vue/compiler-core/dist/compiler-core.cjs.js
。
在debug
終端上面執行yarn dev
後在瀏覽器中開啟對應的頁面,比如:http://localhost:5173/ 。此時斷點就會走到transformElement
函式中,在我們這個場景中簡化後的transformElement
函式程式碼如下:
const transformElement = (node, context) => {
return function postTransformElement() {
let vnodeProps;
const propsBuildResult = buildProps(
node,
context,
undefined,
isComponent,
isDynamicComponent
);
vnodeProps = propsBuildResult.props;
node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren
// ...省略
);
};
};
我們先來看看第一個引數node
,如下圖:
從上圖中可以看到此時的node節點對應的就是<div v-bind:title="title">Hello Word</div>
節點,其中的props陣列中只有一項,對應的就是div標籤中的v-bind:title="title"
部分。
我們接著來看transformElement
函式中的程式碼,可以分為兩部分。
第一部分為呼叫buildProps
函式拿到當前node節點的props屬性賦值給vnodeProps
變數。
第二部分為根據當前node節點vnodeTag
也就是節點的標籤比如div、vnodeProps
也就是節點的props屬性物件、vnodeChildren
也就是節點的children子節點、還有一些其他資訊生成codegenNode
屬性。在之前的 終於搞懂了!原來 Vue 3 的 generate 是這樣生成 render 函式的文章中我們已經講過了編譯階段最終生成render函式就是讀取每個node節點的codegenNode
屬性然後進行字串拼接。
從buildProps
函式的名字我們不難猜出他的作用就是生成node節點的props屬性物件,所以我們接下來需要將目光聚焦到buildProps
函式中,看看是如何生成props物件的。
buildProps
函式
將斷點走進buildProps
函式,在我們這個場景中簡化後的程式碼如下:
function buildProps(node, context, props = node.props) {
let propsExpression;
let properties = [];
for (let i = 0; i < props.length; i++) {
const prop = props[i];
const { name } = prop;
const directiveTransform = context.directiveTransforms[name];
if (directiveTransform) {
const { props } = directiveTransform(prop, node, context);
properties.push(...props);
}
}
propsExpression = createObjectExpression(
dedupeProperties(properties),
elementLoc
);
return {
props: propsExpression,
// ...省略
};
}
由於我們在呼叫buildProps
函式時傳的第三個引數為undefined,所以這裡的props就是預設值node.props
。如下圖:
從上圖中可以看到props陣列中只有一項,props中的name欄位為bind
,說明v-bind指令還未被處理掉。
並且由於我們當前node節點是第一個div標籤:<div v-bind:title="title">
,所以props中的rawName
的值是v-bind:title
。
我們接著來看上面for迴圈遍歷props的程式碼:const directiveTransform = context.directiveTransforms[name]
,現在我們已經知道了這裡的name為bind
。那麼這裡的context.directiveTransforms
物件又是什麼東西呢?我們在debug終端來看看context.directiveTransforms
,如下圖:
從上圖中可以看到context.directiveTransforms
物件中包含許多指令的轉換函式,比如v-bind
、v-cloak
、v-html
、v-model
等。
我們這裡name的值為bind
,並且context.directiveTransforms
物件中有name為bind
的轉換函式。所以const directiveTransform = context.directiveTransforms[name]
就是拿到處理v-bind指令的轉換函式,然後賦值給本地的directiveTransform
函式。
接著就是執行directiveTransform
轉換函式,拿到v-bind指令生成的props陣列。然後執行properties.push(...props)
方法將所有的props陣列都收集到properties
陣列中。
由於node節點中有多個props,在for迴圈遍歷props陣列時,會將經過transform轉換函式處理後拿到的props陣列全部push到properties
陣列中。properties
陣列中可能會有重複的prop,所以需要執行dedupeProperties(properties)
函式對props屬性進行去重。
node節點上的props屬性本身也是一種node節點,所以最後就是執行createObjectExpression
函式生成props屬性的node節點,程式碼如下:
propsExpression = createObjectExpression(
dedupeProperties(properties),
elementLoc
)
其中createObjectExpression
函式的程式碼也很簡單,程式碼如下:
function createObjectExpression(properties, loc) {
return {
type: NodeTypes.JS_OBJECT_EXPRESSION,
loc,
properties,
};
}
上面的程式碼很簡單,properties
陣列就是node節點上的props陣列,根據properties
陣列生成props屬性對應的node節點。
我們在debug終端來看看最終生成的props物件propsExpression
是什麼樣的,如下圖:
從上圖中可以看到此時properties
屬性陣列中已經沒有了v-bind指令了,取而代之的是key
和value
屬性。key.content
的值為title
,說明屬性名為title
。value.content
的值為$setup.title
,說明屬性值為變數$setup.title
。
到這裡v-bind指令已經被完全解析了,生成的props物件中有key
和value
欄位,分別代表的是屬性名和屬性值。後續生成render函式時只需要遍歷所有的props,根據key
和value
欄位進行字串拼接就可以給div標籤生成title屬性了。
接下來我們繼續來看看處理v-bind
指令的transform轉換函式具體是如何處理的。
transformBind
函式
將斷點走進transformBind
函式,在我們這個場景中簡化後的程式碼如下:
const transformBind = (dir, _node) => {
const arg = dir.arg;
let { exp } = dir;
if (!exp) {
const propName = camelize(arg.content);
exp = dir.exp = createSimpleExpression(propName, false, arg.loc);
exp = dir.exp = processExpression(exp, context);
}
return {
props: [createObjectProperty(arg, exp)],
};
};
我們先來看看transformBind
函式接收的第一個引數dir
,從這個名字我想你應該已經猜到了他裡面儲存的是指令相關的資訊。
在debug終端來看看三種寫法的dir
引數有什麼不同。
第一種寫法:<div v-bind:title="title">
的dir
如下圖:
從上圖中可以看到dir.name
的值為bind
,說明這個是v-bind
指令。dir.rawName
的值為v-bind:title
說明沒有使用縮寫模式。dir.arg
表示bind繫結的屬性名稱,這裡繫結的是title屬性。dir.exp
表示bind繫結的屬性值,這裡繫結的是$setup.title
變數。
第二種寫法:<div :title="title">
的dir
如下圖:
從上圖中可以看到第二種寫法的dir
和第一種寫法的dir
只有一項不一樣,那就是dir.rawName
。在第二種寫法中dir.rawName
的值為:title
,說明我們這裡是採用了縮寫模式。
可能有的小夥伴有疑問了,這裡的dir
是怎麼來的?vue是怎麼區分第一種全寫模式和第二種縮寫模式呢?
答案是在parse階段將html編譯成AST抽象語法樹階段時遇到v-bind:title
和:title
時都會將其當做v-bind指令處理,並且將解析處理的指令繫結的屬性名塞到dir.arg
中,將屬性值塞到dir.exp
中。
第三種寫法:<div :title>
的dir
如下圖:
第三種寫法也是縮寫模式,並且將屬性值也一起給省略了。所以這裡的dir.exp
儲存的屬性值為undefined。其他的和第二種縮寫模式基本一樣。
我們再來看transformBind
中的程式碼,if (!exp)
說明將值也一起省略了,是第三種寫法。就會執行如下程式碼:
if (!exp) {
const propName = camelize(arg.content);
exp = dir.exp = createSimpleExpression(propName, false, arg.loc);
exp = dir.exp = processExpression(exp, context);
}
這裡的arg.content
就是屬性名title
,執行camelize
函式將其從kebab-case命名法轉換為駝峰命名法。比如我們給div上面綁一個自定義屬性data-type
,採用第三種縮寫模式就是這樣的:<div :data-type>
。大家都知道變數名稱是不能帶短橫線的,所以這裡的要執行camelize
函式將其轉換為駝峰命名法:改為繫結dataType
變數。
從前面的那幾張dir變數的圖我們知道 dir.exp
變數的值是一個物件,所以這裡需要執行createSimpleExpression
函式將省略的變數值也補全。createSimpleExpression
的函式程式碼如下:
function createSimpleExpression(
content,
isStatic,
loc,
constType
): SimpleExpressionNode {
return {
type: NodeTypes.SIMPLE_EXPRESSION,
loc,
content,
isStatic,
constType: isStatic ? ConstantTypes.CAN_STRINGIFY : constType,
};
}
經過這一步處理後 dir.exp
變數的值如下圖:
還記得前面兩種模式的 dir.exp.content
的值嗎?他的值是$setup.title
,表示屬性值為setup
中定義的title
變數。而我們這裡的dir.exp.content
的值為title
變數,很明顯是不對的。
所以需要執行exp = dir.exp = processExpression(exp, context)
將dir.exp.content
中的值替換為$setup.title
,執行processExpression
函式後的dir.exp
變數的值如下圖:
我們來看transformBind
函式中的最後一塊return的程式碼:
return {
props: [createObjectProperty(arg, exp)],
}
這裡的arg
就是v-bind繫結的屬性名,exp
就是v-bind繫結的屬性值。createObjectProperty
函式程式碼如下:
function createObjectProperty(key, value) {
return {
type: NodeTypes.JS_PROPERTY,
loc: locStub,
key: isString(key) ? createSimpleExpression(key, true) : key,
value,
};
}
經過createObjectProperty
函式的處理就會生成包含key
、value
屬性的物件。key
中存的是繫結的屬性名,value
中存的是繫結的屬性值。
其實transformBind
函式中做的事情很簡單,解析出v-bind指令繫結的屬性名稱和屬性值。如果發現v-bind指令沒有繫結值,那麼就說明當前v-bind將值也給省略掉了,繫結的屬性和屬性值同名才能這樣寫。然後根據屬性名和屬性值生成一個包含key
、value
鍵的props物件。後續生成render函式時只需要遍歷所有的props,根據key
和value
欄位進行字串拼接就可以給div標籤生成title屬性了。
總結
在transform階段處理vue內建的v-for、v-model等指令時會去執行一堆transform轉換函式,其中有個transformElement
轉換函式中會去執行buildProps
函式。
buildProps
函式會去遍歷當前node節點的所有props陣列,此時的props中還是存的是v-bind指令,每個prop中存的是v-bind指令繫結的屬性名和屬性值。
在for迴圈遍歷node節點的所有props時,每次都會執行transformBind
轉換函式。如果我們在寫v-bind時將值也給省略了,此時v-bind指令繫結的屬性值就是undefined。這時就需要將省略的屬性值補回來,補回來的屬性值的變數名稱和屬性名是一樣的。
在transformBind
轉換函式的最後會根據屬性名和屬性值生成一個包含key
、value
鍵的props物件。key
對應的就是屬性名,value
對應的就是屬性值。後續生成render函式時只需要遍歷所有的props,根據key
和value
欄位進行字串拼接就可以給div標籤生成title屬性了。
關注公眾號:【前端歐陽】,給自己一個進階vue的機會