跳到主要内容

认识

2024年03月13日
柏拉文
越努力,越幸运

一、认识


二、初始化


Vue 3 中, createApp 是应用启动的入口,它不仅创建一个应用实例, 还构建了全局的应用上下文。createApp 的主要职责可以归纳为以下几点:

  1. 创建全局应用上下文: 构建并保存全局配置、组件、指令、混入及 provide 数据,供所有组件共享。

  2. 构造应用实例: 封装根组件和初始 props, 准备一个待挂载的应用对象。

  3. 暴露全局 API: 通过 mountusemixincomponentdirectiveprovide 等方法,实现全局资源的注册与扩展。

  4. 根节点挂载: 在调用 mount 时, 创建根虚拟节点,将应用上下文注入其中,并通过渲染器将组件树挂载到指定的 DOM 容器上,同时支持 SSR/hydration 场景。

三、挂载


Vue 3 中,mount() 方法是 createApp() 返回的应用实例上的关键方法,它负责将根组件转换成虚拟节点(VNode)后挂载到指定的 DOM 容器中,从而启动整个应用的渲染和响应式更新。 mount 挂载 工作如下:

一、容器解析与校验, mount() 接受一个容器参数(可以是一个 DOM 元素或者一个选择器字符串),并将其标准化为真实的 DOM 元素。如果传入选择器,内部会通过 document.querySelector() 来获取元素。校验合法性, 如果容器不存在或类型不正确,在开发环境下会触发警告,确保挂载操作是在合法的 DOM 容器上执行。

二、根虚拟节点(VNode)的创建, mount() 会调用 createVNode(rootComponent, rootProps, null) 将根组件及其初始 props 转换为一个 VNode 对象。随后, 注入应用上下文, 生成的 VNode 会挂载上全局应用上下文(app._context), 这样整个组件树都能访问全局注册的组件、指令、混入以及其他配置。

三、调用 render, 渲染过程的启动, 调用渲染器, mount() 内部会调用渲染器的 render() 方法。主要工作如下: render 会首先卸载(或清理)容器中已有的内容,确保挂载操作在一个干净的容器中进行,从而避免因旧 DOM 残留而引起的渲染错误、重复绑定等问题。这也是 Vue 内部保证组件树正确更新和管理的一项必要预防措施。然后调用 patch 方法。

四、调用 patch 方法, patch 方法主要接收两个虚拟节点(旧 vnode 和新 vnode)以及目标容器,并根据这两个 vnode 的状态决定如何更新 DOM。基本流程如下:

  • 4.1 判断挂载或更新: 挂载时, 如果旧 vnodenull,说明当前 vnode 还没有被挂载,则执行挂载操作,即将新 vnode 转换为真实 DOM 并插入到容器中。更新时, 如果旧 vnode 存在,则进行更新操作,即对比新旧 vnode,计算差异,然后更新已有 DOM

  • 4.2 根据 vnode 类型分支处理: patch 会根据 vnode 的类型(例如元素、组件、文本、Fragment 等)进入不同的处理流程: 1. 元素节点, 调用 processElement, 进一步执行挂载(mountElement)或更新(patchElement); 2. 组件节点, 调用 processComponent, 区分首次渲染与组件更新, 调用 mountComponentupdateComponent; 3. 文本节点, 直接创建或更新文本内容; 4. Fragment 和其他特殊节点, 按照对应策略进行处理。

