Vue源碼分析之虛擬DOM詳解
為什么需要虛擬dom?
虛擬DOM就是為了解決瀏覽器性能問(wèn)題而被設(shè)計(jì)出來(lái)的。例如,若一次操作中有10次更新DOM的動(dòng)作,虛擬DOM不會(huì)立即操作DOM,而是將這10次更新的diff內(nèi)容保存到本地一個(gè)JS對(duì)象中,最終將這個(gè)JS對(duì)象一次性attch到DOM樹(shù)上,再進(jìn)行后續(xù)操作,避免大量無(wú)謂的計(jì)算量。簡(jiǎn)單來(lái)說(shuō),可以把Virtual DOM 理解為一個(gè)簡(jiǎn)單的JS對(duì)象,并且最少包含標(biāo)簽名( tag)、屬性(attrs)和子元素對(duì)象( children)三個(gè)屬性。
- ----- 元素節(jié)點(diǎn): 元素節(jié)點(diǎn)更貼近于我們通常所看到的真實(shí)DOM節(jié)點(diǎn),他有描述節(jié)點(diǎn)標(biāo)簽名詞的tag屬性,描述節(jié)點(diǎn)屬性如class,attributes等的data屬性,有描述包含的子節(jié)點(diǎn)信息的children屬性等,由于元素節(jié)點(diǎn)所包含的情況相對(duì)而言比較復(fù)雜,源碼中沒(méi)有像前三種節(jié)點(diǎn)一樣直接寫(xiě)死。
- VNode的作用: 用js的計(jì)算性能來(lái)?yè)Q取操作真實(shí)DOM所消耗的性能,
- ----- VNode在Vue的整個(gè)虛擬DOM過(guò)程起到了什么作用呢。 其實(shí)VNode的作用是相當(dāng)大的,我們?cè)谝晥D渲染之前,把寫(xiě)好的template模板先編譯成VNode并緩存下來(lái),等到數(shù)據(jù)變化頁(yè)面需要重新渲染的時(shí)候,我們把數(shù)據(jù)發(fā)生變化后的生成的VNode與前一次緩存下來(lái)的VNode進(jìn)行對(duì)比,找出差異。然后有差異的VNode對(duì)應(yīng)的真實(shí)的DOM節(jié)點(diǎn)就是需要重新渲染的節(jié)點(diǎn),最后根據(jù)有差異的創(chuàng)建出來(lái)的DOM節(jié)點(diǎn)再插入到視圖中,最終完成一次視圖更新。就是再數(shù)據(jù)變化前后生成真實(shí)的DOM對(duì)應(yīng)的虛擬DOM節(jié)點(diǎn)
為什么要有虛擬DOM:
----- 就是以JS的計(jì)算性能來(lái)?yè)Q取操作真實(shí)DOM所消耗的性能,Vue是通過(guò)VNode類來(lái)實(shí)例化不同類型的虛擬DOM節(jié)點(diǎn),并且學(xué)習(xí)了不同類型節(jié)點(diǎn)生成的屬性的不同,所謂不同類型的節(jié)點(diǎn)其本質(zhì)還是一樣的,都屬VNode類的實(shí)例,只是實(shí)例化的時(shí)候傳入的參數(shù)不同罷了。
有了數(shù)據(jù)變化前后的VNode,我們才能進(jìn)行后續(xù)的DOM-Diff找出差異,最終做到只更新有差異的視圖,從而達(dá)到盡可能少的操作真實(shí)DOM的目的,以節(jié)省性能
----- 而找出更新有差異的DOM節(jié)點(diǎn),已達(dá)到最少操作真實(shí)DOM更新視圖的目的。而對(duì)比新舊兩份VNode并找出差異的過(guò)程就是所謂的DOM-Diff過(guò)程,DOM-Diff算法是整個(gè)虛擬DOM的核心所在。
Patch
在Vue中,把DOM-Diff過(guò)程就叫做patch過(guò)程,patch意思為補(bǔ)丁,一個(gè)思想:所謂舊的VNode(odlNode)就是數(shù)據(jù)變化之前屬于所對(duì)應(yīng)的虛擬DOM節(jié)點(diǎn),而新的NVode是數(shù)據(jù)變化之后將要渲染的視圖所對(duì)應(yīng)的虛擬DOM節(jié)點(diǎn),所以我們要以生成的新的VNode為基準(zhǔn),對(duì)比舊的oldVNode,如果新的VNode上有的節(jié)點(diǎn)而舊的oldVNode沒(méi)有,那么就在舊的oldVNode上加上去,如果新的VNode上沒(méi)有的節(jié)點(diǎn)而舊的oldVNode上有,那么就在舊的oldVnode上去掉。如果新舊Vnode節(jié)點(diǎn)都有,則以新的VNode為準(zhǔn),更新舊的oldVNode,從而讓新舊VNode相同。
整個(gè)patch:就是在創(chuàng)建節(jié)點(diǎn):新的VNode有,舊的沒(méi)有。就在舊的oldVNode中創(chuàng)建
刪除節(jié)點(diǎn):新的VNode中沒(méi)有,而舊的oldVNode有,就從舊的oldVNode中刪除
更新節(jié)點(diǎn):新的舊的都有,就以新的VNode為準(zhǔn),更新舊的oldVNode
更新子節(jié)點(diǎn)
/* 對(duì)比兩個(gè)子節(jié)點(diǎn)數(shù)組肯定是要通過(guò)循環(huán),外層循環(huán)newChildren,內(nèi)層循環(huán)oldCHildren數(shù)組,每循環(huán)外層 newChildren數(shù)組里的每一個(gè)子節(jié)點(diǎn),就去內(nèi)層oldChildren數(shù)組里找看有沒(méi)有與之相同的子節(jié)點(diǎn) */ for (let i = 0; i < newChildred.length; i++) { const newChild = newChildren[i] for (let j = 0; j < oldChildren.length; j++) { const oldChild = oldChildren[i] if (newChild === oldChild) { // ... } } }
那么以上這個(gè)過(guò)程將會(huì)存在一下四種情況
- 創(chuàng)建子節(jié)點(diǎn),如果newChildren里面的某個(gè)子節(jié)點(diǎn)在oldChildren里找不到與之相同的子節(jié)點(diǎn),那么說(shuō)明newChildren里面的這個(gè)子節(jié)點(diǎn)是之前沒(méi)有的,是需要此次新增的節(jié)點(diǎn),那就創(chuàng)建子節(jié)點(diǎn)
- 刪除子節(jié)點(diǎn),如果把newChildren里面的每一個(gè)子節(jié)點(diǎn)都循環(huán)完畢后,oldChildren還有未處理的子節(jié)點(diǎn),那就說(shuō)明未處理的子節(jié)點(diǎn)式需要被廢棄的,那就把這些節(jié)點(diǎn)刪除
- 移動(dòng)子節(jié)點(diǎn),如果newChildren里面的某個(gè)子節(jié)點(diǎn)在oldChildren里找到了與之相同的子節(jié)點(diǎn),但是所處的位置不同,這說(shuō)明此次變化的需要調(diào)整該子節(jié)點(diǎn)的位置,那以newChildren里的子節(jié)點(diǎn)1的位置為基準(zhǔn),調(diào)整oldChildren里該節(jié)點(diǎn)的位置,使之與在newChildren里的位置相同
- 更新節(jié)點(diǎn):如果newChildren里面的某個(gè)子節(jié)點(diǎn)在oldCHildren里找到了與之相同的子節(jié)點(diǎn),并且所處的位置也相同,那么就更新oldChildren里該節(jié)點(diǎn),使之與newChildren里的該節(jié)點(diǎn)相同
我們一再?gòu)?qiáng)調(diào)更新節(jié)點(diǎn)要以新Vnode為基準(zhǔn),然后操作舊的oldVnode,使之最后舊的oldVNode與新的VNode相同。
更新的時(shí)候分為三個(gè)部分:
如果VNode和oldVNode均為靜態(tài)節(jié)點(diǎn),
我們說(shuō)了,靜態(tài)節(jié)點(diǎn)無(wú)論數(shù)據(jù)發(fā)生任何變化都與它無(wú)關(guān),所以都為靜態(tài)節(jié)點(diǎn)的話則直接跳過(guò),無(wú)需處理
如果VNode是文本節(jié)點(diǎn)
如果VNode是我文本節(jié)點(diǎn)即表示這個(gè)節(jié)點(diǎn)內(nèi)只包含純文本,那么只需要看oldVNode是否也是文本節(jié)點(diǎn),如果是那就比較兩個(gè)文本是否不同,如果不用則把oldVNode里的文本改成跟VNode的文本一樣,如果oldVNode不是文本節(jié)點(diǎn),那么不論它是什么,直接調(diào)用setTextNode方法把他改成文本節(jié)點(diǎn),并且文本內(nèi)容跟VNode相同
如果VNode是元素節(jié)點(diǎn),則又細(xì)分以下兩種情況
- 該節(jié)點(diǎn)包含子節(jié)點(diǎn),那么此時(shí)要看舊的節(jié)點(diǎn)是否包含子節(jié)點(diǎn),如果舊的節(jié)點(diǎn)里包含了子節(jié)點(diǎn),那就需要遞歸對(duì)比更新子節(jié)點(diǎn)
- 如果舊的節(jié)點(diǎn)里不包含子節(jié)點(diǎn),那么這個(gè)舊節(jié)點(diǎn)可能是空節(jié)點(diǎn)或者文本節(jié)點(diǎn)
- 如果舊的節(jié)點(diǎn)是空節(jié)點(diǎn)就把新的節(jié)點(diǎn)里的子節(jié)點(diǎn)創(chuàng)建一份然后插入到舊的節(jié)點(diǎn)里面,
- 如果舊的節(jié)點(diǎn)是文本節(jié)點(diǎn),則把文本清空,然后把新的節(jié)點(diǎn)里的子節(jié)點(diǎn)創(chuàng)建一份然后插入到舊的節(jié)點(diǎn)里面
- 該節(jié)點(diǎn)不包含子節(jié)點(diǎn),如果該節(jié)點(diǎn)不包含子節(jié)點(diǎn),同時(shí)他又不是文本節(jié)點(diǎn),那就說(shuō)明該節(jié)點(diǎn)是個(gè)空節(jié)點(diǎn),那就好辦了,不管舊的節(jié)點(diǎn)之前里面有啥,直接清空即可
// 更新節(jié)點(diǎn) function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) { // vnode 與 oldVnode 是否完全一樣,如果是,退出程序 if (oldVnode === vnode) { return } const elm = vnode.elm = oldVnode.elm // vnode 與 oldVnode是否都是靜態(tài)節(jié)點(diǎn),如果是退出程序 if (isTrue(vnode.isStatic) && isTrue(vnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) { return } const oldCh = oldVnode.children const ch = vnode.children // vnode 有 text屬性,若沒(méi)有 if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { // 若都存在,判斷子節(jié)點(diǎn)是否相同,不同則更新子節(jié)點(diǎn) if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } // 若只有vnode的子節(jié)點(diǎn)的存在 else if (isDef(ch)) { /** * 判斷oldVnode是否有文本 * 若沒(méi)有,則把Vnode的子節(jié)點(diǎn)添加到真實(shí)DOM中 * 若有,則清空DOM中的文本,再把vnode的子節(jié)點(diǎn)添加到真實(shí)DOM中 * */ if (isDef(oldVnode.text)) nodeOps.setTextContext(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } // 如果只有oldnode的子節(jié)點(diǎn)存在 else if (isDef(oldCh)) { // 清空DOM中的所有子節(jié)點(diǎn) removeVnodes(elm, oldCh, 0, oldCh.length - 1) } // 若vnode和oldnode都沒(méi)有子節(jié)點(diǎn),但是oldnode中有文本 else if (isDef(oldVnode.text)) { nodeOps.setTextContext(elm, '') } // 上面兩個(gè)判斷一句話概括就是,如果vnode中既沒(méi)有text,也沒(méi)有子節(jié)點(diǎn),那么對(duì)應(yīng)的oldnode中有什么清空什么 } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContext(elm, vnode.text) } }
上面的我們了解了Vue的patch也就是DOM-DIFF算法,并且知道了在patch過(guò)程之中基本會(huì)干三件事,分別是創(chuàng)建節(jié)點(diǎn),刪除節(jié)點(diǎn)和更新節(jié)點(diǎn)。 創(chuàng)建節(jié)點(diǎn)和刪除節(jié)點(diǎn)比較簡(jiǎn)單,而更新節(jié)點(diǎn)因?yàn)橐幚砀鞣N可能出現(xiàn)的情況邏輯就比較復(fù)雜一些。 更新過(guò)程中九點(diǎn)Vnode可能都包含子節(jié)點(diǎn),對(duì)于子系欸但的對(duì)比更新會(huì)有額外的一些邏輯,那么本篇文章就來(lái)學(xué)習(xí)Vue中是如何對(duì)比子節(jié)點(diǎn)的
更新子節(jié)點(diǎn)
當(dāng)新的Vnode與舊的oldVnode都是元素節(jié)點(diǎn)并且都包含子節(jié)點(diǎn)的時(shí)候,那么這連個(gè)節(jié)點(diǎn)VNode實(shí)例上的chidlren屬性就是所包含的子節(jié)點(diǎn)數(shù)組,對(duì)比兩個(gè)子節(jié)點(diǎn)的通過(guò)循環(huán),外層循環(huán)newChildren數(shù)組,內(nèi)層循環(huán)oldChildren數(shù)組,每循環(huán)外層newChildren數(shù)組里的一個(gè)子節(jié)點(diǎn),,就去內(nèi)層oldChiildren數(shù)組里找看有沒(méi)有與之相同的子節(jié)點(diǎn)
. 創(chuàng)建子節(jié)點(diǎn)
創(chuàng)建子節(jié)點(diǎn)的位置應(yīng)該是在所有未處理節(jié)點(diǎn)之前,而并非所有已處理節(jié)點(diǎn)之后。 因?yàn)槿绻炎庸?jié)點(diǎn)插入到已處理后面,如果后續(xù)還要插入新節(jié)點(diǎn),那么新增子節(jié)點(diǎn)就亂了
. 移動(dòng)子節(jié)點(diǎn)
所有未處理結(jié)點(diǎn)之前就是我們要移動(dòng)的目的的位置
優(yōu)化更新子節(jié)點(diǎn):
前面我們介紹了當(dāng)新的VNode與舊的oldVNode都是元素節(jié)點(diǎn)并且都包含了子節(jié)點(diǎn)的時(shí)候,vue對(duì)子節(jié)點(diǎn)是先外層循環(huán)newChildren數(shù)組,再內(nèi)層循環(huán)oldChildren數(shù)組,每循環(huán)外層newChildren數(shù)組里的一個(gè)子節(jié)點(diǎn),就去內(nèi)層oldChildren數(shù)組里找看有沒(méi)有與之相同的子節(jié)點(diǎn),最后根據(jù)不同的情況做出不同的操作。這種還存在可優(yōu)化的地方,比如當(dāng)包含子節(jié)點(diǎn)數(shù)量較多的時(shí)候,這樣循環(huán)算法的時(shí)間復(fù)雜度就會(huì)變得很大,不利于性能提升。
方法:
- 先把newChildren數(shù)組里的所有未處理子節(jié)點(diǎn)的第一個(gè)子節(jié)點(diǎn)和oldChildren數(shù)組里所有未處理子節(jié)點(diǎn)的第一個(gè)子節(jié)點(diǎn)做對(duì),如果相同,那就直接進(jìn)入更新節(jié)點(diǎn)的操作;
- 如果不同,再把newChildren數(shù)組里所有未處理子節(jié)點(diǎn)的最后一個(gè)節(jié)點(diǎn)和oldChildren數(shù)組里所有未處理子節(jié)點(diǎn)的最后一個(gè)子節(jié)點(diǎn)做比對(duì),如果相同,那就直接進(jìn)入更新節(jié)點(diǎn)的操作;
- 如果不同,再把newChildren數(shù)組里所有未處理子節(jié)點(diǎn)的最后一個(gè)子節(jié)點(diǎn)和oldChildren數(shù)組里所有未處理子節(jié)點(diǎn)的第一個(gè)子節(jié)點(diǎn)做比對(duì),如果相同,那就直接進(jìn)入更新節(jié)點(diǎn)的操作,更新完后再將oldChildren數(shù)組里的該節(jié)點(diǎn)移動(dòng)到newChildren數(shù)組里節(jié)點(diǎn)相同的位置;如果不同,
- 再把newChildren數(shù)組里所有未處理子節(jié)點(diǎn)的第一個(gè)子節(jié)點(diǎn)和oldChildren數(shù)組里所有未處理子節(jié)點(diǎn)的最后一個(gè)子節(jié)點(diǎn)做比對(duì),如果相同,那就直接進(jìn)入更新節(jié)點(diǎn)的操作,更新后再將oldChildren數(shù)組里的該節(jié)點(diǎn)移動(dòng)到與newChildren數(shù)組里節(jié)點(diǎn)相同的位置;
- 最后四種情況都試完如果還不同,那就按照之前循環(huán)的方式來(lái)查找節(jié)點(diǎn)。
Vue為了避免雙重循環(huán)數(shù)據(jù)量大時(shí)間復(fù)雜度升高帶來(lái)的性能問(wèn)題,而選擇了從子節(jié)點(diǎn)數(shù)組中的四個(gè)特殊位置互相對(duì)比,分別是:新前和舊前,新后和舊后,新后和舊前,新前和舊后
在前面幾篇文章中,介紹了Vue中的虛擬DOM以及虛擬DOM的patch(DOM-Diff)過(guò)程,而虛擬DOM存在的必要條件是的現(xiàn)有VNode,那么VNode又是從哪里來(lái)的。 把用戶寫(xiě)的模板進(jìn)行編譯,就會(huì)產(chǎn)生VNode
模板編譯:
什么是模板編譯:把用戶template標(biāo)簽里面的寫(xiě)的類似于原生HTML的內(nèi)容進(jìn)行編譯,把原生HTML的內(nèi)容找出來(lái),再把非原生的HTML找出來(lái),經(jīng)過(guò)一系列的邏輯處理生成渲染函數(shù),也就是render函數(shù)的這一段過(guò)程稱之為模板編譯過(guò)程。 render函數(shù)會(huì)將模板內(nèi)容生成VNode
整體渲染流程,所謂渲染流程,就是把用戶寫(xiě)的類似于原生HTML的模板經(jīng)過(guò)一系列的過(guò)程最終反映到視圖中稱之為整個(gè)渲染流程,這個(gè)流程在上文中已經(jīng)說(shuō)到了。
抽象語(yǔ)法樹(shù)AST:
- 用戶在template標(biāo)簽中寫(xiě)的模板對(duì)Vue來(lái)說(shuō)就是一堆字符串,那么如何解析這一堆字符串并且從中提取出來(lái)元素的標(biāo)簽,屬性,變量插值 等有效信息呢,這就需要借助一個(gè)叫做抽象語(yǔ)法樹(shù)的東西。
抽象語(yǔ)法樹(shù)簡(jiǎn)稱語(yǔ)法樹(shù),是源代碼語(yǔ)法結(jié)構(gòu)的一種抽象表示,他以樹(shù)狀的形式表現(xiàn)編程語(yǔ)言的語(yǔ)法結(jié)構(gòu),樹(shù)上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu),之所以說(shuō)語(yǔ)法是抽象的,是因?yàn)檫@里的語(yǔ)法并不會(huì)表示出真實(shí)語(yǔ)法中出現(xiàn)的每個(gè)析姐,比如,嵌套括號(hào)被隱含在樹(shù)的結(jié)構(gòu)中,并沒(méi)有以節(jié)點(diǎn)的i形式呈現(xiàn),
具體流程:
- 將一堆字符串模板解析成抽象語(yǔ)法樹(shù)AST后,我們就可以對(duì)其進(jìn)行各種操作處理了,處理完畢之后的AST來(lái)生成render函數(shù),其具體三個(gè)流程可以分為以下三個(gè)階段
模板解析階段:將一堆模板字符串用正則表達(dá)式解析成抽象語(yǔ)法樹(shù)AST
優(yōu)化階段:編譯AST,找出其中的靜態(tài)節(jié)點(diǎn),并打上標(biāo)記
代碼生成階段: 將AST轉(zhuǎn)換成渲染函數(shù)
有了模板編譯,才有了虛擬DOM,才有了后續(xù)的視圖更新
總結(jié)
到此這篇關(guān)于Vue源碼分析之虛擬DOM的文章就介紹到這了,更多相關(guān)Vue虛擬DOM內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Vue響應(yīng)式原理與虛擬DOM實(shí)現(xiàn)步驟詳細(xì)講解
- React之虛擬DOM的實(shí)現(xiàn)原理
- Vue中簡(jiǎn)單的虛擬DOM是什么樣
- Vue虛擬dom被創(chuàng)建的方法
- vue 虛擬DOM快速入門(mén)
- react中的虛擬dom和diff算法詳解
- vue 虛擬DOM的原理
- 詳解操作虛擬dom模擬react視圖渲染
- 淺談React的最大亮點(diǎn)之虛擬DOM
- React?JSX深入淺出理解
- React jsx轉(zhuǎn)換與createElement使用超詳細(xì)講解
- React詳細(xì)講解JSX和組件的使用
- React基礎(chǔ)-JSX的本質(zhì)-虛擬DOM的創(chuàng)建過(guò)程實(shí)例分析
相關(guān)文章
vue實(shí)現(xiàn)tab標(biāo)簽(標(biāo)簽超出自動(dòng)滾動(dòng))
當(dāng)創(chuàng)建的tab標(biāo)簽超出頁(yè)面可視區(qū)域時(shí)自動(dòng)滾動(dòng)一個(gè)tab標(biāo)簽距離,并可手動(dòng)點(diǎn)擊滾動(dòng)tab標(biāo)簽,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05Vue.js項(xiàng)目前端多語(yǔ)言方案的思路與實(shí)踐
前端的國(guó)際化是一個(gè)比較常見(jiàn)的需求,但網(wǎng)上關(guān)于這一方面的直接可用的方案卻不多,這篇文章主要給大家介紹了關(guān)于Vue.js項(xiàng)目前端多語(yǔ)言方案的思路與實(shí)踐,需要的朋友可以參考下2021-07-07vue實(shí)現(xiàn)帶小數(shù)點(diǎn)的星星評(píng)分
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)帶小數(shù)點(diǎn)的星星評(píng)分,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09Vue使用vue-draggable 插件在不同列表之間拖拽功能
這篇文章主要介紹了使用vue-draggable 插件在不同列表之間拖拽,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-03-03vue路由攔截及頁(yè)面跳轉(zhuǎn)的設(shè)置方法
這篇文章主要介紹了vue路由攔截及頁(yè)面跳轉(zhuǎn)的設(shè)置方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-05-05前端使用print.js實(shí)現(xiàn)打印功能(基于vue)
最近新接了一個(gè)需求,想要在前端實(shí)現(xiàn)打印功能,下面這篇文章主要給大家介紹了關(guān)于前端使用print.js實(shí)現(xiàn)打印功能(基于vue)的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-05-05vue實(shí)現(xiàn)列表垂直無(wú)縫滾動(dòng)
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)列表垂直無(wú)縫滾動(dòng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04