終於搞懂了!原來 Vue 3 的 generate 是這樣生成 render 函數的

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

前言 在之前的 面試官:來說說vue3是怎麼處理內置的v-for、v-model等指令? 文章中講了transform階段處理完v-for、v-model等指令後,會生成一棵javascript AST抽象語法樹。這篇文章我們來接著講generate階段是如何根據這棵javascript AST抽象 ...


前言

在之前的 面試官:來說說vue3是怎麼處理內置的v-for、v-model等指令? 文章中講了transform階段處理完v-for、v-model等指令後,會生成一棵javascript AST抽象語法樹。這篇文章我們來接著講generate階段是如何根據這棵javascript AST抽象語法樹生成render函數字元串的,本文中使用的vue版本為3.4.19

看個demo

還是一樣的套路,我們通過debug一個demo來搞清楚render函數字元串是如何生成的。demo代碼如下:

<template>
  <p>{{ msg }}</p>
</template>

<script setup lang="ts">
import { ref } from "vue";

const msg = ref("hello world");
</script>

上面這個demo很簡單,使用p標簽渲染一個msg響應式變數,變數的值為"hello world"。我們在瀏覽器中來看看這個demo生成的render函數是什麼樣的,代碼如下:

import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/deps/vue.js?v=23bfe016";
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return _openBlock(), _createElementBlock(
    "p",
    null,
    _toDisplayString($setup.msg),
    1
    /* TEXT */
  );
}

上面的render函數中使用了兩個函數:openBlockcreateElementBlock。在之前的 vue3早已具備拋棄虛擬DOM的能力了文章中我們已經講過了這兩個函數:

  • openBlock的作用為初始化一個全局變數currentBlock數組,用於收集dom樹中的所有動態節點。

  • createElementBlock的作用為生成根節點p標簽的虛擬DOM,然後將收集到的動態節點數組currentBlock塞到根節點p標簽的dynamicChildren屬性上。

render函數的生成其實很簡單,經過transform階段處理後會生成一棵javascript AST抽象語法樹,這棵樹的結構和要生成的render函數結構是一模一樣的。所以在generate函數中只需要遞歸遍歷這棵樹,進行字元串拼接就可以生成render函數啦!

關註公眾號:【前端歐陽】,解鎖我更多vue原理文章。

加我微信heavenyjj0012回覆「666」,免費領取歐陽研究vue源碼過程中收集的源碼資料,歐陽寫文章有時也會參考這些資料。同時讓你的朋友圈多一位對vue有深入理解的人。

generate函數

首先給generate函數打個斷點,generate函數在node_modules/@vue/compiler-core/dist/compiler-core.cjs.js文件中。

然後啟動一個debug終端,在終端中執行yarn dev(這裡是以vite舉例)。在瀏覽器中訪問  http://localhost:5173/ ,此時斷點就會走到generate函數中了。在我們這個場景中簡化後的generate函數是下麵這樣的:

function generate(ast) {
  const context = createCodegenContext();
  const { push, indent, deindent } = context;

  const preambleContext = context;
  genModulePreamble(ast, preambleContext);

  const functionName = `render`;
  const args = ["_ctx", "_cache"];
  args.push("$props", "$setup", "$data", "$options");
  const signature = args.join(", ");
  push(`function ${functionName}(${signature}) {`);

  indent();
  push(`return `);
  genNode(ast.codegenNode, context);

  deindent();
  push(`}`);
  return {
    ast,
    code: context.code,
  };
}

