status
Published
slug
vuejs-learning-note
type
Post
category
Technology
date
Jul 21, 2023 → Nov 14, 2023
tags
框架
Vue
summary
阅读过程中的笔记和补充,深入浅出了解Vue
第1章 权衡的艺术
- 命令式和声明式:
- 命令式:jQuery
- 关注过程,自然语言描述能够与代码产生一一对应
- 可以做到极致的性能优化
- 声明式:Vue.js
- 关注结果,封装了过程
- 声明式代码的性能不优于命令式代码的性能
- 声明式代码的更新性能消耗=找出差异的性能消耗+直接修改的性能消耗
- 声明式代码的可维护性更强
⇒ 框架设计目标:在保持可维护性的同时让性能损失最小化
- 虚拟DOM:虚拟DOM在更新页面时只会更新必要的元素
- 最小化找出差异的性能消耗
- 创建页面的性能差异
- 更新页面的性能差异
- 框架分类:运行时、编译时、运行时+编译时
- 运行时:纯运行时的框架,由于没有编译的过程,因此没办法分析用户提供的内容
- 运行时+编译时:可以分析用户提供的内容,提取变化信息,进行性能优化
- 编译时:也可以分析用户提供的内容,但有损灵活性,即用户提供的内容必须编译后才能用
第2章 框架设计的核心要素
- 提升用户的开发体验
- 提供必要的警告信息
- 自定义formatter,自定义输出形式
- 控制框架代码的体积
- 使用
__DEV__
来区分构建开发环境和生产环境的代码 - Vue.js使用rollup.js对项目进行构建
__DEV__
利用了rollup.js的预定义常量插件实现- 做到在开发环境中为用户提供友好的警告信息,不会增加生产环境代码的体积
- 框架要做到良好的Tree-Shaking
- Tree-Shaking指消除那些永远不会被执行的代码(排除dead code)
- 实现Tree-Shaking必须满足模块是ESM(ES Module),因为Tree-Shaking依赖ESM的静态结构
- 副作用:指当调用函数的时候会对外部产生影响
- 纯静态分析进行Tree-Shaking难度大
/*#__PURE__*/
用于显式指明函数调用不会产生副作用,可以进行Tree-Shaking
- 框架应该输出怎样的构建产物
- IIFE(Immediately Invoked Function Expression):立即调用的函数表达式
- HTML中通过
<script>
引入的文件就是IIFE形式的资源,引入后立刻执行 - 产物:vue.global.js
- ESM格式的资源
- 主流浏览器支持ESM格式资源,可以通过
<script type="module">
标签引入 - 产物:
- vue.esm-browser.js:给
<script type="module">
使用的 - vue.esm-bundler.js:给rollup.js或webpack等打包工具使用的
- 允许用户通过webpack配置自行决定构建资源的目标环境
- 两者的区别为在-bundler中
__DEV__
常量使用(process.env.NODE_ENV !== ‘production’
)替换 - cjs(CommonJS)模块的资源:
- 提供在Node.js环境下运行
- 特性开关
- 对于用户关闭的特性可以使用Tree-Shaking机制让其不包含在最终的资源中
- 为框架设计带来灵活性,通过开关任意为框架添加新的特性
- 在框架升级的时候,可以使用特性开关支持遗留API
- 错误处理
- 使用
callWithErrorHandling
函数为用户提供同意的错误处理接口 - Vue中还可以再注册统一的错误处理函数:
- 良好的TypeScript类型支持
runtime-core/src/apiDefineComponent.ts
第3章 Vue.js3的设计思路
- 声明式地描述UI
- 虚拟DOM:使用JavaScript对象来描述UI的方式h
- h函数:一个辅助创建虚拟DOM的工具函数
- 渲染函数:一个组件要渲染的内容是通过渲染函数来描述的
- 渲染器:把虚拟DOM渲染为真实DOM
- 基本逻辑:创建元素;为元素添加属性和时间;处理children
- 重点:找到vnode对象的变更点并只更新变更点
- Vue渲染页面的过程:
- 编译器把模板编译为渲染函数(或直接手写渲染函数)
- 渲染函数返回虚拟DOM
- 渲染器再把返回的虚拟DOM渲染为真实DOM
- 组件的本质:一组虚拟DOM的封装
- 可以是一个返回虚拟DOM的函数
- 可以是一个对象(下面有函数用来产出组件要渲染的虚拟DOM)
- Vue.js是各个模块组成的有机整体
- 组件的实现依赖于渲染器
- 模板的编译依赖于编译器
- 编译器在生成代码时,可以携带关于属性的动态信息,在后续渲染器渲染时,就可以省去寻找变更点的工作量
- 编译器和渲染器之间交流的媒介是虚拟DOM对象,两者相互配合使得性能进一步提升
第4章 响应系统的作用与实现
- 响应式数据:Vue3采用Proxy实现响应式数据
- 对对象的值进行修改后,副作用函数自动重新执行
- 副作用函数:指会产生副作用的函数
- 副作用:可以简单理解为会直接或间接影响其他函数的执行
- 对全局变量的修改就是一个很常见的副作用
- 响应系统的设计:
- 当读取操作发生时,将副作用函数收集到bucket中
- 当设置操作发生事,从bucket中取出副作用函数并执行
- 建立副作用函数与被操作的目标字段之间明确的联系
bucket专门用于存储副作用函数
- 设计的bucket:使用WeakMap作为数据结构
- WeakMap和Map的区别:
- WeakMap对key是弱引用,不影响垃圾回收器的工作,适用于存储那些只有当key所引用的对象存在时(没有被回收)才有价值的信息
- 分支切换:分支切换可能会产生遗留的副作用函数(触发不必要的更新)
- 解决方法:
- 每次副作用函数执行时,将其从所有与之关联的以来集合中删除
- cleanup
- 当副作用执行完毕后,重新建立副作用函数和响应数据的联系
- effect和effect嵌套:
- 使用栈解决嵌套导致的覆盖问题
- 避免无限递归调用,当trigger触发执行的副作用函数和当前正在执行的副作用函数相同则不触发执行
- 调度执行:决定副作用函数执行的时机、次数以及方式
- 计算属性computed和lazy
- 计算属性computed的本质就是一个懒执行的副作用函数,当读取value时才会触发副作用函数的执行
- 为了正确执行副作用函数
- 读取计算属性的值时,手动调用track函数进行追踪
- 计算属性依赖的响应式数据发生变化时,手动调用trigger函数
- watch的实现原理:
- watch的本质是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数
- watch的本质是对effect的二次封装
- watch的组成:
- 副作用函数effect
- option.scheduler选项,使用调度函数作为回调函数
- getter函数用于指定watch该依赖的响应式数据
- lazy函数获取oldValue(手动调用effectFn函数得到)、newValue(触发scheduler调度函数执行时,调用effectFn得到)
- 立即执行的watch与回调执行时机
- 抽取出scheduler的逻辑,封装为通用函数
- 根据需求指定执行的时机(条件)
- 过期的副作用:
- 竞态问题
- 使用标识变量expired,标识当前副作用函数的执行是否过期
第5章 非原始值的响应式方案
- Proxy:可以创建一个代理对象,实现对其他对象的代理
- 什么是代理:指的是对一个对象基本语义的代理,允许我们拦截并重新定义对一个对象的基本操作
- 什么是基本语义:类似读取、设置属性值的操作属于基本语义操作,可以使用Proxy拦截
- Reflect:全局对象,提供拦截 JavaScript 操作的方法,函数与Proxy同名
- JavaScript对象:
- ordinary object(常规对象)
- exotic object(异质对象)
创建代理对象时指定的拦截函数,实际上用来定义代理对象本身的内部方法和行为的
- 如何处理代理Object
- 合理地触发响应
- 浅响应与深响应:
- shallowReactive(浅响应):只有对象的第一层属性是响应的
- Reactive(深响应)
- 只读和浅只读
- 代理数组
- 代理Set和Map
- 数据污染:把响应式数据设置到原始数据上的行为称为数据污染
第6章 原始值的响应式方案
- 引入ref的概念
- Proxy的代理目标必须是非原始值
- 使用对象包裹原始值,再使用Proxy代理,封装为ref函数
- ref可以用于实现原始值的响应式方案,还能解决响应丢失的问题(包装一层toRef)
- 自动脱ref功能
第7章 渲染器的设计
- 渲染器:用于执行渲染任务,把虚拟DOM渲染为特定平台上的真实元素
- 在浏览器平台上渲染器会把虚拟DOM渲染为真实DOM元素
- 渲染器把虚拟DOM节点渲染为真实DOM节点的过程叫作——挂载(mount)
第8章 挂载和更新
- 挂载子节点和元素的属性
- HTML Attributes和DOM Properties
- HTML Attributes:指定义在HTML标签上的属性
- DOM Properties:指DOM对象的属性
- 很多HTML Attributes在DOM对象上有与之同名的DOM Properties
- 但DOM Properties和HTML Attributes的命名并不总是一样的
- 不是所有的DOM Properties都有对应的HTML Attributes
- HTML Attributes的作用是设置与之对应的DOM Properties的初始值,一旦值改变,那么DOM Properties始终存储着当前值,通过getAttribute函数得到的仍然是初始值
- 卸载操作:
- 根据vnode对象获取与其关联的真实DOM元素,使用原生DOM操作方法将DOM元素移除
- 使用清空innerHTML容器元素内容方法实现的卸载操作存在问题,不会移除绑定在DOM元素上的事件处理函数
- 事件冒泡与更新时机问题
- 更新子节点
- Fragment:
- Vue.js3中新增的vnode类型
- 为什么需要Fragment
- 为了实现多根节点版本
- Fragment类型的虚拟节点本身并不会渲染任何内容,只需要处理它的子节点即可
第9章 简单Diff算法
- 什么是Diff算法?
- 简单来说,当新旧 vnode 的子节点都是一组节点时,为了以最小的性能开销完成更新操作,需要比较两组子节点,用于比较的算法就叫作 Diff 算法。
- 如果只是卸载所有旧节点,挂载所有新节点,这是需要大量的 DOM 操作才能完成更新,非常消耗性能
- 减少DOM操作的性能开销
- 主要通过遍历新旧子节点,对于类型相同子节点仅进行内容的更新,节点需要删除则卸载,需要添加则挂载
- DOM复用与key的作用:
- 对于新旧子节点相同,但顺序相同的情况,可以通过对DOM进行移动完成更新,但是仅通过vnode.type判断能否使用移动的方法并不合适,于是引入key——用于标识vnode
- 当新旧子节点的vnode.type与key都相同时,说明当前旧子节点可以复用
- 找到需要移动的元素:根据索引进行
- 如何移动元素:
- 移动节点指的是,移动一个虚拟节点所对应的真实 DOM 节点,并不是移动虚拟节点本身。
- 移除不存在的元素
第10章 双端Diff算法
- 双端 Diff 算法可以用于解决简单 Diff 算法存在的缺陷
- 【Vue2.js中使用】
- 简单 Diff 存在的问题:
- 对 DOM 的移动操作并不是最优的
⇒ 提出双端 Diff 算法
- 双端 Diff 算法的原理:
- 双端 Diff 算法是一种同时对新旧两组子节点的两个端点进行比较的算法。
- 使用四个索引,指向两组新旧子节点的头部节点和尾部节点,通过分组比较的方式判断当前的节点是否复用以及移动的位置
- 双端比较的优势:
- 减少DOM 移动操作次数
第11章 快速Diff算法
- 什么是快速 Diff 算法?
- 【Vue3.js中使用】
- 快速 Diff 算法借鉴了纯文本 Diff 算法的思路,包含预处理步骤
- 先处理新旧两组子节点中相同的前置节点和相同的后置节点
- 最长递增子序列:
- 当前置节点和后置节点全部处理完毕后,如果无法简单地通过挂载新节点或者卸载已经不存在的节点来完成更新,则需要根据节点的索引关系,构造出一个最长递增子序列。
- 最长递增子序列所指向的节点即为不需要移动的节点。
第12章 组件的实现原理
- 为什么需要组件化?
- 组件化可以将一个页面拆成多个部分,每个部分都可以作为单独的组件,这些组件共同组成完整的页面。
- 渲染组件:
- 渲染器会使用虚拟节点的 type 属性来区分其类型
- 对于不同类型的节点,需要采用不同的处理方法来完成挂载和更新
- 组件在渲染器内部实现来看是一个特殊类型的虚拟DOM节点
- vnode.type属性值类型为对象的时候,作为组件来处理
- 渲染函数:组件本身是对页面内容的封装,它用来描述页面内容的一部分。因此,一个组件必须包含的一个渲染函数(render)
- 渲染器解析的时候调用
- 组件状态与自更新
- 用户必须使用 data 函数来定义组件自身的状态,同时可以在渲染函数中通过 this 访问由 data 函数返回的状态数据。
- 实现组件自身状态的初始化
- 通过组件的选项对象取得data函数并执行,调用reactive函数将data函数包装为响应式数据
- 调用render函数,将this的指向设置为响应式数据state(render.call(state, state))
- 自更新:effect函数
- 一旦组件自身的响应式数据发生变化,组件就会自动重新执行渲染函数,从而完成更新
- 副作用函数:
- 由于 effect 的执行是同步的,因此当响应式数据发生变化时,与之关联的副作用函数会同步执行【不必要】
- ⇒ 调度器:当副作用函数需要重新执行时,将它缓冲到一个微任务队列中,等到执行栈清空后,再将它从微任务队列中取出并执行。
- 缓存机制,支持对任务进行去重,从而避免多次执行副作用函数带来的性能开销。
- 利用了微任务的异步执行机制
- 异步和同步?
- 同步的任务进入主线程,排队依次执行,执行过程中会阻塞后续代码的执行
- 异步的任务进入Event Table并注册成函数,当异步任务有了运行结果,会在“任务队列”中放置一个事件,在主线程中的所有同步任务执行结束后,系统会读取“任务队列”,将对应的一步任务放入主线程执行
- 异步任务
- 宏任务:包含整个script代码块,setTimeout,setIntval,setImmediate,Ajax,DOM事件
- 微任务:Promise.then catch finally,process.nextTick
- 任务执行顺序
什么是微任务?
首先JS是单线程语言,一次只能做一件事,若当前任务不执行结束会阻塞后续代码执行,为了在执行过程中不阻塞后续代码的执行, 在JS中会采用异步操作
- 组件实例:本质上是状态集合,维护着组件运行过程中的所有信息
- 使用对象来表示组件实例
- state:组件自身的状态数据,即 data。
- isMounted:一个布尔值,用来表示组件是否被挂载。
- subTree:存储组件的渲染函数返回的虚拟DOM,即组件的子树(subTree)。
- 依据isMounted属性来区分生命周期,在合适的时机调用组件对应的生命周期钩子
- props
- 一个组件中有两个部分关于props
- 为组件传递的 props 数据,即组件的vnode.props 对象;
- 组件选项对象中定义的 props 选项,即MyComponent.props 对象。
- 为组件传递的props数据在组件自身的props选项中有定义,才视为合法的props
- 渲染上下文:用于拦截数据状态的读取和设置操作,也是为了让props数据和组件的自身状态数据(state)暴露的渲染函数中
- 作为渲染函数以及生命周期钩子的 this 值
- 渲染函数能够通过this访问props数据和组件的状态数据
- setup函数:为组合式 API 而生
- Vue.js 3新增的组件选项,主要用于配合组合式API,用于建立组合逻辑、创建响应式数据、创建通用函数、注册生命周期钩子等能力
- 在组件的整个生命周期中,setup 函数只会在被挂载时执行一次
- 返回值的两种情况
- 返回一个函数,该函数将作为组件的 render 函数
- 只能用于组件不是以模板表达渲染内容的情况,不然会与模板编译生成的渲染函数产生冲突
- 返回一个对象,该对象中包含的数据将暴露给模板使用
- setup 函数暴露的数据可以在渲染函数中通过 this 来访问
- 接收参数
- props:取得外部为组件传递的 props 数据对象
- setupContext:保存着与组件接口相关的数据和方法
- slots、emit、attrs、expose
- 组件事件
- emit 用来发射组件的自定义事件,在使用组件的时候可以进行监听
- 发射自定义事件的本质就是根据事件名称去 props 数据对象中寻找对应的事件处理函数并执行
@change
被编译成组件上名为onChange
的属性- 插槽的工作原理
- 组件的插槽指组件会留一个槽位,该槽位的具体内容将由用户传入
- 具体的工作原理
- 组件模板中的插槽内容会被编译为插槽函数,而插槽函数的返回值就是具体的插槽内容
- 渲染插槽内容的过程,就是调用插槽函数并渲染由其返回的内容的过程
- 注册生命周期:
- 维护一个变量 currentInstance,用它来存储当前组件实例
- 每当初始化组件并执行组件的 setup 函数之前,先将 currentInstance 设置为当前组件实例,再执行组件的 setup 函数
- 这样我们就可以通过 currentInstance 来获取当前正在被初始化的组件实例,从而将那些通过onMounted 函数注册的钩子函数与组件实例进行关联。
- instance.mounted 数组:
- 当组件调用onMounted函数的时候,将注册的生命周期函数存在instance.mounted 数组中
第13章 异步组件与函数式组件
- 什么是异步组件?
- 在异步组件中,“异步”二字指的是,以异步的方式加载并渲染一个组件。
- 实现很简单,最简单的示例
- 什么是函数式组件?
- 函数式组件允许使用一个普通函数定义组件,并使用该函数的返回值作为组件要渲染的内容。
- 函数式组件的特点是:无状态、编写简单且直观。
- 异步组件要解决的问题:
- 允许用户指定加载出错时要渲染的组件。
- 允许用户指定 Loading 组件,以及展示该组件的延迟时间。
- 允许用户设置加载组件的超时时长。
- 组件加载失败时,为用户提供重试的能力
- 异步组件本质上是通过封装手段来实现友好的用户接口,从而降低用户层面的使用复杂度
- defineAsyncComponent 函数
- 函数式组件的vnode.type 是一个函数
第14章 内建组件和模块
- KeepAlive组件
- 什么是KeepAlive?在 HTTP 协议中,KeepAlive又称 HTTP 持久连接(HTTP persistent connection)
- 其作用是允许多个请求或响应共用一个 TCP 连接。
- 在没有 KeepAlive 的情况下,一个 HTTP 连接会在每次请求/响应结束后关闭,当下一次请求发生时,会建立一个新的HTTP 连接。
- 频繁地销毁、创建 HTTP 连接会带来额外的性能开销
- 什么是KeepAlive组件?
- Vue.js 内建的 KeepAlive 组件可以避免一个组件被频繁地销毁/重建
- KeepAlive的本质是缓存管理,再加上特殊的挂载/卸载逻辑,对应生命周期(deactivated/activated)
- 卸载(deactivated):被KeepAlive的组件在卸载的时候,从原容器搬运到另外一个隐藏的容器中,实现“假卸载”
- 挂载(activated):把该组件从隐藏容器中再搬运到原容器
- KeepAlive组件的实现是在“内部组件”的 vnode 对象上添加一些标记属性,以便渲染器能够据此执行特定的逻辑。
- shouldKeepAlive:该属性会被添加到“内部组件”的 vnode 对象上,这样当渲染器卸载“内部组件”时,可以通过检查该属性得知“内部组件”需要被 KeepAlive。
- keepAliveInstance:“内部组件”的 vnode 对象会持有 KeepAlive 组件实例,在 unmount 函数中会通过 keepAliveInstance 来访问_deActivate 函数。
- keptAlive:“内部组件”如果已经被缓存,则还会为其添加一个 keptAlive 标记。
- include和exclude
- 在默认情况下,KeepAlive 组件会对所有“内部组件”进行缓存。但有时候用户期望只缓存特定组件。
- include 用来显式地配置应该被缓存组件
- exclude 用来显式地配置不应该被缓存组件
- 缓存管理
- 如果缓存存在,则继承组件实例,并将用于描述组件的 vnode 对象标记为 keptAlive,这样渲染器就不会重新创建新的组件实例
- 如果缓存不存在,则设置缓存
- 如果缓存超过了设置的缓存阈值,对缓存进行修剪
- 最新一次访问
- Teleport组件
- Vue.js 3新增的内建组件,该组件可以将指定内容渲染到特定容器中,而不受DOM 层级的限制
- Teleport可以用于实现跨 DOM 层级的渲染
- Teleport 组件的渲染逻辑从渲染器中分离出来
- 可以避免渲染器逻辑代码“膨胀”
- 可以利用 Tree-Shaking 机制在最终的 bundle 中删除 Teleport 相关的代码,使得最终构建包的体积变小。
- Transition组件
- 核心原理:
- 当 DOM 元素被挂载时,将动效附加到该 DOM 元素上
- 当 DOM 元素被卸载时,不要立即卸载 DOM 元素,而是等到附加到该 DOM 元素上的动效执行完成后再卸载它
- 原生DOM的过渡
- transition可以通过CSS样式设置,运动属性为transform
- Transition 组件是基于虚拟 DOM 实现的
- 将 DOM 元素的生命周期分割为beforeEnter、enter、leave等几个阶段,并在特定阶段执行对应的回调函数。
- Transition 组件本身不会渲染任何额外的内容,它只是通过默认插槽读取过渡元素,并渲染需要过渡的元素;
- Transition 组件的作用,就是在过渡元素的虚拟节点上添加 transition 相关的钩子函数。
- 渲染器在执行挂载和卸载操作时,会优先检查该虚拟节点是否需要进行过渡,如果需要,则会在合适的时机执行 vnode.transition 对象中定义的过渡相关钩子函数。
第15章 编译器核心技术概览
- 编译技术应用的几个方面:
- 通用用途语言:C/Javascript等语言
- 对于编译技术要求比较高,需要掌握上下文无关文法、语法推导、消除左递归、递归下降算法等内容
- 表格、报表中的自定义公式计算器
- 简单,只涉及编译前端技术
- 领域特定语言(DSL):Vue.js的模板和JSX
- 实现难度为中低级别,只需要掌握基本的编译技术理论即可
- 编译
- 编译器的本质是程序:能将源代码编译为目标代码
- 编译:编译器将源代码翻译为目标代码的过程
- 整个编译过程分为
- 编译前端:编译前端包含词法分析、语法分析和语义分析,它通常与目标平台无关,仅负责分析源代码。
- 编译后端:编译后端则通常与目标平台有关,编译后端涉及中间代码生成和优化以及目标代码生成。
编译后端并不一定会包含中间代码生成和优化这两个环节,这取决于具体的场景和实现。中间代码生成和优化这两个环节有时也叫“中端
- 模板DSL的编译器:
- 源代码:组件的模板
- 目标代码:能够在浏览器平台上运行的JavaScript代码,或其他拥有 JavaScript 运行时的平台代码
- Vue.js 模板编译器的目标代码其实就是渲染函数
- Vue.js 模板编译器会首先对模板进行词法分析和语法分析,得到模板 AST(abstract syntax tree,抽象语法树)。接着,将模板 AST 转换(transform)成JavaScript AST。最后,根据 JavaScript AST 生成 JavaScript 代码
- parse函数:接收字符串模板作为参数,并将解析后得到的 AST 作为返回值返回
- transform函数:完成模板 AST 到JavaScript AST 的转换工作
- generate函数:根据JavaScript AST 生成渲染函数
- Vue.js模板编译器的基本结构:
- 用来将模板字符串解析为模板 AST 的解析器(parser)
- 用来将模板 AST 转换为 JavaScript AST 的转换器(transformer)
- 用来根据 JavaScript AST 生成渲染函数代码的生成器(generator)。
- parser的实现原理与状态机
- Token的解析:通过有限自动状态机进行状态迁移获得
- 解析可以使用正则表达式简化
解析HTML并构造Token的过程有对应的规范
正则表达式的本质就是有限自动机
- 构造AST
- 对于GPL:构造 AST,较常用的一种算法叫作递归下降算法,需要解决 GPL 层面才会遇到的很多问题,例如最基本的运算符优先级问题。
- 对于DSL:不具有运算符,所以也就没有所谓的运算符优先级问题
- DSL 与 GPL 的区别在于,GPL 是图灵完备的,我们可以使用 GPL 来实现 DSL。而 DSL 不要求图灵完备,它只需要满足特定场景下的特定用途即可。
- 根据Token列表构建AST:
- 对Token列表进行扫描,用栈进行维护元素之间的父子关系
- 开始标签创建节点入栈,结束标签栈顶出栈
- 栈顶的节点将始终充当父节点的角色
- 扫描过程中遇到的所有节点,都会作为当前栈顶节点的子节点,并添加到栈顶节点的 children 属性下
- AST 的转换与插件化架构
- dfs算法遍历访问节点
- 解耦:使用context来封装节点操作,解决了功能增加所导致的 traverseNode 函数“臃肿”的问题
- 转换上下文与节点操作
- 将转换上下文对象进行扩展。存储当前节点,当前节点的父节点等信息,以便实现节点替换功能
- 进入与退出:
- 在转换 AST 节点的过程中,往往需要根据其子节点的情况来决定如何对当前节点进行转换。这就要求父节点的转换操作必须等待其所有子节点全部转换完毕后再执行。
- 转换函数对节点的访问分为两个阶段,即进入阶段和退出阶段
- 进入阶段:先进入父节点,再进入子节点
- 退出阶段:先退出子节点,再退出父节点
- 增加了一个数组exitFns,用来存储由转换函数返回的回调函数。接着,在 traverseNode 函数的最后,执行这些缓存在 exitFns 数组中的回调函数。
⇒ 需要先进入子节点再退出到父节点的工作流
在退出节点阶段对当前访问的节点进行处理,就一定能够保证其子节点全部处理完毕,还能够保证所有后续注册的转换函数执行完毕。
- 将模板AST转为JavaScript AST
- 设计基本的数据结构FunctionDecl来描述函数声明语句
- 使用 CallExpression 类型的节点来描述函数调用语句
- 使用类型为 StringLiteral 的节点来描述字符串字面量
- 使用类型为 ArrayExpression 的节点来描述数组参数
- 两个转换函数:transformElement 和transformText
- 代码生成
- context:上下文对象用来维护代码生成过程中程序的运行状态
- 补充实现代码缩进
- 生成器:为对应的AST节点写生成器函数
第16章 解析器
- 文本模式及其对解析器的影响:
- 什么是文件模式?文本模式指的是解析器在工作时所进入的一些特殊状态,在不同的特殊状态下,解析器对文本的解析行为会有所不同。
- 特殊标签:
- <title> 标签、<textarea> 标签,当解析器遇到这两个标签时,会切换到 RCDATA 模式
- <style>、<xmp>、<iframe>、<noembed>、<noframes>、<noscript> 等标签,当解析器遇到这些标签时,会切换到RAWTEXT 模式
- 当解析器遇到 <![CDATA[ 字符串时,会进入CDATA 模式。
- 递归下降算法构造模板AST
- 状态机的开启和关闭
- 开始标签,入栈(父级节点栈),开启新的状态机,遇到结束标签出栈,关闭当前状态机
- 解析标签节点
- 解析属性
- 解析文本与解码 HTML 实体
- 解析插值与注释
第17章 编译优化
- 什么是编译优化?
- 编译优化指的是编译器将模板编译为渲染函数的过程中,尽可能多地提取关键信息,并以此指导生成最优代码的过程
- 动态节点收集与补丁标志
- diff算法需要对虚拟 DOM 树进行层级遍历比对
- 存在优化空间(如某个节点仅更新文本内容时)
- patchFlag(补丁标志):只要虚拟节点存在该属性,我们就认为它是一个动态节点
- dynamicChildren:把带有该属性的虚拟节点称为“块”,即Block
- Block:除了模板中的根节点需要作为 Block 角色之外,任何带有 v-for、v-if/v-else-if/v-else 等指令的节点都需要作为 Block 节点
⇒ 动态节点
- 收集动态节点:
- createVNode 函数就是用来创建虚拟 DOM 节点的辅助函数
- 在实际使用的时候,createVNode函数的调用是层层嵌套的关系,函数的执行顺序是“内层先执行,外层后执行”
- createBlock函数用于完成虚拟Block节点的创建
- 当 createBlock 函数执行时,内层的所有 createVNode 函数已经执行完毕了
- currentDynamicChildren 数组中所存储的就是属于当前 Block 的所有动态子代节点
- 动态节点集合能够使得渲染器在执行更新时跳过静态节点
- 检测补丁标识(patchFlag)
- 在 patchElement 函数内,我们通过检测补丁标志实现了 props 的靶向更新。这样就避免了全量的 props 更新,从而最大化地提升性能。
⇒ 为了让外层block节点能够收集到内层动态节点,使用动态节点栈来临时存储内层的动态节点
- Block树
- 为什么v-if/v-else-if/v-else等结构化指令的节点也作为Block
- dynamicChildren 数组中收集的动态节点是忽略虚拟 DOM 树层级的
- 结构化指令会导致更新前后模板的结构发生变化,即模板结构不稳定
- v-for指令的节点:
- 带有 v-for 指令的节点也会让虚拟DOM 树变得不稳定
- 使用类型为 Fragment 的节点来表达 v-for 指令的渲染结果,并作为 Block 角色。
- Fragment的稳定性
- 由于Fragment本身手机的动态节点仍然存在结构不稳定,直接使用 Fragment 的 children 而非dynamicChildren 来进行 Diff 操作
- 当Fragment稳定时,不需要回退到传统diff操作
- 静态提升:能够减少更新时创建虚拟 DOM 带来的性能开销和内存占用
- 把静态的节点提升到渲染函数之外
- 将纯静态的属性提升到选卷函数之外
- 预字符串化
- 大块的静态内容可以通过 innerHTML 进行设置,在性能上具有一定优势。
- 减少创建虚拟节点产生的性能开销。
- 减少内存占用。
- 缓存内联事件处理函数:避免无用的内联事件处理函数的更新
- v-once:
- v-once 可实现对虚拟 DOM 的缓存,编译器在遇到v-once指令时,会利用cache数组来缓存渲染函数的全部或部分执行结果
- v-once 指令通常用于不会发生改变的动态绑定中,使用v-once可以提升性能
- v-once提升性能的两个方面
- 避免组件更新时重新创建虚拟 DOM 带来的性能开销。因为虚拟 DOM 被缓存了,所以更新时无须重新创建。
- 避免无用的 Diff 开销。这是因为被 v-once 标记的虚拟 DOM 树不会被父级 Block 节点收集。
第18章 同构渲染
- Vue.js的两种渲染方式:
- 客户端渲染(client-side rendering,CSR)
- 服务端渲染(server-side rendering,SSR)
- 同构渲染(isomorphic rendering):客户端渲染和服务端渲染结合
- 不同渲染方式的比较
- 优缺点比较
- 同构渲染:“同构”一词的含义是,同样一套代码既可以在服务端运行,也可以在客户端运行
- 分为首次渲染(即首次访问或刷新页面)以及非首次渲染
- 同构渲染中的首次渲染与 SSR 的工作流程是一致的
- 当首次访问或者刷新页面时,整个页面的内容是在服务端完成渲染的,浏览器最终得到的是渲染好的 HTML 页面
- 假设浏览器已经接收到初次渲染的静态 HTML 页面,接下来浏览器会解析并渲染该页面。在解析过程中,浏览器会发现 HTML 代码中存在 <link>和 <script> 标签,于是会从 CDN 或服务器获取相应的资源,这一步与 CSR 一致。
- 当 JavaScript 资源加载完毕后,会进行激活操作,这里的激活就是我们在 Vue.js 中常说的 “hydration”。激活包含两部分工作内容。
- Vue.js 在当前页面已经渲染的 DOM 元素以及Vue.js 组件所渲染的虚拟 DOM 之间建立联系。
- Vue.js 从 HTML 页面中提取由服务端序列化后发送过来的数据,用以初始化整个 Vue.js 应用程序。
- 激活完成后,整个应用程序已经完全被 Vue.js 接管为 CSR 应用程序了
- 将虚拟 DOM 渲染为 HTML 字符串
- 将组件渲染为 HTML 字符串
- 客户端激活的原理
- 组件代码在客户端运行时
- 在页面中的 DOM 元素与虚拟节点对象之间建立联系;
- 为页面中的 DOM 元素添加事件绑定。
- 编写同构的代码:在编写组件代码时,应该额外注意因代码运行环境的不同所导致的差异
- 组件的生命周期
- 当组件的代码在服务端运行时,不会对组件进行真正的挂载操作,即不会把虚拟DOM 渲染为真实 DOM 元素,所以组件的beforeMount 与 mounted 这两个钩子函数不会执行。
- 服务端渲染的是应用的快照,所以不存在数据变化后的重新渲染,组件的beforeUpdate 与 updated 这两个钩子函数也不会执行
- 在服务端渲染时,也不会发生组件被卸载的情况,所以组件的 beforeUnmount 与unmounted 这两个钩子函数也不会执行。
- 只有 beforeCreate 与 created 这两个钩子函数会在服务端执行
- 使用跨平台的 API
- 在不得不使用例如特定环境下才能使用的API时,注意使用import.meta.env.SSR 这样的环境变量来做代码守卫
- 只在某一端引入模块
- 使用条件引入,实现仅在特定环境下才加载模板
- 根据环境的不同,引入不用的模块实现。
- 避免交叉请求引起的状态污染
- 在服务端渲染时,我们会为每一个请求创建一个全新的应用实例
- <ClientOnly> 组件
- 使用 <ClientOnly> 组件包裹了不兼容 SSR 的 <SsrIncompatibleComp/> 组件。这样,在服务端渲染时就会忽略该组件,且该组件仅会在客户端被渲染。
⇒ 通过 import.meta.env.SSR 来使代码只在特定环境中运行