文章目录
-
- vue的响应式原理-发布订阅者模式
- [vue3 响应式原理及优化](#vue3 响应式原理及优化)
- fiber
- [fiber 与 虚拟dom](#fiber 与 虚拟dom)
vue的响应式原理-发布订阅者模式
- Vue响应式原理概述
- Vue.js的响应式原理是其核心特性之一。它使得当数据发生变化时,与之绑定的DOM元素能够自动更新。其主要基于数据劫持和发布 - 订阅模式(也有观察者模式的概念在里面)来实现。
- 数据劫持
-
概念 :数据劫持是指在访问或者修改对象的属性时,通过一些方法拦截这个操作,添加自己的处理逻辑。在Vue中,主要是通过
Object.defineProperty()
方法来实现数据劫持。 -
示例 :
javascriptlet data = { name: 'John' }; let value; Object.defineProperty(data, 'name', { get() { console.log('获取name属性'); return value; }, set(newValue) { console.log('设置name属性'); value = newValue; } }); console.log(data.name); data.name = 'Jane';
-
在这个例子中,当读取
data.name
时,会触发get
方法,当修改data.name
时,会触发set
方法。这样就可以在获取和设置属性的时候添加自己的逻辑,比如在set
方法中可以通知相关的依赖进行更新。
-
- 发布 - 订阅者模式(观察者模式)
-
概念 :
- 发布 - 订阅模式:包含发布者和订阅者。发布者发布消息,订阅者订阅消息,当发布者发布消息时,所有订阅该消息的订阅者都会收到通知并执行相应的操作。
- 观察者模式:和发布 - 订阅模式类似,主要区别在于观察者模式中,被观察的对象(目标对象)直接维护一组观察者对象,当自身状态改变时,直接通知观察者。而发布 - 订阅模式有一个中间的消息队列或者事件中心来管理消息的发布和订阅。在Vue的响应式原理中,这两种模式的概念都有涉及。
-
示例(简单的发布 - 订阅模式实现) :
javascriptclass EventEmitter { constructor() { this.events = {}; } on(eventName, callback) { if (!this.events[eventName]) { this.events[eventName] = []; } this.events[eventName].push(callback); } emit(eventName, data) { if (this.events[eventName]) { this.events[eventName].forEach(callback => { callback(data); }); } } } let emitter = new EventEmitter(); function callback1(data) { console.log('订阅者1收到消息:', data); } function callback2(data) { console.log('订阅者2收到消息:', data); } emitter.on('message', callback1); emitter.on('message', callback2); emitter.emit('message', 'Hello, World!');
- 在这个例子中,
EventEmitter
是发布 - 订阅模式的核心类。on
方法用于订阅消息,emit
方法用于发布消息。当发布message
消息时,订阅了message
消息的callback1
和callback2
函数都会被执行。
- 在这个例子中,
-
- Vue中响应式原理的实现步骤
- 数据观察(数据劫持) :
- Vue会遍历数据对象的所有属性。对于每个属性,使用
Object.defineProperty()
进行数据劫持,设置get
和set
方法。在get
方法中,收集依赖(将当前的Watcher添加到依赖列表中),在set
方法中,当属性值发生变化时,通知所有依赖(通过发布 - 订阅模式,通知订阅了该属性变化的Watcher)。
- Vue会遍历数据对象的所有属性。对于每个属性,使用
- Watcher创建 :
- 当模板中使用到数据时,会创建一个Watcher实例。Watcher实例主要负责观察数据的变化,并且在数据变化时更新与之绑定的DOM元素。Watcher会在自身初始化时,读取数据属性的值,从而触发数据劫持的
get
方法,将自己添加到该属性的依赖列表中。
- 当模板中使用到数据时,会创建一个Watcher实例。Watcher实例主要负责观察数据的变化,并且在数据变化时更新与之绑定的DOM元素。Watcher会在自身初始化时,读取数据属性的值,从而触发数据劫持的
- 依赖收集与更新 :
- 依赖收集:在
get
方法中,将当前的Watcher添加到一个全局的依赖收集器(Dep)中。这个Dep类主要用于管理依赖(Watcher),它有一个subs
数组,用于存储订阅了该属性变化的Watcher。 - 更新:当数据发生变化,在
set
方法中,会遍历Dep
中的所有Watcher,调用它们的update
方法。Watcher的update
方法会执行更新DOM等操作,从而实现数据变化驱动DOM更新的响应式效果。
- 依赖收集:在
- 数据观察(数据劫持) :
通过数据劫持和发布 - 订阅模式(观察者模式)的结合,Vue.js能够高效地实现响应式数据绑定,让开发者能够以声明式的方式构建用户界面。
vue3 响应式原理及优化
- Vue3响应式原理优化点
- 性能优化 :
- 基于Proxy的响应式系统 :Vue3使用
Proxy
代替Object.defineProperty
来实现数据劫持。Proxy
可以直接代理整个对象,而不是像Object.defineProperty
那样只能劫持对象的已有属性。这意味着可以检测到对象属性的新增和删除操作。例如,在Vue3中,当给一个响应式对象添加新属性时,它会自动成为响应式的,而在Vue2中需要使用Vue.set
方法来确保新属性是响应式的。 - 静态提升(Tree - Shaking):Vue3的编译过程中,编译器会对模板进行静态分析,将一些静态的节点和属性提升出来,这样在组件更新时,这些静态部分就不需要重新渲染,减少了不必要的性能开销。例如,在一个组件的模板中有一些纯文本或者永远不会改变的HTML标签,这些部分可以被静态提升,提高渲染效率。
- 基于Proxy的响应式系统 :Vue3使用
- TypeScript支持优化 :
- Vue3在设计时就考虑了与TypeScript的良好集成。它的响应式系统提供了更好的类型推断,使得在使用TypeScript编写Vue应用时更加方便和准确。例如,在定义响应式数据时,能够更精确地推断出数据的类型,减少类型错误。
- Composition API带来的优化 :
- 代码组织和复用性 :Composition API允许开发者根据逻辑功能来组织代码,而不是像Vue2的Options API那样按照选项(如
data
、methods
等)来划分。这使得代码的复用性更高。例如,在多个组件中都需要使用的数据获取和状态管理逻辑,可以通过自定义的组合式函数进行封装,然后在不同组件中复用。 - 响应式状态的细粒度控制:在Composition API中,可以更灵活地控制响应式状态的创建和使用。可以在函数内部创建和管理响应式数据,并且可以精确地决定哪些数据需要被响应式处理,哪些不需要,从而提高性能和代码的可维护性。
- 代码组织和复用性 :Composition API允许开发者根据逻辑功能来组织代码,而不是像Vue2的Options API那样按照选项(如
- 性能优化 :
- Vue3响应式原理实现步骤
- 响应式对象创建(基于Proxy) :
-
步骤一:创建响应式对象 :使用
reactive
函数来创建响应式对象。reactive
函数内部会使用Proxy
来代理传入的对象。例如:javascriptimport { reactive } from 'vue'; const state = reactive({ count: 0 });
-
步骤二:Proxy拦截操作 :
Proxy
会拦截对象的基本操作,如get
、set
、deleteProperty
等。当访问state.count
(get
操作)时,会进行依赖收集;当修改state.count
(set
操作)时,会触发更新。例如,Proxy
的get
拦截器可能如下:javascriptconst getInterceptor = function(target, key, receiver) { track(target, key); // 进行依赖收集 return Reflect.get(target, key, receiver); };
这里的
track
函数用于收集依赖,它会将访问该属性的Watcher
(在Vue3中是effect
)添加到依赖列表中。 -
步骤三:依赖收集(track)和触发更新(trigger) :
- 依赖收集(track) :在
get
拦截器中,track
函数会根据目标对象和属性,找到对应的Dep
(依赖收集器),并将当前的effect
(类似于Vue2中的Watcher)添加到Dep
中。例如,如果有一个computed
属性或者一个渲染函数访问了响应式对象的属性,就会触发track
操作。 - 触发更新(trigger) :当响应式对象的属性被修改(在
set
拦截器中),trigger
函数会被调用。trigger
会遍历该属性对应的Dep
中的所有effect
,并执行它们,从而实现更新。例如,修改state.count
后,相关的组件渲染函数或者computed
属性的计算函数会被重新执行。
- 依赖收集(track) :在
-
- Computed属性处理 :
-
步骤一:创建Computed属性 :使用
computed
函数来创建computed
属性。例如:javascriptimport { computed, reactive } from 'vue'; const state = reactive({ count: 0 }); const doubleCount = computed(() => state.count * 2);
-
步骤二:计算和缓存机制 :
computed
属性会有自己的effect
,这个effect
会在首次访问时执行计算,并缓存结果。当依赖的响应式属性没有变化时,直接返回缓存结果;当依赖的响应式属性发生变化时,会重新计算。例如,只要state.count
没有改变,doubleCount
的值就会直接从缓存中获取,而不需要重新计算。
-
- WatchEffect和Watch API使用 :
-
WatchEffect :
watchEffect
是一个用于自动收集依赖并在依赖变化时重新执行的函数。例如:javascriptimport { watchEffect, reactive } from 'vue'; const state = reactive({ count: 0 }); watchEffect(() => { console.log('count has changed to', state.count); }); state.count++;
当
watchEffect
内部的函数访问了state.count
,就会自动收集这个依赖。当state.count
发生变化时,watchEffect
内部的函数会自动重新执行。 -
Watch API :
watch
API可以用于更精确地监听特定的数据源(可以是响应式对象的一个属性或者一个getter
函数)的变化。例如:javascriptimport { watch, reactive } from 'vue'; const state = reactive({ count: 0 }); watch( () => state.count, (newValue, oldValue) => { console.log('count changed from', oldValue, 'to', newValue); } ); state.count++;
这里
watch
精确地监听了state.count
这个属性的变化,并且在变化时执行回调函数,比较新旧值。
-
- 响应式对象创建(基于Proxy) :
fiber
-
React Fiber架构的背景和目的
- 背景:随着React应用的复杂性增加,传统的同步渲染方式在处理大型复杂组件树或者高频率更新场景时,会出现性能瓶颈。例如,当有一个包含大量子组件的页面,一次更新可能会导致长时间的阻塞,使得页面响应迟钝。
- 目的:Fiber架构的主要目的是实现异步可中断的渲染。它将渲染工作拆分成小的单元(Fiber节点),使得React能够在执行渲染任务的过程中,根据任务的优先级暂停、恢复或者重新安排任务,从而提供更流畅的用户体验,尤其在处理复杂的动画、交互以及高优先级更新时非常有用。
-
Fiber节点的概念和结构
- 概念:Fiber节点是Fiber架构中的基本单元,它可以看作是对组件、元素或者DOM节点的一种抽象表示。每个Fiber节点包含了组件的状态、更新队列、子节点等信息,并且记录了与渲染任务相关的一些属性,如优先级、是否正在处理等。
- 结构 :
- 属性部分 :
- type:表示Fiber节点对应的组件类型,例如是一个函数组件还是类组件。
- key:用于在列表渲染等场景中区分不同的节点,帮助React确定是否需要更新或重新创建节点。
- stateNode:对于类组件,这个属性指向组件实例;对于DOM元素,它指向真实的DOM节点。
- pendingProps和memoizedProps:分别表示即将应用的新属性和上一次渲染时使用的属性,通过比较这两个属性来决定是否需要更新组件。
- pendingWorkPriority:表示这个Fiber节点任务的优先级,React会根据优先级来安排任务的执行顺序。
- 关联部分 :
- return:指向父级Fiber节点,用于在遍历组件树时返回上一层。
- child:指向第一个子Fiber节点,用于在遍历组件树时进入下一层。
- sibling:指向同一层级的下一个Fiber节点,用于在遍历完一个子树后切换到同层的其他子树。
- 属性部分 :
-
Fiber架构下的更新过程
- 任务调度(Scheduling) :
- React会根据更新的来源(如用户交互、网络请求返回等)为每个更新任务分配优先级。例如,用户直接操作(如点击按钮)产生的更新通常会被赋予较高的优先级,而一些后台数据更新可能会被赋予较低的优先级。这些更新任务会被放入一个任务队列中,React会根据优先级和当前的资源情况来决定从队列中取出哪些任务进行处理。
- 渲染阶段(Reconciliation) :
- 阶段一:从根节点开始遍历:React从根Fiber节点开始,按照深度优先遍历的方式遍历组件树。在遍历过程中,它会为每个Fiber节点执行一些操作,如检查属性是否变化、比较新旧状态等,以确定是否需要更新组件。如果需要更新,会创建新的Fiber节点或者更新现有Fiber节点的属性。
- 阶段二:生成新的Fiber树(Work in Progress Tree):在遍历过程中,React会构建一个新的Fiber树,这个树是在旧的Fiber树基础上更新而来的。这个过程中,React会利用Fiber节点的各种属性来高效地判断哪些组件需要更新、哪些可以复用等。例如,如果一个组件的属性没有变化,React可以直接复用旧的Fiber节点对应的DOM节点,而不需要重新创建。
- 阶段三:标记副作用(Side Effects):在生成新的Fiber树的过程中,React会标记出一些需要执行副作用的Fiber节点。副作用包括但不限于更新DOM、调用生命周期钩子(在类组件中)、执行useEffect钩子(在函数组件中)等。这些标记会在后续的提交阶段被处理。
- 提交阶段(Commit) :
- 一旦新的Fiber树构建完成并且副作用被标记好,React就进入提交阶段。在这个阶段,React会根据标记执行相应的副作用。例如,将新的DOM节点插入到文档中、更新现有DOM节点的属性等操作,以将更新后的组件状态反映到实际的页面上。同时,在这个阶段也会执行一些清理工作,如清除旧的Fiber树等。
- 任务调度(Scheduling) :
-
Fiber架构带来的优势
- 性能优化 :
- 通过异步可中断的渲染,Fiber架构可以避免长时间的渲染阻塞。例如,在一个包含复杂动画的页面中,动画相关的更新可以被赋予较高优先级,React可以暂停其他低优先级的更新任务,优先处理动画更新,从而保证动画的流畅性。
- 更好的用户体验 :
- 由于能够及时响应高优先级的更新,如用户交互,用户会感觉应用更加灵敏。例如,当用户在一个表单中输入内容时,输入相关的更新会被快速处理,而不会因为其他复杂的后台更新而延迟。
- 支持更复杂的应用场景 :
- Fiber架构使得React能够更好地处理大型复杂的组件树。它可以根据任务的优先级和资源情况,灵活地安排渲染任务,从而使得应用在复杂的场景下依然能够保持良好的性能和响应性。
- 性能优化 :
fiber 与 虚拟dom
-
虚拟DOM(Virtual DOM)的基本概念和作用
- 基本概念 :虚拟DOM是一种对真实DOM的抽象表示。它是一个JavaScript对象,以树形结构描述了真实DOM的层次结构和节点属性。例如,一个简单的HTML元素
<div id="app"><p>Hello</p></div>
在虚拟DOM中可能被表示为一个对象,如{ type: 'div', props: { id: 'app' }, children: [{ type: 'p', props: {}, children: ['Hello'] }] }
。 - 作用 :
- 性能优化:在传统的DOM操作中,每次更新都会直接操作真实DOM,而真实DOM的操作是比较昂贵的(因为会引起浏览器的重排和重绘)。虚拟DOM通过在内存中进行比较和计算,找出需要更新的最小部分,然后批量更新真实DOM,减少了不必要的DOM操作次数,从而提高性能。
- 跨平台兼容:虚拟DOM可以很容易地被转换为其他平台的原生组件表示。例如,React Native使用虚拟DOM的概念来将组件渲染为原生的iOS和Android应用,使得开发者可以使用相似的代码逻辑在不同平台上构建应用。
- 基本概念 :虚拟DOM是一种对真实DOM的抽象表示。它是一个JavaScript对象,以树形结构描述了真实DOM的层次结构和节点属性。例如,一个简单的HTML元素
-
Fiber架构与虚拟DOM的关系
- 继承与扩展:Fiber架构是在虚拟DOM基础上的一种改进和扩展。Fiber节点本身可以看作是对虚拟DOM节点的一种增强表示。它继承了虚拟DOM描述组件结构和状态的功能,同时增加了更多与渲染任务管理相关的属性,如任务优先级、是否正在处理等。
- 更新过程中的协作:在更新过程中,Fiber架构利用虚拟DOM的比较算法来确定组件的更新范围。例如,在Fiber的渲染阶段(Reconciliation),它会比较新旧虚拟DOM树(实际上是新旧Fiber树),通过标记差异来决定哪些组件需要更新、添加或删除。这个过程类似于传统虚拟DOM的Diff算法,但Fiber架构使得这个过程更加灵活和高效,因为它可以根据任务优先级中断和恢复比较过程。
-
Fiber架构对虚拟DOM更新过程的优化
- 异步可中断的Diff操作 :
- 传统的虚拟DOM Diff操作是同步的,一旦开始就会一直执行直到完成整个组件树的比较。而Fiber架构下,Diff操作可以被中断。例如,当一个高优先级的更新任务(如用户的交互操作)到来时,React可以暂停当前正在进行的低优先级的虚拟DOM Diff操作,先处理高优先级任务。这就好比在一个工厂的生产线上,原本按部就班地组装产品(传统Diff操作),但当有一个紧急订单(高优先级更新)时,可以暂停当前的生产,先处理紧急订单。
- 更精细的任务划分和优先级调度 :
- Fiber将渲染任务划分为一个个小的Fiber节点任务,并为每个任务分配优先级。在虚拟DOM更新过程中,它会根据这些优先级来安排任务的执行顺序。比如,在一个复杂的网页中,页面可视区域内的组件更新可能会被赋予较高优先级,而页面底部或隐藏部分的组件更新优先级较低。这样可以确保最重要的部分先更新,提高用户体验。
- 更好的内存管理和复用 :
- 在构建新的Fiber树(Work in Progress Tree)过程中,Fiber架构会更加精细地考虑组件的复用。它会根据Fiber节点的各种属性(如
key
属性等)来判断是否可以复用旧的组件和DOM节点。这类似于虚拟DOM的复用策略,但Fiber架构能够在更新过程中更灵活地处理复用情况,减少不必要的内存占用和重新创建DOM节点的操作。
- 在构建新的Fiber树(Work in Progress Tree)过程中,Fiber架构会更加精细地考虑组件的复用。它会根据Fiber节点的各种属性(如
- 异步可中断的Diff操作 :
-
对比传统虚拟DOM更新方式的优势
- 响应速度更快 :
- 由于Fiber架构能够及时处理高优先级的更新,应用在面对用户交互等紧急情况时能够更快地做出响应。例如,在一个实时聊天应用中,用户发送消息(高优先级更新)能够迅速在界面上显示,而不会因为正在进行的大规模数据更新(低优先级)而延迟。
- 性能提升在复杂场景下更明显 :
- 在处理大型复杂的组件树或者高频率更新场景时,Fiber架构的优势更加突出。传统虚拟DOM更新可能会在这种情况下出现性能瓶颈,而Fiber通过异步可中断的渲染和精细的任务调度,能够更好地平衡性能和用户体验,使应用在复杂场景下依然能够保持流畅。
- 响应速度更快 :