认识
一、认识
二、初始化
在 Vue 3
中, createApp
是应用启动的入口,它不仅创建一个应用实例, 还构建了全局的应用上下文。createApp
的主要职责可以归纳为以下几点:
-
创建全局应用上下文: 构建并保存全局配置、组件、指令、混入及
provide
数据,供所有组件共享。 -
构造应用实例: 封装根组件和初始
props
, 准备一个待挂载的应用对象。 -
暴露全局
API
: 通过mount
、use
、mixin
、component
、directive
、provide
等方法,实现全局资源的注册与扩展。 -
根节点挂载: 在调用
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 判断挂载或更新: 挂载时, 如果旧
vnode
为null
,说明当前vnode
还没有被挂载,则执行挂载操作,即将新vnode
转换为真实DOM
并插入到容器中。更新时, 如果旧vnode
存在,则进行更新操作,即对比新旧vnode
,计算差异,然后更新已有DOM
。 -
4.2 根据
vnode
类型分支处理:patch
会根据vnode
的类型(例如元素、组件、文本、Fragment
等)进入不同的处理流程: 1. 元素节点, 调用processElement
, 进一步执行挂载(mountElement
)或更新(patchElement
); 2. 组件节点, 调用processComponent
, 区分首次渲染与组件更新, 调用mountComponent
或updateComponent
; 3. 文本节点, 直接创建或更新文本内容; 4. Fragment 和其他特殊节点, 按照对应策略进行处理。
五、调用 mountComponent
挂载组件:
-
5.1 创建组件实例, 首先创建组件实例,
createComponentInstance()
生成了包含props
、attrs
、slots
、parent
、context
等信息的组件实例对象 -
5.2 组件初始化:
-
初始化
Props
, 调用内部的initProps
方法,将vnode
上的rawProps
转换为响应式的props
数据, 对props
进行校验和合并(比如默认值、类型检查),并将最终结果挂载到组件实例上, 此时props
已经是响应式的,后续在setup
或render
中访问props
就能响应更新; -
初始化
Slots
, 调用initSlots
方法,处理组件的children
,将它们解析为具备一定格式的插槽对象,slots
数据同样挂载到组件实例上,供组件内部使用。 -
执行
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
返回的是一个Promise
,Vue
会把该组件标记为异步组件,需要等待Promise resolve
后再继续后续的渲染流程。 -
完成组件的最终设置, 在执行完
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
(如data
、computed
、methods
)和组合式AP
I(setup
),那么setupState
优先级更高。4.3 处理生命周期钩子, 在setup
中注册的生命周期钩子(例如onBeforeMount
、onMounted
)会被存储下来,后续在组件挂载和更新时执行。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
函数在执行过程中访问的所有响应式数据(例如props
、data
、computed
等)都会被自动收集为依赖。当这些依赖发生变化时,effect
就会重新执行,从而触发组件更新。创建Effect
时, 通常会传入一个调度器选项, 利用Vue
的Scheduler
来对effect
的更新进行调度,Vue 3
内部通过scheduler
实现了一个队列系统, 当多个响应式依赖同时触发更新时, 会把这些更新任务合并到微任务队列中,从而在当前调用栈结束后再执行更新,保证更新过程更加平滑, 这样可以将多次响应式数据变化合并成一次更新, 避免不必要的重复渲染。此外, 在执行effect
时会使用flushPreRenderEffect
和flushPostRenderEffect
来保证生命周期钩子(如onMounted
、onUpdated
)按预期顺序执行。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、data
、computed
和 setupState
的值, 同时, 组件的全局上下文也会被注入到 VNode
中, 确保全局注册的组件、指令等能够生效, 最后, render
方法会生成 SubTree VNode
。2. 基于 SubTree VNode
调用 patch
, 根据 subTree
的类型判断, 对于普通元素, patch
会调用 mountElement
, 使用 document.createElement
创建对应的 DOM
元素, 设置该元素的属性、事件、样式等, 递归调用 patch
对 subTree.children
进行挂载, 将它们转换为真实 DOM
并插入当前元素中, 最终,将整个生成的 DOM
树插入到指定的容器中。
四、更新
当模版的依赖发生变化时, Scheduler
调度器 在某个时机调度触发 组件渲染 Effect
重新执行, 进而重新调用 componentUpdateFn
函数。此时, 处于更新阶段, 会重新执行 render
得到新子树, 调用 patch(prevSubTree, nextSubTree, container, anchor, instance, ...)
执行 diff
和更新挂载操作。
五、思考与沉底
5.1 什么是 MVVM?
MVVM
(Model-View-ViewModel
) 是一种软件架构模式,用于实现用户界面(UI
)和业务逻辑的分离。它的设计目标是将界面的开发与后端的业务逻辑分离,使代码更易于理解、维护和测试。在 MVVM
中,各个组成部分的职责如下:
-
Model
(模型): 表示应用程序的数据和业务逻辑。它负责数据的存储、检索和更新,并封装了与数据相关的操作和规则。 -
View
(视图): 展示用户界面,通常是由UI元素组成的。它是用户与应用程序进行交互的界面,负责将数据呈现给用户,并接收用户的输入。 -
ViewModel
(视图模型): 连接View
和Model
,负责处理业务逻辑和数据的交互。它从Model
中获取数据,并将数据转换为View
可以理解和展示的格式。ViewModel
还负责监听View
的变化,并根据用户的输入更新Model
中的数据。
MVVM
的核心思想是双向数据绑定, 通过双向绑定机制将 View
和 ViewModel
中的数据保持同步。当 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
的依赖追踪与触发更新函数:
-
依赖追踪
track
函数: 当响应式数据(无论是通过reactive
包装的对象还是ref
包装的值)在读取时,其getter
拦截器会调用内部的依赖收集函数(track
)。这个函数会将当前全局活跃的effect
(ReactiveEffect
实例)添加到该数据对应的依赖集合中。依赖集合通常存储在全局的targetMap
中,使用WeakMap + Map + Set
的嵌套结构管理每个响应式对象、每个属性和对应的effect
。 -
触发更新
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
函数在执行过程中访问的所有响应式数据(例如 props
、data
、computed
等)都会被自动收集为依赖。当这些依赖发生变化时,effect
就会重新执行,从而触发组件更新。创建 Effect
时, 通常会传入一个调度器选项, 利用 Vue
的 Scheduler
来对 effect
的更新进行调度, Vue 3
内部通过 scheduler
实现了一个队列系统, 当多个响应式依赖同时触发更新时, 会把这些更新任务合并到微任务队列中,从而在当前调用栈结束后再执行更新,保证更新过程更加平滑, 这样可以将多次响应式数据变化合并成一次更新, 避免不必要的重复渲染。此外, 在执行 effect
时会使用 flushPreRenderEffect
和 flushPostRenderEffect
来保证生命周期钩子(如 onMounted
、onUpdated
)按预期顺序执行。
四、基于访问器和设置器实现的 ref
: 1. 依赖收集(trackRefValue
): 当 ref.value
被读取时,内部调用 trackRefValue(this)
。该函数利用 Vue 3
的响应式追踪机制(全局 activeEffect
和 dependency collection
)将当前正在执行的响应式 effect
收集到这个 ref
的依赖集合中。这样, 当 ref
的值更新时,所有依赖它的 effect
都能被通知重新执行。2. 触发更新(triggerRefValue
): 当 ref.value
被修改并且新值与旧值不同,内部调用 triggerRefValue(this)
。该函数会遍历之前收集的依赖(effect
),并触发它们的调度器执行更新任务,从而使组件或计算属性重新运行。3. 深度转换与浅引用: 如果传入 ref
的值是一个对象,并且不是 shallowRef
,Vue
会调用 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 in
、for of
遍历操作, 收集ITERATE_KEY
相关的副作用。因为ownKeys
只有一个target
参数, 没有key
, 对于对象来说需要一个ITERATE_KEY
来充当key
, 后续之后新增属性时, 才会触发副作用函数执行, 已有属性不触发。对于数组而言, 无论是为数组添加新元素还是直接修改数组的长度, 本质上都是因为修改了数组的length
属性。 一旦数组的length
被修改, 那么for…in
、for……of
循环对数组的遍历结果就会改变。 -
5.6
deleteProperty
拦截器, 监听属性删除操作, 触发对应属性副作用函数重新执行
六、computed
与 watch
的构建:
-
computed
:Computed
具有惰性计算、缓存更新等特点。在Vue 3
中,Computed
的实现是建立在响应式系统和ReactiveEffect
之上的一种缓存计算机制, 主要依赖于ReactiveEffect
来包装计算函数, 并利用调度器和dirty
标记以及_value
实现惰性计算与缓存更新。这种设计既保证了Computed
只在必要时重新计算,又能高效地追踪依赖,实现高性能的响应式数据更新,是Vue 3
响应式系统的重要组成部分。 -
watch
:Watch
具体工作流如下: 1. 响应式依赖追踪,Watch
的本质是创建一个ReactiveEffect
来观察 数据源(可以是ref
、reactive
对象、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
的依赖追踪与触发更新函数:
-
依赖追踪
track
函数: 当响应式数据(无论是通过reactive
包装的对象还是ref
包装的值)在读取时,其getter
拦截器会调用内部的依赖收集函数(track
)。这个函数会将当前全局活跃的effect
(ReactiveEffect
实例)添加到该数据对应的依赖集合中。依赖集合通常存储在全局的targetMap
中,使用WeakMap + Map + Set
的嵌套结构管理每个响应式对象、每个属性和对应的effect
。 -
触发更新
trigger
: 当响应式数据发生写入(set
操作)时,拦截器会调用trigger
函数,遍历依赖该属性的所有effect``,并通知它们重新执行。重新执行时,ReactiveEffect
会调用用户提供的副作用函数,从而更新视图或计算属性。调度器(scheduler
)选项允许Vue
对这些更新进行批量处理和异步调度,从而避免不必要的重复计算和DOM
更新。
三、基于访问器和设置器实现的 ref
: 1. 依赖收集(trackRefValue
): 当 ref.value
被读取时,内部调用 trackRefValue(this)
。该函数利用 Vue 3
的响应式追踪机制(全局 activeEffect
和 dependency collection
)将当前正在执行的响应式 effect
收集到这个 ref
的依赖集合中。这样, 当 ref
的值更新时,所有依赖它的 effect
都能被通知重新执行。2. 触发更新(triggerRefValue
): 当 ref.value
被修改并且新值与旧值不同,内部调用 triggerRefValue(this)
。该函数会遍历之前收集的依赖(effect
),并触发它们的调度器执行更新任务,从而使组件或计算属性重新运行。3. 深度转换与浅引用: 如果传入 ref
的值是一个对象,并且不是 shallowRef
,Vue
会调用 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 in
、for of
遍历操作, 收集ITERATE_KEY
相关的副作用。因为ownKeys
只有一个target
参数, 没有key
, 对于对象来说需要一个ITERATE_KEY
来充当key
, 后续之后新增属性时, 才会触发副作用函数执行, 已有属性不触发。对于数组而言, 无论是为数组添加新元素还是直接修改数组的长度, 本质上都是因为修改了数组的length
属性。 一旦数组的length
被修改, 那么for…in
、for……of
循环对数组的遍历结果就会改变。 -
4.6
deleteProperty
拦截器, 监听属性删除操作, 触发对应属性副作用函数重新执行
五、computed
与 watch
的构建:
-
computed
:Computed
具有惰性计算、缓存更新等特点。在Vue 3
中,Computed
的实现是建立在响应式系统和ReactiveEffect
之上的一种缓存计算机制, 主要依赖于ReactiveEffect
来包装计算函数, 并利用调度器和dirty
标记以及_value
实现惰性计算与缓存更新。这种设计既保证了Computed
只在必要时重新计算,又能高效地追踪依赖,实现高性能的响应式数据更新,是Vue 3
响应式系统的重要组成部分。 -
watch
:Watch
具体工作流如下: 1. 响应式依赖追踪,Watch
的本质是创建一个ReactiveEffect
来观察 数据源(可以是ref
、reactive
对象、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
(业务逻辑层) 负责将数据和视图关联起来, 提供了 数据变化后更新视图 和 视图变化后更新数据 这样一个功能,就是传统意义上的双向绑定。
MVVM
(Model-View-ViewModel
) 是一种软件架构模式,用于实现用户界面(UI
)和业务逻辑的分离。它的设计目标是将界面的开发与后端的业务逻辑分离,使代码更易于理解、维护和测试。在 MVVM
中,各个组成部分的职责如下:
-
Model
(模型): 表示应用程序的数据和业务逻辑。它负责数据的存储、检索和更新,并封装了与数据相关的操作和规则。 -
View
(视图): 展示用户界面,通常是由UI元素组成的。它是用户与应用程序进行交互的界面,负责将数据呈现给用户,并接收用户的输入。 -
ViewModel
(视图模型): 连接View
和Model
,负责处理业务逻辑和数据的交互。它从Model
中获取数据,并将数据转换为View
可以理解和展示的格式。ViewModel
还负责监听View
的变化,并根据用户的输入更新Model
中的数据。
MVVM
的核心思想是双向数据绑定, 通过双向绑定机制将 View
和 ViewModel
中的数据保持同步。当 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
的依赖追踪与触发更新函数:
-
依赖追踪
track
函数: 当响应式数据(无论是通过reactive
包装的对象还是ref
包装的值)在读取时,其getter
拦截器会调用内部的依赖收集函数(track
)。这个函数会将当前全局活跃的effect
(ReactiveEffect
实例)添加到该数据对应的依赖集合中。依赖集合通常存储在全局的targetMap
中,使用WeakMap + Map + Set
的嵌套结构管理每个响应式对象、每个属性和对应的effect
。 -
触发更新
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
函数在执行过程中访问的所有响应式数据(例如 props
、data
、computed
等)都会被自动收集为依赖。当这些依赖发生变化时,effect
就会重新执行,从而触发组件更新。创建 Effect
时, 通常会传入一个调度器选项, 利用 Vue
的 Scheduler
来对 effect
的更新进行调度, Vue 3
内部通过 scheduler
实现了一个队列系统, 当多个响应式依赖同时触发更新时, 会把这些更新任务合并到微任务队列中,从而在当前调用栈结束后再执行更新,保证更新过程更加平滑, 这样可以将多次响应式数据变化合并成一次更新, 避免不必要的重复渲染。此外, 在执行 effect
时会使用 flushPreRenderEffect
和 flushPostRenderEffect
来保证生命周期钩子(如 onMounted
、onUpdated
)按预期顺序执行。
四、基于访问器和设置器实现的 ref
: 1. 依赖收集(trackRefValue
): 当 ref.value
被读取时,内部调用 trackRefValue(this)
。该函数利用 Vue 3
的响应式追踪机制(全局 activeEffect
和 dependency collection
)将当前正在执行的响应式 effect
收集到这个 ref
的依赖集合中。这样, 当 ref
的值更新时,所有依赖它的 effect
都能被通知重新执行。2. 触发更新(triggerRefValue
): 当 ref.value
被修改并且新值与旧值不同,内部调用 triggerRefValue(this)
。该函数会遍历之前收集的依赖(effect
),并触发它们的调度器执行更新任务,从而使组件或计算属性重新运行。3. 深度转换与浅引用: 如果传入 ref
的值是一个对象,并且不是 shallowRef
,Vue
会调用 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 in
、for of
遍历操作, 收集ITERATE_KEY
相关的副作用。因为ownKeys
只有一个target
参数, 没有key
, 对于对象来说需要一个ITERATE_KEY
来充当key
, 后续之后新增属性时, 才会触发副作用函数执行, 已有属性不触发。对于数组而言, 无论是为数组添加新元素还是直接修改数组的长度, 本质上都是因为修改了数组的length
属性。 一旦数组的length
被修改, 那么for…in
、for……of
循环对数组的遍历结果就会改变。 -
5.6
deleteProperty
拦截器, 监听属性删除操作, 触发对应属性副作用函数重新执行
六、computed
与 watch
的构建:
-
computed
:Computed
具有惰性计算、缓存更新等特点。在Vue 3
中,Computed
的实现是建立在响应式系统和ReactiveEffect
之上的一种缓存计算机制, 主要依赖于ReactiveEffect
来包装计算函数, 并利用调度器和dirty
标记以及_value
实现惰性计算与缓存更新。这种设计既保证了Computed
只在必要时重新计算,又能高效地追踪依赖,实现高性能的响应式数据更新,是Vue 3
响应式系统的重要组成部分。 -
watch
:Watch
具体工作流如下: 1. 响应式依赖追踪,Watch
的本质是创建一个ReactiveEffect
来观察 数据源(可以是ref
、reactive
对象、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
函数在执行过程中访问的所有响应式数据(例如 props
、data
、computed
等)都会被自动收集为依赖。当这些依赖发生变化时,effect
就会重新执行,从而触发组件更新。创建 Effect
时, 通常会传入一个调度器选项, 利用 Vue
的 Scheduler
来对 effect
的更新进行调度, Vue 3
内部通过 scheduler
实现了一个队列系统, 当多个响应式依赖同时触发更新时, 会把这些更新任务合并到微任务队列中,从而在当前调用栈结束后再执行更新,保证更新过程更加平滑, 这样可以将多次响应式数据变化合并成一次更新, 避免不必要的重复渲染。此外, 在执行 effect
时会使用 flushPreRenderEffect
和 flushPostRenderEffect
来保证生命周期钩子(如 onMounted
、onUpdated
)按预期顺序执行。