據國家統計局2021年發布的第七次全國人口普查的數據顯示,我國60歲及以上人口的比重達到18.7%,有2.6億人,老年人口規模龐大,我國已邁入嚴重的人口老齡化階段[1]。隨著老齡化程度加深,我國已面臨代際財富如何傳承的嚴峻問題。
[1]羅知之、呂騫:國家統計局:60歲及以上人口比重達18.7% 老齡化進程明顯加快,http://finance.people.com.cn/n1/2021/0511/c1004-32100026.html,最后訪問時間:2022年6月5日。
遺囑是指人生前在法律允許的范圍內,按照法律規定的方式對其財產所作的個人處理,并于創立遺囑人死亡時發生效力的法律行為。
自書遺囑,又稱親筆遺囑,是《中華人民共和國民法典》(以下簡稱“《民法典》”)規定的六種法定遺囑形式之一,是指遺囑人生前在法律允許的范圍內,按照法律規定的方式對其財產所作的個人處分,并于遺囑人死亡時發生效力的法律行為。
1985年頒布的《中華人民共和國繼承法》(以下簡稱“《繼承法》”)(已失效)第十七條就規定了自書遺囑。
《民法典》第一千一百三十四條也沿用了《繼承法》的規定,即自書遺囑由遺囑人親筆書寫,簽名,注明年、月、日。
(一)形式要件
《民法典》第一千一百三十四條規定,自書遺囑的形式要件包括須由遺囑人親筆書寫,簽名,注明年、月、日。這三個形式要件必須同時滿足,一般而言,遺囑如果非親筆書寫或者沒有簽名或者沒有日期是無效的。
1.自書遺囑全部內容均由遺囑人親筆書寫。
2021年1月1日起《民法典》正式施行后,合法的遺囑形式包含:自書遺囑、代書遺囑、打印遺囑、錄音錄像遺囑、口頭遺囑、公證遺囑等6種法定遺囑形式。
與《繼承法》相比,《民法典》新增了打印遺囑和錄像遺囑兩種形式,且這兩種形式均有兩個以上見證人在場見證等要求,如自書遺囑采用打印或錄像的形式,則很有可能會被歸為打印遺囑和錄像遺囑兩種形式,從而需要滿足兩個以上見證人在場見證等要求。因此,自書遺囑須全文由遺囑人親筆書寫,既不能由他人代筆,也不能用打印機打印。
對于自書遺囑的內容,應當清晰準確,寫明遺囑人的身份、作出遺囑的時間地點、遺產具體信息、分配方案等。且遺囑內容盡量不做修改,否則可能因此產生效力上的爭議。
2.自書遺囑應由遺囑人親筆簽名。
遺囑人書寫完成遺囑內容后,在自書遺囑末尾應當寫明自己的全名。第一,遺囑人應親筆簽名,不能由他人代簽等;第二,所簽姓名為遺囑人身份證、戶口本上現在的姓名,非曾用名、藝名等;第三,如果遺囑為多頁,建議在每一頁簽字并捺手印。
3.自書遺囑應由遺囑人注明年、月、日。
自書遺囑應當注明年、月、日。立遺囑的時間是確定立遺囑人是否具有遺囑能力的依據,關系到遺囑的成立時間。
4.可以輔助全程錄像來佐證自書遺囑的真實性以及不存在其他導致遺囑無效的情形。錄像要保存好原始載體,不能隨意進行剪輯。
(二)實質要件
1.遺囑人設立遺囑時應當具有遺囑能力。
設立遺囑是民事法律行為,因此遺囑人設立遺囑時必須有相應的民事行為能力。根據《民法典》第一千一百四十三條的規定,只有完全民事行為能力人才具有遺囑能力,無民事行為能力人或者限制民事行為能力人所立的遺囑無效。
2.遺囑必須是遺囑人的真實意思表示。
自書遺囑是遺囑人生前的單方意思表示,無需他人的同意即可設立,故自書遺囑應當是遺囑人自己的真實意思表示,如果遺囑人受到脅迫、欺詐而設立遺囑的,則自書遺囑不具有法律效力。自書遺囑被偽造和篡改的,偽造的遺囑無效,被篡改的部分無效。
根據《民法典》第一千一百四十三條的規定遺囑必須表示遺囑人的真實意思,受欺詐、脅迫所立的遺囑無效。偽造的遺囑無效。遺囑被篡改的,篡改的內容無效。
3.自書遺囑的內容不得違反法律、行政法規的強制性規定。
遺囑人作出自書遺囑屬于民事法律行為。根據《民法典》第一百五十三條違反法律、行政法規的強制性規定的民事法律行為無效。但是,該強制性規定不導致該民事法律行為無效的除外。違背公序良俗的民事法律行為無效。因此,自書遺囑也應當符合法律規定,不得違反法律、行政法規的強制性規定。
(一)形式要件
1.自書遺囑應由遺囑人親筆書寫,簽名,注明年、月、日。
案例1:(2021)京民申4006號
裁判觀點:自書遺囑應由遺囑人親筆書寫,簽名,注明年、月、日。2012年7月1日《議定書》為蘇某4親筆書寫并有簽名、日期,故該《議定書》為蘇某4的自書遺囑。
案例2:(2021)京民申5622號
裁判觀點:蔡某2提供的蔡中河所立遺囑,經筆跡鑒定系蔡中河本人書寫并簽名注明日期,形式上符合自書遺囑的法定要件,能夠反映立遺囑人的真實意思表示,故該份遺囑中涉及蔡中河的財產部分應屬合法有效,涉及郜靜霞的財產部分無效。
2.自書遺囑應當注明年、月、日,若有筆誤或日期不準確等情況,可以結合其他證據進行補正,如:訂立遺囑時簽訂的筆錄、錄像、證人證言等。
案例3:(2021)京民申5445號
裁判觀點:本案爭議的焦點在于王炳宗所立遺囑的效力問題,王某4所提交的王炳宗親筆書寫并簽名的遺囑的落款處寫明了“立遺囑日期:203424”的字樣,因“203424”之前有“立遺囑日期”,并結合本案其他證據可以看出,該字樣是立遺囑人在書寫年份時存在筆誤。關于立遺囑的具體年份日期,結合律師見證談話筆錄王炳宗寫的日期以及訂立遺囑現場照片顯示的時間等證據,確定王炳宗訂立遺囑的日期是2013年4月24日,“203424”是日期“2013424”在書寫時的筆誤。綜上,王炳宗于2013年4月24日訂立的遺囑合法有效。
案例4:(2021)京民申4434號
裁判觀點:本院經審查認為,本案在再審審查階段的焦點系原某6遺囑效力的認定問題。本案中,原某2提交的原某6自書遺囑為其本人書寫、簽名,但僅注明了年、月,未注明具體日期,在遺囑形式上存在瑕疵。但原某2提交了唐某手寫的《證明書》、唐某、劉某、高某等保管人的證人證言,形成了完整的證據鏈條,對形式上的瑕疵進行了補正。二審法院根據遺囑要式的立法目的、本案的綜合情況,認定原某6的遺囑有效,并無不當。
案例5:(2021)京民申1788號
裁判觀點:對于陳某2所持曹恩秀遺囑,該遺囑形式上屬于自書遺囑,雖未注明日期,但在錄像中,曹恩秀手持本案自書遺囑,錄像資料中對當日時間亦有記載。
3.自書遺囑的形式要件僅有三個:遺囑應由遺囑人親筆書寫,簽名,注明年、月、日。是否有見證人、是否全程錄像等均不影響自書遺囑的效力。
案例6:(2021)京民申5445號
裁判觀點:王炳宗所訂立的遺囑是自書遺囑,全程錄像和見證人見證并非必要條件,且王某4對沒有全程錄像問題作了說明,是因為王炳宗書寫較慢,書寫的過程沒有錄像,錄像中亦反映了見證人書寫全過程,王某4女朋友的父親在場并不影響自書遺囑的效力。
(二)實質要件
1.結合自書遺囑以及其他證據,形成證據鏈,共同證明遺囑系遺囑人的真實意思表示。
案例7:(2021)京民申5080號
裁判觀點:本案中,韓某2提交了韓鴻林2018年7月11 日的自書遺囑,韓鴻林在該遺囑中將涉案房產中屬于韓鴻林的產權和部分繼承權由韓某2繼承,韓某2同時提交了兩份贈與書及現場照片、錄像等證據。一、二審法院根據查明的事實和在案證據,結合雙方的訴辯主張及舉證情況,認定審理中雖未對上述遺囑的真實性進行鑒定,但韓某2提交的證據佐證了遺囑的真實性,可與其他證據形成證據鏈,共同證明該遺囑系韓鴻林的真實意思表示,進而對韓鴻林的遺產按遺囑繼承處理并無不當,所作判決認定事實清楚,適用法律正確。
2.患有精神方面的疾病并不必然導致喪失行為能力,不能證明遺囑人不具有民事行為能力。
案例8:(2019)京民申5115號
裁判觀點:萬蔚在去世前留有自書遺囑,對于李某、段某3提出萬蔚在去世前患有抑郁癥多年,寫出大段遺囑不合常理的抗辯意見。經查,萬蔚雖然患有混合型焦慮和抑郁障礙,但患有精神方面的疾病并不必然導致喪失行為能力,且在萬蔚生前的醫院記錄中多次明確載明神清,精神可或神志清楚,查體合作等字樣,亦表明萬蔚在生前的精神狀態良好,具有完全的民事行為能力,因此李某、段某3的此項主張亦不能成立。
(三)其他
1.遺囑人以遺囑處分了屬于他人所有的財產,遺囑的這部分,應認定無效。
案例9:(2021)京民申6994號
2.被繼承人生前與他人訂有遺贈扶養協議,同時又立有遺囑的,繼承開始后,如果遺贈扶養協議與遺囑沒有抵觸,遺產分別按協議和遺囑處理;如果有抵觸,按協議處理,與協議抵觸的遺囑全部或部分無效。
案例10:(2021)京民申993號
(一)相關概念及規定【首次引入《民法典》】
在《民法典》“繼承編”第四章中,新增了遺產管理制度。遺產管理制度,是指在繼承開始后遺產交付前,有關主體依據法律規定或有關機關的指定,以維護遺產價值和遺產權利人合法利益為宗旨,對被繼承人的遺產實施管理、清算的制度。遺產管理人,則是對死者的財產進行妥善保存和管理分配的人。[2]其功能在于確保遺產得到妥善管理、順利分割,更好地維護繼承人、債權人利益。[3]
[2]最高人民法院民法典貫徹實施工作領導小組主編:《民法典婚姻家庭編繼承編理解與適用》,人民法院出版社2020年7月第1版。
[3]全國人民代表大會常務委員會副委員長王晨在2020年5月22日第十三屆全國人民代表大會第三次會議上所做的報告:《關于〈中華人民共和國民法典(草案)〉的說明》。
(二)產生、資質及職責
1.產生
《民法典》第一千一百四十五條規定,繼承開始后,遺囑執行人為遺產管理人;沒有遺囑執行人的,繼承人應當及時推選遺產管理人;繼承人未推選的,由繼承人共同擔任遺產管理人;沒有繼承人或者繼承人均放棄繼承的,由被繼承人生前住所地的民政部門或者村民委員會擔任遺產管理人。
第一千一百四十六條規定,對遺產管理人的確定有爭議的,利害關系人可以向人民法院申請指定遺產管理人。
因此,遺產管理人的產生應按照以下順序:
①由被繼承人指定遺囑執行人;
②繼承人推選;
③繼承人共同擔任;
④被繼承人生前住所地的民政部門或者村民委員會擔任。
2.資質
遺產的管理行為是一種民事法律行為,并且遺囑的執行涉及相關利害關系人的利益,因此遺產管理人須具備相應的民事行為能力。雖然現行法律沒有明確規定,但遺囑的執行屬于重大、復雜民事行為,故遺產管理人應具有完全民事行為能力。
此外,實務中通常會選擇律師作為遺產管理人,其作為專業法律人士,具有天然的優勢。第一,具有法律專業知識與豐富經驗,可以有效厘清遺產管理過程中涉及的婚姻、物權、知識產權等多項法律關系,并妥善處理相關爭議;第二,律師不同于繼承人或其他利害關系人,其地位是中立的,有助于公平、公正、有序地管理、分割遺產。
3.職責
《民法典》第一千一百四十七條簡單規定了遺產管理人的六項職責:
(一)清理遺產并制作遺產清單;
(二)向繼承人報告遺產情況;
(三)采取必要措施防止遺產毀損、滅失;
(四)處理被繼承人的債權債務;
(五)按照遺囑或者依照法律規定分割遺產;
(六)實施與管理遺產有關的其他必要行為。
此外,如果遺產管理人因故意或者重大過失造成繼承人、受遺贈人、債權人損害的,應當承擔民事責任。
(三)以案說法
由于遺產管理人在遺產分配中起主導作用,其權利行使的恰當與否,將極大程度地影響繼承人、債權人及受遺贈人三方的利益,以及遺產繼承過程的公正性與有效性,因此實務中一般被繼承人會選擇律師作為其遺產管理人,并簽訂委托合同以明確雙方權利義務。委托律師流程如下:
1.立遺囑人向律師說明要求、家庭情況等;
2.立遺囑人與律師簽訂委托協議,指定律師為遺囑執行人;
3.與律師詳細溝通遺囑內容,選擇合適的遺囑設立形式并設立遺囑;
經典案例:
李某和王某生育四個子女,兩人先后去世,未留遺囑,兩老人的子女、侄甥、好友等均向法院主張參與遺產分配,且各方遲遲無法達成一致意見。法院在案件審理過程中,充分考慮案情實際情況,經各方當事人推選,指定原告委托的律師成為該案遺產管理人,由遺產管理人對遺產進行統一清理、查明,法院在此基礎上高效且合理地將包括房產、存款、醫療保險、貴重物品在內的所有遺產及債權債務進行一一處理,成功化解各方矛盾。
附:遺囑執行人權利
除遺囑中另有特別規定外,遺囑執行人可執行下列事務:
(一)查明遺囑是否合法真實;
(二)清理遺產;
(三)管理遺產;
(四)訴訟代理;
(五)召集全體遺囑繼承人和受遺贈人,公開遺囑內容;
(六)按照遺囑內容將遺產最終轉移給遺囑繼承人和受遺贈人;
(七)排除各種執行遺囑的妨礙;
(八)請求繼承人賠償因執行遺囑受到的意外損害。
《中華人民共和國民法典》
第一百五十三條違反法律、行政法規的強制性規定的民事法律行為無效。但是,該強制性規定不導致該民事法律行為無效的除外。違背公序良俗的民事法律行為無效。
第一千一百三十四條自書遺囑由遺囑人親筆書寫,簽名,注明年、月、日。
第一千一百四十三條無民事行為能力人或者限制民事行為能力人所立的遺囑無效。遺囑必須表示遺囑人的真實意思,受欺詐、脅迫所立的遺囑無效。偽造的遺囑無效。遺囑被篡改的,篡改的內容無效。
第一千一百四十五條繼承開始后,遺囑執行人為遺產管理人;沒有遺囑執行人的,繼承人應當及時推選遺產管理人;繼承人未推選的,由繼承人共同擔任遺產管理人;沒有繼承人或者繼承人均放棄繼承的,由被繼承人生前住所地的民政部門或者村民委員會擔任遺產管理人。
第一千一百四十六條對遺產管理人的確定有爭議的,利害關系人可以向人民法院申請指定遺產管理人。
第一千一百四十七條遺產管理人應當履行下列職責:
(一)清理遺產并制作遺產清單;
(二)向繼承人報告遺產情況;
(三)采取必要措施防止遺產毀損、滅失;
(四)處理被繼承人的債權債務;
(五)按照遺囑或者依照法律規定分割遺產;
(六)實施與管理遺產有關的其他必要行為。
第一千一百四十八條遺產管理人應當依法履行職責,因故意或者重大過失造成繼承人、受遺贈人、債權人損害的,應當承擔民事責任。
第一千一百四十九條遺產管理人可以依照法律規定或者按照約定獲得報酬。
最高人民法院關于適用《中華人民共和國民法典》繼承編的解釋
(一)第二十七條自然人在遺書中涉及死后個人財產處分的內容,確為死者的真實意思表示,有本人簽名并注明了年、月、日,又無相反證據的,可以按自書遺囑對待。
北京市高級人民法院關于審理繼承糾紛案件若干疑難問題的解答(2018)
17. 遺囑的形式要件認定規則?
未嚴格按照法律規定的形式要件作出的遺囑,人民法院應認定無效。
簽署日期不全的自書遺囑應為無效。以遺書形式處分遺產的,如該遺書具備法律規定的自書遺囑形式要件的,應認定有效。
18. 打印遺囑的性質與效力?
繼承案件中當事人以打印遺囑系被繼承人自己制作為由請求確認打印遺囑為有效自書遺囑的,人民法院不予支持。但確有達到排除合理懷疑程度的證據表明打印遺囑由被繼承人全程制作完成,并具備自書遺囑形式要件的,可認定為有效自書遺囑。
打印遺囑由被繼承人以外的人制作的,應符合法律規定的代書遺囑形式要件。
參考文獻
1. 胡政.論自書遺囑形式要件的緩和[D].蘇州大學,
2020.DOI:10.27351/d.cnki.gszhu.2020.001362.
2. 張仕訓. 我國自書遺囑的效力研究[D].上海師范大學,2019.
3. 羅晨.民法典遺產管理人制度評析[J].現代交際,2021(23):251-253.
4. 陳振安.遺產管理人的法定訴訟擔當資格研究——以無人繼承情形為視角[J].浙江萬里學院學報,
2021,34(06):41-46.DOI:10.13777/j.cnki.issn1671-2250.2021.06.007.
5. 李敏. 民法典視角下遺產管理人制度研究[D].安徽大學,2021.
6. 楊璐嘉,廖惠敏,葉鑫欣.遺產管理人制度建構的“非訟法理”——以《民法典》繼承編為視角[J].法治論壇,2020(02):318-327.
者:HcySunYang https://www.zhihu.com/people/huo-chun-yang-77/posts
Vue3 的 Compiler 與 runtime 緊密合作,充分利用編譯時信息,使得性能得到了極大的提升。本文的目的告訴你 Vue3 的 Compiler 到底做了哪些優化,以及一些你可能希望知道的優化細節,在這個基礎上我們試著總結出一套手寫優化模式的高性能渲染函數的方法,這些知識也可以用于實現一個 Vue3 的 jsx babel 插件中,讓 jsx 也能享受優化模式的運行時收益,這里需要澄清的是,即使在非優化模式下,理論上 Vue3 的 Diff 性能也是要優于 Vue2 的。另外本文不包括 SSR 相關優化,希望在下篇文章總結。
篇幅較大,花費了很大的精力整理,對于對 Vue3 還沒有太多了解的同學閱讀起來也許會吃力,不妨先收藏,以后也許會用得到。
按照慣例 TOC:
Block Tree 和 PatchFlags 是 Vue3 充分利用編譯信息并在 Diff 階段所做的優化。尤大已經不止一次在公開場合聊過思路,我們深入細節的目的是為了更好的理解,并試圖手寫出高性能的 VNode。
傳統 Diff 算法的問題
“傳統 vdom”的 Diff 算法總歸要按照 vdom 樹的層級結構一層一層的遍歷(如果你對各種傳統 diff 算法不了解,可以看我之前寫《渲染器》這套文章,里面總結了三種傳統 Diff方式),舉個例子如下模板所示:
<div>
<p class="foo">bar</p>
</div>
對于傳統 diff 算法來說,它在 diff 這段 vnode(模板編譯后的 vnode)時會經歷:
但是很明顯,這明明就是一段靜態 vdom,它在組件更新階段是不可能發生變化的。如果能在 diff 階段跳過靜態內容,那就會避免無用的 vdom 樹的遍歷和比對,這應該就是最早的優化思路來源——跳過靜態內容,只對比動態內容。
Block 配合 PatchFlags 做到靶向更新
咱們先說 Block 再聊 Block Tree。現在思路有了,我們只希望對比非靜態的內容,例如:
<div>
<p>foo</p>
<p>{{ bar }}</p>
</div>
在這段模板中,只有 <p>{{ bar }}</p> 中的文本節點是動態的,因此只需要靶向更新該文本節點即可,這在包含大量靜態內容而只有少量動態內容的場景下,性能優勢尤其明顯。可問題是怎么做呢?我們需要拿到整顆 vdom 樹中動態節點的能力,其實可能沒有大家想像的復雜,來看下這段模板對應的傳統 vdom 樹大概長什么樣:
const vnode={
tag: 'div',
children: [
{ tag: 'p', children: 'foo' },
{ tag: 'p', children: ctx.bar }, // 這是動態節點
]
}
在傳統的 vdom 樹中,我們在運行時得不到任何有用信息,但是 Vue3 的 compiler 能夠分析模板并提取有用信息,最終體現在 vdom 樹上。例如它能夠清楚的知道:哪些節點是動態節點,以及為什么它是動態的(是綁定了動態的 class?還是綁定了動態的 style?亦或是其它動態的屬性?),總之編譯器能夠提取我們想要的信息,有了這些信息我們就可以在創建 vnode的過程中為動態的節點打上標記:也就是傳說中的 PatchFlags。
我們可以把 PatchFlags 簡單的理解為一個數字標記,把這些數字賦予不同含義,例如:
總之我們可以預設這些含義,最后體現在 vnode 上:
const vnode={
tag: 'div',
children: [
{ tag: 'p', children: 'foo' },
{ tag: 'p', children: ctx.bar, patchFlag: 1 /* 動態的 textContent */ },
]
}
有了這個信息,我們就可以在 vnode 的創建階段把動態節點提取出來,什么樣的節點是動態節點呢?帶有 patchFlag 的節點就是動態節點,我們將它提取出來放到一個數組中存著,例如:
const vnode={
tag: 'div',
children: [
{ tag: 'p', children: 'foo' },
{ tag: 'p', children: ctx.bar, patchFlag: 1 /* 動態的 textContent */ },
],
dynamicChildren: [
{ tag: 'p', children: ctx.bar, patchFlag: 1 /* 動態的 textContent */ },
]
}
dynamicChildren 就是我們用來存儲一個節點下所有子代動態節點的數組,注意這里的用詞哦:“子代”,例如:
const vnode={
tag: 'div',
children: [
{ tag: 'section', children: [
{ tag: 'p', children: ctx.bar, patchFlag: 1 /* 動態的 textContent */ },
]},
],
dynamicChildren: [
{ tag: 'p', children: ctx.bar, patchFlag: 1 /* 動態的 textContent */ },
]
}
如上 vnode 所示,div 節點不僅能收集直接動態子節點,它還能收集所有子代節點中的動態節點。為什么 div 節點這么厲害呢?因為它擁有一個特殊的角色:Block,沒錯這個 div 節點就是傳說中的 Block。一個 Block 其實就是一個 VNode,只不過它有特殊的屬性(其中之一就是 dynamicChildren)。
現在我們已經拿到了所有的動態節點,它們存儲在 dynamicChildren 中,因此在 diff 過程中就可以避免按照 vdom 樹一層一層的遍歷,而是直接找到 dynamicChildren 進行更新。除了跳過無用的層級遍歷之外,由于我們早早的就為 vnode 打上了 patchFlag,因此在更新 dynamicChildren 中的節點時,可以準確的知道需要為該節點應用哪些更新動作,這基本上就實現了靶向更新。
節點不穩定 - Block Tree
一個 Block 怎么也構不成 Block Tree,這就意味著在一顆 vdom 樹中,會有多個 vnode 節點充當 Block 的角色,進而構成一顆 Block Tree。那么什么情況下一個 vnode 節點會充當 block 的角色呢?
來看下面這段模板:
<div>
<section v-if="foo">
<p>{{ a }}</p>
</section>
<div v-else>
<p>{{ a }}</p>
</div>
</div>
假設只要最外層的 div 標簽是 Block 角色,那么當 foo 為真時,block 收集到的動態節點為:
cosnt block={
tag: 'div',
dynamicChildren: [
{ tag: 'p', children: ctx.a, patchFlag: 1 }
]
}
當 foo 為假時,block 的內容如下:
cosnt block={
tag: 'div',
dynamicChildren: [
{ tag: 'p', children: ctx.a, patchFlag: 1 }
]
}
可以發現無論 foo 為真還是假,block 的內容是不變的,這就意味什么在 diff 階段不會做任何更新,但是我們也看到了:v-if 的是一個 <section> 標簽,v-else 的是一個 <div> 標簽,所以這里就出問題了。實際上問題的本質在于 dynamicChildren 的 diff是忽略 vdom 樹層級的,如下模板也有同樣的問題:
<div>
<section v-if="foo">
<p>{{ a }}</p>
</section>
<section v-else> <!-- 即使這里是 section -->
<div> <!-- 這個 div 標簽在 diff 過程中被忽略 -->
<p>{{ a }}</p>
</div>
</section >
</div>
即使 v-else 的也是一個 <section> 標簽,但由于前后 DOM 樹的不穩定,也會導致問題。這時我們就思考,如何讓 DOM 樹的結構變穩定呢?
v-if 的元素作為 Block
如果讓使用了 v-if/v-else-if/v-else 等指令的元素也作為 Block 會怎么樣呢?我們拿如下模板為例:
<div>
<section v-if="foo">
<p>{{ a }}</p>
</section>
<section v-else> <!-- 即使這里是 section -->
<div> <!-- 這個 div 標簽在 diff 過程中被忽略 -->
<p>{{ a }}</p>
</div>
</section >
</div>
如果我們讓這兩個 section 標簽都作為 block,那么將構成一顆 block tree:
Block(Div)
- Block(Section v-if)
- Block(Section v-else)
父級 Block 除了會收集子代動態節點之外,也會收集子 Block,因此兩個 Block(section) 將作為 Block(div) 的 dynamicChildren:
cosnt block={
tag: 'div',
dynamicChildren: [
{ tag: 'section', { key: 0 }, dynamicChildren: [...]}, /* Block(Section v-if) */
{ tag: 'section', { key: 1 }, dynamicChildren: [...]} /* Block(Section v-else) */
]
}
這樣當 v-if 條件為真時,dynamicChildren 中包含的是 Block(section v-if),當條件為假時 dynamicChildren 中包含的是 Block(section v-else),在 Diff 過程中,渲染器知道這是兩個不同的 Block,因此會做完全的替換,這樣就解決了 DOM 結構不穩定引起的問題。而這就是 Block Tree。
v-for 的元素作為 Block
不僅 v-if 會讓 DOM 結構不穩定,v-for 也會,但是 v-for 的情況稍微復雜一些。思考如下模板:
<div>
<p v-for="item in list">{{ item }}</p>
<i>{{ foo }}</i>
<i>{{ bar }}</i>
</div>
假設 list 值由 ?[1 ,2]? 變為 ?[1]?,按照之前的思路,最外層的 <div> 標簽作為一個 Block,那么它更新前后對應的 Block Tree 應該是:
// 前
const prevBlock={
tag: 'div',
dynamicChildren: [
{ tag: 'p', children: 1, 1 /* TEXT */ },
{ tag: 'p', children: 2, 1 /* TEXT */ },
{ tag: 'i', children: ctx.foo, 1 /* TEXT */ },
{ tag: 'i', children: ctx.bar, 1 /* TEXT */ },
]
}
// 后
const nextBlock={
tag: 'div',
dynamicChildren: [
{ tag: 'p', children: item, 1 /* TEXT */ },
{ tag: 'i', children: ctx.foo, 1 /* TEXT */ },
{ tag: 'i', children: ctx.bar, 1 /* TEXT */ },
]
}
prevBlcok 中有四個動態節點,nextBlock 中有三個動態節點。這時候要如何進行 Diff?有的同學可能會說拿 dynamicChildren 進行傳統 Diff,這是不對的,因為傳統 Diff 的一個前置條件是同層級節點間的 Diff,但是 dynamicChildren 內的節點未必是同層級的,這一點我們之前就提到過。
實際上我們只需要讓 v-for 的元素也作為一個 Block 就可以了。這樣無論 v-for 怎么變化,它始終都是一個 Block,這保證了結構穩定,無論 v-for 怎么變化,這顆 Block Tree 看上去都是:
const block={
tag: 'div',
dynamicChildren: [
// 這是一個 Block 哦,它有 dynamicChildren
{ tag: Fragment, dynamicChildren: [/*.. v-for 的節點 ..*/] }
{ tag: 'i', children: ctx.foo, 1 /* TEXT */ },
{ tag: 'i', children: ctx.bar, 1 /* TEXT */ },
]
}
不穩定的 Fragment
剛剛我們使用一個 Fragment 并讓它充當 Block 的角色解決了 v-for 元素所在層級的結構穩定,但我們來看一下這個 Fragment 本身:
{ tag: Fragment, dynamicChildren: [/*.. v-for 的節點 ..*/] }
對于如下這樣的模板:
<p v-for="item in list">{{ item }}</p>
在 list 由 ?[1, 2]? 變成 ?[1]? 的前后,Fragment 這個 Block 看上去應該是:
// 前
const prevBlock={
tag: Fragment,
dynamicChildren: [
{ tag: 'p', children: item, 1 /* TEXT */ },
{ tag: 'p', children: item, 2 /* TEXT */ }
]
}
// 后
const prevBlock={
tag: Fragment,
dynamicChildren: [
{ tag: 'p', children: item, 1 /* TEXT */ }
]
}
我們發現,Fragment 這個 Block 仍然面臨結構不穩定的情況,所謂結構不穩定從結果上看指的是更新前后一個 block 的 dynamicChildren 中收集的動態節點數量或順序的不一致。這種不一致會導致我們沒有辦法直接進行靶向 Diff,怎么辦呢?其實對于這種情況是沒有辦法的,我們只能拋棄 dynamicChildren 的 Diff,并回退到傳統 Diff:即 DiffFragment 的 children 而非 dynamicChildren。
但需要注意的是 Fragment 的子節點(children)仍然可以是 Block:
const block={
tag: Fragment,
children: [
{ tag: 'p', children: item, dynamicChildren: [/*...*/], 1 /* TEXT */ },
{ tag: 'p', children: item, dynamicChildren: [/*...*/], 1 /* TEXT */ }
]
}
這樣,對于 <p> 標簽及其子代節點的 Diff 將恢復 Block Tree 的 Diff 模式。
穩定的 Fragment
既然有不穩定的 Fragment,那就有穩定的 Fragment,什么樣的 Fragment 是穩定的呢?
由于 ?10? 和 ?'abc'? 是常量,所有這兩個 Fragment 是不會變化的,因此它是穩定的,對于穩定的 Fragment 是不需要回退到傳統 Diff 的,這在性能上會有一定的優勢。
Vue3 不再限制組件的模板必須有一個根節點,對于多個根節點的模板,例如:
<template>
<div></div>
<p></p>
<i></i>
</template>
如上,這也是一個穩定的 Fragment,有的同學或許會想如下模板也是穩定的 Fragment 嗎:
<template>
<div v-if="condition"></div>
<p></p>
<i></i>
</template>
這其實也是穩定的,因為帶有 v-if 指令的元素本身作為 Block 存在,所以這段模板的 Block Tree 結構總是:
Block(Fragment)
- Block(div v-if)
- VNode(p)
- VNode(i)
對應到 VNode 應該類似于:
const block={
tag: Fragment,
dynamicChildren: [
{ tag: 'div', dynamicChildren: [...] },
{ tag: 'p' },
{ tag: 'i' },
],
PatchFlags.STABLE_FRAGMENT
}
無論如何,它的結構都是穩定的。需要注意的是這里的 ?PatchFlags.STABLE_FRAGMENT?,該標志必須存在,否則會回退傳統 ?Diff? 模式。
如下模板所示:
<Comp>
<p v-if="ok"></p>
<i v-else></i>
</Comp>
組件 <Comp> 內的 children 將作為插槽內容,在經過編譯后,應該作為 Block 角色的內容自然會是 Block,已經能夠保證結構的穩定了,例如如上代碼相當于:
render(ctx) {
return createVNode(Comp, null, {
default: ()=> ([
ctx.ok
// 這里已經是 Block 了
? (openBlock(), createBlock('p', { key: 0 }))
: (openBlock(), createBlock('i', { key: 1 }))
]),
_: 1 // 注意這里哦
})
}
既然結構已經穩定了,那么在渲染出口處 Comp.vue:
<template>
<slot/>
</template>
相當于:
render() {
return (openBlock(), createBlock(Fragment, null,
this.$slots.default() || []
), PatchFlags.STABLE_FRAGMENT)
}
這自然就是 STABLE_FRAGMENT,大家注意前面代碼中 ?_: 1? 這是一個編譯的 slot hint,當我們手寫優化模式的渲染函數時必須要使用這個標志才能讓 runtime 知道 slot是穩定的,否則會退出非優化模式。另外還有一個 $stable hint,在文末會講解。
如下模板所示:
<template>
<template v-for="item in list">
<p>{{ item.name }}</P>
<p>{{ item.age }}</P>
</template>
</template>
對于帶有 v-for 的 template 元素本身來說,它是一個不穩定的 Fragment,因為 list 不是常量。除此之外,由于 <template> 元素本身不渲染任何真實 DOM,因此如果它含有多個元素節點,那么這些元素節點也將作為 Fragment 存在,但這個 Fragment 是穩定的,因為它不會隨著 list 的變化而變化。
以上內容差不多就是 Block Tree 配合 PatchFlags 是如何做到靶向更新以及一些具體的思路細節了。
提升靜態節點樹
Vue3 的 Compiler 如果開啟了 hoistStatic 選項則會提升靜態節點,或靜態的屬性,這可以減少創建 VNode 的消耗,如下模板所示:
<div>
<p>text</p>
</div>
在沒有被提升的情況下其渲染函數相當于:
function render() {
return (openBlock(), createBlock('div', null, [
createVNode('p', null, 'text')
]))
}
很明顯,p 標簽是靜態的,它不會改變。但是如上渲染函數的問題也很明顯,如果組件內存在動態的內容,當渲染函數重新執行時,即使 p 標簽是靜態的,那么它對應的 VNode 也會重新創建。當開啟靜態提升后,其渲染函數如下:
const hoist1=createVNode('p', null, 'text')
function render() {
return (openBlock(), createBlock('div', null, [
hoist1
]))
}
這就實現了減少 VNode 創建的性能消耗。需要了解的是,靜態提升是以樹為單位的,如下模板所示:
<div>
<section>
<p>
<span>abc</span>
</p>
</section >
</div>
除了根節點的 div 作為 block 不可被提升之外,整個 <section> 元素及其子代節點都會被提升,因為他們是整棵樹都是靜態的。如果我們把上面代碼中的 abc 換成 {{ abc }},那么整棵樹都不會被提升。再看如下代碼:
<div>
<section>
{{ dynamicText }}
<p>
<span>abc</span>
</p>
</section >
</div>
由于 section 標簽內包含動態插值,因此以 section 為根節點的子樹就不會被提升,但是 p 標簽以及其子代節點都是靜態的,是可以被提升的。
元素不會被提升的情況
除了剛剛講到的元素的所有子代節點必須都是靜態的才會被提升之外還有哪些情況下會阻止提升呢?
如果一個元素有動態的 key 綁定那么它是不會被提升的,例如:
<div :key="foo"></div>
實際上一個元素擁有任何動態綁定都不應該被提升,那么為什么 key 會被單獨拿出來?實際上 key 和普通的 props 相比,它對于 VNode 的意義是不一樣的,普通的 props 如果它是動態的,那么只需要體現在 PatchFlags 上就可以了,例如:
<div>
<p :foo="bar"></p>
</div>
我們可以為 p 標簽打上 PatchFlags:
render(ctx) {
return (openBlock(), createBlock('div', null, [
createVNode('p', { foo: ctx }, null, PatchFlags.PROPS, ['foo'])
]))
}
注意到在創建 VNode 時,為其打上了 PatchFlags.PROPS,代表這個元素需要更新 PROPS,并且需要更新的 PROPS 的名字叫 foo。
h但是 key 本身具有特殊意hi義,它是 VNode(或元素) 的唯一標識,即使兩個元素除了 key 以外一切都相同,但這兩個元素仍然是不同的元素,對于不同的元素需要做完全的替換處理才行,而 PatchFlags 用于在同一個元素上的屬性補丁,因此 key 是不同于其它 props的。
正因為 key 的值是動態的可變的,因此對于擁有動態 key 的元素,它始終都應該參與到 diff 中并且不能簡單的打 PatchFlags 補丁標識,那應該怎么做呢?很簡單,讓擁有動態 key 的元素也作為 Block 即可,以如下模板為例:
<div>
<div :key="foo"></div>
</div>
它對應的渲染函數應該是:
render(ctx) {
return (openBlock(), createBlock('div', null, [
(openBlock(), createBlock('div', { key: ctx.foo }))
]))
}
Tips:手寫優化模式的渲染函數時,如果使用動態的 key,記得要使用 Block 哦,我們在后文還會總結。
如果一個元素使用了 ref,無論是否動態綁定的值,那么這個元素都不會被靜態提升,這是因為在每一次 patch 時都需要設置 ref 的值,如下模板所示:
<div ref="domRef"></div>
乍一看覺得這完全就是一個靜態元素,沒錯,元素本身不會發生變化,但由于 ref 的特性,導致我們必須在每次 Diff 的過程中重新設置 ref 的值,為什么呢?來看一個使用 ref 的場景:
<template>
<div>
<p ref="domRef"></p>
</div>
</template>
<script>
export default {
setup() {
const refP1=ref(null)
const refP2=ref(null)
const useP1=ref(true)
return {
domRef: useP1 ? refP1 : refP2
}
}
}
</script>
如上代碼所示,p 標簽使用了一個非動態的 ref 屬性,值為字符串 domRef,同時我們注意到 setupContext(我們把 setup 函數返回的對象叫做 setupContext) 中也包含了同名的 domRef 屬性,這不是偶然,他們之間會建立聯系,最終結果就是:
因此,即使 ref 是靜態的,但很顯然在更新的過程中由于 useP1 的變化,我們不得不更新 domRef,所以只要一個元素使用了 ref,它就不會被靜態提升,并且這個元素對應的 VNode 也會被收集到父 Block 的 dynamicChildren 中。
但由于 p 標簽除了需要更新 ref 之外,并不需要更新其他 props,所以在真實的渲染函數中,會為它打上一個特殊的 PatchFlag,叫做:PatchFlags.NEED_PATCH:
render() {
return (openBlock(), createBlock('div', null, [
createVNode('p', { ref: 'domRef' }, null, PatchFlags.NEED_PATCH)
]))
}
實際上一個元素如果使用除 v-pre/v-cloak 之外的所有 Vue 原生提供的指令,都不會被提升,使用自定義指令也不會被提升,例如:
<p v-custom></p>
和使用 key 一樣,會為這段模板對應的 VNode 打上 NEED_PATCH 標志。順便講一下手寫渲染函數時如何應用自定義指令,自定義指令是一種運行時指令,與組件的生命周期類似,一個 VNode 對象也有它自己生命周期:
編寫一個自定義指令:
const myDir: Directive={
beforeMount(el, binds) {
console.log(el)
console.log(binds.value)
console.log(binds.oldValue)
console.log(binds.arg)
console.log(binds.modifiers)
console.log(binds.instance)
}
}
使用該指令:
const App={
setup() {
return ()=> {
return h('div', [
// 調用 withDirectives 函數
withDirectives(h('h1', 'hahah'), [
// 四個參數分別是:指令、值、參數、修飾符
[myDir, 10, 'arg', { foo: true }]
])
])
}
}
}
一個元素可以綁定多個指令:
const App={
setup() {
return ()=> {
return h('div', [
// 調用 withDirectives 函數
withDirectives(h('h1', 'hahah'), [
// 四個參數分別是:指令、值、參數、修飾符
[myDir, 10, 'arg', { foo: true }],
[myDir2, 10, 'arg', { foo: true }],
[myDir3, 10, 'arg', { foo: true }]
])
])
}
}
}
提升靜態 PROPS
前面說過,靜態節點的提升以樹為單位,如果一個 VNode 存在非靜態的子代節點,那么該 VNode 就不是靜態的,也就不會被提升。但這個 VNode 的 props 卻可能是靜態的,這使我們可以將它的 props 進行提升,這同樣可以節約 VNode 對象的創建開銷,內存占用等,例如:
<div>
<p foo="bar" a=b>{{ text }}</p>
</div>
在這段模板中 p 標簽有動態的文本內容,因此不可以被提升,但 p 標簽的所有屬性都是靜態的,因此可以提升它的屬性,經過提升后其渲染函數如下:
const hoistProp={ foo: 'bar', a: 'b' }
render(ctx) {
return (openBlock(), createBlock('div', null, [
createVNode('p', hoistProp, ctx.text)
]))
}
即使動態綁定的屬性值,但如果值是常量,那么也會被提升:
<p :foo="10" :bar="'abc' + 'def'">{{ text }}</p>
'abc' + 'def' 是常量,可以被提升。
靜態提升的 VNode 節點或節點樹本身是靜態的,那么能否將其預先字符串化呢?如下模板所示:
<div>
<p></p>
<p></p>
...20 個 p 標簽
<p></p>
</div>
假設如上模板中有大量連續的靜態的 p 標簽,當采用了 hoist 優化時,結果如下:
cosnt hoist1=createVNode('p', null, null, PatchFlags.HOISTED)
cosnt hoist2=createVNode('p', null, null, PatchFlags.HOISTED)
... 20 個 hoistx 變量
cosnt hoist20=createVNode('p', null, null, PatchFlags.HOISTED)
render() {
return (openBlock(), createBlock('div', null, [
hoist1, hoist2, ...20 個變量, hoist20
]))
}
預字符串化會將這些靜態節點序列化為字符串并生成一個 Static 類型的 VNode:
const hoistStatic=createStaticVNode('<p></p><p></p><p></p>...20個...<p></p>')
render() {
return (openBlock(), createBlock('div', null, [
hoistStatic
]))
}
這有幾個明顯的優勢:
靜態節點在運行時會通過 innerHTML 來創建真實節點,因此并非所有靜態節點都是可以預字符串化的,可以預字符串化的靜態節點需要滿足以下條件:
當一個節點滿足這些條件時代表這個節點是可以預字符串化的,但是如果只有一個節點,那么并不會將其字符串化,可字符串化的節點必須連續且達到一定數量才行:
或者在這些連續的節點中有 5 個及以上的節點是有屬性綁定的節點:
<div>
<p id="a"></p>
<p id="b"></p>
<p id="c"></p>
<p id="d"></p>
<p id="e"></p>
</div>
這段節點的數量雖然沒有達到 20 個,但是滿足 5 個節點有屬性綁定。
這些節點不一定是兄弟關系,父子關系也是可以的,只要閾值滿足條件即可,例如:
<div>
<p id="a">
<p id="b">
<p id="c">
<p id="d">
<p id="e"></p>
</p>
</p>
</p>
</p>
</div>
預字符串化會在編譯時計算屬性的值,例如:
<div>
<p :id="'id-' + 1">
<p :id="'id-' + 2">
<p :id="'id-' + 3">
<p :id="'id-' + 4">
<p :id="'id-' + 5"></p>
</p>
</p>
</p>
</p>
</div>
在與字符串化之后:
const hoistStatic=createStaticVNode('<p id="id-1"></p><p id="id-2"></p>.....<p id="id-5"></p>')
可見 id 屬性值時計算后的。
如下組件的模板所示:
<Comp @change="a + b" />
這段模板如果手寫渲染函數的話相當于:
render(ctx) {
return h(Comp, {
onChange: ()=> (ctx.a + ctx.b)
})
}
很顯然,每次 render 函數執行的時候,Comp 組件的 props 都是新的對象,onChange 也會是全新的函數。這會導致觸發 Comp 組件的更新。
當 Vue3 Compiler 開啟 prefixIdentifiers 以及 cacheHandlers 時,這段模板會被編譯為:
render(ctx, cache) {
return h(Comp, {
onChange: cache[0] || (cache[0]=($event)=> (ctx.a + ctx.b))
})
}
這樣即使多次調用渲染函數也不會觸發 Comp 組件的更新,因為 Vue 在 patch 階段比對 props 時就會發現 onChange 的引用沒變。
如上代碼中 render 函數的 cache 對象是 Vue 內部在調用渲染函數時注入的一個數組,像下面這種:
render.call(ctx, ctx, [])
實際上,我們即使不依賴編譯也能手寫出具備 cache 能力的代碼:
const Comp={
setup() {
// 在 setup 中定義 handler
const handleChange=()=> {/* ... */}
return ()=> {
return h(AnthorComp, {
onChange: handleChange // 引用不變
})
}
}
}
因此我們最好不要寫出如下這樣的代碼:
const Comp={
setup() {
return ()=> {
return h(AnthorComp, {
onChang(){/*...*/} // 每次渲染函數執行,都是全新的函數
})
}
}
}
這是 Vue2 就支持的功能,v-once 是一個“很指令”的指令,因為它就是給編譯器看的,當編譯器遇到 v-once 時,會利用我們剛剛講過的 cache 來緩存全部或者一部分渲染函數的執行結果,例如如下模板:
<div>
<div v-once>{{ foo }}</div>
</div>
會被編譯為:
render(ctx, cache) {
return (openBlock(), createBlock('div', null, [
cache[1] || (cache[1]=h("div", null, ctx.foo, 1 /* TEXT */))
]))
}
這樣就緩存了這段 vnode。既然 vnode 已經被緩存了,后續的更新就都會讀取緩存的內容,而不會重新創建 vnode 對象了,同時在 Diff 的過程中也就不需要這段 vnode 參與了,因此你通常會看到編譯后的代碼更接近如下內容:
render(ctx, cache) {
return (openBlock(), createBlock('div', null, [
cache[1] || (
setBlockTracking(-1), // 阻止這段 VNode 被 Block 收集
cache[1]=h("div", null, ctx.foo, 1 /* TEXT */),
setBlockTracking(1), // 恢復
cache[1] // 整個表達式的值
)
]))
}
稍微解釋一下這段代碼,我們已經講解過何為 “Block Tree”,而 openBlock() 和 createBlock() 函數用來創建一個 Block。而 setBlockTracking(-1) 則用來暫停收集的動作,所以在 v-once 編譯生成的代碼中你會看到它,這樣使用 v-once 包裹的內容就不會被收集到父 Block 中,也就不參與 Diff 了。
所以,v-once 帶來的性能提升來自兩方面:
但其實我們不通過模板編譯,一樣可以通過緩存 VNode 來減少 VNode 的創建開銷:
const Comp={
setup() {
// 緩存 content
const content=h('div', 'xxxx')
return ()=> {
return h('section', content)
}
}
}
但這樣避免不了無用的 Diff 開銷,因為我們沒有使用 Block Tree 優化模式。
這里有必要提及的一點是:在 Vue2.5.18+ 以及 Vue3 中 VNode 是可重用的,例如我們可以在不同的地方多次使用同一個 VNode 節點:
const Comp={
setup() {
const content=h('div', 'xxxx')
return ()=> {
// 多次渲染 content
return h('section', [content, content, content])
}
}
}
接下來我們將進入重頭戲環節,我們嘗試手寫優化模式的渲染函數。
幾個需要記住的小點:
Block Tree 是靈活的:
在之前的介紹中根節點以 Block 的角色存在的,但是根節點并不必須是 Block,我們可以在任意節點開啟 Block:
setup() {
return ()=> {
return h('div', [
(openBlock(), createBlock('p', null, [/*...*/]))
])
}
}
這也是可以的,因為渲染器在 Diff 的過程中如果 VNode 帶有 dynamicChildren 屬性,會自動進入優化模式。但是我們通常會讓根節點充當 Block 角色。
正確地使用 PatchFlags:
PatchFlags 用來標記一個元素需要更新的內容,例如當元素有動態的 class 綁定時,我們需要使用 PatchFlags.CLASS 標記:
const App={
setup() {
const refOk=ref(true)
return ()=> {
return (openBlock(), createBlock('div', null, [
createVNode('p', { class: { foo: refOk.value } }, 'hello', PatchFlags.CLASS) // 使用 CLASS 標記
]))
}
}
}
如果使用了錯誤的標記則可能導致更新失敗,下面列出詳細的標記使用方式:
這里需要注意的是,除了要使用 PatchFlags.PROPS 之外,還要提供第五個參數,一個數組,包含了動態屬性的名字。
實際上使用 FULL_PROPS 等價于對 props 的 Diff 與傳統 Diff 一樣。其實,如果覺得心智負擔大,我們大可以全部使用 FULL_PROPS,這么做的好處是:
當同時存在多種更新,需要將 PatchFlags 進行按位或運算,例如:?PatchFlags.CLASS | PatchFlags.STYLE? 。
NEED_PATCH 標識
為什么單獨把這個標志拿出來講呢,它比較特殊,需要我們額外注意。當我們使用 ref 或 onVNodeXXX 等 hook 時(包括自定義指令),需要使用該標志,以至于它可以被父級 Block 收集,詳細原因我們在靜態提升一節里面講解過了:
const App={
setup() {
const refDom=ref(null)
return ()=> {
return (openBlock(), createBlock('div', null,[
createVNode('p',
{
ref: refDom,
onVnodeBeforeMount() {/* ... */}
},
null,
PatchFlags.NEED_PATCH
)
]))
}
}
}
該使用 Block 的地方必須用
在最開始的時候,我們講解了有些指令會導致 DOM 結構不穩定,從而必須使用 Block 來解決問題。手寫渲染函數也是一樣:
這里使用 Block 的原因我們在前文已經講解過了,但這里需要強調的是,除了分支判斷要使用 Block 之外,還需要為 Block 指定不同的 key 才行。
當我們渲染列表時,我們常常寫出如下代碼:
const App={
setup() {
const obj=reactive({ list: [ { val: 1 }, { val: 2 } ] })
return ()=> {
return (openBlock(), createBlock('div', null,
// 渲染列表
obj.list.map(item=> {
return createVNode('p', null, item.val, PatchFlags.TEXT)
})
))
}
}
}
這么寫在非優化模式下是沒問題的,但我們現在使用了 Block,前文已經講過為什么 v-for需要使用 Block 的原因,試想當我們執行如下語句修改數據:
obj.list.splice(0, 1)
這就會導致 Block 中收集的動態節點不一致,最終 Diff 出現問題。解決方案就是讓整個列表作為一個 Block,這時我們需要使用 Fragment:
const App={
setup() {
const obj=reactive({ list: [ { val: 1 }, { val: 2 } ] })
return ()=> {
return (openBlock(), createBlock('div', null, [
// 創建一個 Fragment,并作為 Block 角色
(openBlock(true), createBlock(Fragment, null,
// 在這里渲染列表
obj.list.map(item=> {
return createVNode('p', null, item.val, PatchFlags.TEXT)
}),
// 記得要指定正確的 PatchFlags
PatchFlags.UNKEYED_FRAGMENT
))
]))
}
}
}
總結一下:
這里還有一點需要注意,在 Diff Fragment 時,由于回退了傳統 Diff,我們希望盡快恢復優化模式,同時保證后續收集的可控性,因此通常會讓 Fragment 的每一個子節點都作為 Block 的角色:
const App={
setup() {
const obj=reactive({ list: [ { val: 1 }, { val: 2 } ] })
return ()=> {
return (openBlock(), createBlock('div', null, [
(openBlock(true), createBlock(Fragment, null,
obj.list.map(item=> {
// 修改了這里
return (openBlock(), createBlock('p', null, item.val, PatchFlags.TEXT))
}),
PatchFlags.UNKEYED_FRAGMENT
))
]))
}
}
}
最后再說一下穩定的 Fragment,如果你能確定列表永遠不會變化,例如你能確定 obj.list是不會變化的,那么你應該使用:PatchFlags.STABLE_FRAGMENT 標志,并且調用 openBlcok() 去掉參數,代表收集 dynamicChildren:
const App={
setup() {
const obj=reactive({ list: [ { val: 1 }, { val: 2 } ] })
return ()=> {
return (openBlock(), createBlock('div', null, [
// 調用 openBlock() 不要傳參
(openBlock(), createBlock(Fragment, null,
obj.list.map(item=> {
// 列表中的任何節點都不需要是 Block 角色
return createVNode('p', null, item.val, PatchFlags.TEXT)
}),
// 穩定的片段
PatchFlags.STABLE_FRAGMENT
))
]))
}
}
}
如上注釋所述。
正如在靜態提升一節中所講的,當元素使用動態 key 的時候,即使兩個元素的其他方面完全一樣,那也是兩個不同的元素,需要做替換處理,在 Block Tree 中應該以 Block 的角色存在,因此如果一個元素使用了動態 key,它應該是一個 Block:
const App={
setup() {
const refKey=ref('foo')
return ()=> {
return (openBlock(), createBlock('div', null,[
// 這里應該是 Block
(openBlock(), createBlock('p', { key: refKey.value }))
]))
}
}
}
這實際上是必須的,詳情查看 https://github.com/vuejs/vue-next/issues/938 。
使用 Slot hint
我們在“穩定的 Fragment”一節中提到了 slot hint,當我們為組件編寫插槽內容時,為了告訴 runtime:“我們已經能夠保證插槽內容的結構穩定”,則需要使用 slot hint:
render() {
return (openBlock(), createBlock(Comp, null, {
default: ()=> [
refVal.value
? (openBlock(), createBlock('p', ...))
? (openBlock(), createBlock('div', ...))
],
// slot hint
_: 1
}))
}
當然如果你不能保證這一點,或者覺得心智負擔大,那么就不要寫 hint 了。
使用 $stable hint
$stable hint 和之前講的優化策略不同,前文中的策略都是假設渲染器在優化模式下工作的,而 $stable 用于非優化模式,也就是我們平時寫的渲染函數。那么它有什么用呢?如下代碼所示(使用 tsx 演示):
export const App=defineComponent({
name: 'App',
setup() {
const refVal=ref(true)
return ()=> {
refVal.value
return (
<Hello>
{
{ default: ()=> [<p>hello</p>] }
}
</Hello>
)
}
}
})
如上代碼所示,渲染函數中讀取了 refVal.value 的值,建立了依賴收集關系,當修改 refVal 的值時,會觸發 <Hello> 組件的更新,但是我們發現 Hello 組件一來沒有 props 變化,二來它的插槽內容是靜態的,因此不應該更新才對,這時我們可以使用 $stable hint:
export const App=defineComponent({
name: 'App',
setup() {
const refVal=ref(true)
return ()=> {
refVal.value
return (
<Hello>
{
{ default: ()=> [<p>hello</p>], $stable: true } // 修改了這里
}
</Hello>
)
}
}
})
為組件正確地使用 DYNAMIC_SLOTS
當我們動態構建 slots 時,需要為組件的 VNode 指定 PatchFlags.DYNAMIC_SLOTS,否則將導致更新失敗。什么是動態構建 slots 呢?通常情況下是指:依賴當前 scope 變量構建的 slots,例如:
render() {
// 使用當前組件作用域的變量
const slots={}
// 常見的場景
// 情況一:條件判斷
if (refVal.value) {
slots.header=()=> [h('p', 'hello')]
}
// 情況二:循環
refList.value.forEach(item=> {
slots[item.name]=()=> [...]
})
// 情況三:動態 slot 名稱,情況二包含情況三
slots[refName.value]=()=> [...]
return (openBlock(), createBlock('div', null, [
// 這里要使用 PatchFlags.DYNAMIC_SLOTS
createVNode(Comp, null, slots, PatchFlags.DYNAMIC_SLOTS)
]))
}
如上注釋所述。
以上,不知道到達這里的同學有多少,Don’t stop learning…
*請認真填寫需求信息,我們會在24小時內與您取得聯系。