五、调用 mountComponent 挂载组件:

  • 5.1 创建组件实例, 首先创建组件实例, createComponentInstance() 生成了包含 propsattrsslotsparentcontext 等信息的组件实例对象

  • 5.2 组件初始化:

    1. 初始化 Props, 调用内部的 initProps 方法,将 vnode 上的 rawProps 转换为响应式的 props 数据, 对 props 进行校验和合并(比如默认值、类型检查),并将最终结果挂载到组件实例上, 此时 props 已经是响应式的,后续在 setuprender 中访问 props 就能响应更新;

    2. 初始化 Slots, 调用 initSlots 方法,处理组件的 children,将它们解析为具备一定格式的插槽对象, slots 数据同样挂载到组件实例上,供组件内部使用。

    3. 执行 Setup 函数, 接下来,setupComponent 检查组件是否定义了 setup() 函数。如果定义了 setup,则会按以下流程执行: 3.1 构造 setup 上下文,传递给 setup 的第二个参数是一个 context 对象, 包含: attrs, 除了 props 之外的所有非 prop 属性; slot 已经初始化的插槽; emit 用于触发事件的函数(封装了组件实例的 emit 功能); 3.2 调用 setup(), setup 会以 (props, context) 形式调用,其中 props 已经是响应式的, setup 的返回值可以是, 一个函数, 此时这个函数将被当作组件的渲染函数, 也可以是一个对象, 该对象会被挂载到组件实例的 setupState 上, 成为模板或 render 函数中可直接访问的部分。3.3 处理 setup 的返回值, 如果返回的是一个函数,那么在后续调用 finishComponentSetup 时, 会将这个函数设置为组件实例的 render 函数, 如果返回的是一个对象, 则通过内置的 proxy 将其暴露出来, 使得模板、计算属性等能优先访问 setupState 中的内容, 如果 setup 返回的是一个 PromiseVue 会把该组件标记为异步组件,需要等待 Promise resolve 后再继续后续的渲染流程。

    4. 完成组件的最终设置, 在执行完 setup 后,setupComponent 还需要完成最终的配置工作,这部分通常由内部的 finishComponentSetup 完成,主要包括: 4.1 确定 Render 函数, 如果 setup 返回了 render 函数,就直接使用, 否则,如果组件选项中已有 render 函数,则使用选项中的 render, 如果既没有 setup 返回 render,也没有 options.render,会走模板编译流程, 此时会调用运行时编译器将 template 转换为 render 函数。4.2 创建组件代理, Vue 会为组件创建一个代理对象。这个代理负责实现数据访问的优先级(setupState > props > data > computed)以及依赖收集, 此外,组件内部通过 proxy 访问的 ref 会自动解包,确保模板使用时语法更加友好, 如果同时使用了 Options API(如 datacomputedmethods)和组合式 API(setup),那么 setupState 优先级更高。4.3 处理生命周期钩子, 在 setup 中注册的生命周期钩子(例如 onBeforeMountonMounted)会被存储下来,后续在组件挂载和更新时执行。4.4 处理 Options API, 组件依然需要 Options API 的部分处理时, finishComponentSetup 会启动 Options API 的初始化, initState 会被调用, 其中包括: 4.4.1 调用 data() 函数 生成组件的响应式数据,并将其挂载到组件实例上; 4.4.2 初始化 computed 属性, 创建对应的计算属性对象; 4.4.3 同时绑定 methods 等其他选项

  • 5.3 创建组件渲染副作用 Reactive Effect: 1. 封装函数组件 componentUpdateFn 函数, 主要逻辑为: 首次渲染时, 调用组件实例的 render 方法, 组件实例的 render 方法策略为: 如果 setup 返回了 render 函数,就直接使用, 否则,如果组件选项中已有 render 函数,则使用选项中的 render, 如果既没有 setup 返回 render,也没有 options.render,会走模板编译流程, 此时会调用运行时编译器将 template 转换为 render 函数。得到最终组件实例的 render 方法, 生成子树(subTree); 调用 patch(null, subTree, container, anchor, instance, …) 挂载子树; 调用 onMounted 钩子等。在更新阶段时, 重新执行 render 得到新子树; 调用 patch(prevSubTree, nextSubTree, container, anchor, instance, ...) 执行 diff 和更新挂载操作。2. 创建组件响应式 Effect, 将封装的 componentUpdateFn 函数封装成一个响应式 effect, componentUpdateFn 函数在执行过程中访问的所有响应式数据(例如 propsdatacomputed 等)都会被自动收集为依赖。当这些依赖发生变化时,effect 就会重新执行,从而触发组件更新。创建 Effect 时, 通常会传入一个调度器选项, 利用 VueScheduler 来对 effect 的更新进行调度, Vue 3 内部通过 scheduler 实现了一个队列系统, 当多个响应式依赖同时触发更新时, 会把这些更新任务合并到微任务队列中,从而在当前调用栈结束后再执行更新,保证更新过程更加平滑, 这样可以将多次响应式数据变化合并成一次更新, 避免不必要的重复渲染。此外, 在执行 effect 时会使用 flushPreRenderEffectflushPostRenderEffect 来保证生命周期钩子(如 onMountedonUpdated)按预期顺序执行。3. 错误处理与边界处理, 在 effect 内部,setupRenderEffect 会包裹错误处理逻辑,确保如果 render 函数或 patch 过程中出现错误,这些错误能够被捕获并传递给错误边界或者全局错误处理器,而不会直接导致整个应用崩溃。

六、生成子树(subTree)并挂载: 1. 调用组件实例的 render 方法, 组件实例的 render 方法策略为: 如果 setup 返回了 render 函数,就直接使用, 否则,如果组件选项中已有 render 函数,则使用选项中的 render, 如果既没有 setup 返回 render,也没有 options.render,会走模板编译流程, 此时会调用运行时编译器将 template 转换为 render 函数。得到最终组件实例的 render 方法, 在调用 render 时,组件实例的 proxy 对象(通过 proxyHandler 包装的)会确保在 render 内访问到 props、datacomputedsetupState 的值, 同时, 组件的全局上下文也会被注入到 VNode 中, 确保全局注册的组件、指令等能够生效, 最后, render 方法会生成 SubTree VNode。2. 基于 SubTree VNode 调用 patch, 根据 subTree 的类型判断, 对于普通元素, patch 会调用 mountElement, 使用 document.createElement 创建对应的 DOM 元素, 设置该元素的属性、事件、样式等, 递归调用 patchsubTree.children 进行挂载, 将它们转换为真实 DOM 并插入当前元素中, 最终,将整个生成的 DOM 树插入到指定的容器中。

四、更新


当模版的依赖发生变化时, Scheduler 调度器 在某个时机调度触发 组件渲染 Effect 重新执行, 进而重新调用 componentUpdateFn 函数。此时, 处于更新阶段, 会重新执行 render 得到新子树, 调用 patch(prevSubTree, nextSubTree, container, anchor, instance, ...) 执行 diff 和更新挂载操作。

五、思考与沉底


