# 框架设计概览

# 权衡的艺术

  • 命令式和声明式:

    命令式关注过程,声明式关注结果。Vue的内部实现是命令式,暴露给用户是声明式。

  • 性能和可维护性:

    性能上命令式更优,声明式多了查找变更的性能消耗。但是声明式可维护性高。

  • 虚拟DOM的性能:增量更新

    虚拟DOM为了最小化找出差异的性能消耗

    与innerHTML对比:

    • 创建页面差距不大
    • 更新页面时差异较大,innerHTML需要销毁旧DOM元素,全量创建新的DOM元素。
  • 运行时和编译时:

    • 纯运行时:灵活性高,性能和安全性低
    • 运行时+编译时:保留灵活性,尽可能优化性能
    • 纯编译时:性能高,但灵活性较低

# 框架设计的额核心要素

  • 提升用户的开发体验:控制台信息
  • 控制框架代码的体积:__DEV__区分开发环境
  • 做到良好的Tree-Shaking
/*#__PURE__*/  //告诉rollup.js没有副作用,可以放心删除
  • 框架的构建产物:IIFE、ESM
  • 特性开关:使用rollup.js的预定义常量插件实现
  • 错误处理:统一错误处理接口

# vue3的设计思路

  • 虚拟DOM就是用JS对象来描述DOM,渲染函数返回的结果就是虚拟DOM
  • 渲染器的作用是将虚拟DOM渲染成真实DOM
  • 组件的本质:组件就是一组虚拟DOM元素的封装,可以是函数或者JS对象
  • 编译器:作用是将template模板编译为渲染函数,并添加在script标签块的组件对象上

# 响应系统

# 响应式系统的作用与实现

  1. 副作用函数effect,执行会直接或间接影响其他函数的执行。√

  2. 通过拦截字段的读取和设置操作,读取时,将effect放置在桶中,设置时,拿出effect并执行。需要将一个effrct函数绑定在一个某个对象的某个属性上。√

  3. 数据结构使用了weakMap、Map和Set。weakMap的键为对象,值为对一个map,存储该对象所有属性的依赖;Map中的key为对象的属性,值为key相关的响应函数集合set。把副作用函数收集到桶里的逻辑封装为track,把触发副作用函数执行的逻辑封装到trigger中。变量activeEffect至关重要√

  4. 分支切换带来的effect函数遗留问题:为effect添加依赖集合deps,每次执行时先把所有的相关依赖删除,这部分逻辑封装在cleanup里面,trigger中用一个新的set防止一直循环√

  5. 嵌套的effect和effect栈,避免相互影响√

  6. 避免无限递归循环,在同一个effect中读取和设置时会触发,trigger前添加守卫。√

  7. 调度执行,让用户控制trigger的执行方式、执行次数执行时机

  8. 计算属性computed和lazy,添加值缓存、懒执行的副作用函数√

  9. watch的实现原理,基于调度器实现递归地watch对象的所有属性获取旧值和新值

  10. 立即执行的watch和回调执行时机,immediate指定回调是否立即执行,flush指定同步和异步执行√

  11. 副作用过期,OnInvalidate注册过期回调,通过闭包exppired实现的

# 非原始值的响应式方案

# 基本原理

  1. Proxy能够拦截对象的基本操作,例如读取、复制、函数调用。
  2. Reflect.get(),可以绕过对象的getter函数获取属性值。不改变语言原本行为的情况下进行扩展和定制,是一种元编程
Reflect.get(target, propertyKey [, receiver])

target为对象,propertyKey为属性名,receiver表示 getter 函数的执行上下文对象

  1. 常规对象和异质对象。函数对象会部署[[call]]内部方法,普通对象则不会。

    常规对象:内部方法用ECMA规范10.1给出的定义实现、[[call]]通过10.2.1实现、[[construct]]10.2.2实现;其他的都是异质对象,如Proxy

# 代理对象

要重点处理for...in的读取、添加、更新、删除操作。

读取操作:不同的读取,根据ECMA规范中使用的基本方法区分

  • 访问属性:obj.foo,用拦截get
  • 判断是否存在指定key:key in obj,has拦截函数
  • for...in循环,ownkeys拦截

设置/添加操作:

  • set拦截函数,区分添加和更新

删除操作:

  • delete p.foo

合理的触发响应:

  • 值是否真的发生了变化
  • 屏蔽原型带来的更新

**深响应与浅响应:**是否递归地把所有子属性也设置为可响应

**只读与浅只读:**递归实现,拦截设置和删除操作

# 代理数组(异质对象)

读取操作

  • 通过索引访问:arr[0]
  • 访问数组长度: arr.length
  • for...in
  • for...of
  • 原型方法,concat/join/every/some/find/findIndex/includes

