Vue3 中的 v-bind 指令:你不知道的那些工作原理

来源:https://www.cnblogs.com/heavenYJJ/p/18263874
-Advertisement-
Play Games

前言 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

從上圖中可以看到三個div標簽上面都有title屬性,並且屬性值都是一樣的。

transformElement函數

在之前的 面試官:來說說vue3是怎麼處理內置的v-for、v-model等指令?文章中我們講過了在編譯階段會執行一堆transform轉換函數,用於處理vue內置的v-for等指令。而v-bind指令就是在這一堆transform轉換函數中的transformElement函數中處理的。

還是一樣的套路啟動一個debug終端。這裡以vscode舉例,打開終端然後點擊終端中的+號旁邊的下拉箭頭,在下拉中點擊Javascript Debug Terminal就可以啟動一個debug終端。
debug-terminal

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

從上圖中可以看到此時的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數組中只有一項,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,如下圖:
directiveTransforms

從上圖中可以看到context.directiveTransforms對象中包含許多指令的轉換函數,比如v-bindv-cloakv-htmlv-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是什麼樣的,如下圖:
propsExpression

從上圖中可以看到此時properties屬性數組中已經沒有了v-bind指令了,取而代之的是keyvalue屬性。key.content的值為title,說明屬性名為titlevalue.content的值為$setup.title,說明屬性值為變數$setup.title

到這裡v-bind指令已經被完全解析了,生成的props對象中有keyvalue欄位,分別代表的是屬性名和屬性值。後續生成render函數時只需要遍歷所有的props,根據keyvalue欄位進行字元串拼接就可以給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如下圖:
dir1

從上圖中可以看到dir.name的值為bind,說明這個是v-bind指令。dir.rawName的值為v-bind:title說明沒有使用縮寫模式。dir.arg表示bind綁定的屬性名稱,這裡綁定的是title屬性。dir.exp表示bind綁定的屬性值,這裡綁定的是$setup.title變數。

第二種寫法:<div :title="title">dir如下圖:
dir2

從上圖中可以看到第二種寫法的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如下圖:
dir3

第三種寫法也是縮寫模式,並且將屬性值也一起給省略了。所以這裡的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變數的值如下圖:
exp1

還記得前面兩種模式的 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變數的值如下圖:
exp2

我們來看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函數的處理就會生成包含keyvalue屬性的對象。key中存的是綁定的屬性名,value中存的是綁定的屬性值。

其實transformBind函數中做的事情很簡單,解析出v-bind指令綁定的屬性名稱和屬性值。如果發現v-bind指令沒有綁定值,那麼就說明當前v-bind將值也給省略掉了,綁定的屬性和屬性值同名才能這樣寫。然後根據屬性名和屬性值生成一個包含keyvalue鍵的props對象。後續生成render函數時只需要遍歷所有的props,根據keyvalue欄位進行字元串拼接就可以給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轉換函數的最後會根據屬性名和屬性值生成一個包含keyvalue鍵的props對象。key對應的就是屬性名,value對應的就是屬性值。後續生成render函數時只需要遍歷所有的props,根據keyvalue欄位進行字元串拼接就可以給div標簽生成title屬性了。

關註公眾號:【前端歐陽】,給自己一個進階vue的機會


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前提: Xcode 16.0 beta 設置 Scheme設置中勾選Malloc Scribble、Malloc Stack Logging。 這麼做是為了在Memory Graph、Profile中追溯數據在哪句代碼生成。 此設置會導致App硬碟占用異常增多,調試完畢之後需要把選項關閉。 Allo ...
  • Kotlin中變數類型由值決定,如Int、Double、Char、Boolean、String。通常可省略類型聲明,但有時需指定。數字類型分整數(Byte, Short, Int, Long)和浮點(Float, Double),預設整數為Int,浮點為Double。布爾值是true或false,C... ...
  • ASeeker 是一個 Android 源碼應用系統服務介面掃描工具。是我們在做虛擬化分身產品『 空殼 』過程中的內部開發工具,目的是為了提升 Android 系統各版本適配效率。 ...
  • 11.2警告郵件內容 Hello XXX, We're writing to inform you that your company isn't in compliance with the Apple Developer Program License Agreement (DPLA). Sec ...
  • 使用Flutter自帶的SearchDelegate組件實現搜索界面,通過魔改實現如下效果:搜素建議、搜索結果,支持刷新和載入更多,解決IOS中文輸入拼音問題。 ...
  • 場景:多個tab切換,顯示不同的Fragment,其中一個Fragment佈局是兩個RecyclerView,分別位於左右兩側 需求:首次從tabView切換到改tab頁時,焦點從tabView首次往下移動時,需要落焦在右側的第一個item上面 如果按照系統原生邏輯,從tabView下移,可能預設位 ...
  • ‍ 寫在開頭 點贊 + 收藏 學會 如題,慣性思路很簡單,就是直接擼上一個空內容的html。 註:以下都是在現代瀏覽器中執行,主要為**Chrome 版本 120.0.6099.217(正式版本) (64 位)和Firefox123.0.1 (64 位) ** <!DOCTYPE ...
  • 本文概述了Nuxt 3框架的升級特點,對比Nuxt 2,詳細解析中間件應用、配置策略與實戰示例,涵蓋功能、錯誤管理、優化技巧,並探討與Nuxt 3核心組件集成方法,給出最佳實踐和問題解決方案,強調利用Vue 3和Serverless Functions提升中間件效能。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...