整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

人间最后的告白-自书遗嘱写作规范及法律风险提示

人间最后的告白-自书遗嘱写作规范及法律风险提示

据国家统计局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.

发编程

  • asyncio - (Python标准库)异步I/O,事件循环,协程和任务。
  • multiprocessing——(Python标准库)基于进程的并行性。

身份验证

OAuth

  • authlib - JavaScript对象签名和加密草案实现。
  • Django-allauth - Django的认证应用程序,“只是工作”。

JWT

  • pyjwt - Python中的JSON Web令牌实现。
  • Python-JOSE -一个Python中的JOSE实现。

内置类增强

  • dataclass——(Python标准库)数据类。

缓存

  • Django-cache-machine - Django模型的自动缓存和失效时间。
  • django-cacheops——一个灵巧的ORM缓存,带有自动粒度事件驱动的失效时间。

配置文件

  • configparser - (Python标准库)INI文件解析器。

数据分析

  • pandas - Python数据分析库。
  • NumPy - Python科学计算库。
  • matplotlib - Python数据可视化库。

Database 数据库

  • pickleDB -一个简单轻量级的Python键值存储。

数据库驱动程序

mysql

  • mysqlclient - Python MySQL驱动程序。
  • PyMySQL - Python MySQL驱动程序。
  • sqlAlchemy - Python ORM数据库驱动。
  • peewee - Python轻量级ORM。

postgresql

  • psycopg2 - Python PostgreSQL驱动程序。

sqlite

  • sqlite3 - Python SQLite3驱动程序。

NOSQL

  • pymongo - Python MongoDB驱动程序。
  • redis - Python Redis驱动程序。
  • Redis -py - Redis的Python客户端。
  • pymemcache - Python Memcached驱动程序。
  • elasticsearch - Python Elasticsearch驱动程序。

时间和日期

  • arrow - Python中的时间与日期。
  • datetime - (Python标准库)日期和时间。
  • pytz - Python时区支持。
  • dateutil - Python日期和时间工具。
  • time - (Python标准库)时间。
  • pendulum - Python中的时间与日期。

DevOps的工具

  • ansible - Python的配置管理工具。
  • saltstack - Python的配置管理工具。
  • fabric——用于远程执行和部署的简单python工具。
  • psutil - Python进程和系统工具。
  • Paramiko库:SSH远程连接与文件传输.

环境管理

  • virtualenv - Python虚拟环境管理器。
  • virtualenvwrapper - Python虚拟环境管理器。
  • pipenv - Python虚拟环境管理器。
  • poetry - Python虚拟环境管理器。
  • conda - Python虚拟环境管理器。
  • pip - Python包管理器。

文件处理

  • pathlib - (Python标准库)路径对象。
  • shutil - (Python标准库)文件操作。
  • watchdog——用于监视文件系统事件的API和shell实用程序。

GUI开发

  • PyQt5 - Python的GUI库。
  • Tkinter——Tkinter是原生标准GUI包。

HTML解析

  • BeautifulSoup - Python的HTML和XML解析器。
  • lxml - Python的HTML和XML解析器。

HTTP client

  • requests - Python的HTTP库。
  • httpx - Python的HTTP库。
  • aiohttp - Python的异步HTTP库。
  • urllib3 - Python的HTTP库。

作业调度器

  • schedule -用于人类的Python作业调度。
  • celery -用于Python的分布式异步任务队列。
  • APScheduler -用于Python的作业调度器。
  • Django-schedule -一个Django日历应用程序。

日志记录

  • logging - (Python标准库)日志记录。
  • loguru - Python日志记录库。
  • logbook - Python日志记录库。

RESTful API

  • flask - Python的轻量级Web框架。
  • django-rest-framework——一个强大而灵活的构建web api的工具包。
  • fastapi - Python的现代、快速、高性能的web框架。
  • sanic - Python的异步web框架。

模板引擎

  • jinja2 - Python的模板引擎。

爬虫

  • scrapy - Python的爬虫框架。
  • requests - Python的HTTP库。
  • beautifulsoup4 - Python的HTML和XML解析器。
  • selenium - Python的Web浏览器自动化工具。

