1|权衡的艺术

  1. 命令式和声明式的差异:
    1. 命令式更关注过程, 理论上可以做到极致优化,但是用户要承受巨大的心智负担
    2. 声明式更关注结果,够有效减轻用户的心智负担,但是性能上有一定牺牲
  2. 虚拟dom(js对象)的性能 : 声明式的更新性能消耗 = 找出差异的性能消耗+直接修改的性能消耗
  3. 运行时和编译时

2|框架设计的核心要素

  1. 提升用户的开发体验
  2. 控制框架代码的体积
  3. 良好的Tree-shaking:排除dead code的机制 ,基于ESM
    1. /*#__PURE__*/ // 注释
  1. 构建产物
  2. 特性开关
  3. 错误处理
  4. 良好的TypeScript类型支持

3|Vue.js 3 的设计思路

  1. 声明式的描述UI
  2. 渲染器
    1. 作用: 把虚拟DOM对象渲染为真实的DOM元素
    2. 工作原理: 递归地遍历虚拟DOM对象,并调用原生DOM API来完成真实DOM的创建
    3. 精髓: 后续的更新, 通过Diff算法找出变更点,并且只更新需要更新的内容
  3. 组件的本质
    1. 一组虚拟DOM元素的封装, 可以是一个返回虚拟DOM的函数,也可以是一个对象,但这个对象下必须要有一个函数来铲除组件要渲染的虚拟DOM
    2. 先获取组件要渲染的内容, 即subtree,最后递归地调用渲染器将subtree渲染出来
  4. 编译器:将模板编译为渲染函数

4|响应系统的作用于实现

  1. 响应式数据与副作用函数
    1. 副作用函数:会产生副作用的函数
    2. 值变化后,副作用函数自动重新执行的数据
  2. 响应式数据的基本实现
  3. 设计一个完善的响应系统
    1. 使用WeakMap配合构建新的”桶”结构,从而能够在响应式数据与副作用函数之间建立更加精确的联系
    2. WeakMap是弱引用的,它不影响垃圾回收器的工作
    3. 当用户代码对一个对象没有引用关系时,WeakMap不会阻止垃圾回收器回收该对象
  4. 分支切换与cleanup
    1. 分支切换导致的冗余副作用,会导致副作用函数进行不必要的更新
    2. 解决:每次副作用函数重新执行之前,清除上一次建立的响应联系,当副作用函数重新执行后,再次建立新的响应联系,新的响应联系中不存在冗余副作用
    3. 遍历Set叨叨无限循环
    4. 原因:ECMA规范”在调用forEach遍历Set集合时,如果一个值已经被访问过了,但这个值被删除并重新添加到集合,如果此时forEach遍历没有结束,那么这个值会被重新访问”
    5. 解决:简历一个新的Set用来遍历
  5. 嵌套的effect与effect栈
    1. 嵌套的副作用函数发生在组件嵌套的场景中,即父子组件关系
    2. 为了避免响应式数据与副作用函数之间建立的响应联系发生错乱,需要使用副作用函数栈来存储不同的副作用函数
    3. 当一个副作用函数执行完毕后,将其从栈中弹出
    4. 当读取响应式数据的时候,被读取的响应式数据只会与当前栈顶的副作用函数建立响应联系
  6. 避免无限递归循环
    1. 无限递归调用自身,会导致栈溢出
    2. 原因:对响应式数据的读取和设置操作发生在同一个副作用函数
    3. 解决:如果trigger触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
  7. 调度执行
    1. 可调度:当trigger动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机/次数以及方式
    2. 为了实现调度能力,为effect函数增加了第二个参数选项,可以通过scheduler选项指定调用器,从而通过调度器自行完成任务的调度
    3. 通过一个微任务队列对任务进行缓存,从而实现去重
  8. 计算属性computed与lazy
    1. 懒执行的副作用函数, 通过lazy选项使得副作用函数可以懒执行
    2. 被标记为懒执行的副作用函数可以通过手动方式让其执行
    3. 当读取计算属性的值时,只需手动执行副作用函数
    4. 依赖的响应式数据发生变化时,通过scheduler将dirty标记设置为true,代表脏
    5. 下次读取时,重新计算
  9. watch的实现原理
    1. 利用了副作用函数重新执行时的可调度性.
    2. 一个watch本身会创建一个effect,当effect依赖的响应数据发生变化时,会执行该effect的调度器函数,即scheduler
    3. scheduler理解为”回调”,所以只需在scheduler中执行用户通过watch函数注册的回调函数即可
  10. 立即执行的watch与回调执行时机
    1. 立即执行的watch: 通过添加新的immediate选项来实现
    2. 控制函数的回调执行时机:通过flush选项里指定回调函数具体的执行时机
  11. 过期的副作用
    1. 过期的副作用函数,会导致竞态问题,为watch增加第三个参数,onInvalidate
    2. onInvalidate是个函数,用来注册过期回调
    3. 每当watch的回调函数执行之前,会优先执行用户通过onInvalidate注册的过期回调
    4. 用户有机会在过期回调中将上已测的副作用标记为”过期”,从而解决竞态问题

