Hello,大家好,我是 Sunday。
最近一位同學在學習 vue3 源碼的時候,把 vue 3 的大部分核心邏輯都整理到了腦圖之中:
整理的內容非常詳細。應該會對所有還在學習 vue3 源碼的同學都有所幫助。所以分享給大家!
那么今天,咱們就借助這位同學的腦圖作為契機,來為大家捋一捋 【Vue3 框架設計原理】(看完設計原理之后,再看腦圖收獲會更大哦~)
在了解 Vue3 框架設計之前,我們需要做兩件事情,而這兩件事情也是今天的主要內容。
那么準備好了?
我們開始吧!
針對于目前的前端開發(fā)而言,主要存在兩種 編程范式:
這兩種 范式 一般是相對來去說的。
那么首先我們先來說什么叫做 命令式。
具體例子:
張三的媽媽讓張三去買醬油。
那么張三怎么做的呢?
- 張三拿起錢
- 打開門
- 下了樓
- 到商店
- 拿錢買醬油
- 回到家
以上的流程詳細的描述了,張三在買醬油的過程中,每一步都做了什么。那么這樣一種:詳細描述做事過程 的方式就可以被叫做 命令式。
那么如果把這樣的方式放到具體的代碼實現(xiàn)之中,又應該怎么做呢?
我們來看以下這樣的一個事情:
在指定的 div 中展示 “hello world”
那么如果想要完成這樣的事情,通過命令式的方式我們如何實現(xiàn)呢?
我們知道命令式的核心在于:關注過程。
所以,以上事情通過命令式實現(xiàn)則可得出以下邏輯與代碼:
// 1. 獲取到指定的 divconst divEle = document.querySelector('#app')// 2. 為該 div 設置 innerHTML 為 hello worlddivEle.innerHTML = 'hello world'
該代碼雖然只有兩步,但是它清楚的描述了:完成這件事情,所需要經(jīng)歷的過程
那么假如我們所做的事情,變得更加復雜了,則整個過程也會變得更加復雜。
比如:
為指定的 div 的子元素 div 的子元素 p 標簽,展示變量 msg
那么通過命令式完成以上功能,則會得出如下邏輯與代碼:
// 1. 獲取到第一層的 divconst divEle = document.querySelector('#app')// 2. 獲取到它的子 divconst subDivEle = divEle.querySelector('div')// 3. 獲取第三層的 pconst subPEle = subDivEle.querySelector('p')// 4. 定義變量 msgconst msg = 'hello world'// 5. 為該 p 元素設置 innerHTML 為 hello worldsubPEle.innerHTML = msg
那么通過以上例子,相信大家可以對命令式的概念有了一個基礎的認識。
最后做一個總結,什么叫做命令式呢?
命令式是:關注過程 的一種編程范式,他描述了完成一個功能的 詳細邏輯與步驟。
當了解完命令式之后,那么接下來我們就來看 聲明式 編程。
針對于聲明式而言,大家其實都是非常熟悉的了。
比如以下代碼,就是一個典型的 聲明式 :
<div>{{ msg }}</div>
對于這個代碼,大家是不是感覺有些熟悉?
沒錯,這就是 Vue 中非常常見的雙大括號語法。所以當我們在寫 Vue 模板語法 的時候,其實一直寫的就是 聲明式 編程。
那么聲明式編程具體指的是什么意思呢?
還是以剛才的例子為例:
張三的媽媽讓張三去買醬油。
那么張三怎么做的呢?
在這個例子中,我們說:張三所做的事情就是命令式。那么張三媽媽所做的事情就是 聲明式。
在這樣一個事情中,張三媽媽只是發(fā)布了一個聲明,她并不關心張三如何去買的醬油,只關心最后的結果。
所以說,所謂聲明式指的是:不關注過程,只關注結果 的范式。
同樣,如果我們通過代碼來進行表示的話,以下例子:
為指定的 div 的子元素 div 的子元素 p 標簽,展示變量 msg
將會得出如下代碼:
<div id="app"> <div> <p>{{ msg }}</p> </div></div>
在這樣的代碼中,我們完全不關心 msg 是怎么被渲染到 p 標簽中的,我們所關心的只是:在 p 標簽中,渲染指定文本而已。
最后做一個總結,什么叫做聲明式呢?
聲明式是:關注結果 的一種編程范式,他 并不關心 完成一個功能的 詳細邏輯與步驟。(注意:這并不意味著聲明式不需要過程!聲明式只是把過程進行了隱藏而已!)
那么在我們講解完成 命令式 和 聲明式 之后,很多同學肯定會對這兩種編程范式進行一個對比。
是命令式好呢?還是聲明式好呢?
那么想要弄清楚這個問題,那么我們首先就需要先搞清楚,評價一種編程范式好還是不好的標準是什么?
通常情況下,我們評價一個編程范式通常會從兩個方面入手:
那么接下來我們就通過這兩個方面,來分析一下命令式和聲明式。
性能一直是我們在進行項目開發(fā)時特別關注的方向,那么我們通常如何來表述一個功能的性能好壞呢?
我們來看一個例子:
為指定 div 設置文本為 “hello world”
那么針對于這個需求而言,最簡單的代碼就是:
div.innerText = "hello world" // 耗時為:1
你應該找不到比這個更簡單的代碼實現(xiàn)了。
那么此時我們把這個操作的 耗時 比作 :1 。(PS:耗時越少,性能越強)
然后我們來看聲明式,聲明式的代碼為:
<div>{{ msg }}</div> <!-- 耗時為:1 + n --><!-- 將 msg 修改為 hello world -->
那么:已知修改text最簡單的方式是innerText ,所以說無論聲明式的代碼是如何實現(xiàn)的文本切換,那么它的耗時一定是 > 1 的,我們把它比作 1 + n(對比的性能消耗)。
所以,由以上舉例可知:命令式的性能 > 聲明式的性能
可維護性代表的維度非常多,但是通常情況下,所謂的可維護性指的是:對代碼可以方便的 閱讀、修改、刪除、增加 。
那么想要達到這個目的,說白了就是:代碼的邏輯要足夠簡單,讓人一看就懂。
那么明確了這個概念,我們來看下命令式和聲明式在同一段業(yè)務下的代碼邏輯:
// 命令式// 1. 獲取到第一層的 divconst divEle = document.querySelector('#app')// 2. 獲取到它的子 divconst subDivEle = divEle.querySelector('div')// 3. 獲取第三層的 pconst subPEle = subDivEle.querySelector('p')// 4. 定義變量 msgconst msg = 'hello world'// 5. 為該 p 元素設置 innerHTML 為 hello worldsubPEle.innerHTML = msg
// 聲明式<div id="app"> <div> <p>{{ msg }}</p> </div></div>
對于以上代碼而言,聲明式 的代碼明顯更加利于閱讀,所以也更加利于維護。
所以,由以上舉例可知:**命令式的可維護性 < 聲明式的可維護性 **
由以上分析可知兩點內容:
那么雙方各有優(yōu)劣,我們在日常開發(fā)中應該使用哪種范式呢?
想要搞明白這點,那么我們還需要搞明白更多的知識。
企業(yè)應用的設計原則,想要描述起來比較復雜,為什么呢?
因為對于 不同的企業(yè)類型(大廠、中小廠、人員外包、項目外包),不同的項目類型(前臺、中臺、后臺)來說,對應的企業(yè)應用設計原則上可能會存在一些差異。
所以我們這里所做的描述,會拋棄一些細微的差異,僅抓住核心的重點來進行闡述。
無論什么類型的企業(yè),也無論它們在開發(fā)什么類型的項目,那么最關注的點無非就是兩個:
項目成本非常好理解,它決定了一個公司完成“這件事”所付出的代價,從而直接決定了這個項目是否是可以盈利的(大廠的燒錢項目例外)。
那么既然項目成本如此重要,大家可以思考一下,決定項目成本的又是什么?
沒錯!就是你的 開發(fā)周期。
開發(fā)周期越長,所付出的人員成本就會越高,從而導致項目成本變得越高。
通過我們前面的分析可知,聲明式的開發(fā)范式在 可維護性 上,是 大于 命令式的。
而可維護性從一定程度上就決定了,它會使項目的:開發(fā)周期變短、升級變得更容易 從而大量節(jié)約開發(fā)成本。
所以這也是為什么 Vue 會變得越來越受歡迎的原因。
決定開發(fā)者開發(fā)體驗的核心要素,主要是在開發(fā)時和閱讀時的難度,這個被叫做:心智負擔。
心智負擔可以作為衡量開發(fā)難易度的一個標準,心智負擔高則證明開發(fā)的難度較高,心智負擔低則表示開發(fā)的難度較低,開發(fā)更加舒服。
那么根據(jù)我們之前所說,聲明式的開發(fā)難度明顯低于命令式的開發(fā)難度。
所以對于開發(fā)體驗而言,聲明式的開發(fā)體驗更好,也就是 心智負擔更低。
Vue 作者尤雨溪在一次演講中說道:框架的設計過程其實是一個不斷取舍的過程 。
這代表的是什么意思呢?
想要搞明白這個,那么再來明確一下之前說過的概念:
當我們明確好了這樣的一個問題之后,那么我們接下來來思考一個問題:框架的開發(fā)與設計原則是什么呢?
我們知道對于 Vue 而言,當我們使用它的是通過 聲明式 的方式進行使用,但是對于 Vue 內部而言,是通過 命令式 來進行的實現(xiàn)。
所以我們可以理解為:Vue 封裝了命令式的邏輯,而對外暴露出了聲明式的接口
那么既然如此,我們明知 命令式的性能 > 聲明式的性能 。那么 Vue 為什么還要選擇聲明式的方案呢?
其實原因非常的簡單,那就是因為:命令式的可維護性 < 聲明式的可維護性 。
為指定的 div 的子元素 div 的子元素 p 標簽,展示變量 msg
以這個例子為例。
對于開發(fā)者而言,不需要關注實現(xiàn)過程,只需要關注最終的結果即可。
而對于 Vue 而言,他所需要做的就是:封裝命令式邏輯,同時 盡可能的減少性能的損耗!它需要在 性能 與 可維護性 之間,找到一個平衡。從而找到一個 可維護性更好,性能相對更優(yōu) 的一個點。
所以對于 Vue 而言,它的設計原則就是:在保證可維護性的基礎上,盡可能的減少性能的損耗。
那么回到我們的標題:為什么說框架的設計過程其實是一個不斷取舍的過程?
答案也就呼之欲出了,因為:
我們需要在可維護性和性能之間,找到一個平衡點。在保證可維護性的基礎上,盡可能的減少性能的損耗。
所以框架的設計過程其實是一個不斷在 可維護性和性能 之間進行取舍的過程
在 Vue 3 的 源代碼 中存在一個 runtime-core 的文件夾,該文件夾內存放的就是 運行時 的核心代碼邏輯。
runtime-core 中對外暴露了一個函數(shù),叫做 渲染函數(shù)render
我們可以通過 render 代替 template 來完成 DOM 的渲染:
有些同學可能看不懂當前代碼是什么意思,沒有關系,這不重要,后面我們會詳細去講。
<head> <meta charset="UTF-8"> <title>Document</title> <script src="https://unpkg.com/vue@3.2.36/dist/vue.global.js"></script></head><body> <div id="app"></div></body><script> const { render, h } = Vue // 生成 VNode const vnode = h('div', { class: 'test' }, 'hello render') // 承載的容器 const container = document.querySelector('#app') // 渲染函數(shù) render(vnode, container)</script>
我們知道,在 Vue 的項目中,我們可以通過 tempalte 渲染 DOM 節(jié)點,如下:
<template> <div class="test">hello render</div></template>
但是對于 render 的例子而言,我們并沒有使用 tempalte,而是通過了一個名字叫做 render 的函數(shù),返回了一個不知道是什么的東西,為什么也可以渲染出 DOM 呢?
帶著這樣的問題,我們來看:
我們知道在上面的代碼中,存在一個核心函數(shù):渲染函數(shù) render,那么這個 render 在這里到底做了什么事情呢?
我們通過一段代碼實例來去看下:
假設有一天你們領導跟你說:
我希望根據(jù)如下數(shù)據(jù):
渲染出這樣一個 div:
{ type: 'div', props: { class: test }, children: 'hello render'}
<div class="test">hello render</div>
那么針對這樣的一個需求你會如何進行實現(xiàn)呢?大家可以在這里先思考一下,嘗試進行一下實現(xiàn),然后我們再繼續(xù)往下看..........
那么接下來我們根據(jù)這個需求來實現(xiàn)以下代碼:
<script> const VNode = { type: 'div', props: { class: 'test' }, children: 'hello render' } // 創(chuàng)建 render 渲染函數(shù) function render(vnode) { // 根據(jù) type 生成 element const ele = document.createElement(vnode.type) // 把 props 中的 class 賦值給 ele 的 className ele.className = vnode.props.class // 把 children 賦值給 ele 的 innerText ele.innerText = vnode.children // 把 ele 作為子節(jié)點插入 body 中 document.body.appendChild(ele) } render(VNode)</script>
在這樣的一個代碼中,我們成功的通過一個 render 函數(shù)渲染出了對應的 DOM,和前面的 render 示例 類似,它們都是渲染了一個 vnode,你覺得這樣的代碼真是 妙極了!
但是你的領導用了一段時間你的 render 之后,卻說:天天這樣寫也太麻煩了,每次都得寫一個復雜的 vnode,能不能讓我直接寫 HTML 標簽結構的方式 你來進行渲染呢?
你想了想之后,說:如果是這樣的話,那就不是以上 運行時 的代碼可以解決的了!
沒錯!我們剛剛所編寫的這樣的一個“框架”,就是 運行時 的代碼框架。
那么最后,我們做一個總結:運行時可以利用render 把 vnode 渲染成真實 dom 節(jié)點。
在剛才,我們明確了,如果只靠 運行時,那么是沒有辦法通過 HTML 標簽結構的方式 的方式來進行渲染解析的。
那么想要實現(xiàn)這一點,我們就需要借助另外一個東西,也就是 編譯時。
Vue 中的編譯時,更準確的說法應該是 編譯器 的意思。它的代碼主要存在于 compiler-core 模塊下。
我們來看如下代碼:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Document</title> <script src="https://unpkg.com/vue@3.2.36/dist/vue.global.js"></script></head><body> <div id="app"></div></body><script> const { compile, createApp } = Vue // 創(chuàng)建一個 html 結構 const html = ` <div class="test">hello compiler</div> ` // 利用 compile 函數(shù),生成 render 函數(shù) const renderFn = compile(html) // 創(chuàng)建實例 const app = createApp({ // 利用 render 函數(shù)進行渲染 render: renderFn }) // 掛載 app.mount('#app')</script></html>
對于編譯器而言,它的主要作用就是:把 template 中的 html 編譯成 render 函數(shù)。然后再利用 運行時 通過 render 掛載對應的 DOM。
那么最后,我們做一個總結:編譯時可以把html 的節(jié)點,編譯成 render函數(shù)
前面兩小節(jié)我們已經(jīng)分別了解了 運行時 和 編譯時,同時我們也知道了:vue 是一個 運行時+編譯時 的框架!
vue 通過 compiler 解析 html 模板,生成 render 函數(shù),然后通過 runtime 解析 render,從而掛載真實 dom。
那么看到這里可能有些同學就會有疑惑了,既然 compiler 可以直接解析 html 模板,那么為什么還要生成 render 函數(shù),然后再去進行渲染呢?為什么不直接利用 compiler 進行渲染呢?
即:為什么 vue 要設計成一個 運行時+編譯時的框架呢?
那么想要理清楚這個問題,我們就需要知道 dom 渲染是如何進行的。
對于 dom 渲染而言,可以被分為兩部分:
那么什么是初次渲染呢?
當初始 div 的 innerHTML 為空時,
<div id="app"></div>
我們在該 div 中渲染如下節(jié)點:
<ul> <li>1</li> <li>2</li> <li>3</li></ul>
那么這樣的一次渲染,就是 初始渲染。在這樣的一次渲染中,我們會生成一個 ul 標簽,同時生成三個 li 標簽,并且把他們掛載到 div 中。
那么此時如果 ul 標簽的內容發(fā)生了變化:
<ul> <li>3</li> <li>1</li> <li>2</li></ul>
li - 3 上升到了第一位,那么此時大家可以想一下:我們期望瀏覽器如何來更新這次渲染呢?
瀏覽器更新這次渲染無非有兩種方式:
那么大家覺得這兩種方式哪一種方式更好呢?那么我們來分析一下:
那么根據(jù)以上分析,我們知道了:
那么這兩種方式,哪一種更快呢?我們來實驗一下:
const length = 10000 // 增加一萬個dom節(jié)點,耗時 3.992919921875 ms console.time('element') for (let i = 0; i < length; i++) { const newEle = document.createElement('div') document.body.appendChild(newEle) } console.timeEnd('element') // 增加一萬個 js 對象,耗時 0.402099609375 ms console.time('js') const divList = [] for (let i = 0; i < length; i++) { const newEle = { type: 'div' } divList.push(newEle) } console.timeEnd('js')
從結果可以看出,dom 的操作要比 js 的操作耗時多得多,即:dom** 操作比 js 更加耗費性能**。
那么根據(jù)這樣的一個結論,回到我們剛才所說的場景中:
對比 舊節(jié)點 和 新節(jié)點 之間的差異
根據(jù)差異,刪除一個 舊節(jié)點,增加一個 新節(jié)點
根據(jù)結論可知:方式一會比方式二更加消耗性能(即:性能更差)。
那么得出這樣的結論之后,我們回過頭去再來看最初的問題:為什么 vue 要設計成一個 運行時+編譯時的框架呢?
答:
在 vue 的源碼中,會大量的涉及到一個概念,那就 副作用。
所以我們需要先了解一下副作用代表的是什么意思。
副作用指的是:當我們 對數(shù)據(jù)進行 setter 或 getter 操作時,所產(chǎn)生的一系列后果。
那么具體是什么意思呢?我們分別來說一下:
setter 所表示的是 賦值 操作,比如說,當我們執(zhí)行如下代碼時 :
msg = '你好,世界'
這時 msg 就觸發(fā)了一次 setter 的行為。
那么假如說,msg 是一個響應性數(shù)據(jù),那么這樣的一次數(shù)據(jù)改變,就會影響到對應的視圖改變。
那么我們就可以說:msg 的 setter 行為,觸發(fā)了一次副作用,導致視圖跟隨發(fā)生了變化。
getter 所表示的是 取值 操作,比如說,當我們執(zhí)行如下代碼時:
element.innerText = msg
此時對于變量 msg 而言,就觸發(fā)了一次 getter 操作,那么這樣的一次取值操作,同樣會導致 element 的 innerText 發(fā)生改變。
所以我們可以說:msg 的 getter 行為觸發(fā)了一次副作用,導致 element 的 innterText 發(fā)生了變化。
那么明確好了副作用的基本概念之后,那么大家想一想:副作用可能會有多個嗎?
答案是:可以的。
舉個簡單的例子:
<template> <div> <p>姓名:{{ obj.name }}</p> <p>年齡:{{ obj.age }}</p> </div></template><script> const obj = ref({ name: '張三', age: 30 }) obj.value = { name: '李四', age: 18 }</script>
在這樣的一個代碼中 obj.value 觸發(fā)了一次 setter 行為,但是會導致兩個 p 標簽的內容發(fā)生改變,也就是產(chǎn)生了兩次副作用。
根據(jù)本小節(jié)我們知道了:
根據(jù)前面的學習我們已經(jīng)知道了:
那么了解了這些內容之后,下來 vue3 的一個基本框架設計:
對于 vue3 而言,核心大致可以分為三大模塊:
我們以以下基本結構來描述一下三者之間的基本關系:
<template> <div>{{ proxyTarget.name }}</div></template><script>import { reactive } from 'vue'export default { setup() { const target = { name: '張三' } const proxyTarget = reactive(target) return { proxyTarget } }}</script>
在以上代碼中:
該方法是 reactivity 模塊對外暴露的一個方法
可以接收一個復雜數(shù)據(jù)類型,作為 Proxy (現(xiàn)在很多同學可能還不了解什么是 proxy ,沒有關系后面我們會詳細介紹它,現(xiàn)在只需要有個印象即可)的 被代理對象(target)
返回一個 Proxy 類型的 代理對象(proxyTarget)
當 proxyTarget 觸發(fā) setter 或 getter 行為時,會產(chǎn)生對應的副作用
以上就是 reactivity、runtime、compiler 三者之間的運行關系。
當然除了這三者之外, vue 還提供了很多其他的模塊,比如:SSR ,我們這里只是 概述了基本的運行邏輯。
本文鏈接:http://www.tebozhan.com/showinfo-26-97893-0.html這張圖,把 vue3 的源碼講清楚了?。。?/p>
聲明:本網(wǎng)頁內容旨在傳播知識,若有侵權等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 使用 Python 進行財務數(shù)據(jù)分析實戰(zhàn)
下一篇: 都用10年了,gRPC有什么不好的?