generate中主要分為四部分:

  • 生成context上下文對象。

  • 執行genModulePreamble函數生成:import { xxx } from "vue";

  • 生成render函數中的函數名稱和參數,也就是function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {

  • 生成render函數中return的內容

context上下文對象

context上下文對象是執行createCodegenContext函數生成的,將斷點走進createCodegenContext函數。簡化後的代碼如下:

function createCodegenContext() {
  const context = {
    code: ``,
    indentLevel: 0,
    helper(key) {
      return `_${helperNameMap[key]}`;
    },
    push(code) {
      context.code += code;
    },
    indent() {
      newline(++context.indentLevel);
    },
    deindent(withoutNewLine = false) {
      if (withoutNewLine) {
        --context.indentLevel;
      } else {
        newline(--context.indentLevel);
      }
    },
    newline() {
      newline(context.indentLevel);
    },
  };

  function newline(n) {
    context.push("\n" + `  `.repeat(n));
  }

  return context;
}

為了代碼具有較強的可讀性,我們一般都會使用換行和鎖進。context上下文中的這些屬性和方法作用就是為了生成具有較強可讀性的render函數。

  • code屬性:當前生成的render函數字元串。

  • indentLevel屬性:當前的鎖進級別,每個級別對應兩個空格的鎖進。

  • helper方法:返回render函數中使用到的vue包中export導出的函數名稱,比如返回openBlockcreateElementBlock等函數

  • push方法:向當前的render函數字元串後插入字元串code。

  • indent方法:插入換行符,並且增加一個鎖進。

  • deindent方法:減少一個鎖進,或者插入一個換行符並且減少一個鎖進。

  • newline方法:插入換行符。

生成import {xxx} from "vue"

我們接著來看generate函數中的第二部分,生成import {xxx} from "vue"。將斷點走進genModulePreamble函數,在我們這個場景中簡化後的genModulePreamble函數代碼如下:

function genModulePreamble(ast, context) {
  const { push, newline, runtimeModuleName } = context;
  if (ast.helpers.size) {
    const helpers = Array.from(ast.helpers);
    push(
      `import { ${helpers
        .map((s) => `${helperNameMap[s]} as _${helperNameMap[s]}`)
        .join(", ")} } from ${JSON.stringify(runtimeModuleName)}
`,
      -1 /* End */
    );
  }
  genHoists(ast.hoists, context);
  newline();
  push(`export `);
}

其中的ast.helpers是在transform階段收集的需要從vue中import導入的函數,無需將vue中所有的函數都import導入。在debug終端看看helpers數組中的值如下圖:
helpers

從上圖中可以看到需要從vue中import導入toDisplayStringopenBlockcreateElementBlock這三個函數。

在執行push方法之前我們先來看看此時的render函數字元串是什麼樣的,如下圖:
before-import

從上圖中可以看到此時生成的render函數字元串還是一個空字元串,執行完push方法後,我們來看看此時的render函數字元串是什麼樣的,如下圖:
after-import

從上圖中可以看到此時的render函數中已經有了import {xxx} from "vue"了。

這裡執行的genHoists函數就是前面 搞懂 Vue 3 編譯優化:靜態提升的秘密文章中講過的靜態提升的入口。

生成render函數中的函數名稱和參數

執行完genModulePreamble函數後,已經生成了一條import {xxx} from "vue"了。我們接著來看generate函數中render函數的函數名稱和參數是如何生成的,代碼如下:

const functionName = `render`;
const args = ["_ctx", "_cache"];
args.push("$props", "$setup", "$data", "$options");
const signature = args.join(", ");
push(`function ${functionName}(${signature}) {`);

上面的代碼很簡單,都是執行push方法向render函數中添加code字元串,其中args數組就是render函數中的參數。我們在來看看執行完上面這塊代碼後的render函數字元串是什麼樣的,如下圖:
before-genNode

從上圖中可以看到此時已經生成了render函數中的函數名稱和參數了。

生成render函數中return的內容

接著來看generate函數中最後一塊代碼,如下:

indent();
push(`return `);
genNode(ast.codegenNode, context);

首先調用indent方法插入一個換行符並且增加一個鎖進,然後執行push方法添加一個return字元串。

接著以根節點的codegenNode屬性為參數執行genNode函數生成return中的內容,在我們這個場景中genNode函數簡化後的代碼如下:

function genNode(node, context) {
  switch (node.type) {
    case NodeTypes.SIMPLE_EXPRESSION:
      genExpression(node, context)
      break
    case NodeTypes.INTERPOLATION:
      genInterpolation(node, context);
      break;
    case NodeTypes.VNODE_CALL:
      genVNodeCall(node, context);
      break;
  }
}

這裡涉及到SIMPLE_EXPRESSIONINTERPOLATIONVNODE_CALL三種AST抽象語法樹node節點類型:

  • INTERPOLATION:表示當前節點是雙大括弧節點,我們這個demo中就是:{{msg}}這個文本節點。

  • SIMPLE_EXPRESSION:表示當前節點是簡單表達式節點,在我們這個demo中就是雙大括弧節點{{msg}}中的更裡層節點msg

  • VNODE_CALL:表示當前節點是虛擬節點,比如我們這裡第一次調用genNode函數傳入的ast.codegenNode(根節點的codegenNode屬性)就是虛擬節點。

genVNodeCall函數

由於當前節點是虛擬節點,第一次進入genNode函數時會執行genVNodeCall函數。在我們這個場景中簡化後的genVNodeCall函數代碼如下:

const OPEN_BLOCK = Symbol(`openBlock`);
const CREATE_ELEMENT_BLOCK = Symbol(`createElementBlock`);

function genVNodeCall(node, context) {
  const { push, helper } = context;
  const { tag, props, children, patchFlag, dynamicProps, isBlock } = node;
  if (isBlock) {
    push(`(${helper(OPEN_BLOCK)}(${``}), `);
  }
  const callHelper = CREATE_ELEMENT_BLOCK;
  push(helper(callHelper) + `(`, -2 /* None */, node);

  genNodeList(
    // 將參數中的undefined轉換成null
    genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
    context
  );

  push(`)`);
  if (isBlock) {
    push(`)`);
  }
}

首先判斷當前節點是不是block節點,由於此時的node為根節點,所以isBlock為true。將斷點走進helper方法,我們來看看helper(OPEN_BLOCK)返回值是什麼。helper方法的代碼如下:

const helperNameMap = {
  [OPEN_BLOCK]: `openBlock`,
  [CREATE_ELEMENT_BLOCK]: `createElementBlock`,
  [TO_DISPLAY_STRING]: `toDisplayString`,
  // ...省略
};

helper(key) {
  return `_${helperNameMap[key]}`;
}

helper方法中的代碼很簡單,這裡的helper(OPEN_BLOCK)返回的就是_openBlock

將斷點走到第一個push方法,代碼如下:

push(`(${helper(OPEN_BLOCK)}(${``}), `);

執行完這個push方法後在debug終端看看此時的render函數字元串是什麼樣的,如下圖:
after-block

從上圖中可以看到,此時render函數中增加了一個_openBlock函數的調用。

將斷點走到第二個push方法,代碼如下:

const callHelper = CREATE_ELEMENT_BLOCK;
push(helper(callHelper) + `(`, -2 /* None */, node);

同理helper(callHelper)方法返回的是_createElementBlock,執行完這個push方法後在debug終端看看此時的render函數字元串是什麼樣的,如下圖:
after-createElementBlock

從上圖中可以看到,此時render函數中增加了一個_createElementBlock函數的調用。

繼續將斷點走到genNodeList部分,代碼如下:

genNodeList(
  genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
  context
);

其中的genNullableArgs函數功能很簡單,將參數中的undefined轉換成null。比如此時的props就是undefined,經過genNullableArgs函數處理後傳給genNodeList函數的props就是null

genNodeList函數

繼續將斷點走進genNodeList函數,在我們這個場景中簡化後的代碼如下:

function genNodeList(nodes, context, multilines = false, comma = true) {
  const { push } = context;
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i];
    if (shared.isString(node)) {
      push(node);
    } else {
      genNode(node, context);
    }
    if (i < nodes.length - 1) {
      comma && push(", ");
    }
  }
}