5|非原始值的响应式方案

  1. 理解Proxy和Reflect
    1. Proxy可以为其他对象创建一个代理对象. 代理指的是对一个对象基本语义的代理
    2. 允许拦截并重新定义对一个对象的基本操作
    3. 实现代理中,会有访问器属性this只想的问题,需要使用Reflect.*方法并指定正确的receiver来解决
  2. JavaScript对象及Proxy的工作原理
    1. 常规对象
    2. 异质对象
  3. 如何代理Object
  4. 合理地触发响应
  5. 浅响应与深响应
  6. 只读和浅只读
  7. 代理数组
  8. 数组的索引与 length
  9. 遍历数组
  10. 数组的查找方法
  11. 隐式修改数组长度的原型方法
  12. 代理Set和Map
  13. 如何代理Set和Map
  14. 建立响应联系
  15. 避免污染原始数据
  16. 处理forEach
  17. 迭代器方法
  18. values与keys方法
  19. 总结 155

6|原始值的响应式方案

6.1 引入ref的概念 158
6.2 响应丢失问题 160
6.3 自动脱ref 164
6.4 总结 166

7|渲染器的设计

7.1 渲染器与响应系统的结合 170
7.2 渲染器的基本概念 172
7.3 自定义渲染器 175
7.4 总结 179

8|挂载与更新

8.1 挂载子节点和元素的属性 180
8.2 HTML Attributes与DOM Properties 182
8.3 正确地设置元素属性 184
8.4 class的处理 189
8.5 卸载操作 192
8.6 区分vnode的类型 195
8.7 事件的处理 196
8.8 事件冒泡与更新时机问题 201
8.9 更新子节点 204
8.10 文本节点和注释节点 209
8.11 Fragment 212
8.12 总结 215

9|简单Diff算法

  1. 减少DOM操作的性能开销
  2. DOM复用与key的作用
  3. 找到需要移动的元素
  4. 如何移动元素
    1. 拿新的一组自己诶但中的节点去旧的一组子节点中寻找可复用的节点
    2. 如果找到了, 则记录该节点的位置索引. 称为最大索引
    3. 在整个更新过程中,如果一个节点的索引值小于最大索引值,则说明改节点对应的真实dom元素需要移动
  5. 添加新元素
  6. 移除不存在的元素
  7. 总结

10|双端Diff算法

  1. 双端比较的原理
  2. 双端比较的优势
  3. 非理想状况的处理方式
  4. 添加新元素
  5. 移除不存在的元素
  6. 总结
    1. 在新旧两组子节点的四个断点之间分别进行比较 ,并试图找到可复用的节点
    2. 优势:对于同样的更新场景,执行的DOM移动操作次数更少

11|快速Diff算法

  1. 相同的前置元素和后置元素
  2. 判断是否需要进行DOM移动操作
  3. 如何移动元素
  4. 总结
    1. 节前了文本Diff中的预处理思路, 先处理新旧两组子节点中相同的前置节点和相同的后置节点.
    2. 当前置节点和后置节点全部处理完毕后,如果无法简单地通过挂载节点或者卸载已经不存在的节点来完成更新, 则需要根据节点的索引关系,构造出一个最长递增子序列
    3. 最长递增子序列所指向的节点即为不需要移动的节点