5.1 什么是 MVVM?

MVVMModel-View-ViewModel 是一种软件架构模式,用于实现用户界面(UI)和业务逻辑的分离。它的设计目标是将界面的开发与后端的业务逻辑分离,使代码更易于理解、维护和测试。在 MVVM 中,各个组成部分的职责如下:

  • Model(模型): 表示应用程序的数据和业务逻辑。它负责数据的存储、检索和更新,并封装了与数据相关的操作和规则。

  • View(视图): 展示用户界面,通常是由UI元素组成的。它是用户与应用程序进行交互的界面,负责将数据呈现给用户,并接收用户的输入。

  • ViewModel(视图模型): 连接 ViewModel,负责处理业务逻辑和数据的交互。它从 Model 中获取数据,并将数据转换为 View 可以理解和展示的格式。ViewModel还负责监听View的变化,并根据用户的输入更新Model中的数据。

MVVM 的核心思想是双向数据绑定, 通过双向绑定机制将 ViewViewModel 中的数据保持同步。当 ViewModel 中的数据发生变化时,View会自动更新,反之亦然。这种数据驱动的方式使得开发者可以专注于业务逻辑的实现,而无需手动操作 DOM 元素来更新界面。

总结来说,MVVM 是一种将数据驱动视图的设计模式,通过 ViewModel 作为中间层来实现数据和视图之间的解耦。Vue作为一种流行的MVVM框架,提供了强大的数据绑定和响应式系统,使开发者能够更轻松地构建交互性强的Web应用程序。

Vue3.0 中, 基于 MVVM 的实现思想如下:

一、ReactiveEffect 作为响应式副作用的核心: 它封装了一个响应式计算(effect), 使得该计算在依赖的响应式数据发生变化时能够重新执行, 从而实现组件更新、计算属性、watcher 等功能。具体工作流为: 1. 封装响应式副作用函数, 通过 ReactiveEffect 对象封装 effectFn 副作用函数, 使其能在依赖变化时重新执行; 2. 依赖收集, 在执行 effectFn 副作用函数过程中, 将访问的响应式数据(通过 getter 拦截)收集到依赖集合中, 同时, 每个响应式数据内部通过一个依赖集合(Dep)记录当前活跃的 effect; 3. 调度更新, 当响应式数据发生变化时, 会遍历 Dep 依赖集合, 触发 effect 重新执行, 或通过调度器进行批量更新; 4. 清理依赖, 提供 stop() 方法,能在 effect 停止后清理所有依赖,避免内存泄漏和不必要的更新。

二、基于 ReactiveEffect 的依赖追踪与触发更新函数:

  1. 依赖追踪 track 函数: 当响应式数据(无论是通过 reactive 包装的对象还是 ref 包装的值)在读取时,其 getter 拦截器会调用内部的依赖收集函数(track)。这个函数会将当前全局活跃的 effectReactiveEffect 实例)添加到该数据对应的依赖集合中。依赖集合通常存储在全局的 targetMap 中,使用 WeakMap + Map + Set 的嵌套结构管理每个响应式对象、每个属性和对应的 effect

  2. 触发更新 trigger: 当响应式数据发生写入(set 操作)时,拦截器会调用 trigger 函数,遍历依赖该属性的所有 effect``,并通知它们重新执行。重新执行时,ReactiveEffect 会调用用户提供的副作用函数,从而更新视图或计算属性。调度器(scheduler)选项允许 Vue 对这些更新进行批量处理和异步调度,从而避免不必要的重复计算和 DOM 更新。

三、初始化组件时, 创建组件渲染副作用 Reactive Effect: 1. 封装函数组件 componentUpdateFn 函数, 主要逻辑为: 首次渲染时, 调用组件实例的 render 方法, 组件实例的 render 方法策略为: 如果 setup 返回了 render 函数,就直接使用, 否则,如果组件选项中已有 render 函数,则使用选项中的 render, 如果既没有 setup 返回 render,也没有 options.render,会走模板编译流程, 此时会调用运行时编译器将 template 转换为 render 函数。得到最终组件实例的 render 方法, 生成子树(subTree); 调用 patch(null, subTree, container, anchor, instance, …) 挂载子树; 调用 onMounted 钩子等。在更新阶段时, 重新执行 render 得到新子树; 调用 patch(prevSubTree, nextSubTree, container, anchor, instance, ...) 执行 diff 和更新挂载操作。2. 创建组件响应式 Effect, 将封装的 componentUpdateFn 函数封装成一个响应式 effect, componentUpdateFn 函数在执行过程中访问的所有响应式数据(例如 propsdatacomputed 等)都会被自动收集为依赖。当这些依赖发生变化时,effect 就会重新执行,从而触发组件更新。创建 Effect 时, 通常会传入一个调度器选项, 利用 VueScheduler 来对 effect 的更新进行调度, Vue 3 内部通过 scheduler 实现了一个队列系统, 当多个响应式依赖同时触发更新时, 会把这些更新任务合并到微任务队列中,从而在当前调用栈结束后再执行更新,保证更新过程更加平滑, 这样可以将多次响应式数据变化合并成一次更新, 避免不必要的重复渲染。此外, 在执行 effect 时会使用 flushPreRenderEffectflushPostRenderEffect 来保证生命周期钩子(如 onMountedonUpdated)按预期顺序执行。