WSGI 服务器

  • gunicorn - Python的WSGI服务器。
  • uvicorn - Python的异步WSGI服务器。
  • uwsgi——这个项目旨在开发一个完整的栈来构建托管服务,用C语言编写。

者: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
  • 传统 Diff 算法的问题
  • Block 配合 PatchFlags 做到靶向更新
  • 节点不稳定 - Block Tree
  • v-if 的元素作为 Block
  • v-for 的元素作为 Block
  • 不稳定的 Fragment
  • 稳定的 Fragment
  • v-for 的表达式是常量
  • 多个根元素
  • 插槽出口
  • <template v-for>
  • 静态提升
  • 提升静态节点树
  • 元素不会被提升的情况
  • 元素带有动态的 key 绑定
  • 使用 ref 的元素
  • 使用自定义指令的元素
  • 提升静态 PROPS
  • 预字符串化
  • Cache Event handler
  • v-once
  • 手写高性能渲染函数
  • 几个需要记住的小点
  • Block Tree 是灵活的
  • 正确地使用 PatchFlags
  • NEED_PATCH
  • 该使用 Block 的地方必须用
  • 分支判断使用 Block
  • 列表使用 Block
  • 使用动态 key 的元素应该是 Block
  • 使用 Slot hint
  • 为组件正确地使用 DYNAMIC_SLOTS
  • 使用 $stable hint

Block Tree 和 PatchFlags

Block Tree 和 PatchFlags 是 Vue3 充分利用编译信息并在 Diff 阶段所做的优化。尤大已经不止一次在公开场合聊过思路,我们深入细节的目的是为了更好的理解,并试图手写出高性能的 VNode。

传统 Diff 算法的问题

“传统 vdom”的 Diff 算法总归要按照 vdom 树的层级结构一层一层的遍历(如果你对各种传统 diff 算法不了解,可以看我之前写《渲染器》这套文章,里面总结了三种传统 Diff方式),举个例子如下模板所示:

<div>
    <p class="foo">bar</p>
</div>

对于传统 diff 算法来说,它在 diff 这段 vnode(模板编译后的 vnode)时会经历:

  • Div 标签的属性 + children
  • <p> 标签的属性(class) + children
  • 文本节点:bar

但是很明显,这明明就是一段静态 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 简单的理解为一个数字标记,把这些数字赋予不同含义,例如:

  • 数字 1:代表节点有动态的 textContent(例如上面模板中的 p 标签)
  • 数字 2:代表元素有动态的 class 绑定
  • 数字 3:代表xxxxx

总之我们可以预设这些含义,最后体现在 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 是稳定的呢?

  • v-for 的表达式是常量<p v-for="n in 10"></p><!-- 或者 --><p v-for="s in 'abc'"></p>

由于 ?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 v-for>

如下模板所示:

<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 绑定

除了刚刚讲到的元素的所有子代节点必须都是静态的才会被提升之外还有哪些情况下会阻止提升呢?

如果一个元素有动态的 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 的元素

如果一个元素使用了 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 属性,这不是偶然,他们之间会建立联系,最终结果就是:

  • 当 useP1 为真时,refP1.value 引用 p 元素
  • 当 useP1 为假时,refP2.value 引用 p 元素

因此,即使 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 对象也有它自己生命周期:

  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeUnmount
  • unmounted

编写一个自定义指令:

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
    ]))
}

这有几个明显的优势:

  • 生成代码的体积减少
  • 减少创建 VNode 的开销
  • 减少内存占用

静态节点在运行时会通过 innerHTML 来创建真实节点,因此并非所有静态节点都是可以预字符串化的,可以预字符串化的静态节点需要满足以下条件:

  • 非表格类标签:caption 、thead、tr、th、tbody、td、tfoot、colgroup、col
  • 标签的属性必须是:
  • 标准 HTML attribute: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
  • 或 data-/aria- 类属性