12| 组件的实现原理

  1. 渲染组件
    1. 使用虚拟节点的vnode.type属性来存储组件对象,渲染器根据虚拟节点的该属性的类型来判断它是否是组件
    2. 如果是.渲染器使用mountComponent和patchComponent完成组件的挂载和更新
  2. 组件状态与自更新
    1. 在组件挂在阶段,会为组件创建一个用于渲染其内容的副作用函数。该副作用函数会与组件自身的响应式数据建立响应练习。当组件自身的响应数据发生变化时,会出发渲染副作用函数重新执行,即重新渲染。
    2. 但与与默认情况下重新渲染是同步执行的,这导致无法对任务去重,因此在创建渲染副作用函数时,制定了自定义的调度器。该调度器的作用是,当组件自身的响应式数据发生变化时,将渲染复用函数缓冲到微任务队列中。有了缓冲队列,即可实现对显然任务的去重。从而避免无用的重新渲染导致的额外性能开销
  3. 组件实例与组件的生命周期
    1. 组件实例本质是个对象,包含了组件运行过程中的状态, 如组件是否挂载,组件自身的响应式数据,一级组件所渲染的内容(即substree)
    2. 有了组件实例后,在渲染副作用函数内,就可以根据组件实例上的状态标识,来决定应该进行全新的挂载,还是应该打补丁
  4. props与组件的被动更新
    1. 副作用的自更新所引起的子组件更新叫做子组件的被动更新
    2. 渲染上下文(renderContext), 它实际上是组件实例的代理对象
    3. 在渲染函数内访问组件实例所暴露的数据都是通过该代理对象实现的
  5. setup函数的作用与实现
    1. 组合式API
    2. 返回值
      1. 函数,将该函数作为组件的渲染函数
      2. 数据对象,则将该对象暴露到渲染上下文中
  6. 组件事件与emit的实现
    1. emit函数包含在setupContext对象中,可以通过emit函数发射组件的自定时事件。通过v-on指令为组件绑定的事件在经过编译后,会以onXxx的形式存储到props对象中。当emit函数执行时,会在props对象中寻找对应的事件处理函数并执行它
  7. 插槽的工作原理与实现
    1. 借鉴了Web Component中标签的改签
    2. 插槽内容会被编译为插槽函数,插槽函数的返回值就是向槽位填充的内容。
    3. 标签会被编译为插槽函数的调用,通过执行对应的插槽函数,得到外部向槽位条冲的内容(即虚拟DOM),最后将该内容渲染到槽位中
  8. 注册生命周期
    1. 通过onMounted注册的生命周期函数会被注册到当前组件实例的instance.mounted数组中。为了维护当前正在初始化的组件实例,定义了全局变量currentInstance,以及用来设置该变量的setCurrentInstance函数
  9. 总结

13| 异步组件与函数式组件

  1. 异步组件要解决的问题
  2. 异步组件的实现原理
  3. 封装defineAsyncComponent函数
  4. 超时与Error组件
  5. 延迟与Loading组件
  6. 重试机制
  7. 函数式组件
    1. 本质为函数,其内部实现逻辑可以复用有状态组件的实现逻辑
    2. 可以接受外部props
    3. 没有自身状态
    4. 没有生命周期
  8. 总结 335

14|内建组件和模块

  1. KeepAlive组件的实现原理
  2. 组件的激活与失活
  3. include和exclude
  4. 缓存管理
  5. Teleport组件的实现原理
  6. Teleport组件要解决的问题
  7. 实现Teleport组件
  8. Transition组件的实现原理
  9. 原生DOM的过渡
  10. 实现Transition组件
  11. 总结 360

15|编译器核心技术概览

  1. 模板DSL的编译器
  2. parser的实现原理与状态机
  3. 构造AST
  4. AST的转换与插件化架构
  5. 节点的访问
  6. 转换上下文与节点操作
  7. 进入与退出
  8. 将模板AST转为JavaScript AST
  9. 代码生成
  10. 总结 407

16| 解析器

  1. 文本模式及其对解析器的影响
  2. 递归下降算法构造模板AST
  3. 状态机的开启与停止
  4. 解析标签节点
  5. 解析属性
  6. 解析文本与解码HTML实体
  7. 解析文本
  8. 解码命名字符引用
  9. 解码数字字符引用
  10. 解析插值与注释
  11. 总结 451

17| 编译优化 453

  1. 动态节点收集与补丁标志
  2. 传统Diff算法的问题
  3. Block与PatchFlags
  4. 收集动态节点
  5. 渲染器的运行时支持
  6. Block树
  7. 带有v-if指令的节点
  8. 带有v-for指令的节点
  9. Fragment的稳定性
  10. 静态提升
  11. 预字符串化
  12. 缓存内联事件处理函数
  13. -once
  14. 总结

18| 同构渲染 474

  1. CSR、SSR以及同构渲染
  2. 将虚拟DOM渲染为HTML字符串
  3. 将组件渲染为HTML字符串
  4. 客户端激活的原理
  5. 编写同构的代码
  6. 组件的生命周期
  7. 使用跨平台的API
  8. 只在某一端引入模块
  9. 避免交叉请求引起的状态污染
  10. 组件
  11. 总结