四、基于访问器和设置器实现的 ref: 1. 依赖收集(trackRefValue: 当 ref.value 被读取时,内部调用 trackRefValue(this)。该函数利用 Vue 3 的响应式追踪机制(全局 activeEffectdependency collection)将当前正在执行的响应式 effect 收集到这个 ref 的依赖集合中。这样, 当 ref 的值更新时,所有依赖它的 effect 都能被通知重新执行。2. 触发更新(triggerRefValue: 当 ref.value 被修改并且新值与旧值不同,内部调用 triggerRefValue(this)。该函数会遍历之前收集的依赖(effect),并触发它们的调度器执行更新任务,从而使组件或计算属性重新运行。3. 深度转换与浅引用: 如果传入 ref 的值是一个对象,并且不是 shallowRefVue 会调用 reactive 将该对象递归转换为响应式对象。这保证了对象内部的属性也会触发响应式更新。同时, Vue 3 提供了 shallowRef 来避免深度转换,对于只需要追踪顶层变化的情况,可以提高性能。

五、基于 Proxy 代理实现的 reactive:

  • 5.1 缓存与复用, 为了避免对同一个对象多次包装,Vue 使用 WeakMap 来缓存已经创建的 reactive 对象。每个目标对象只会有一个唯一的 Proxy 包装,这样可以保证响应式对象的一致性和性能。

  • 5.2. get 拦截器, 读取属性时,调用 track(target, key) 进行依赖收集,将当前激活的 effect(即正在执行的响应式函数)与该属性关联。

    • 5.2.1 对象: 如果访问的属性值本身是一个对象,还会递归调用 reactive 将其转换为响应式对象(除非使用 shallowReactive)。

    • 5.2.2 数组: 如果访问数组 array.includes() array.indexOf() array.lastIndexOf() 查找类型的数组 API, 查找的目标元素有可能是代理数据, 有可能是原始数据, 但是 array 肯定是代理数据, 所以需要对查找类型的 API 重写, 添加代理数组与原始数组的映射关系, 之后优先在代理数组中查找, 如果找不到再去原始数组中查找。如果访问数组 array.push() array.push() array.shift() array.unshift() array.splice() 增删类型 API, 会读取数组中的 length 属性,也会设置数组中的 length 属性, 这会导致两个独立的副作用函数相互影响。只要屏蔽了对 length 属性的读取, 从而避免在它与副作用函数之间建立响应联系。具体策略: 重写array.push() array.push() array.shift() array.unshift() array.splice() 方法, 在调用原始方法之前, 停止追踪, 调用原始方法之后, 继续追踪

  • 5.3 set 拦截器, 当设置属性时,先判断新值和旧值是否真正发生变化(例如通过比较)。若有变化,则更新 target 中对应的属性,并调用 trigger(target, key, newVal) 通知所有依赖此属性的 effect 执行更新。

  • 5.4 has 拦截器, 监听 xx in yy 操作符, Vue 会调用内部的 track 函数,将当前正在运行的响应式 effect 收集为依赖。

  • 5.5 ownKeys 拦截器, 监听 for infor of 遍历操作, 收集 ITERATE_KEY 相关的副作用。因为 ownKeys 只有一个 target 参数, 没有 key, 对于对象来说需要一个 ITERATE_KEY 来充当 key, 后续之后新增属性时, 才会触发副作用函数执行, 已有属性不触发。对于数组而言, 无论是为数组添加新元素还是直接修改数组的长度, 本质上都是因为修改了数组的 length 属性。 一旦数组的 length 被修改, 那么 for…infor……of 循环对数组的遍历结果就会改变。

  • 5.6 deleteProperty 拦截器, 监听属性删除操作, 触发对应属性副作用函数重新执行

六、computedwatch 的构建:

  • computed: Computed 具有惰性计算、缓存更新等特点。在 Vue 3 中, Computed 的实现是建立在响应式系统和 ReactiveEffect 之上的一种缓存计算机制, 主要依赖于 ReactiveEffect 来包装计算函数, 并利用调度器和 dirty 标记以及 _value 实现惰性计算与缓存更新。这种设计既保证了 Computed 只在必要时重新计算,又能高效地追踪依赖,实现高性能的响应式数据更新,是 Vue 3 响应式系统的重要组成部分。

  • watch: Watch 具体工作流如下: 1. 响应式依赖追踪, Watch 的本质是创建一个 ReactiveEffect 来观察 数据源(可以是 refreactive 对象、getter 函数或它们的组合)。当数据源中依赖的数据发生变化时, ReactiveEffect 会触发调度器,进而执行 watch 回调。2. 懒执行与调度, Watch 通过传入自定义的调度器(scheduler), 可以控制何时执行回调函数, 默认情况下, watch 会异步调度更新(比如在微任务队列中), 也支持同步(flush: 'sync')或 post-render(flush: 'post')等不同的调度时机。3. 清理与失效, 为了处理异步回调中可能存在的竞争问题,watch 提供了 onInvalidate 回调,允许用户注册清理函数, 如果在异步任务期间依赖发生变化,则会调用该清理函数,以防止过时的回调继续执行。

5.2 Vue 3.0 响应式原理

Vue 3.0 的响应式系统基于以下核心机制构建:

一、ReactiveEffect 作为响应式副作用的核心: 它封装了一个响应式计算(effect), 使得该计算在依赖的响应式数据发生变化时能够重新执行, 从而实现组件更新、计算属性、watcher 等功能。具体工作流为: 1. 封装响应式副作用函数, 通过 ReactiveEffect 对象封装 effectFn 副作用函数, 使其能在依赖变化时重新执行; 2. 依赖收集, 在执行 effectFn 副作用函数过程中, 将访问的响应式数据(通过 getter 拦截)收集到依赖集合中, 同时, 每个响应式数据内部通过一个依赖集合(Dep)记录当前活跃的 effect; 3. 调度更新, 当响应式数据发生变化时, 会遍历 Dep 依赖集合, 触发 effect 重新执行, 或通过调度器进行批量更新; 4. 清理依赖, 提供 stop() 方法,能在 effect 停止后清理所有依赖,避免内存泄漏和不必要的更新。

二、基于 ReactiveEffect 的依赖追踪与触发更新函数:

  1. 依赖追踪 track 函数: 当响应式数据(无论是通过 reactive 包装的对象还是 ref 包装的值)在读取时,其 getter 拦截器会调用内部的依赖收集函数(track)。这个函数会将当前全局活跃的 effectReactiveEffect 实例)添加到该数据对应的依赖集合中。依赖集合通常存储在全局的 targetMap 中,使用 WeakMap + Map + Set 的嵌套结构管理每个响应式对象、每个属性和对应的 effect

  2. 触发更新 trigger: 当响应式数据发生写入(set 操作)时,拦截器会调用 trigger 函数,遍历依赖该属性的所有 effect``,并通知它们重新执行。重新执行时,ReactiveEffect 会调用用户提供的副作用函数,从而更新视图或计算属性。调度器(scheduler)选项允许 Vue 对这些更新进行批量处理和异步调度,从而避免不必要的重复计算和 DOM 更新。

三、基于访问器和设置器实现的 ref: 1. 依赖收集(trackRefValue: 当 ref.value 被读取时,内部调用 trackRefValue(this)。该函数利用 Vue 3 的响应式追踪机制(全局 activeEffectdependency collection)将当前正在执行的响应式 effect 收集到这个 ref 的依赖集合中。这样, 当 ref 的值更新时,所有依赖它的 effect 都能被通知重新执行。2. 触发更新(triggerRefValue: 当 ref.value 被修改并且新值与旧值不同,内部调用 triggerRefValue(this)。该函数会遍历之前收集的依赖(effect),并触发它们的调度器执行更新任务,从而使组件或计算属性重新运行。3. 深度转换与浅引用: 如果传入 ref 的值是一个对象,并且不是 shallowRefVue 会调用 reactive 将该对象递归转换为响应式对象。这保证了对象内部的属性也会触发响应式更新。同时, Vue 3 提供了 shallowRef 来避免深度转换,对于只需要追踪顶层变化的情况,可以提高性能。

四、基于 Proxy 代理实现的 reactive:

  • 4.1 缓存与复用, 为了避免对同一个对象多次包装,Vue 使用 WeakMap 来缓存已经创建的 reactive 对象。每个目标对象只会有一个唯一的 Proxy 包装,这样可以保证响应式对象的一致性和性能。

  • 4.2. get 拦截器, 读取属性时,调用 track(target, key) 进行依赖收集,将当前激活的 effect(即正在执行的响应式函数)与该属性关联。

    • 4.2.1 对象: 如果访问的属性值本身是一个对象,还会递归调用 reactive 将其转换为响应式对象(除非使用 shallowReactive)。

    • 4.2.2 数组: 如果访问数组 array.includes() array.indexOf() array.lastIndexOf() 查找类型的数组 API, 查找的目标元素有可能是代理数据, 有可能是原始数据, 但是 array 肯定是代理数据, 所以需要对查找类型的 API 重写, 添加代理数组与原始数组的映射关系, 之后优先在代理数组中查找, 如果找不到再去原始数组中查找。如果访问数组 array.push() array.push() array.shift() array.unshift() array.splice() 增删类型 API, 会读取数组中的 length 属性,也会设置数组中的 length 属性, 这会导致两个独立的副作用函数相互影响。只要屏蔽了对 length 属性的读取, 从而避免在它与副作用函数之间建立响应联系。具体策略: 重写array.push() array.push() array.shift() array.unshift() array.splice() 方法, 在调用原始方法之前, 停止追踪, 调用原始方法之后, 继续追踪

  • 4.3 set 拦截器, 当设置属性时,先判断新值和旧值是否真正发生变化(例如通过比较)。若有变化,则更新 target 中对应的属性,并调用 trigger(target, key, newVal) 通知所有依赖此属性的 effect 执行更新。

  • 4.4 has 拦截器, 监听 xx in yy 操作符, Vue 会调用内部的 track 函数,将当前正在运行的响应式 effect 收集为依赖。

  • 4.5 ownKeys 拦截器, 监听 for infor of 遍历操作, 收集 ITERATE_KEY 相关的副作用。因为 ownKeys 只有一个 target 参数, 没有 key, 对于对象来说需要一个 ITERATE_KEY 来充当 key, 后续之后新增属性时, 才会触发副作用函数执行, 已有属性不触发。对于数组而言, 无论是为数组添加新元素还是直接修改数组的长度, 本质上都是因为修改了数组的 length 属性。 一旦数组的 length 被修改, 那么 for…infor……of 循环对数组的遍历结果就会改变。

  • 4.6 deleteProperty 拦截器, 监听属性删除操作, 触发对应属性副作用函数重新执行

五、computedwatch 的构建:

  • computed: Computed 具有惰性计算、缓存更新等特点。在 Vue 3 中, Computed 的实现是建立在响应式系统和 ReactiveEffect 之上的一种缓存计算机制, 主要依赖于 ReactiveEffect 来包装计算函数, 并利用调度器和 dirty 标记以及 _value 实现惰性计算与缓存更新。这种设计既保证了 Computed 只在必要时重新计算,又能高效地追踪依赖,实现高性能的响应式数据更新,是 Vue 3 响应式系统的重要组成部分。

  • watch: Watch 具体工作流如下: 1. 响应式依赖追踪, Watch 的本质是创建一个 ReactiveEffect 来观察 数据源(可以是 refreactive 对象、getter 函数或它们的组合)。当数据源中依赖的数据发生变化时, ReactiveEffect 会触发调度器,进而执行 watch 回调。2. 懒执行与调度, Watch 通过传入自定义的调度器(scheduler), 可以控制何时执行回调函数, 默认情况下, watch 会异步调度更新(比如在微任务队列中), 也支持同步(flush: 'sync')或 post-render(flush: 'post')等不同的调度时机。3. 清理与失效, 为了处理异步回调中可能存在的竞争问题,watch 提供了 onInvalidate 回调,允许用户注册清理函数, 如果在异步任务期间依赖发生变化,则会调用该清理函数,以防止过时的回调继续执行。

5.3 Vue 3.0 双向数据绑定

双向数据绑定 基于 MVVM 模型, 包括 数据层Model视图层View业务逻辑层ViewModel。其中 ViewModel(业务逻辑层) 负责将数据和视图关联起来, 提供了 数据变化后更新视图视图变化后更新数据 这样一个功能,就是传统意义上的双向绑定。

MVVMModel-View-ViewModel 是一种软件架构模式,用于实现用户界面(UI)和业务逻辑的分离。它的设计目标是将界面的开发与后端的业务逻辑分离,使代码更易于理解、维护和测试。在 MVVM 中,各个组成部分的职责如下:

  • Model(模型): 表示应用程序的数据和业务逻辑。它负责数据的存储、检索和更新,并封装了与数据相关的操作和规则。

  • View(视图): 展示用户界面,通常是由UI元素组成的。它是用户与应用程序进行交互的界面,负责将数据呈现给用户,并接收用户的输入。

  • ViewModel(视图模型): 连接 ViewModel,负责处理业务逻辑和数据的交互。它从 Model 中获取数据,并将数据转换为 View 可以理解和展示的格式。ViewModel还负责监听View的变化,并根据用户的输入更新Model中的数据。

MVVM 的核心思想是双向数据绑定, 通过双向绑定机制将 ViewViewModel 中的数据保持同步。当 ViewModel 中的数据发生变化时,View会自动更新,反之亦然。这种数据驱动的方式使得开发者可以专注于业务逻辑的实现,而无需手动操作 DOM 元素来更新界面。

总结来说,MVVM 是一种将数据驱动视图的设计模式,通过 ViewModel 作为中间层来实现数据和视图之间的解耦。Vue作为一种流行的MVVM框架,提供了强大的数据绑定和响应式系统,使开发者能够更轻松地构建交互性强的Web应用程序。

Vue3.0 中, 基于 MVVM 的实现思想如下:

一、ReactiveEffect 作为响应式副作用的核心: 它封装了一个响应式计算(effect), 使得该计算在依赖的响应式数据发生变化时能够重新执行, 从而实现组件更新、计算属性、watcher 等功能。具体工作流为: 1. 封装响应式副作用函数, 通过 ReactiveEffect 对象封装 effectFn 副作用函数, 使其能在依赖变化时重新执行; 2. 依赖收集, 在执行 effectFn 副作用函数过程中, 将访问的响应式数据(通过 getter 拦截)收集到依赖集合中, 同时, 每个响应式数据内部通过一个依赖集合(Dep)记录当前活跃的 effect; 3. 调度更新, 当响应式数据发生变化时, 会遍历 Dep 依赖集合, 触发 effect 重新执行, 或通过调度器进行批量更新; 4. 清理依赖, 提供 stop() 方法,能在 effect 停止后清理所有依赖,避免内存泄漏和不必要的更新。

二、基于 ReactiveEffect 的依赖追踪与触发更新函数:

  1. 依赖追踪 track 函数: 当响应式数据(无论是通过 reactive 包装的对象还是 ref 包装的值)在读取时,其 getter 拦截器会调用内部的依赖收集函数(track)。这个函数会将当前全局活跃的 effectReactiveEffect 实例)添加到该数据对应的依赖集合中。依赖集合通常存储在全局的 targetMap 中,使用 WeakMap + Map + Set 的嵌套结构管理每个响应式对象、每个属性和对应的 effect

  2. 触发更新 trigger: 当响应式数据发生写入(set 操作)时,拦截器会调用 trigger 函数,遍历依赖该属性的所有 effect``,并通知它们重新执行。重新执行时,ReactiveEffect 会调用用户提供的副作用函数,从而更新视图或计算属性。调度器(scheduler)选项允许 Vue 对这些更新进行批量处理和异步调度,从而避免不必要的重复计算和 DOM 更新。

三、初始化组件时, 创建组件渲染副作用 Reactive Effect: 1. 封装函数组件 componentUpdateFn 函数, 主要逻辑为: 首次渲染时, 调用组件实例的 render 方法, 组件实例的 render 方法策略为: 如果 setup 返回了 render 函数,就直接使用, 否则,如果组件选项中已有 render 函数,则使用选项中的 render, 如果既没有 setup 返回 render,也没有 options.render,会走模板编译流程, 此时会调用运行时编译器将 template 转换为 render 函数。得到最终组件实例的 render 方法, 生成子树(subTree); 调用 patch(null, subTree, container, anchor, instance, …) 挂载子树; 调用 onMounted 钩子等。在更新阶段时, 重新执行 render 得到新子树; 调用 patch(prevSubTree, nextSubTree, container, anchor, instance, ...) 执行 diff 和更新挂载操作。2. 创建组件响应式 Effect, 将封装的 componentUpdateFn 函数封装成一个响应式 effect, componentUpdateFn 函数在执行过程中访问的所有响应式数据(例如 propsdatacomputed 等)都会被自动收集为依赖。当这些依赖发生变化时,effect 就会重新执行,从而触发组件更新。创建 Effect 时, 通常会传入一个调度器选项, 利用 VueScheduler 来对 effect 的更新进行调度, Vue 3 内部通过 scheduler 实现了一个队列系统, 当多个响应式依赖同时触发更新时, 会把这些更新任务合并到微任务队列中,从而在当前调用栈结束后再执行更新,保证更新过程更加平滑, 这样可以将多次响应式数据变化合并成一次更新, 避免不必要的重复渲染。此外, 在执行 effect 时会使用 flushPreRenderEffectflushPostRenderEffect 来保证生命周期钩子(如 onMountedonUpdated)按预期顺序执行。

四、基于访问器和设置器实现的 ref: 1. 依赖收集(trackRefValue: 当 ref.value 被读取时,内部调用 trackRefValue(this)。该函数利用 Vue 3 的响应式追踪机制(全局 activeEffectdependency collection)将当前正在执行的响应式 effect 收集到这个 ref 的依赖集合中。这样, 当 ref 的值更新时,所有依赖它的 effect 都能被通知重新执行。2. 触发更新(triggerRefValue: 当 ref.value 被修改并且新值与旧值不同,内部调用 triggerRefValue(this)。该函数会遍历之前收集的依赖(effect),并触发它们的调度器执行更新任务,从而使组件或计算属性重新运行。3. 深度转换与浅引用: 如果传入 ref 的值是一个对象,并且不是 shallowRefVue 会调用 reactive 将该对象递归转换为响应式对象。这保证了对象内部的属性也会触发响应式更新。同时, Vue 3 提供了 shallowRef 来避免深度转换,对于只需要追踪顶层变化的情况,可以提高性能。

五、基于 Proxy 代理实现的 reactive:

  • 5.1 缓存与复用, 为了避免对同一个对象多次包装,Vue 使用 WeakMap 来缓存已经创建的 reactive 对象。每个目标对象只会有一个唯一的 Proxy 包装,这样可以保证响应式对象的一致性和性能。

  • 5.2. get 拦截器, 读取属性时,调用 track(target, key) 进行依赖收集,将当前激活的 effect(即正在执行的响应式函数)与该属性关联。

    • 5.2.1 对象: 如果访问的属性值本身是一个对象,还会递归调用 reactive 将其转换为响应式对象(除非使用 shallowReactive)。

    • 5.2.2 数组: 如果访问数组 array.includes() array.indexOf() array.lastIndexOf() 查找类型的数组 API, 查找的目标元素有可能是代理数据, 有可能是原始数据, 但是 array 肯定是代理数据, 所以需要对查找类型的 API 重写, 添加代理数组与原始数组的映射关系, 之后优先在代理数组中查找, 如果找不到再去原始数组中查找。如果访问数组 array.push() array.push() array.shift() array.unshift() array.splice() 增删类型 API, 会读取数组中的 length 属性,也会设置数组中的 length 属性, 这会导致两个独立的副作用函数相互影响。只要屏蔽了对 length 属性的读取, 从而避免在它与副作用函数之间建立响应联系。具体策略: 重写array.push() array.push() array.shift() array.unshift() array.splice() 方法, 在调用原始方法之前, 停止追踪, 调用原始方法之后, 继续追踪

  • 5.3 set 拦截器, 当设置属性时,先判断新值和旧值是否真正发生变化(例如通过比较)。若有变化,则更新 target 中对应的属性,并调用 trigger(target, key, newVal) 通知所有依赖此属性的 effect 执行更新。

  • 5.4 has 拦截器, 监听 xx in yy 操作符, Vue 会调用内部的 track 函数,将当前正在运行的响应式 effect 收集为依赖。

  • 5.5 ownKeys 拦截器, 监听 for infor of 遍历操作, 收集 ITERATE_KEY 相关的副作用。因为 ownKeys 只有一个 target 参数, 没有 key, 对于对象来说需要一个 ITERATE_KEY 来充当 key, 后续之后新增属性时, 才会触发副作用函数执行, 已有属性不触发。对于数组而言, 无论是为数组添加新元素还是直接修改数组的长度, 本质上都是因为修改了数组的 length 属性。 一旦数组的 length 被修改, 那么 for…infor……of 循环对数组的遍历结果就会改变。

  • 5.6 deleteProperty 拦截器, 监听属性删除操作, 触发对应属性副作用函数重新执行

六、computedwatch 的构建:

  • computed: Computed 具有惰性计算、缓存更新等特点。在 Vue 3 中, Computed 的实现是建立在响应式系统和 ReactiveEffect 之上的一种缓存计算机制, 主要依赖于 ReactiveEffect 来包装计算函数, 并利用调度器和 dirty 标记以及 _value 实现惰性计算与缓存更新。这种设计既保证了 Computed 只在必要时重新计算,又能高效地追踪依赖,实现高性能的响应式数据更新,是 Vue 3 响应式系统的重要组成部分。

  • watch: Watch 具体工作流如下: 1. 响应式依赖追踪, Watch 的本质是创建一个 ReactiveEffect 来观察 数据源(可以是 refreactive 对象、getter 函数或它们的组合)。当数据源中依赖的数据发生变化时, ReactiveEffect 会触发调度器,进而执行 watch 回调。2. 懒执行与调度, Watch 通过传入自定义的调度器(scheduler), 可以控制何时执行回调函数, 默认情况下, watch 会异步调度更新(比如在微任务队列中), 也支持同步(flush: 'sync')或 post-render(flush: 'post')等不同的调度时机。3. 清理与失效, 为了处理异步回调中可能存在的竞争问题,watch 提供了 onInvalidate 回调,允许用户注册清理函数, 如果在异步任务期间依赖发生变化,则会调用该清理函数,以防止过时的回调继续执行。

5.4 Vue 3.0 挂载更新流程

5.5 createApp() 初始化过程

5.6 Vue 3.0 组件是异步渲染的?

Vue 3 的组件渲染本身并不是 异步渲染 的,而是借助异步调度器对更新进行批量处理,从而在更新触发时实现更高的性能。当组件的响应式数据发生变化时,组件的 render 函数(或通过 ReactiveEffect 包裹的副作用函数)会被调用来生成新的虚拟 DOM(VNode)树,这个过程本质上是同步执行的。也就是说,render 函数在执行时是一个同步函数。但是, 为了避免多次数据变化导致重复渲染,Vue 3 内部使用调度器(scheduler),将多次更新任务合并并放入微任务队列中。这样,多个数据变化可以在同一个更新批次中一起处理,从而减少不必要的重复计算和 DOM 操作。这种调度机制给人一种 异步 的感觉,但实际上每次渲染(即 render 函数执行)是同步的。

初始化组件时, 创建组件渲染副作用 Reactive Effect: 1. 封装函数组件 componentUpdateFn 函数, 主要逻辑为: 首次渲染时, 调用组件实例的 render 方法, 组件实例的 render 方法策略为: 如果 setup 返回了 render 函数,就直接使用, 否则,如果组件选项中已有 render 函数,则使用选项中的 render, 如果既没有 setup 返回 render,也没有 options.render,会走模板编译流程, 此时会调用运行时编译器将 template 转换为 render 函数。得到最终组件实例的 render 方法, 生成子树(subTree)这个过程本质上是同步执行的。也就是说,render 函数在执行时是一个同步函数; 调用 patch(null, subTree, container, anchor, instance, …) 挂载子树; 调用 onMounted 钩子等。在更新阶段时, 重新执行 render 得到新子树; 调用 patch(prevSubTree, nextSubTree, container, anchor, instance, ...) 执行 diff 和更新挂载操作。2. 创建组件响应式 Effect, 将封装的 componentUpdateFn 函数封装成一个响应式 effect, componentUpdateFn 函数在执行过程中访问的所有响应式数据(例如 propsdatacomputed 等)都会被自动收集为依赖。当这些依赖发生变化时,effect 就会重新执行,从而触发组件更新。创建 Effect 时, 通常会传入一个调度器选项, 利用 VueScheduler 来对 effect 的更新进行调度, Vue 3 内部通过 scheduler 实现了一个队列系统, 当多个响应式依赖同时触发更新时, 会把这些更新任务合并到微任务队列中,从而在当前调用栈结束后再执行更新,保证更新过程更加平滑, 这样可以将多次响应式数据变化合并成一次更新, 避免不必要的重复渲染。此外, 在执行 effect 时会使用 flushPreRenderEffectflushPostRenderEffect 来保证生命周期钩子(如 onMountedonUpdated)按预期顺序执行。

5.7 Vue 组件是如何渲染和更新的?