设置操作

  • 索引修改:arr[0]=0
  • 修改长度:arr.length = 0
  • 栈方法
  • 原型方法:split/fill/sort
  1. 数组的索引与length

    1. 设置索引时可能会修改length,设置length时可能会影响数组元素
  2. 遍历数组

    • for...in和遍历对象时相同,使用length属性作为追踪的key
    • for...of遍历可迭代对象的,数组内建了Symbol.iterator方法的实现
  3. 数组的查找方法,incodes/indexOf/lastIndexOf,通过apply实现的,将this指向代理对象。实现了对原始对象和代理对象的查找

  4. 栈方法通过设置是否追踪,实现对track的屏蔽

# 代理Set和Map(有空了再看)

# 原始值的响应式方案

  1. 使用一个非原始值包裹原始值,
// 封装一个ref函数
fuction ref(val){
    // 内部创建一个包裹对象
	const wrapper = {
		value:val
	}
    // 将包裹对象变成响应式数据
	return reactive(wrapper)
}

区分包裹对象和非原始值的响应式对象

  1. 响应丢失问题:...展开运算符,将响应式数据转换成类似于ref结构的数据
  2. 自动脱ref去掉.value,通过Proxy代理实现,如果访问的属性是ref,直接返回它的value

# 渲染器renderer

# 渲染器的设计

利用响应系统,自动调用渲染器,实现页面的渲染和更新。

渲染器吧虚拟DOM节点渲染成真实DOM节点的过程叫做挂载

自定义渲染器,将依赖于浏览器的API进行抽离。这样可以在NodeJs中执行。

# 挂载与更新

  1. 挂载子节点和元素的属性,setAttribute

  2. HTML attributes作用是DOM properties的初始值

  3. 正确地设置元素属性:优先使用DOM properties,封装为一个pathProps函数

  4. class的特殊处理,结构转换,el.className/el.classList/setAttribute性能更好

  5. 卸载操作:渲染null空内容时发生

    innerHTML清空的缺点:

    • 不能正确的调用子组件的生命周期钩子函数
    • 不会触发自定义指令的钩子函数
    • 不会移除DOM元素上的事件处理函数

让vnode.el引用真实的DOM元素,并封装为unmount函数

  1. 区分vnode的类型
  2. 事件的处理:约定以on开头的属性是事件;伪造一个事件处理函数invoker,把它的值指向具体的事件,避免更新事件时移除上一个事件,单一事件可以有多个处理函数,使用数组
  3. 事件的冒泡和更新时机,需要屏蔽所有绑定时间晚于事件触发时间的事件处理函数的执行
  4. 更新子节点,子节点的类型有三种:文本/空/其他,将逻辑封装为patchChildren,理清判断逻辑,总共有9种情况
  5. 文本节点和注释节点
  6. Fragment 本身不会渲染任何DOM元素,只需要渲染Fragment的所有子节点

# 简单diff算法(从头到尾)

  1. 减少DOM操作的性能开销,遍历较短的一组,如果新的长,说明需要挂载,旧的长,说明需要卸载
  2. DOM的复用和Key的作用,引入额外的key作为vnode的标识,key相同进行patch
  3. 找到需要移动的元素
  4. 如何移动元素,遍历新的子节点,根据在旧子节点中的引用,移动真实DOM,记录最大索引,核心是移动到前一个对应的真实DOM后面
  5. 添加新元素,在旧的中找不到时find=false
  6. 移除不存在的元素。在新的中遍历完旧的之后,需要反过来遍历一次,查找需要删除的元素。

# 双端diff算法(从头尾到中间)

同时对新旧子节点的两个端点进行扫描,需要四个索引值

每一轮步骤:以新的数组为移动依据

  • 比较旧的起始与新的起始,如果key匹配,patch后,newstart++,oldstart++
  • 比较旧的结束语新的结束,如果key匹配,patch后,newend--,oldend--
  • 比较旧的开始与新的结束,如果key匹配,patch后,oldstart的真实DOM移动到oldend真实DOM之后,,newstart++,oldend--
  • 比较旧的结束与新的开始,如果key匹配,patch后,oldend的真实DOM移动到oldstart真实DOM之前,,oldstart++,newend--

​ 每一轮移动两个指针,直至新旧两组指针相遇

非理想情况:

  • 找到newstart在old中的索引,并移动到最前面,old中的原来的位置变为undefined
  • old中遇到undefined,跳过

添加新元素:diff完之后,再遍历一次new节点,每次都插在oldstart之前

移除不存在的元素:遍历old,依次卸载剩余的。

# 快速diff算法

类似于文本diff,先对前缀,后缀进行预处理

预处理流程:

  • 判断前缀,指针j
  • 判断后缀,newend,oldend

非理想情况:预处理结束后,new、old都有剩余,用source记录new的剩余,source存储new中的节点在old中的索引。建立索引表,时间复杂度降为O(n)

  • 在source中寻找最长递增子序列(可以不连续),从后向前遍历source[i]及其最长递增子序列seq[s]。
    • source[i]==-1,则挂载该节点
    • i!=seq[s],需要移动节点