我們先來看看此時的nodes參數,如下圖:
nodes

這裡的nodes就是調用genNodeList函數時傳的數組:[tag, props, children, patchFlag, dynamicProps],只是將數組中的undefined轉換成了null

  • nodes數組中的第一項為字元串p,表示當前節點是p標簽。

  • 由於當前p標簽沒有props,所以第二項為null的字元串。

  • 第三項為p標簽子節點:{{msg}}

  • 第四項也是一個字元串,標記當前節點是否是動態節點。

在講genNodeList函數之前,我們先來看一下如何使用h函數生成一個<p>{{ msg }}</p>標簽的虛擬DOM節點。根據vue官網的介紹,h函數定義如下:

// 完整參數簽名
function h(
  type: string | Component,
  props?: object | null,
  children?: Children | Slot | Slots
): VNode

h函數接收的第一個參數是標簽名稱或者一個組件,第二個參數是props對象或者null,第三個參數是子節點。

所以我們要使用h函數生成demo中的p標簽虛擬DOM節點代碼如下:

h("p", null, msg)

h函數生成虛擬DOM實際就是調用的createBaseVNode函數,而我們這裡的createElementBlock函數生成虛擬DOM也是調用的createBaseVNode函數。兩者的區別是createElementBlock函數多接收一些參數,比如patchFlagdynamicProps

