這張圖,把 vue3 的源碼講清楚了!??!
Hello,大家好,我是 Sunday。
最近一位同學(xué)在學(xué)習(xí) vue3 源碼的時(shí)候,把 vue 3 的大部分核心邏輯都整理到了腦圖之中:
整理的內(nèi)容非常詳細(xì)。應(yīng)該會(huì)對(duì)所有還在學(xué)習(xí) vue3 源碼的同學(xué)都有所幫助。所以分享給大家!
那么今天,咱們就借助這位同學(xué)的腦圖作為契機(jī),來(lái)為大家捋一捋 【Vue3 框架設(shè)計(jì)原理】(看完設(shè)計(jì)原理之后,再看腦圖收獲會(huì)更大哦~)
01:前言
在了解 Vue3 框架設(shè)計(jì)之前,我們需要做兩件事情,而這兩件事情也是今天的主要內(nèi)容。
- 我們需要同步并明確一些詞匯的概念,比如:聲明式、命令式、運(yùn)行時(shí)、編譯時(shí)...。這些詞匯將會(huì)在后面的框架設(shè)計(jì)中被經(jīng)常涉及到。
- 我們需要了解一些關(guān)于 前端框架 的一些基礎(chǔ)的概念??蚣艿脑O(shè)計(jì)原則,開(kāi)發(fā)者開(kāi)發(fā)體驗(yàn)原則。以此來(lái)幫助大家解決一些固有的疑惑,從而揭開(kāi) vue 神秘的面紗。
那么準(zhǔn)備好了?
我們開(kāi)始吧!
02:編程范式之命令式編程
針對(duì)于目前的前端開(kāi)發(fā)而言,主要存在兩種 編程范式:
- 命令式編程
- 聲明式編程
這兩種 范式 一般是相對(duì)來(lái)去說(shuō)的。
命令式
那么首先我們先來(lái)說(shuō)什么叫做 命令式。
具體例子:
張三的媽媽讓張三去買醬油。
那么張三怎么做的呢?
- 張三拿起錢(qián)
- 打開(kāi)門(mén)
- 下了樓
- 到商店
- 拿錢(qián)買醬油
- 回到家
以上的流程詳細(xì)的描述了,張三在買醬油的過(guò)程中,每一步都做了什么。那么這樣一種:詳細(xì)描述做事過(guò)程 的方式就可以被叫做 命令式。
那么如果把這樣的方式放到具體的代碼實(shí)現(xiàn)之中,又應(yīng)該怎么做呢?
我們來(lái)看以下這樣的一個(gè)事情:
在指定的 div 中展示 “hello world”
那么如果想要完成這樣的事情,通過(guò)命令式的方式我們?nèi)绾螌?shí)現(xiàn)呢?
我們知道命令式的核心在于:關(guān)注過(guò)程。
所以,以上事情通過(guò)命令式實(shí)現(xiàn)則可得出以下邏輯與代碼:
// 1. 獲取到指定的 div
const divEle = document.querySelector('#app')
// 2. 為該 div 設(shè)置 innerHTML 為 hello world
divEle.innerHTML = 'hello world'
該代碼雖然只有兩步,但是它清楚的描述了:完成這件事情,所需要經(jīng)歷的過(guò)程
那么假如我們所做的事情,變得更加復(fù)雜了,則整個(gè)過(guò)程也會(huì)變得更加復(fù)雜。
比如:
為指定的 div 的子元素 div 的子元素 p 標(biāo)簽,展示變量 msg
那么通過(guò)命令式完成以上功能,則會(huì)得出如下邏輯與代碼:
// 1. 獲取到第一層的 div
const divEle = document.querySelector('#app')
// 2. 獲取到它的子 div
const subDivEle = divEle.querySelector('div')
// 3. 獲取第三層的 p
const subPEle = subDivEle.querySelector('p')
// 4. 定義變量 msg
const msg = 'hello world'
// 5. 為該 p 元素設(shè)置 innerHTML 為 hello world
subPEle.innerHTML = msg
那么通過(guò)以上例子,相信大家可以對(duì)命令式的概念有了一個(gè)基礎(chǔ)的認(rèn)識(shí)。
最后做一個(gè)總結(jié),什么叫做命令式呢?
命令式是:關(guān)注過(guò)程 的一種編程范式,他描述了完成一個(gè)功能的 詳細(xì)邏輯與步驟。
03:編程范式之聲明式編程
當(dāng)了解完命令式之后,那么接下來(lái)我們就來(lái)看 聲明式 編程。
針對(duì)于聲明式而言,大家其實(shí)都是非常熟悉的了。
比如以下代碼,就是一個(gè)典型的 聲明式 :
<div>{{ msg }}</div>
對(duì)于這個(gè)代碼,大家是不是感覺(jué)有些熟悉?
沒(méi)錯(cuò),這就是 Vue 中非常常見(jiàn)的雙大括號(hào)語(yǔ)法。所以當(dāng)我們?cè)趯?xiě) Vue 模板語(yǔ)法 的時(shí)候,其實(shí)一直寫(xiě)的就是 聲明式 編程。
那么聲明式編程具體指的是什么意思呢?
還是以剛才的例子為例:
張三的媽媽讓張三去買醬油。
那么張三怎么做的呢?
- 張三拿起錢(qián)
- 打開(kāi)門(mén)
- 下了樓
- 到商店
- 拿錢(qián)買醬油
- 回到家
在這個(gè)例子中,我們說(shuō):張三所做的事情就是命令式。那么張三媽媽所做的事情就是 聲明式。
在這樣一個(gè)事情中,張三媽媽只是發(fā)布了一個(gè)聲明,她并不關(guān)心張三如何去買的醬油,只關(guān)心最后的結(jié)果。
所以說(shuō),所謂聲明式指的是:不關(guān)注過(guò)程,只關(guān)注結(jié)果 的范式。
同樣,如果我們通過(guò)代碼來(lái)進(jìn)行表示的話,以下例子:
為指定的 div 的子元素 div 的子元素 p 標(biāo)簽,展示變量 msg
將會(huì)得出如下代碼:
<div id="app">
<div>
<p>{{ msg }}</p>
</div>
</div>
在這樣的代碼中,我們完全不關(guān)心 msg 是怎么被渲染到 p 標(biāo)簽中的,我們所關(guān)心的只是:在 p 標(biāo)簽中,渲染指定文本而已。
最后做一個(gè)總結(jié),什么叫做聲明式呢?
聲明式是:關(guān)注結(jié)果 的一種編程范式,他 并不關(guān)心 完成一個(gè)功能的 詳細(xì)邏輯與步驟。(注意:這并不意味著聲明式不需要過(guò)程!聲明式只是把過(guò)程進(jìn)行了隱藏而已!)
04:命令式 VS 聲明式
那么在我們講解完成 命令式 和 聲明式 之后,很多同學(xué)肯定會(huì)對(duì)這兩種編程范式進(jìn)行一個(gè)對(duì)比。
是命令式好呢?還是聲明式好呢?
那么想要弄清楚這個(gè)問(wèn)題,那么我們首先就需要先搞清楚,評(píng)價(jià)一種編程范式好還是不好的標(biāo)準(zhǔn)是什么?
通常情況下,我們?cè)u(píng)價(jià)一個(gè)編程范式通常會(huì)從兩個(gè)方面入手:
- 性能
- 可維護(hù)性
那么接下來(lái)我們就通過(guò)這兩個(gè)方面,來(lái)分析一下命令式和聲明式。
性能
性能一直是我們?cè)谶M(jìn)行項(xiàng)目開(kāi)發(fā)時(shí)特別關(guān)注的方向,那么我們通常如何來(lái)表述一個(gè)功能的性能好壞呢?
我們來(lái)看一個(gè)例子:
為指定 div 設(shè)置文本為 “hello world”
那么針對(duì)于這個(gè)需求而言,最簡(jiǎn)單的代碼就是:
div.innerText = "hello world" // 耗時(shí)為:1
你應(yīng)該找不到比這個(gè)更簡(jiǎn)單的代碼實(shí)現(xiàn)了。
那么此時(shí)我們把這個(gè)操作的 耗時(shí) 比作 :1 。(PS:耗時(shí)越少,性能越強(qiáng))
然后我們來(lái)看聲明式,聲明式的代碼為:
<div>{{ msg }}</div> <!-- 耗時(shí)為:1 + n -->
<!-- 將 msg 修改為 hello world -->
那么:已知修改text最簡(jiǎn)單的方式是innerText ,所以說(shuō)無(wú)論聲明式的代碼是如何實(shí)現(xiàn)的文本切換,那么它的耗時(shí)一定是 > 1 的,我們把它比作 1 + n(對(duì)比的性能消耗)。
所以,由以上舉例可知:命令式的性能 > 聲明式的性能
可維護(hù)性
可維護(hù)性代表的維度非常多,但是通常情況下,所謂的可維護(hù)性指的是:對(duì)代碼可以方便的 閱讀、修改、刪除、增加 。
那么想要達(dá)到這個(gè)目的,說(shuō)白了就是:代碼的邏輯要足夠簡(jiǎn)單,讓人一看就懂。
那么明確了這個(gè)概念,我們來(lái)看下命令式和聲明式在同一段業(yè)務(wù)下的代碼邏輯:
// 命令式
// 1. 獲取到第一層的 div
const divEle = document.querySelector('#app')
// 2. 獲取到它的子 div
const subDivEle = divEle.querySelector('div')
// 3. 獲取第三層的 p
const subPEle = subDivEle.querySelector('p')
// 4. 定義變量 msg
const msg = 'hello world'
// 5. 為該 p 元素設(shè)置 innerHTML 為 hello world
subPEle.innerHTML = msg
// 聲明式
<div id="app">
<div>
<p>{{ msg }}</p>
</div>
</div>
對(duì)于以上代碼而言,聲明式 的代碼明顯更加利于閱讀,所以也更加利于維護(hù)。
所以,由以上舉例可知:**命令式的可維護(hù)性 < 聲明式的可維護(hù)性 **
小結(jié)一下
由以上分析可知兩點(diǎn)內(nèi)容:
- 命令式的性能 > 聲明式的性能
- 命令式的可維護(hù)性 < 聲明式的可維護(hù)性
那么雙方各有優(yōu)劣,我們?cè)谌粘i_(kāi)發(fā)中應(yīng)該使用哪種范式呢?
想要搞明白這點(diǎn),那么我們還需要搞明白更多的知識(shí)。
05:企業(yè)應(yīng)用的開(kāi)發(fā)與設(shè)計(jì)原則
企業(yè)應(yīng)用的設(shè)計(jì)原則,想要描述起來(lái)比較復(fù)雜,為什么呢?
因?yàn)閷?duì)于 不同的企業(yè)類型(大廠、中小廠、人員外包、項(xiàng)目外包),不同的項(xiàng)目類型(前臺(tái)、中臺(tái)、后臺(tái))來(lái)說(shuō),對(duì)應(yīng)的企業(yè)應(yīng)用設(shè)計(jì)原則上可能會(huì)存在一些差異。
所以我們這里所做的描述,會(huì)拋棄一些細(xì)微的差異,僅抓住核心的重點(diǎn)來(lái)進(jìn)行闡述。
無(wú)論什么類型的企業(yè),也無(wú)論它們?cè)陂_(kāi)發(fā)什么類型的項(xiàng)目,那么最關(guān)注的點(diǎn)無(wú)非就是兩個(gè):
- 項(xiàng)目成本
- 開(kāi)發(fā)體驗(yàn)
項(xiàng)目成本
項(xiàng)目成本非常好理解,它決定了一個(gè)公司完成“這件事”所付出的代價(jià),從而直接決定了這個(gè)項(xiàng)目是否是可以盈利的(大廠的燒錢(qián)項(xiàng)目例外)。
那么既然項(xiàng)目成本如此重要,大家可以思考一下,決定項(xiàng)目成本的又是什么?
沒(méi)錯(cuò)!就是你的 開(kāi)發(fā)周期。
開(kāi)發(fā)周期越長(zhǎng),所付出的人員成本就會(huì)越高,從而導(dǎo)致項(xiàng)目成本變得越高。
通過(guò)我們前面的分析可知,聲明式的開(kāi)發(fā)范式在 可維護(hù)性 上,是 大于 命令式的。
而可維護(hù)性從一定程度上就決定了,它會(huì)使項(xiàng)目的:開(kāi)發(fā)周期變短、升級(jí)變得更容易 從而大量節(jié)約開(kāi)發(fā)成本。
所以這也是為什么 Vue 會(huì)變得越來(lái)越受歡迎的原因。
開(kāi)發(fā)體驗(yàn)
決定開(kāi)發(fā)者開(kāi)發(fā)體驗(yàn)的核心要素,主要是在開(kāi)發(fā)時(shí)和閱讀時(shí)的難度,這個(gè)被叫做:心智負(fù)擔(dān)。
心智負(fù)擔(dān)可以作為衡量開(kāi)發(fā)難易度的一個(gè)標(biāo)準(zhǔn),心智負(fù)擔(dān)高則證明開(kāi)發(fā)的難度較高,心智負(fù)擔(dān)低則表示開(kāi)發(fā)的難度較低,開(kāi)發(fā)更加舒服。
那么根據(jù)我們之前所說(shuō),聲明式的開(kāi)發(fā)難度明顯低于命令式的開(kāi)發(fā)難度。
所以對(duì)于開(kāi)發(fā)體驗(yàn)而言,聲明式的開(kāi)發(fā)體驗(yàn)更好,也就是 心智負(fù)擔(dān)更低。
06:為什么說(shuō)框架的設(shè)計(jì)過(guò)程其實(shí)是一個(gè)不斷取舍的過(guò)程?
Vue 作者尤雨溪在一次演講中說(shuō)道:框架的設(shè)計(jì)過(guò)程其實(shí)是一個(gè)不斷取舍的過(guò)程 。
這代表的是什么意思呢?
想要搞明白這個(gè),那么再來(lái)明確一下之前說(shuō)過(guò)的概念:
- 命令式的性能 > 聲明式的性能
- 命令式的可維護(hù)性 < 聲明式的可維護(hù)性
- 聲明式的框架本質(zhì)上是由命令式的代碼來(lái)去實(shí)現(xiàn)的
- 企業(yè)項(xiàng)目開(kāi)發(fā)時(shí),大多使用聲明式框架
當(dāng)我們明確好了這樣的一個(gè)問(wèn)題之后,那么我們接下來(lái)來(lái)思考一個(gè)問(wèn)題:框架的開(kāi)發(fā)與設(shè)計(jì)原則是什么呢?
我們知道對(duì)于 Vue 而言,當(dāng)我們使用它的是通過(guò) 聲明式 的方式進(jìn)行使用,但是對(duì)于 Vue 內(nèi)部而言,是通過(guò) 命令式 來(lái)進(jìn)行的實(shí)現(xiàn)。
所以我們可以理解為:Vue 封裝了命令式的邏輯,而對(duì)外暴露出了聲明式的接口
那么既然如此,我們明知 命令式的性能 > 聲明式的性能 。那么 Vue 為什么還要選擇聲明式的方案呢?
其實(shí)原因非常的簡(jiǎn)單,那就是因?yàn)椋好钍降目删S護(hù)性 < 聲明式的可維護(hù)性 。
為指定的 div 的子元素 div 的子元素 p 標(biāo)簽,展示變量 msg
以這個(gè)例子為例。
對(duì)于開(kāi)發(fā)者而言,不需要關(guān)注實(shí)現(xiàn)過(guò)程,只需要關(guān)注最終的結(jié)果即可。
而對(duì)于 Vue 而言,他所需要做的就是:封裝命令式邏輯,同時(shí) 盡可能的減少性能的損耗!它需要在 性能 與 可維護(hù)性 之間,找到一個(gè)平衡。從而找到一個(gè) 可維護(hù)性更好,性能相對(duì)更優(yōu) 的一個(gè)點(diǎn)。
所以對(duì)于 Vue 而言,它的設(shè)計(jì)原則就是:在保證可維護(hù)性的基礎(chǔ)上,盡可能的減少性能的損耗。
那么回到我們的標(biāo)題:為什么說(shuō)框架的設(shè)計(jì)過(guò)程其實(shí)是一個(gè)不斷取舍的過(guò)程?
答案也就呼之欲出了,因?yàn)椋?/p>
我們需要在可維護(hù)性和性能之間,找到一個(gè)平衡點(diǎn)。在保證可維護(hù)性的基礎(chǔ)上,盡可能的減少性能的損耗。
所以框架的設(shè)計(jì)過(guò)程其實(shí)是一個(gè)不斷在 可維護(hù)性和性能 之間進(jìn)行取舍的過(guò)程
07:什么是運(yùn)行時(shí)?
在 Vue 3 的 源代碼 中存在一個(gè) runtime-core 的文件夾,該文件夾內(nèi)存放的就是 運(yùn)行時(shí) 的核心代碼邏輯。
runtime-core 中對(duì)外暴露了一個(gè)函數(shù),叫做 渲染函數(shù)render
我們可以通過(guò) render 代替 template 來(lái)完成 DOM 的渲染:
有些同學(xué)可能看不懂當(dāng)前代碼是什么意思,沒(méi)有關(guān)系,這不重要,后面我們會(huì)詳細(xì)去講。
<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 的項(xiàng)目中,我們可以通過(guò) tempalte 渲染 DOM 節(jié)點(diǎn),如下:
<template>
<div class="test">hello render</div>
</template>
但是對(duì)于 render 的例子而言,我們并沒(méi)有使用 tempalte,而是通過(guò)了一個(gè)名字叫做 render 的函數(shù),返回了一個(gè)不知道是什么的東西,為什么也可以渲染出 DOM 呢?
帶著這樣的問(wèn)題,我們來(lái)看:
我們知道在上面的代碼中,存在一個(gè)核心函數(shù):渲染函數(shù) render,那么這個(gè) render 在這里到底做了什么事情呢?
我們通過(guò)一段代碼實(shí)例來(lái)去看下:
假設(shè)有一天你們領(lǐng)導(dǎo)跟你說(shuō):
我希望根據(jù)如下數(shù)據(jù):
渲染出這樣一個(gè) div:
{
type: 'div',
props: {
class: test
},
children: 'hello render'
}
<div class="test">hello render</div>
那么針對(duì)這樣的一個(gè)需求你會(huì)如何進(jìn)行實(shí)現(xiàn)呢?大家可以在這里先思考一下,嘗試進(jìn)行一下實(shí)現(xiàn),然后我們?cè)倮^續(xù)往下看..........
那么接下來(lái)我們根據(jù)這個(gè)需求來(lái)實(shí)現(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é)點(diǎn)插入 body 中
document.body.appendChild(ele)
}
render(VNode)
</script>
在這樣的一個(gè)代碼中,我們成功的通過(guò)一個(gè) render 函數(shù)渲染出了對(duì)應(yīng)的 DOM,和前面的 render 示例 類似,它們都是渲染了一個(gè) vnode,你覺(jué)得這樣的代碼真是 妙極了!
但是你的領(lǐng)導(dǎo)用了一段時(shí)間你的 render 之后,卻說(shuō):天天這樣寫(xiě)也太麻煩了,每次都得寫(xiě)一個(gè)復(fù)雜的 vnode,能不能讓我直接寫(xiě) HTML 標(biāo)簽結(jié)構(gòu)的方式 你來(lái)進(jìn)行渲染呢?
你想了想之后,說(shuō):如果是這樣的話,那就不是以上 運(yùn)行時(shí) 的代碼可以解決的了!
沒(méi)錯(cuò)!我們剛剛所編寫(xiě)的這樣的一個(gè)“框架”,就是 運(yùn)行時(shí) 的代碼框架。
那么最后,我們做一個(gè)總結(jié):運(yùn)行時(shí)可以利用render 把 vnode 渲染成真實(shí) dom 節(jié)點(diǎn)。
08:什么是編譯時(shí)?
在剛才,我們明確了,如果只靠 運(yùn)行時(shí),那么是沒(méi)有辦法通過(guò) HTML 標(biāo)簽結(jié)構(gòu)的方式 的方式來(lái)進(jìn)行渲染解析的。
那么想要實(shí)現(xiàn)這一點(diǎn),我們就需要借助另外一個(gè)東西,也就是 編譯時(shí)。
Vue 中的編譯時(shí),更準(zhǔn)確的說(shuō)法應(yīng)該是 編譯器 的意思。它的代碼主要存在于 compiler-core 模塊下。
我們來(lái)看如下代碼:
<!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)建一個(gè) html 結(jié)構(gòu)
const html = `
<div class="test">hello compiler</div>
`
// 利用 compile 函數(shù),生成 render 函數(shù)
const renderFn = compile(html)
// 創(chuàng)建實(shí)例
const app = createApp({
// 利用 render 函數(shù)進(jìn)行渲染
render: renderFn
})
// 掛載
app.mount('#app')
</script>
</html>
對(duì)于編譯器而言,它的主要作用就是:把 template 中的 html 編譯成 render 函數(shù)。然后再利用 運(yùn)行時(shí) 通過(guò) render 掛載對(duì)應(yīng)的 DOM。
那么最后,我們做一個(gè)總結(jié):編譯時(shí)可以把html 的節(jié)點(diǎn),編譯成 render函數(shù)
09:運(yùn)行時(shí) + 編譯時(shí)
前面兩小節(jié)我們已經(jīng)分別了解了 運(yùn)行時(shí) 和 編譯時(shí),同時(shí)我們也知道了:vue 是一個(gè) 運(yùn)行時(shí)+編譯時(shí) 的框架!
vue 通過(guò) compiler 解析 html 模板,生成 render 函數(shù),然后通過(guò) runtime 解析 render,從而掛載真實(shí) dom。
那么看到這里可能有些同學(xué)就會(huì)有疑惑了,既然 compiler 可以直接解析 html 模板,那么為什么還要生成 render 函數(shù),然后再去進(jìn)行渲染呢?為什么不直接利用 compiler 進(jìn)行渲染呢?
即:為什么 vue 要設(shè)計(jì)成一個(gè) 運(yùn)行時(shí)+編譯時(shí)的框架呢?
那么想要理清楚這個(gè)問(wèn)題,我們就需要知道 dom 渲染是如何進(jìn)行的。
對(duì)于 dom 渲染而言,可以被分為兩部分:
- 初次渲染 ,我們可以把它叫做 掛載
- 更新渲染 ,我們可以把它叫做 打補(bǔ)丁
初次渲染
那么什么是初次渲染呢?
當(dāng)初始 div 的 innerHTML 為空時(shí),
<div id="app"></div>
我們?cè)谠?nbsp;div 中渲染如下節(jié)點(diǎn):
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
那么這樣的一次渲染,就是 初始渲染。在這樣的一次渲染中,我們會(huì)生成一個(gè) ul 標(biāo)簽,同時(shí)生成三個(gè) li 標(biāo)簽,并且把他們掛載到 div 中。
更新渲染
那么此時(shí)如果 ul 標(biāo)簽的內(nèi)容發(fā)生了變化:
<ul>
<li>3</li>
<li>1</li>
<li>2</li>
</ul>
li - 3 上升到了第一位,那么此時(shí)大家可以想一下:我們期望瀏覽器如何來(lái)更新這次渲染呢?
瀏覽器更新這次渲染無(wú)非有兩種方式:
- 刪除原有的所有節(jié)點(diǎn),重新渲染新的節(jié)點(diǎn)
- 刪除原位置的 li - 3,在新位置插入 li - 3
那么大家覺(jué)得這兩種方式哪一種方式更好呢?那么我們來(lái)分析一下:
- 首先對(duì)于第一種方式而言:它的好處在于不需要進(jìn)行任何的比對(duì),需要執(zhí)行 6 次(刪除 3 次,重新渲染 3 次)dom 處理即可。
- 對(duì)于第二種方式而言:在邏輯上相對(duì)比較復(fù)雜。他需要分成兩步來(lái)做:
- 對(duì)比 舊節(jié)點(diǎn) 和 新節(jié)點(diǎn) 之間的差異
- 根據(jù)差異,刪除一個(gè) 舊節(jié)點(diǎn),增加一個(gè) 新節(jié)點(diǎn)
那么根據(jù)以上分析,我們知道了:
- 第一種方式:會(huì)涉及到更多的 dom 操作
- 第二種方式:會(huì)涉及到 js 計(jì)算 + 少量的 dom 操作
那么這兩種方式,哪一種更快呢?我們來(lái)實(shí)驗(yàn)一下:
const length = 10000
// 增加一萬(wàn)個(gè)dom節(jié)點(diǎn),耗時(shí) 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')
// 增加一萬(wàn)個(gè) js 對(duì)象,耗時(shí) 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')
從結(jié)果可以看出,dom 的操作要比 js 的操作耗時(shí)多得多,即:dom** 操作比 js 更加耗費(fèi)性能**。
那么根據(jù)這樣的一個(gè)結(jié)論,回到我們剛才所說(shuō)的場(chǎng)景中:
- 首先對(duì)于第一種方式而言:它的好處在于不需要進(jìn)行任何的比對(duì),僅需要執(zhí)行 6 次(刪除 3 次,重新渲染 3 次)dom 處理即可。
- 對(duì)于第二種方式而言:在邏輯上相對(duì)比較復(fù)雜。他需要分成兩步來(lái)做:
對(duì)比 舊節(jié)點(diǎn) 和 新節(jié)點(diǎn) 之間的差異
根據(jù)差異,刪除一個(gè) 舊節(jié)點(diǎn),增加一個(gè) 新節(jié)點(diǎn)
根據(jù)結(jié)論可知:方式一會(huì)比方式二更加消耗性能(即:性能更差)。
那么得出這樣的結(jié)論之后,我們回過(guò)頭去再來(lái)看最初的問(wèn)題:為什么 vue 要設(shè)計(jì)成一個(gè) 運(yùn)行時(shí)+編譯時(shí)的框架呢?
答:
- 針對(duì)于 純運(yùn)行時(shí) 而言:因?yàn)椴淮嬖诰幾g器,所以我們只能夠提供一個(gè)復(fù)雜的 JS 對(duì)象。
- 針對(duì)于 純編譯時(shí) 而言:因?yàn)槿鄙龠\(yùn)行時(shí),所以它只能把分析差異的操作,放到 編譯時(shí) 進(jìn)行,同樣因?yàn)槭÷粤诉\(yùn)行時(shí),所以速度可能會(huì)更快。但是這種方式這將損失靈活性(具體可查看第六章虛擬 DOM ,或可點(diǎn)擊 這里 查看官方示例)。比如 svelte ,它就是一個(gè)純編譯時(shí)的框架,但是它的實(shí)際運(yùn)行速度可能達(dá)不到理論上的速度。
- 運(yùn)行時(shí) + 編譯時(shí):比如 vue 或 react 都是通過(guò)這種方式來(lái)進(jìn)行構(gòu)建的,使其可以在保持靈活性的基礎(chǔ)上,盡量的進(jìn)行性能的優(yōu)化,從而達(dá)到一種平衡。
10:什么是副作用
在 vue 的源碼中,會(huì)大量的涉及到一個(gè)概念,那就 副作用。
所以我們需要先了解一下副作用代表的是什么意思。
副作用指的是:當(dāng)我們 對(duì)數(shù)據(jù)進(jìn)行 setter 或 getter 操作時(shí),所產(chǎn)生的一系列后果。
那么具體是什么意思呢?我們分別來(lái)說(shuō)一下:
setter
setter 所表示的是 賦值 操作,比如說(shuō),當(dāng)我們執(zhí)行如下代碼時(shí) :
msg = '你好,世界'
這時(shí) msg 就觸發(fā)了一次 setter 的行為。
那么假如說(shuō),msg 是一個(gè)響應(yīng)性數(shù)據(jù),那么這樣的一次數(shù)據(jù)改變,就會(huì)影響到對(duì)應(yīng)的視圖改變。
那么我們就可以說(shuō):msg 的 setter 行為,觸發(fā)了一次副作用,導(dǎo)致視圖跟隨發(fā)生了變化。
getter
getter 所表示的是 取值 操作,比如說(shuō),當(dāng)我們執(zhí)行如下代碼時(shí):
element.innerText = msg
此時(shí)對(duì)于變量 msg 而言,就觸發(fā)了一次 getter 操作,那么這樣的一次取值操作,同樣會(huì)導(dǎo)致 element 的 innerText 發(fā)生改變。
所以我們可以說(shuō):msg 的 getter 行為觸發(fā)了一次副作用,導(dǎo)致 element 的 innterText 發(fā)生了變化。
副作用會(huì)有多個(gè)嗎?
那么明確好了副作用的基本概念之后,那么大家想一想:副作用可能會(huì)有多個(gè)嗎?
答案是:可以的。
舉個(gè)簡(jiǎn)單的例子:
<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>
在這樣的一個(gè)代碼中 obj.value 觸發(fā)了一次 setter 行為,但是會(huì)導(dǎo)致兩個(gè) p 標(biāo)簽的內(nèi)容發(fā)生改變,也就是產(chǎn)生了兩次副作用。
小節(jié)一下
根據(jù)本小節(jié)我們知道了:
- 副作用指的是:對(duì)數(shù)據(jù)進(jìn)行 setter 或 getter 操作時(shí),所產(chǎn)生的一系列后果
- 副作用可能是會(huì)有多個(gè)的。
11:Vue 3 框架設(shè)計(jì)概述
根據(jù)前面的學(xué)習(xí)我們已經(jīng)知道了:
- 什么是聲明式
- 什么是命令式
- 什么是運(yùn)行時(shí)
- 什么是編譯時(shí)
- 什么是運(yùn)行時(shí)+編譯時(shí)
- 同時(shí)也知道了 框架的設(shè)計(jì)過(guò)程本身是一個(gè)不斷取舍的過(guò)程
那么了解了這些內(nèi)容之后,下來(lái) vue3 的一個(gè)基本框架設(shè)計(jì):
對(duì)于 vue3 而言,核心大致可以分為三大模塊:
- 響應(yīng)性:reactivity
- 運(yùn)行時(shí):runtime
- 編譯器:compiler
我們以以下基本結(jié)構(gòu)來(lái)描述一下三者之間的基本關(guān)系:
<template>
<div>{{ proxyTarget.name }}</div>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
const target = {
name: '張三'
}
const proxyTarget = reactive(target)
return {
proxyTarget
}
}
}
</script>
在以上代碼中:
- 首先,我們通過(guò) reactive 方法,聲明了一個(gè)響應(yīng)式數(shù)據(jù)。
該方法是 reactivity 模塊對(duì)外暴露的一個(gè)方法
可以接收一個(gè)復(fù)雜數(shù)據(jù)類型,作為 Proxy (現(xiàn)在很多同學(xué)可能還不了解什么是 proxy ,沒(méi)有關(guān)系后面我們會(huì)詳細(xì)介紹它,現(xiàn)在只需要有個(gè)印象即可)的 被代理對(duì)象(target)
返回一個(gè) Proxy 類型的 代理對(duì)象(proxyTarget)
當(dāng) proxyTarget 觸發(fā) setter 或 getter 行為時(shí),會(huì)產(chǎn)生對(duì)應(yīng)的副作用
- 然后,我們?cè)?nbsp;tempalte 標(biāo)簽中,寫(xiě)入了一個(gè) div。我們知道這里所寫(xiě)入的 html 并不是真實(shí)的 html,我們可以把它叫做 模板,該模板的內(nèi)容會(huì)被 編譯器( compiler ) 進(jìn)行編譯,從而生成一個(gè) render 函數(shù)
- 最后,vue 會(huì)利用 運(yùn)行時(shí)(runtime) 來(lái)執(zhí)行 render 函數(shù),從而渲染出真實(shí) dom
以上就是 reactivity、runtime、compiler 三者之間的運(yùn)行關(guān)系。
當(dāng)然除了這三者之外, vue 還提供了很多其他的模塊,比如:SSR ,我們這里只是 概述了基本的運(yùn)行邏輯。