最长递增子序列中的节点位置是不需要移动的

# 组件化

# 组件的实现原理

  1. 渲染组件用vnode.type存储组件对象

  2. 组件状态与自更新

    • 执行data()函数,并用reactive()包装为响应式对象
    • 调用render函数,this指向state
    • 将渲染任务包装在effect中
    • 通过调度器,将任务缓冲到微任务队列中,这样就有机会对任务进行去重
  3. 组件实例与组件的生命周期

    组件实例是一个状态合集,维护者组件运行过程中的所有信息

    • state:组件自身状态数据
    • isMounted:表示是否被挂载
    • subTree:渲染函数返回的虚拟DOM
  4. props与组件的被动更新

    • 将MyComponent.props和vnode.props结合,解析出props和attrs,其中attrs是MyComponent中没有定义的props
    • 被动更新就是父组件更新引起的子组件更新
    • 封装一个渲染上下文对象renderContext,并优先从上下文中读取,实质上是组件实例的代理对象
  5. setup函数的作用与实现(只会在被挂载时执行一次)

    • 配合组合式API,建立组合逻辑、创建响应式数据、创建函数、注册生命周期钩子等等
    • 接受两个参数props,setupContext,后者存储着slots、emit、attrs、expose
    • 如果返回数据对象,则需要暴露在渲染上下文中。
  6. 注册事件和emit的实现

    通过v-on指令为组件绑定的事件,经过编译后,会以OnXXX形式存储在props属性中。

    触发自定义事件的本质就是,根据事件名称去props数据对象中寻找相应的事件处理函数并执行。

  7. 插槽的工作原理和实现

    组件模板的插槽内容会被编译为插槽函数,插槽函数执行的返回值就是具体的插槽内容

  8. 注册生命周期

    维护一个当前组件实例currentInstance和mount数组

# 异步组件与函数式组件

# 异步组件的实现原理

  • 封装defineAsyncComponent函数,判断是否加载成功
  • 加载器、超时时间、并可以指定出错是渲染的模块。把错误对象传递到Error组件中
  • loading组件,delay后加载loading
  • 重试机制,为用户提供重试的能力

# 函数式组件

函数式组件本质就是一个普通函数,返回值是一个虚拟DOM

# 内建组件和模块

# KeepAlive组件

本质是缓存管理,加上特殊的挂载、卸载逻辑。

在内部组件的vnode对象上添加一些属性,以便渲染器能够据此执行特定的逻辑。

  • shouldKeepAlive:渲染之卸载内部组件时可以判断是否需要被keepalive
  • keepAliveInstance:便于卸载时访问失活函数
  • KeptAlive:标记已经被缓存

include与exclude

缓存管理:Map

  • map的键是组件选项对象,vnode.type,值是vnode对象,虚拟DOM
  • 设置缓存的阈值,超出时修剪,策略最新一次访问

# Teleport组件

将执行内容渲染到特定容器中,不受DOM层级的限制。

const Teleportt = {
	__isTeleport: true,
    process(n1,n2,container,anchor){
        // 渲染逻辑
    }
}

# Transition组件

帮助制作基于状态变化的过渡和动画

核心原理:

  • 当DOM元素被挂载时,将动效附加在该DOM上
  • 当DOM元素被卸载时,不立即卸载,而是等到附加在DOM上的动效执行完成后在卸载

# 编译器

# 编译器核心技术概览(3步)

完整的编译:词法分析、语法分析、语义分析、中间代码生成、优化、目标代码生成

vue的编译是DSL,将vue编译为渲染函数

vue的编译:模板、词法分析、语法分析、模板AST、Transformer、JS AST、代码生成、渲染函数

模板AST:

  • type区分不同节点
  • 标签节点的子节点在children中
  • 属性和指令在props数组
  • 不同节点的对象属性不同

语义分析:

  • 检查v-else是否存在对应的v-if
  • 分析属性值是否是静态的
const templateAST = parse(template)
const jsAST = transform(templateAST)
const code = generate(jsAST)

# parser的实现原理与状态机

解析器逐个读取字符串模板的字符,根据一定规则,切割成一个个token

有限状态自动机,随着字符的输入,解析器会在不同状态间迁移,将模板解析为一个一个的token

# 构造模板AST

递归下降算法,

栈+token列表可以实现

# AST的转换

  • 深度优先遍历
  • 对节点的操作和访问解耦,存放在上下文context
    • currentNode: 当前正转换的节点
    • childIndex:当前节点在父节点的children中的位置索引
    • parent:用来存储当前转换的父节点
  • 元素的进入与退出

# 生成JS AST

函数声明语句:

  • id 函数名称
  • params:函数的参数
  • body:函数体

# 解析器

# 编译优化

# 服务端渲染

# 同构渲染

Last Updated: 11/18/2024, 4:01:47 PM