現在我想你應該已經反應過來了,為什麼調用genNodeList函數時傳入的第一個參數nodes為:[tag, props, children, patchFlag, dynamicProps]。這個數組的順序就是調用createElementBlock函數時傳入的參數順序。

所以在genNodeList中會遍歷nodes數組生成調用createElementBlock函數需要傳入的參數。

先來看第一個參數tag,這裡tag的值為字元串"p"。所以在for迴圈中會執行push(node),生成調用createElementBlock函數的第一個參數"p"。在debug終端看看此時的render函數,如下圖:
arg1

從上圖中可以看到createElementBlock函數的第一個參數"p"

接著來看nodes數組中的第二個參數:props,由於p標簽中沒有props屬性。所以第二個參數props的值為字元串"null",在for迴圈中同樣會執行push(node),生成調用createElementBlock函數的第二個參數"null"。在debug終端看看此時的render函數,如下圖:
arg2

從上圖中可以看到createElementBlock函數的第二個參數null

接著來看nodes數組中的第三個參數:children,由於children是一個對象,所以以當前children節點作為參數執行genNode函數。

這個genNode函數前面已經執行過一次了,當時是以根節點的codegenNode屬性作為參數執行的。回顧一下genNode函數的代碼,如下:

function genNode(node, context) {
  switch (node.type) {
    case NodeTypes.SIMPLE_EXPRESSION:
      genExpression(node, context)
      break
    case NodeTypes.INTERPOLATION:
      genInterpolation(node, context);
      break;
    case NodeTypes.VNODE_CALL:
      genVNodeCall(node, context);
      break;
  }
}

前面我們講過了NodeTypes.INTERPOLATION類型表示當前節點是雙大括弧節點,而我們這次執行genNode函數傳入的p標簽children,剛好就是{{msg}}雙大括弧節點。所以代碼會走到genInterpolation函數中。

genInterpolation函數

將斷點走進genInterpolation函數中,genInterpolation代碼如下:

function genInterpolation(node, context) {
  const { push, helper } = context;
  push(`${helper(TO_DISPLAY_STRING)}(`);
  genNode(node.content, context);
  push(`)`);
}

首先會執行push方法向render函數中插入一個_toDisplayString函數調用,在debug終端看看執行完這個push方法後的render函數,如下圖:
toDisplayString

從上圖中可以看到此時createElementBlock函數的第三個參數只生成了一半,調用_toDisplayString函數傳入的參數還沒生成。

接著會以node.content作為參數執行genNode(node.content, context);生成_toDisplayString函數的參數,此時代碼又走回了genNode函數。

將斷點再次走進genNode函數,看看此時的node是什麼樣的,如下圖:
simple-expression

從上圖中可以看到此時的node節點是一個簡單表達式節點,表達式為:$setup.msg。所以代碼會走進genExpression函數。

genExpression函數

接著將斷點走進genExpression函數中,genExpression函數中的代碼如下:

function genExpression(node, context) {
  const { content, isStatic } = node;
  context.push(
    isStatic ? JSON.stringify(content) : content,
    -3 /* Unknown */,
    node
  );
}