当一个节点满足这些条件时代表这个节点是可以预字符串化的,但是如果只有一个节点,那么并不会将其字符串化,可字符串化的节点必须连续且达到一定数量才行:

  • 如果节点没有属性,那么必须有连续 20 个及以上的静态节点存在才行,例如:<div><p></p><p></p>… 20 个 p 标签<p></p></div>

或者在这些连续的节点中有 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 属性值时计算后的。

Cache Event handler

如下组件的模板所示:

<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(){/*...*/}  // 每次渲染函数执行,都是全新的函数
            })
        }
    }
}

v-once

这是 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 带来的性能提升来自两方面:

  • 1、VNode 的创建开销
  • 2、无用的 Diff 开销

但其实我们不通过模板编译,一样可以通过缓存 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])
        }
    }
}

手写高性能渲染函数

接下来我们将进入重头戏环节,我们尝试手写优化模式的渲染函数。

几个需要记住的小点:

  1. 一个 Block 就是一个特殊的 VNode,可以理解为它只是比普通 VNode 多了一个 dynamicChildren 属性
  2. createBlock() 函数和 createVNode() 函数的调用签名几乎相同,实际上 createBlock() 函数内部就是封装了 createVNode(),这再次证明 Block 就是 VNode。
  3. 在调用 createBlock() 创建 Block 前要先调用 openBlock() 函数,通常这两个函数配合逗号运算符一同出现:render() {return (openBlock(), createBlock('div'))}

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.CLASS - 当有动态的 class 绑定时使用
  • PatchFlags.STYLE - 当有动态的 style 绑定时使用,例如:createVNode(‘p’, { style: { color: refColor.value } }, ‘hello’, PatchFlags.STYLE)
  • PatchFlags.TEXT - 当有动态的文本节点是使用,例如:createVNode(‘p’, null, refText.value, PatchFlags.TEXT)
  • PatchFlags.PROPS - 当有除了 class 和 style 之外的其他动态绑定属性时,例如:createVNode(‘p’, { foo: refVal.value }, ‘hello’, PatchFlags.PROPS, [‘foo’])

这里需要注意的是,除了要使用 PatchFlags.PROPS 之外,还要提供第五个参数,一个数组,包含了动态属性的名字。

  • PatchFlags.FULL_PROPS - 当有动态 name 的 props 时使用,例如:createVNode(‘p’, { [refKey.value]: ‘val’ }, ‘hello’, PatchFlags.FULL_PROPS)

实际上使用 FULL_PROPS 等价于对 props 的 Diff 与传统 Diff 一样。其实,如果觉得心智负担大,我们大可以全部使用 FULL_PROPS,这么做的好处是:

  • 避免误用 PatchFlags 导致的 bug
  • 减少心智负担的同时,虽然失去了 props diff 的性能优势,但是仍然可以享受 Block Tree 的优势。

当同时存在多种更新,需要将 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:const App={setup() {const refOk=ref(true) return ()=> { return (openBlock(), createBlock('div', null, [ refOk.value // 这里使用 Block ? (openBlock(), createBlock('div', { key: 0 }, [/* ... */])) : (openBlock(), createBlock('div', { key: 1 }, [/* ... */])) ])) }}}

这里使用 Block 的原因我们在前文已经讲解过了,但这里需要强调的是,除了分支判断要使用 Block 之外,还需要为 Block 指定不同的 key 才行。

  • 列表使用 Block:

当我们渲染列表时,我们常常写出如下代码:

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
        ))
      ]))
    }
  }
}

总结一下:

  • 对于列表我们应该始终使用 Fragment,并作为 Block 的角色
  • 如果 Fragment 的 children 没有指定 key,那么应该为 Fragment 打上 PatchFlags.UNKEYED_FRAGMENT。相应的,如果指定了 key 就应该打上 PatchFlags.KEYED_FRAGMENT
  • 注意到在调用 openBlock(true) 时,传递了参数 true,这代表这个 Block 不会收集 dynamicChildren,因为无论是 KEYED 还是 UNKEYED 的 Fragment,在 Diff 它的 children 时都会回退传统 Diff 模式,因此不需要收集 dynamicChildren。

这里还有一点需要注意,在 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

正如在静态提升一节中所讲的,当元素使用动态 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…