前言 v-bind指令想必大家都不陌生,並且都知道他支持各種寫法,比如<div v-bind:title="title">、<div :title="title">、<div :title>(vue3.4中引入的新的寫法)。這三種寫法的作用都是一樣的,將title變數綁定到div標簽的title屬性 ...
前言
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的機會