由於當前的msg變數是一個ref響應式變數,所以isStaticfalse。所以會執行push方法,將$setup.msg插入到render函數中。

執行完push方法後,在debug終端看看此時的render函數字元串是什麼樣的,如下圖:
after-expression

從上圖中可以看到此時的render函數基本已經生成了,剩下的就是調用push方法生成各個函數的右括弧")"和右花括弧"}"。將斷點逐層走出,直到generate函數中。代碼如下:

function generate(ast) {
  // ...省略
  genNode(ast.codegenNode, context);

  deindent();
  push(`}`);
  return {
    ast,
    code: context.code,
  };
}

執行完最後一個 push方法後,在debug終端看看此時的render函數字元串是什麼樣的,如下圖:
render

從上圖中可以看到此時的render函數終於生成啦!

總結

這是我畫的我們這個場景中generate生成render函數的流程圖:

full-progress

  • 執行genModulePreamble函數生成:import { xxx } from "vue";

  • 簡單字元串拼接生成render函數中的函數名稱和參數,也就是function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {

  • 以根節點的codegenNode屬性為參數調用genNode函數生成render函數中return的內容。

    • 此時傳入的是虛擬節點,執行genVNodeCall函數生成return _openBlock(), _createElementBlock(和調用genNodeList函數,生成createElementBlock函數的參數。

    • 處理p標簽的tag標簽名和props,生成createElementBlock函數的第一個和第二個參數。此時render函數return的內容為:return _openBlock(), _createElementBlock("p", null

    • 處理p標簽的children也就是{{msg}}節點,再次調用genNode函數。此時node節點類型為雙大括弧節點,調用genInterpolation函數。

    • genInterpolation函數中會先調用push方法,此時的render函數return的內容為:return _openBlock(), _createElementBlock("p", null, _toDisplayString(。然後以node.content為參數再次調用genNode函數。

    • node.content$setup.msg,是一個簡單表達式節點,所以在genNode函數中會調用genExpression函數。執行完genExpression函數後,此時的render函數return的內容為:return _openBlock(), _createElementBlock("p", null, _toDisplayString($setup.msg

    • 調用push方法生成各個函數的右括弧")"和右花括弧"}",生成最終的render函數

關註(圖1)公眾號:【前端歐陽】,解鎖我更多vue原理文章。
加我(圖2)微信回覆「666」,免費領取歐陽研究vue源碼過程中收集的源碼資料,歐陽寫文章有時也會參考這些資料。同時讓你的朋友圈多一位對vue有深入理解的人。
公眾號微信


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

-Advertisement-
Play Games
更多相關文章
  • 本文首發於公眾號:Hunter後端 原文鏈接:MySQL面試必備三之事務 這一篇筆記介紹一下 MySQL 的事務,面試中常被問到關於事務的幾個問題如下: 事務是什麼 為什麼需要事務,事務有什麼作用 事務的特點 事務可能帶來哪些問題 事務有哪些隔離級別,這些隔離級別都可以解決哪些問題 可重覆讀隔離級別 ...
  • 粗排/精排的個性化多任務學習模型,能預估20多個不同的預估值,如點擊率、有效播放率、播放時長、點贊率、關註率等,那如何用它來排序呢?從多任務學習到多目標排序,中間有一個過渡,即如何把這些預估值融合成一個單一的排序分,最後實現多目標精排。這也就引入了本文要介紹的正題:多目標融合(multi-task ... ...
  • 通過詳細的查詢 SQL 語句分析,深入挖掘潛在的性能瓶頸,結合資料庫架構、索引優化以及查詢執行計劃等方面,提出針對性的優化建議,以提升查詢性能和系統效率。 ...
  • 一、開發卡片事件 HarmonyOS元服務卡片頁面(Metaservice Card Page)是指在HarmonyOS系統中,用於展示元服務的頁面界面。元服務是指一組提供特定功能或服務的組件,例如天氣服務、音樂播放服務等。元服務卡片頁面可以顯示元服務的相關信息和操作選項,用戶可以通過點擊卡片頁 ...
  • 一、開發卡片頁面 HarmonyOS元服務卡片頁面(Metaservice Card Page)是指在HarmonyOS系統中,用於展示元服務的頁面界面。元服務是指一組提供特定功能或服務的組件,例如天氣服務、音樂播放服務等。元服務卡片頁面可以顯示元服務的相關信息和操作選項,用戶可以通過點擊卡片頁 ...
  • 一、服務卡片的模塊和創建 在HarmonyOS中,服務卡片是一種提供即時信息和快速操作的小組件,類似於Android中的通知欄。服務卡片可以顯示各種類型的信息,包括通知、天氣、日曆事件、音樂播放器、快捷方式等。用戶可以根據自己的需求配置服務卡片,並通過輕掃或點擊來查看詳細信息或執行操作。此外,服 ...
  • 一、是什麼 單點登錄(Single Sign On),簡稱為 SSO,是目前比較流行的企業業務整合的解決方案之一 SSO的定義是在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統 SSO 一般都需要一個獨立的認證中心(passport),子系統的登錄均得通過passport,子系統 ...
  • 點贊 + 收藏 = 學會! 本篇我們介紹radash中函數柯里化和Number 相關的方法使用和源碼解析。深入學習radash中的方法思想和底層實現。 ...
一周排行
    -Advertisement-
    Play Games
  • 1、預覽地址:http://139.155.137.144:9012 2、qq群:801913255 一、前言 隨著網路的發展,企業對於信息系統數據的保密工作愈發重視,不同身份、角色對於數據的訪問許可權都應該大相徑庭。 列如 1、不同登錄人員對一個數據列表的可見度是不一樣的,如數據列、數據行、數據按鈕 ...
  • 前言 上一篇文章寫瞭如何使用RabbitMQ做個簡單的發送郵件項目,然後評論也是比較多,也是準備去學習一下如何確保RabbitMQ的消息可靠性,但是由於時間原因,先來說說設計模式中的簡單工廠模式吧! 在瞭解簡單工廠模式之前,我們要知道C#是一款面向對象的高級程式語言。它有3大特性,封裝、繼承、多態。 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 介紹 Nodify是一個WPF基於節點的編輯器控制項,其中包含一系列節點、連接和連接器組件,旨在簡化構建基於節點的工具的過程 ...
  • 創建一個webapi項目做測試使用。 創建新控制器,搭建一個基礎框架,包括獲取當天日期、wiki的請求地址等 創建一個Http請求幫助類以及方法,用於獲取指定URL的信息 使用http請求訪問指定url,先運行一下,看看返回的內容。內容如圖右邊所示,實際上是一個Json數據。我們主要解析 大事記 部 ...
  • 最近在不少自媒體上看到有關.NET與C#的資訊與評價,感覺大家對.NET與C#還是不太瞭解,尤其是對2016年6月發佈的跨平臺.NET Core 1.0,更是知之甚少。在考慮一番之後,還是決定寫點東西總結一下,也回顧一下.NET的發展歷史。 首先,你沒看錯,.NET是跨平臺的,可以在Windows、 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 添加節點(nodes) 通過上一篇我們已經創建好了編輯器實例現在我們為編輯器添加一個節點 添加model和viewmode ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...
  • 類型檢查和轉換:當你需要檢查對象是否為特定類型,並且希望在同一時間內將其轉換為那個類型時,模式匹配提供了一種更簡潔的方式來完成這一任務,避免了使用傳統的as和is操作符後還需要進行額外的null檢查。 複雜條件邏輯:在處理複雜的條件邏輯時,特別是涉及到多個條件和類型的情況下,使用模式匹配可以使代碼更 ...
  • 在日常開發中,我們經常需要和文件打交道,特別是桌面開發,有時候就會需要載入大批量的文件,而且可能還會存在部分文件缺失的情況,那麼如何才能快速的判斷文件是否存在呢?如果處理不當的,且文件數量比較多的時候,可能會造成卡頓等情況,進而影響程式的使用體驗。今天就以一個簡單的小例子,簡述兩種不同的判斷文件是否... ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...