01- vdom 和模板編譯源碼

組件渲染的過程

template --> ast --> render --> vDom --> 真實的Dom --> 頁面

Runtime-Compiler和Runtime-Only的區別 - 簡書

編譯步驟

模板編譯是Vue中比較核心的一部分。關于 Vue 編譯原理這塊的整體邏輯主要分三個部分,也可以說是分三步,前后關系如下:

第一步:將模板字符串轉換成element ASTs( 解析器 parse

第二步:對 AST 進行靜態節點標記,主要用來做虛擬DOM的渲染優化(優化器 optimize

第三步:使用element ASTs生成render函數代碼字符串(代碼生成器 generate

?編譯后的AST結構

template 模板:

<div class="box"><p>{{name}}</p>
</div>

AST 抽象語法樹:

ast: {tag: "div" //  元素標簽名type: 1,  // 元素節點類型 1標簽 2包含字面量表達式的文本節點 3普通文本節點或注釋節點staticRoot: false, // 是否靜態根節點static: false,  // 是否靜態節點plain: true, parent: undefined,attrsList: [], // 標簽節點的屬性名和值的對象集合attrsMap: {}, // 和attrsList類似,不同在它是以鍵值對保存屬性名和值children: [{tag: "p"type: 1,staticRoot: false,static: false,plain: true,parent: {tag: "div", ...},attrsList: [],attrsMap: {},children: [{type: 2,text: "{{name}}",static: false,expression: "_s(name)"  // type為2時才有這個屬性,表示表達式的內容}]}]
}

generate,將AST轉換成可以直接執行的JavaScript字符串

with(this) {return _c('div', [_c('p', [_v(_s(name))]), _v(" "), _m(0)])
}

注意一:平常開發中 我們使用的是不帶編譯版本的 Vue 版本(runtime-only)直接在 options 傳入 template 選項 在開發環境報錯

注意二:這里傳入的 template 選項不要和.vue 文件里面的模板搞混淆了 vue 單文件組件的 template 是需要 vue-loader 進行處理的

我們傳入的 el 或者 template 選項最后都會被解析成 render 函數 這樣才能保持模板解析的一致性

以下代碼實現是在在entry-runtime-with-compiler.js里面,和runtime-only版本需要區分開

1、模板編譯入口

export function initMixin (Vue) {Vue.prototype._init = function (options) {const vm = this;vm.$options = options;initState(vm);// 如果有 el 屬性,進行模板渲染if (vm.$options.el) {vm.$mount(vm.$options.el)}}Vue.prototype.$mount = function (el) {const vm = this;const options = vm.$options;el = document.querySelector(el);// 不存在 render 屬性的幾種情況if (!options.render) {// 不存在 render 但存在 templatelet template = options.template;// 不存在 render 和 template 但是存在 el 屬性,直接將模板賦值到 el 所在的外層 html 結構 (就是 el 本身,并不是父元素)if (!template && el) {template = el.outerHTML;}// 最后把處理好的 template 模板轉化成 render 函數if (template) {const render = compileToFunctions(template);options.render = render;}}}
}
  • 先初始化狀態,initState(vm)
  • 再在 initMixin 函數中,判斷是否有el,有則直接調用 vm.$mount(vm.$options.el) 進行模板渲染,沒有則手動調用;
  • Vue.prototype.$mount 方法中,判斷是否存在 render 屬性,存在則給 template 賦值,如果不存在 render 和 template 但存在 el 屬性,直接將模板賦值到 el 所在的外層 html 結構(就是el本身,并不是父元素);
  • 通過?compileToFunctions 將處理好的?template 模板轉換成 render 函數

咱們主要關心$mount 方法 最終將處理好的 template 模板轉成 render 函數

2、模板轉化核心方法 compileToFunctions

export function compileToFunctions (template) {// html → ast → render函數// 第一步 將 html 字符串轉換成 ast 語法樹let ast = parse(template);// 第二步 優化靜態節點if (mergeOptions.optimize !== false) {optimize(ast, options);}// 第三步 通過 ast 重新生成代碼let code = generate(ast);// 使用 with 語法改變作用域為 this, 之后調用 render 函數可以使用 call 改變 this,方便 code 里面的變量取值。let renderFn = new Function(`with(this){return ${code}}`);return renderFn;
}

html → ast → render函數

這里需要把 html 字符串變成 render 函數,分三步:

(1)parse函數 把 HTML 代碼轉成 AST 語法樹

? ? ? ? AST 是用來描述代碼本身形成的樹形結構,不僅可以描述 HTML,也可以描述 css 和 js 語法;很多庫都運用到了 AST,比如 webpack,babel,eslint 等

(2) optimize函數?優化靜態節點(主要用來做虛擬DOM的渲染優化)

? ? ? ? 先遍歷 AST,對 AST 進行靜態節點標記,(即節點永遠不會發生變化)并做出一些特殊的處理。例如:

  • 移除靜態節點的 v-once 指令,因為它在這里沒有任何意義。
  • 移除靜態節點的 key 屬性。因為靜態節點永遠不會改變,所以不需要 key 屬性。
  • 將一些靜態節點合并成一個節點,以減少渲染的節點數量。例如相鄰的文本節點和元素節點可以被合并為一個元素節點。

(3)generate函數 通過 AST 重新生成代碼

最后生成的代碼需要和 render 函數一樣

類似這樣的結構:

_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
  • _c 代表?createElement 創建節點
  • _v 代表 createTextVNode 創建文本節點
  • _s 代表 toString 把對象解析成字符串
    ?

模板引擎的實現原理 ?with + new Function,使用 with 語法改變作用域為 this, 之后調用 render 函數可以使用 call 改變 this,方便 code 里面的變量取值。

parse函數,解析 html 并生成 ast

// src/compiler/parse.js// 以下為源碼的正則  對正則表達式不清楚的同學可以參考小編之前寫的文章(前端進階高薪必看 - 正則篇);
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; //匹配標簽名 形如 abc-123
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //匹配特殊標簽 形如 abc:234 前面的abc:可有可無
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配標簽開始 形如 <abc-123 捕獲里面的標簽名
const startTagClose = /^\s*(\/?)>/; // 匹配標簽結束  >
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配標簽結尾 如 </abc-123> 捕獲里面的標簽名
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配屬性  形如 id="app"let root, currentParent; //代表根節點 和當前父節點
// 棧結構 來表示開始和結束標簽
let stack = [];
// 標識元素和文本type
const ELEMENT_TYPE = 1;
const TEXT_TYPE = 3;
// 生成ast方法
function createASTElement(tagName, attrs) {return {tag: tagName,type: ELEMENT_TYPE,children: [],attrs,parent: null,};
}// 對開始標簽進行處理
function handleStartTag({ tagName, attrs }) {let element = createASTElement(tagName, attrs);if (!root) {root = element;}currentParent = element;stack.push(element);
}// 對結束標簽進行處理
function handleEndTag(tagName) {// 棧結構 []// 比如 <div><span></span></div> 當遇到第一個結束標簽</span>時 會匹配到棧頂<span>元素對應的ast 并取出來let element = stack.pop();// 當前父元素就是棧頂的上一個元素 在這里就類似divcurrentParent = stack[stack.length - 1];// 建立parent和children關系if (currentParent) {element.parent = currentParent;currentParent.children.push(element);}
}// 對文本進行處理
function handleChars(text) {// 去掉空格text = text.replace(/\s/g, "");if (text) {currentParent.children.push({type: TEXT_TYPE,text,});}
}// 解析標簽生成ast核心
export function parse(html) {while (html) {// 查找<let textEnd = html.indexOf("<");// 如果<在第一個 那么證明接下來就是一個標簽 不管是開始還是結束標簽if (textEnd === 0) {// 如果開始標簽解析有結果const startTagMatch = parseStartTag();if (startTagMatch) {// 把解析好的標簽名和屬性解析生成asthandleStartTag(startTagMatch);continue;}// 匹配結束標簽</const endTagMatch = html.match(endTag);if (endTagMatch) {advance(endTagMatch[0].length);handleEndTag(endTagMatch[1]);continue;}}let text;// 形如 hello<div></div>if (textEnd >= 0) {// 獲取文本text = html.substring(0, textEnd);}if (text) {advance(text.length);handleChars(text);}}// 匹配開始標簽function parseStartTag() {const start = html.match(startTagOpen);if (start) {const match = {tagName: start[1],attrs: [],};//匹配到了開始標簽 就截取掉advance(start[0].length);// 開始匹配屬性// end代表結束符號>  如果不是匹配到了結束標簽// attr 表示匹配的屬性let end, attr;while (!(end = html.match(startTagClose)) &&(attr = html.match(attribute))) {advance(attr[0].length);attr = {name: attr[1],value: attr[3] || attr[4] || attr[5], //這里是因為正則捕獲支持雙引號 單引號 和無引號的屬性值};match.attrs.push(attr);}if (end) {//   代表一個標簽匹配到結束的>了 代表開始標簽解析完畢advance(1);return match;}}}//截取html字符串 每次匹配到了就往前繼續匹配function advance(n) {html = html.substring(n);}//   返回生成的astreturn root;
}

generate函數,把 ast 轉化成 render 函數結構

// src/compiler/codegen.jsconst defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; //匹配花括號 {{  }} 捕獲花括號里面的內容function gen(node) {// 判斷節點類型// 主要包含處理文本核心// 源碼這塊包含了復雜的處理  比如 v-once v-for v-if 自定義指令 slot等等  咱們這里只考慮普通文本和變量表達式{{}}的處理// 如果是元素類型if (node.type == 1) {//   遞歸創建return generate(node);} else {//   如果是文本節點let text = node.text;// 不存在花括號變量表達式if (!defaultTagRE.test(text)) {return `_v(${JSON.stringify(text)})`;}// 正則是全局模式 每次需要重置正則的lastIndex屬性  不然會引發匹配buglet lastIndex = (defaultTagRE.lastIndex = 0);let tokens = [];let match, index;while ((match = defaultTagRE.exec(text))) {// index代表匹配到的位置index = match.index;if (index > lastIndex) {//   匹配到的{{位置  在tokens里面放入普通文本tokens.push(JSON.stringify(text.slice(lastIndex, index)));}//   放入捕獲到的變量內容tokens.push(`_s(${match[1].trim()})`);//   匹配指針后移lastIndex = index + match[0].length;}// 如果匹配完了花括號  text里面還有剩余的普通文本 那么繼續pushif (lastIndex < text.length) {tokens.push(JSON.stringify(text.slice(lastIndex)));}// _v表示創建文本return `_v(${tokens.join("+")})`;}
}// 處理attrs屬性
function genProps(attrs) {let str = "";for (let i = 0; i < attrs.length; i++) {let attr = attrs[i];// 對attrs屬性里面的style做特殊處理if (attr.name === "style") {let obj = {};attr.value.split(";").forEach((item) => {let [key, value] = item.split(":");obj[key] = value;});attr.value = obj;}str += `${attr.name}:${JSON.stringify(attr.value)},`;}return `{${str.slice(0, -1)}}`;
}// 生成子節點 調用gen函數進行遞歸創建
function getChildren(el) {const children = el.children;if (children) {return `${children.map((c) => gen(c)).join(",")}`;}
}
// 遞歸創建生成code
export function generate(el) {let children = getChildren(el);let code = `_c('${el.tag}',${el.attrs.length ? `${genProps(el.attrs)}` : "undefined"}${children ? `,${children}` : ""})`;return code;
}

code 字符串生成 render 函數

export function compileToFunctions (template) {let ast = parseHTML(template);let code = genCode(ast);// 將模板變成 render 函數,通過 with + new Function 的方式讓字符串變成 JS 語法來執行const render = new Function(`with(this){return ${code}}`);return render;
}

?https://juejin.cn/spost/7267858684667035704

Vue 模板 AST 詳解 - 文章教程 - 文江博客

滑動驗證頁面

手寫Vue2.0源碼(二)-模板編譯原理|技術點評_慕課手記

前端鯊魚哥 的個人主頁 - 文章 - 掘金

前端進階高薪必看-正則篇 - 掘金

從vue模板解析學習正則表達式

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/39764.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/39764.shtml
英文地址,請注明出處:http://en.pswp.cn/news/39764.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

《vue3實戰》運用radio單選按鈕或Checkbox復選框實現單選多選的試卷制作

文章目錄 目錄 系列文章目錄 1.《Vue3實戰》使用axios獲取文件數據以及走馬燈Element plus的運用 2.《Vue3實戰》用路由實現跳轉登錄、退出登錄以及路由全局守護 3.《vue3實戰》運用Checkbox復選框實現單選多選的試卷展現&#xff08;本文&#xff09; 文章目錄 前言 radio是什…

Java中List排序的4種方法

開發過程中經常會遇到讀取文件內容的情況&#xff0c;需要判斷文件是否為文本文件&#xff0c;及文件編碼格式&#xff0c;防止無法讀取內容或亂碼出現情況。 我們可以通過 java.io.File 類包找出文件是目錄還是常規文件。java.io.File 類包含兩種方法&#xff0c;它們分別是&…

TCP服務器—實現數據通信

目錄 前言 1.接口介紹 2.編寫服務器 3.編寫客戶端 4.編譯鏈接 5.測試 6.總結 前言 今天我們要介紹的是使用TCP協議實現數據通信&#xff0c;相比于之前寫的UDP服務器實現數據信&#xff0c;在主體邏輯上并沒有差別。客戶端向服務器發送信息&#xff0c;服務器接受信息并回…

JavaEE初階:多線程 - Thread 類的基本用法

上次我們了解了多線程的五種創建方法&#xff0c;今天來學習Thread的基本用法。 目錄 run和start Thread常見的構造方法 Thread的幾個常見屬性 后臺線程 是否存活 線程終止 1.使用標志位 2.使用Thread自帶的標志 等待線程 run和start 首先需要理解Thread的run和star…

JavaWeb-Listener監聽器

目錄 監聽器Listener 1.功能 2.監聽器分類 3.監聽器的配置 4.ServletContext監聽 5.HttpSession監聽 6.ServletRequest監聽 監聽器Listener 1.功能 用于監聽域對象ServletContext、HttpSession和ServletRequest的創建&#xff0c;與銷毀事件監聽一個對象的事件&#x…

Python源碼05:使用Pyecharts畫詞云圖圖

**Pyecharts是一個用于生成 Echarts 圖表的 Python 庫。Echarts 是一個基于 JavaScript 的數據可視化庫&#xff0c;提供了豐富的圖表類型和交互功能。**通過 Pyecharts&#xff0c;你可以使用 Python 代碼生成各種類型的 Echarts 圖表&#xff0c;例如折線圖、柱狀圖、餅圖、散…

Glide 的超時控制相關處理

作者&#xff1a;newki 前言 Glide 相信大家都不陌生&#xff0c;各種源碼分析&#xff0c;使用介紹大家應該都是爛熟于心。但是設置 Glide 的超時問題大家遇到過沒有。 我遇到了&#xff0c;并且掉坑里了&#xff0c;情況是這樣的。 調用接口從網絡拉取用戶頭像&#xff0c…

3.微服務概述

1.大型網絡架構變遷 SOA與微服務最大的差別就是服務拆分的細度&#xff0c;目前大多數微服務實際上是SOA架構&#xff0c;真正的微服務應該是一個接口對應一個服務器&#xff0c;開發速度快、成本高&#xff1b; 微服務SOA能拆分的就拆分是整體的&#xff0c;服務能放一起的都…

自動駕駛HMI產品技術方案

版本變更 序號 日期 變更內容 編制人 審核人 文檔版本 1 2 1.

【計算機網絡】13、ARP 包:廣播自己的 mac 地址和 ip

機器啟動時&#xff0c;會向外廣播自己的 mac 地址和 ip 地址&#xff0c;這個即稱為 arp 協議。范圍是未經過路由器的部分&#xff0c;如下圖的藍色部分&#xff0c;范圍內的設備都會在本地記錄 mac 和 ip 的綁定信息&#xff0c;若有重復則覆蓋更新&#xff08;例如先收到 ma…

【Spring】深入理解 Spring 事務及其傳播機制

文章目錄 一、Spring 事務是什么二、Spring 中事務的實現方法2.1 Spring 編程式事務&#xff08;手動&#xff09;2.1.1 編程式事務的使用演示2.1.2 編程式事務存在的問題 2.2 Spring 聲明式事務&#xff08;自動&#xff09;2.2.1 Transactional 作用范圍2.2.2 Transactional …

騰訊云GPU服務器GN7實例NVIDIA T4 GPU卡

騰訊云GPU服務器GN7實例搭載1顆 NVIDIA T4 GPU&#xff0c;8核32G配置&#xff0c;系統盤為100G 高性能云硬盤&#xff0c;自帶5M公網帶寬&#xff0c;系統鏡像可選Linux和Windows&#xff0c;地域可選廣州/上海/北京/新加坡/南京/重慶/成都/首爾/中國香港/德國/東京/曼谷/硅谷…

安卓純代碼布局開發游戲二:Android Studio開發環境搭建

1.Android Studio下載&#xff1a; Download Android Studio & App Tools - Android Developers 2.安裝 安裝過程非常簡單&#xff0c;找到下載包&#xff0c;一直點Next即可。 3.下載Android SDK 第一次進入Android Studio默認會先下載Android SDK,筆者下載的Android SDK存…

零售行業供應鏈管理核心KPI指標(三)

完美訂單滿足率和退貨率 完美訂單滿足率有三個方面的因素影響&#xff1a;訂單按時、足量、無損交貨。通常情況下零售企業追求線上訂單履行周期慢慢達到行業平均水平&#xff0c;就是交付的速度變快了&#xff0c;這個肯定是一件好事情&#xff0c;趨勢越來越好。 同時&#…

歐拉公式

文章目錄 歐拉公式e歐拉恒等式歐拉公式歐拉公式 推導2步驟1: 使用泰勒級數展開步驟2: 將 i x i x ix 代入 e x e^x ex 復平面上推導歐拉公式步驟1&#xff1a;復平面上的復數表示步驟2&#xff1a;定義復數的指數形式步驟3&#xff1a;求導步驟4&#xff1a;連接兩種形式步驟…

ubuntu安裝opencv4

apt 安裝 sudo apt install libopencv-dev python3-opencvpkg-config查看安裝 sudo apt install pkg-configpkg-config --modversion opencv4pkg-config --libs --cflags opencv4參考 如何在 Ubuntu 20.04 上安裝 OpenCV pkg-config 詳解

spark yarn 開啟動態資源分配

概念 不需要指定并發&#xff0c;只需要指定內存&#xff0c; 程序在運行后會動態調節并發數量&#xff0c;我們只需要設置一個上線即可 在spark 配置文件設置&#xff1a; spark.dynamicAllocation.enabled true spark.shuffle.service.enabled true 準備shuffer jar 將spar…

星際爭霸之小霸王之小蜜蜂(一)

目錄 前言 一、安裝pygame庫 1、pygame庫簡介 2、在windows系統安裝pygame庫 二 、搭建游戲框架 1、創建游戲窗口 2、改變窗口顏色 總結 前言 大家應該都看過或者都聽說過python神書“大蟒蛇”&#xff0c;上面有一個案例是《外星人入侵》&#xff0c;游戲介紹讓我想起了上…

炫酷UI前端效果的CSS生成工具

提升設計人員和前端開發人員的工作 推薦炫酷UI前端效果的CSS生成工具1.Neumorphism2.帶有漸變的圖標3.Interactions4.大型數據庫5.動畫6.Mask7.動畫按鈕8. 自定義形狀分隔線9.背景圖案10. SVG波浪推薦炫酷UI前端效果的CSS生成工具 1.Neumorphism 地址:https://neumorphism.i…

【Nginx17】Nginx學習:目錄索引、字符集與瀏覽器判斷模塊

Nginx學習&#xff1a;目錄索引、字符集與瀏覽器判斷模塊 今天要學習的內容有幾個還是大家比較常見的&#xff0c;所以學習起來也不會特別費勁。對于目錄的默認頁設置大家都不會陌生&#xff0c;字符集的設置也比較常見&#xff0c;而瀏覽器的判斷這一塊&#xff0c;可能有同學…