开始
本文是带着 Vue3.0
已有的一些概念来对比 React
中的一些实现,比如:响应式实现,渲染机制实现,指令实现,组件间的通信和 Vue3.0
框架独有的一些特性。
虚拟 DOM 的比较
虚拟DOM 是一个用于表示真实 DOM 结构和属性的 JavaScript 对象,这个对象用于对比虚拟 DOM 和当前真实 DOM 的差异化,然后进行局部渲染从而实现性能上的优化。
这里只分析它们是如何变成虚拟 DOM 的。
React 中的虚拟DOM
我们都会用 jsx
语法来书写业务代码。jsx
最终转换成了虚拟 DOM。
下面是 jsx
是如何转译为虚拟 DOM 的过程:
-
JSX 编写: 开发者使用 JSX 语法编写 React 组件。JSX 看起来类似 HTML,但实际上是 JavaScript 的语法糖。
jsxconst element = <div className="my-class">Hello, React!</div>;
-
转译为 React.createElement: Babel 或其他类似的工具会将 JSX 代码转译为
React.createElement
函数的调用。上述的 JSX 代码等价于以下的 JavaScript 代码:javascriptconst element = React.createElement('div', { className: 'my-class' }, 'Hello, React!');
React.createElement
接受三个参数:元素的类型(字符串或组件)、元素的属性(一个对象),以及元素的子元素。 -
创建虚拟 DOM 对象:
React.createElement
返回一个描述虚拟 DOM 元素的 JavaScript 对象,通常称为 React 元素。这个对象包含了元素的类型、属性、子元素等信息。javascriptconst element = { type: 'div', props: { className: 'my-class', children: 'Hello, React!', }, };
这个对象就是虚拟 DOM,它描述了 UI 的结构和信息,但并没有直接操作实际的 DOM。
-
虚拟 DOM 的创建和更新: 虚拟 DOM 对象被用来构建整个应用的 UI 结构。当组件的状态或属性发生变化时,React 会生成新的虚拟 DOM 对象,然后通过一种叫做调和(Reconciliation)的过程,比较新旧虚拟 DOM,找出变化的部分,并将这些变化应用到实际的 DOM 上,从而更新用户界面。
Vue 中的虚拟DOM
在 Vue 中,模板语法会经过编译过程,将其转化为虚拟 DOM。
以下是 Vue 模板语法转换为虚拟 DOM 的过程:
-
模板编写: 开发者使用 Vue 的模板语法编写组件模板。模板语法类似于 HTML,但包含了一些 Vue 特有的指令和表达式。
html<template> <div> <h1>{{ message }}</h1> <p v-if="showParagraph">This is a paragraph.</p> </div> </template>
-
模板编译: Vue 的编译器会将模板编译成渲染函数。这个过程会将模板中的指令、插值和其他语法转化为 JavaScript 代码。
javascript// 编译后的渲染函数 render() { return h('div', [ h('h1', this.message), this.showParagraph && h('p', 'This is a paragraph.'), ]); }
这里的
h
函数是createElement
的别名,它用于创建虚拟 DOM 元素。 -
创建虚拟 DOM: 渲染函数执行时,会调用
createElement
来创建虚拟 DOM 元素。这些虚拟 DOM 元素以树形结构组成,对应着模板中的 HTML 结构。javascript// 创建的虚拟 DOM { tag: 'div', children: [ { tag: 'h1', children: this.message }, this.showParagraph && { tag: 'p', children: 'This is a paragraph.' }, ], }
这个对象表示了组件的虚拟 DOM 结构,包含了标签、属性、子元素等信息。
-
虚拟 DOM 的更新: 当组件的状态或属性发生变化时,Vue 会生成新的虚拟 DOM 对象。然后,Vue 会通过一种称为 Virtual DOM Diffing 的算法,比较新旧虚拟 DOM,找出变化的部分。
-
实际 DOM 的更新: 最后,Vue 将变化的部分应用到实际的 DOM 上,更新用户界面。
总体来说,Vue 的模板语法经过编译过程,最终转化为虚拟 DOM。虚拟 DOM 的使用使得 Vue 能够更高效地管理 DOM 操作,以提高性能和开发效率。
区别
虽然 Vue 和 React 都使用虚拟 DOM 来提高性能,但它们在生成和处理虚拟 DOM 方面有一些区别:
-
模板语法 vs JSX:
- Vue: 使用模板语法,类似于 HTML,通过 Vue 模板编译器转换为虚拟 DOM。
- React: 使用 JSX,一种 JavaScript 的语法糖,通过 Babel 或其他工具转译为
React.createElement
函数调用。
-
渲染函数 vs 组件化:
- Vue: 使用渲染函数或者模板,可以将模板转化为渲染函数。Vue 的渲染函数可以是基于模板编译的,也可以直接使用 JavaScript 编写。
- React: 使用组件,每个组件都有一个
render
方法,该方法返回 React 元素的描述。JSX 编写的组件会被转化为React.createElement
调用。
-
虚拟 DOM 对象结构:
- Vue: 虚拟 DOM 对象结构包含了标签名、属性、子元素等信息。Vue 的虚拟 DOM 结构更接近于真实的 DOM 结构。
- React: 虚拟 DOM 对象结构是一个普通的 JavaScript 对象,包含类型、属性、子元素等信息。React 的虚拟 DOM 对象更为简洁。
-
响应式系统差异:
- Vue: Vue 的响应式系统使用了 Object.defineProperty 或 Proxy,能够监听对象属性的变化,并在状态变化时自动更新视图。
- React: React 使用了单向数据流的概念,通过组件的
state
和props
来管理数据,当状态发生变化时,会触发重新渲染。
-
Key 的处理:
- Vue: 在 Vue 中,
key
是作为属性传递给虚拟 DOM 的,Vue 使用key
来优化元素的更新算法。 - React: 在 React 中,
key
是作为特殊属性而不是作为组件的 prop 传递的。key
主要用于优化元素的重新排序。
- Vue: 在 Vue 中,
响应式系统的比较
React 和 Vue 的响应式系统有着本质上的不同,主要体现在依赖追踪的方式不同。
Vue 的响应式系统
Vue3.0 基于 Composition API, 提供了一些声明响应式状态的方法。 比如: ref()
和 reactive()
。
当变量使用上述方法进行声明时,实际上就为这个变量绑定了响应式。 底层是使用 Proxy
实现的。
js
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
}
})
}
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
}
}
return refObject
}
当读取值的时候,调用 get()方法,该方法内部会调用 track() 进行收集依赖。
当设置值的时候,调用 set()方法,该方法内部会调用 trigger() 进行触发更新。
当 effect 函数被执行时,它内部的代码会访问响应式对象,这会触发 Proxy 捕获器中的 get 操作,从而实现依赖追踪。当这些依赖发生变化时,effect 函数会被重新执行。
这也能解释,为什么在 effects
函数中实现状态监听时,不需要显式的指定依赖项,也能实现响应式的执行。
React 的响应式系统
React 基于 hooks 提供了声明响应式状态的方法。比如:useState
。
js
const [count, setCount] = useState(0)
const increase = () => {
setCount(count++)
}
当手动调用 setCount 改变 state 的状态后,触发组件更新,会生成一个新的虚拟DOM,然后 新的虚拟DOM 会和 旧的虚拟 DOM 进行 diff ,将改变的地方进行真实的 dom 修改。
注意由于 state 改变触发组件渲染之后 React 会进行自上而下的全量的组件渲染。所以很多时候,你会发现明明子组件的 state 没有改变,由于父组件的 state 改变了导致子组件也重新渲染了。
区别
- React 需要手动setState触发渲染
- Vue 通过数据关联自动追踪依赖响应
React 的"响应式"更多是从状态 state 和属性 props 控制上,而 Vue 实现了基于数据级别的自动追踪依赖和响应。这是两者响应系统最大的区别。
渲染机制的比较
Vue的渲染机制
Vue 的渲染与 React 一样,同样是基于 虚拟DOM 这个模式来进行的。但是会有很多不一样的地方。 首先来看Vue。
Vue3.0 的渲染机制主要可以分为以下几个步骤:
-
初始化阶段:
- 初始化组件实例对象
- 安装组件的钩子函数
- 初始化Props
- 初始化Slots
- 初始化自定义事件
-
编译阶段:
- 使用 Vue 的编译器把模板解析成渲染函数
- 渲染函数包含了页面实际渲染逻辑
- 标记静态节点和动态绑定
- 构建依赖关系树
-
挂载阶段:
- 调用渲染函数,生成虚拟Node节点树(VNode Tree)
- 构建组件更新机制
- 模拟Patch算法,对比新旧VNode树的不同,得到需要实际更新的节点
- 调用平台API操作真实DOM
-
更新阶段:
- 数据变化触发依赖,进而触发渲染Watcher
- 重新运行渲染函数,生成新的VNode树
- 对比新旧VNode树的差异,计算最小更新范围
- 批量的DOM操作更新视图
Vue 在这个环节还做了很多优化,我们一起来看:
- 静态提升:在某些场景下模板内完全是绑定的静态的内容,那么没必要在重新渲染时再次创建和比对它们。Vue 编译器自动地会提升这部分 vnode 创建函数到这个模板的渲染函数之外,并在每次渲染时都使用这份相同的 vnode,渲染器知道新旧 vnode 在这部分是完全相同的,所以会完全跳过对它们的差异比对。
- DOM 批处理:将多次 DOM 操作合并成一批进行,减少页面回流(reflow)。
- 异步渲染:使用异步队列暂缓非关键渲染与更新,避免同一时间处理太多更新。
Vue 实现 DOM 批处理 和 异步渲染 与一个 api 有很大的关系,那就是 nextTick
。
当我们调用 nextTick()
的时候,实际上就是这一批处理的异步函数完成的时候。 比如在以下一些情况,我们需要调用 nextTick()
。
-
在数据变化之后立即读取DOM 的时候:因为 Vue 的更新是异步执行的,数据改变之后 DOM 不会立即 render,这时候要先使用 nextTick 等待 DOM 更新完毕。
-
在想基于新的 DOM 状态计算一些东西的时候:和情况1类似,都需要确保在 DOM 渲染完毕后再执行某些操作。
-
在更新数据之后立即重绘或者需要重新计算样式的时候:当Mutations触发的时候,DOM 其实并没有立即更新,而是等到 Microtask 时。nextTick 会在 DOM 更新后触发回调,以便获得新的样式。
-
在计时器和动画里需要顺滑渲染的时候:nextTick 使得数据更新后,界面上出现的变化更连贯自然。使用 async / await 也可以实现类似效果。
-
在修改数据后需要做一些动态滚动到视图中的操作:这时候需要先通过 nextTick 确保 DOM 更新后再执行滚动逻辑。
Vue 是通过队列的方式实现 DOM 批处理和异步渲染的。
DOM 批处理:
Vue 将在同一事件循环(event loop)内发生的 DOM 操作存储在一个队列里,然后通过延迟刷新队列的方式把这些 DOM 操作合并成一批进行。
具体实现是通过函数 nextTick
和一个异步队列来完成的:
nextTick
会把传给它的回调推送到一个异步队列中- 在事件循环结束前,Vue 会清空队列并执行每个回调
- 这样多个
nextTick
的回调都会在同一时间执行 - 回调函数中包含的 DOM 操作由此被合并在一起成批执行
异步渲染:
Vue 通过异步队列可以控制在同一时间处理的组件数量,避免大量同时渲染导致主线程被阻塞。
具体逻辑是:
- 数据变更时,会向异步队列中添加"待更新组件"
- 每次事件循环时,从队列中取出一部分组件进行渲染
- 直至队列清空,完成所有组件的更新
通过限制同一事件循环中处理的组件数,实现了渲染的异步分批,防止主线程被大量渲染操作堵塞。
其中Diff算法是Vue提高渲染效率的关键。新旧节点对比区分静态节点和动态节点,最大限度重用相同节点,只替换实际改变的部分,避免不必要的DOM操作。
React 的渲染机制
其实,React 的渲染机制大体上与 Vue 是一致的。 流程如下:
-
初始化阶段:
- React组件初始化,调用构造函数初始化state等内部属性
- 将children,props等初始化传入组件
-
编译阶段:
- JSX被babel编译为React.createElement调用,生成虚拟DOM元素
- 递归调用所有组件的render方法,生成虚拟DOM树
-
挂载阶段:
- 调用ReactDOM.render渲染根组件,插入容器DOM中
- 递归比较和更新所有子组件的虚拟DOM和真实DOM
- 完成首屏的组件渲染和DOM更新
-
更新阶段:
- 组件props或state变更,触发重新render
- 子组件也会递归调用render生成新的虚拟DOM树
- React Diff算法递归对比新旧虚拟DOM树差异
- 将需要更新的最小DOM操作批处理到真实DOM上
- 重复更新过程,重新渲染组件和更新界面视图
-
卸载阶段:
- 调用ReactDOM.unmountComponentAtNode卸载组件
- 递归解绑子组件的事件监听和定时器
- 删除DOM节点,完成组件卸载
所以整个生命周期,React组件都根据自身状态计算生成虚拟DOM,通过递归对比和批量更新机制实现高效渲染。
同样,React 也采取类似 DOM批处理 和 异步渲染这样的方法来进行性能优化。
区别
React 使用了 Fiber
架构。React Fiber 是 React 16 引入的新的协调引擎,它重新实现了协调算法,支持增量渲染和优先级调度。
React 普遍使用 jsx
语法来进行UI层面代码的书写,而 Vue 普通使用 模板语法来进行UI层面代码的书写(虽然它也支持使用 jsx
语法来进行书写)。
React 和 Vue 的响应式系统不一样, React 是通过 hooks API 和 proxy 来实现的。 vue 是通过 Composition API 和 proxy 来实现的。
React 和 Vue 的UI更新机制不一样。
React 更新UI的方式是通过组件重新渲染来实现的。具体工作原理是:
- 状态数据(state)改变
- triger重新调用render()方法
- 生成新的虚拟DOM树
- 和旧的虚拟DOM树做diff比对
- 将需要改变的部分反映到真实DOM树上
所以React是从组件重新渲染开始更新的。
而Vue更新UI的方式是通过数据绑定来实现的,不需要手动控制组件渲染。具体工作流程是:
- 数据对象改变
- 触发 setter
- 通知依赖的订阅者Watcher
- 订阅者会使关联组件重新渲染
- 组件重新渲染时可以读取到最新的数据
所以Vue是从数据驱动开始,自动更新相关联的组件的。
可能你会想,在 React 中 调用了 setState 这样的 hooks 之后,会导致 state 的数据发生改变,导致组件重新渲染,导致UI界面发生改变,这不也是数据驱动吗? 实际上是有区别的。
React中:
- 状态数据放在组件内部(state)
- 需要通过setState主动触发更新
- 从组件重新渲染开始更新视图
而Vue中:
- 数据存储在单独的数据对象中(data)
- 数据改变自动驱动依赖组件更新
- 组件只是被动依赖和重新渲染
所以核心区别在于更新的主动性和响应机制不同:
- React是主动setState触发渲染
- Vue是被动依赖检测自动触发更新
的确,二者都体现了某种数据驱动视图,但本质上的响应系统和更新机制是有区别的。
React是组件主动渲染驱动,Vue是依赖侦测被动更新。
react hooks 和 Composition API 的比较
React Hooks 和 Vue 3.0 的 Composition API 都是为了更好地组织和重用组件逻辑而引入的概念。尽管它们有一些相似之处,但在具体的使用和实现上存在一些差异。以下是 React Hooks 和 Vue 3.0 Composition API 的一些比较:
1. 语法风格和使用方式:
-
React Hooks:
- 使用函数的方式定义和使用 Hooks。
- 常见的 Hooks 包括
useState
用于管理状态、useEffect
用于处理副作用、useContext
用于访问上下文等。
-
Vue 3.0 Composition API:
- 使用
setup
函数来定义组件的逻辑,返回一个包含响应式对象、方法等的对象。 - 使用
ref
和reactive
来创建响应式对象,使用toRefs
来将对象的属性转换为响应式对象。
- 使用
2. 逻辑复用和组合:
-
React Hooks:
- 通过将相关的 Hooks 封装成自定义 Hook 来实现逻辑的复用。
- 自定义 Hook 本质上是一个函数,可以包含多个其他 Hooks。
-
Vue 3.0 Composition API:
- 支持更灵活的逻辑复用,可以通过函数、对象等方式组织逻辑。
- 允许使用
setup
函数内部定义并返回的函数、对象等来实现逻辑复用。
3. 对比 Hooks 的一些常用 API 和 Composition API 的对应关系:
-
React Hooks:
useState
: 用于在函数组件中添加状态。useEffect
: 处理副作用,如数据获取、订阅等。useContext
: 从上下文中获取值。useReducer
: 更复杂的状态管理。useCallback
和useMemo
: 优化性能。
-
Vue 3.0 Composition API:
ref
和reactive
: 创建响应式对象。toRefs
: 将对象的属性转换为响应式对象。watch
和watchEffect
: 监听数据变化。provide
和inject
: 跨层级传递数据。computed
: 创建计算属性。
举一个栗子,封装一个异步获取数据的工具函数。
Vue:
js
// fetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const fetchData = () => {
// 重置之前的状态
data.value = null
error.value = null
fetch(toValue(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
watchEffect(() => {
fetchData()
})
return { data, error }
}
react:
js
import { useState, useEffect } from 'react';
export function useFetch(url) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
// 重置之前的状态
setData(null)
setError(null)
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (err) {
setError(err);
}
};
fetchData();
}, [url]);
return { data, error };
}
然后,你会发现一个神奇的现象,除了 api 使用的不同,实现逻辑的流程和思路是完全相同的!
4. 区别
其实,严格意义上讲,不应该是 react hooks 和 Composition API 的区别,应该是两个框架特性之间的区别。不信?,不信接着看。
- React Hooks 在组件每次更新时都会重新调用。这可能会带来一些性能问题。
- React Hooks 有严格的调用顺序,并不可以写在条件分支中。
- React Hooks 需要显式的指定 deps(依赖项),如果你传递了错误的依赖项,那么会带来一些问题,比如:没有正确的按照逻辑进行状态的刷新。
- React Hooks 中的,useMemo 和 useCallback 是用来处理性能优化的,useMemo 用来缓存一些代价昂贵的计算,useCallback 是用来缓存函数的引用的,避免每次父组件刷新时向子组件传递的都是一个新的函数引用导致子组件刷新。同样它们都是需要指定 deps(依赖项)。
相比起来,Vue 的组合式 API:
仅调用 setup() 或 <script setup>
的代码一次。这使得代码更符合日常 JavaScript 的直觉,不需要担心闭包变量的问题。Composition API 也并不限制调用顺序,还可以有条件地进行调用。
Vue 的响应性系统运行时会自动收集计算属性和侦听器的依赖,因此无需手动声明依赖。
无需手动缓存回调函数来避免不必要的组件更新。Vue 细粒度的响应性系统能够确保在绝大部分情况下组件仅执行必要的更新。对 Vue 开发者来说几乎不怎么需要对子组件更新进行手动优化。
生命周期的比较
React 类组件和函数式组件在生命周期方面存在一些区别,尤其是在 React 16.8 版本引入 Hooks 后,函数式组件也拥有了生命周期的能力。以下是 React 类组件和函数式组件的生命周期及其区别:
React 类组件的生命周期
-
Mounting:
constructor
: 在组件被创建时调用,用于初始化状态和绑定方法。static getDerivedStateFromProps
: 在组件创建时和每次接收新的 props 时调用,用于派生状态。render
: 渲染组件的 UI。componentDidMount
: 在组件被挂载到 DOM 后调用,可以进行网络请求、订阅等操作。
-
Updating:
static getDerivedStateFromProps
: 同样在更新时调用,用于派生状态。shouldComponentUpdate
: 在组件更新前调用,用于控制是否进行组件更新。render
: 重新渲染组件的 UI。getSnapshotBeforeUpdate
: 在最终提交到 DOM 之前调用,用于获取更新前的快照。componentDidUpdate
: 在组件更新后调用,可以进行 DOM 操作等。
-
Unmounting:
componentWillUnmount
: 在组件即将被卸载和销毁之前调用,用于清理资源、取消订阅等。
React 函数式组件的生命周期(使用 Hooks)
-
Mounting:
useState
: 在组件内部创建状态变量。useEffect
: 在组件挂载后和每次更新时调用,用于处理副作用。
-
Updating:
useState
: 使用更新函数来更新状态。useEffect
: 在每次更新时调用,可以处理更新后的副作用。useMemo
和useCallback
: 用于优化性能。
-
Unmounting:
useEffect
: 返回一个清理函数,用于在组件被卸载时执行清理操作。
函数式组件可以使用 useEffect
来模拟实现类组件完整的生命周期。
js
import React, { useState, useEffect } from 'react';
function FunctionalComponentWithLifecycle() {
const [count, setCount] = useState(0);
// componentDidMount
useEffect(() => {
console.log('Component did mount');
// 在这里可以进行一些初始化操作
return () => {
console.log('Component will unmount');
// 在这里进行一些清理操作
};
}, []);
// componentDidUpdate
useEffect(() => {
console.log('Component did update');
// 在这里可以处理 componentDidUpdate 操作
}, [count]);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default FunctionalComponentWithLifecycle;
再说一下, useEffect 函数, 各种 deps 的情况。
js
import React, { useEffect, useState } from 'react';
export function App(props) {
const [count, setCount] = useState(0)
const [obj, setObj] = useState({ name: 'hit' })
useEffect(() => {
console.log('deps无')
})
useEffect(() => {
console.log('deps为空数组')
}, [])
useEffect(() => {
console.log('deps 为 number 类型参数')
}, [count])
useEffect(() => {
console.log('deps 为 object 类型参数')
}, [obj])
const changeObj = () => {
const _obj = obj
_obj.name = 'hittttt'
setObj(_obj)
}
return (
<div className='App'>
<h1>Hello React.</h1>
<button onClick={() => setCount(count+1)}>加1</button>
<button onClick={() => setObj({ name: 'hittt'})}>改变obj</button>
<button onClick={changeObj}>改变obj,不改变引用</button>
</div>
);
}
直接上结论:
- 如果没有提供 deps 参数,useEffect 将在每次组件渲染后都执行。这意味着每次组件更新时都会执行其中的代码。
- 如果提供了一个空数组 [] 作为 deps 参数,useEffect 将只在组件挂载时执行一次,并在组件卸载时清理。相当于 componentDidMount 和 componentWillUnmount。
- 如果提供了一个值类型的参数,比如字符串,useEffect 将在组件挂载时执行一次,并且只在这个值发生变化时重新执行。相当于 componentDidMount 和 componentDidUpdate。
- 如果提供了一个引用类型的参数,比如对象,useEffect 将在组件挂载时执行一次,并且只在这个对象的引用发生变化时重新执行。如果对象的属性发生变化,但引用不变,useEffect 将不会重新执行。相当于 componentDidMount 和 shouldComponentUpdate(如果使用浅比较)。
区别和注意事项
-
语法和写法:
- 类组件使用类的语法,而函数式组件使用函数的语法。
- 类组件使用
this
关键字引用实例,而函数式组件没有实例。
-
状态管理:
- 类组件使用
this.state
和this.setState
来管理状态。 - 函数式组件使用
useState
来创建和更新状态。
- 类组件使用
-
副作用处理:
- 类组件的生命周期方法中处理副作用,如网络请求、订阅等。
- 函数式组件使用
useEffect
处理副作用。
-
性能优化:
- 类组件可以使用
shouldComponentUpdate
进行性能优化。 - 函数式组件可以使用
React.memo
包裹组件来进行浅层比较,以优化性能。
- 类组件可以使用
-
代码组织:
- 函数式组件通过多个自定义 Hook 的组合来组织逻辑,而不是将所有逻辑放在生命周期方法中。
总体而言,React 类组件和函数式组件的生命周期有一些相似之处,但语法和写法上存在明显的区别,而函数式组件通过 Hooks 提供了更灵活和简洁的方式来处理组件的生命周期和逻辑。在新的 React 项目中,函数式组件与 Hooks 的使用已经成为主流,它们提供了更好的可读性、复用性,以及更易于测试的优势。
Vue 3.0 在生命周期方面进行了一些调整和优化,与 Vue 2.0 相比有一些不同之处。以下是 Vue 2.0 和 Vue 3.0 生命周期的主要差异:
Vue 2.0 生命周期
-
Initialization:
beforeCreate
: 在实例初始化之后,数据观测 (data observation) 之前调用。created
: 在实例创建完成后调用,此时可以访问数据,但尚未挂载到 DOM。
-
Mounting:
beforeMount
: 在挂载开始之前被调用。mounted
: 在挂载完成后调用,此时组件已经被挂载到 DOM 中,可以进行 DOM 操作。
-
Updating:
beforeUpdate
: 在数据更新之前调用。updated
: 在数据更新完成后调用,DOM 已经重新渲染。
-
Destroying:
beforeDestroy
: 在实例销毁之前调用,此时实例仍然完全可用。destroyed
: 在实例销毁后调用,此时组件已经从 DOM 中移除。
-
Error Handling:
errorCaptured
: 用于捕获子组件抛出的异常,类似于 React 的componentDidCatch
。
Vue 3.0 生命周期
-
Initialization:
- 没有了
beforeCreate
、created
由setup
进行替代。
- 没有了
-
Mounting:
onBeforeMount
: 在挂载开始之前被调用。onMounted
: 在挂载完成后调用,类似于 Vue 2.0 的mounted
。onBeforeUpdate
: 在数据更新之前调用。onUpdated
: 在数据更新完成后调用,类似于 Vue 2.0 的updated
。
-
Unmounting:
onBeforeUnmount
: 在卸载开始之前调用。onUnmounted
: 在卸载完成后调用,类似于 Vue 2.0 的destroyed
。
-
Error Handling:
onErrorCaptured
: 用于捕获子组件抛出的异常,类似于 Vue 2.0 的errorCaptured
。
-
Composition API(新增):
setup
: 在组件创建之前调用,用于设置组件的初始状态和逻辑。onActivated
和onDeactivated
: 在组件被激活和失活时调用,用于处理 keep-alive 组件的生命周期。onRenderTracked
和onRenderTriggered
: 用于追踪渲染过程中依赖的变化,用于开发工具的支持。
区别和注意事项
-
setup 替代 beforeCreate 和 created: 在 Vue 3.0 中,setup 函数替代了 Vue 2.0 中的 beforeCreate 和 created 钩子。setup 中进行了响应式数据的设置。
-
onBeforeMount 和 onMounted 替代 beforeMount 和 mounted: Vue 3.0 中引入了 onBeforeMount 和 onMounted 钩子,分别用于在挂载开始之前和之后执行逻辑。
-
Composition API 的灵活性: Vue 3.0 的 Composition API 提供了更灵活和组合性更强的方式来组织组件逻辑,相较于 Vue 2.0 更加推荐使用 Composition API。
-
注意 setup 函数内的异步处理: 在 setup 函数内部进行的异步操作不会阻止组件的渲染,这是与 Vue 2.0 不同的地方。如果需要等待异步操作完成再渲染,可以使用 reactive 或 ref 包裹异步数据。
框架特性的比较
一些语法糖和指令
Vue 的 ref 和 reactive
Vue 中能够使用 ref 或者 reactive 声明响应式状态。当状态更新时,UI会自动更新。
ref 和 reactive 的区别:
- ref 用于将基本类型的数据(如字符串、数字,布尔值等)和引用数据类型(对象) 转换为响应式数据。使用 ref 定义的数据可以通过 .value 属性访问和修改。
- reactive 用于将对象转换为响应式数据,包括复杂的嵌套对象和数组。使用 reactive 定义的数据可以直接访问和修改属性。
在 React 中,我们通常会使用 useState 这个hook 来定义状态。
js
const [count, setCount] = useState(0)
当需要改变状态时,需要手动调用 setCount
,而不是直接修改 count
的状态。
Vue 的计算属性
Vue 的计算属性和方法: computed() 方法期望接收一个 getter 函数,返回值为一个计算属性 ref。 计算属性是对结果进行了缓存的,当依赖的变量没有改变时,那么计算属性也不会改变,不会重新执行。 但是方法在每一次组件渲染的时候都会去重新执行一遍。
js
// 使用计算属性缓存计算结果,只有当 inputData 变化时才重新计算
const expensiveResult = computed(() => {
console.log('Executing expensive calculation...');
// 模拟一个计算代价较高的操作
let result = 0;
for (let i = 0; i < inputData.value.length; i++) {
result += inputData.value[i];
}
return result;
});
另外,在 React 中 如何实现一个 类似计算属性的功能呢?
使用 useMemo 确保只有当 dep 发生变化时才会重新计算,否则直接返回缓存结果,优化性能。
js
// 使用 useMemo 缓存计算结果,只有当 data 变化时才重新计算
const expensiveResult = useMemo(() => {
console.log('Executing expensive calculation...');
// 模拟一个计算代价较高的操作
let result = 0;
for (let i = 0; i < data.length; i++) {
result += data[i];
}
return result;
}, [data]);
相同点:都会对结果进行缓存,只有当依赖的项发生改变后才会重新去计算。
不同点是: Vue 的 computed
会自动追踪依赖而React 的 useMemo
需要手动指定依赖。
Vue的 v-if 和 v-for
v-if
是 Vue 中用来处理条件渲染的快捷指令。
js
<h1 v-if="awesome">Vue is awesome!</h1>
这块内容只会在指令的表达式返回真值时才被渲染。
v-for
是 Vue 中用来处理列表渲染的快捷指令。
js
const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
<li v-for="item in items">
{{ item.message }}
</li>
当它们同时存在于一个节点上时,v-if 比 v-for 的优先级更高。
所以,如果一定要同时使用 v-if 比 v-for, 那么 提高 v-for 的层级。
在 React 中,一般使用 三元表达式 或者 && 来实现条件渲染。
js
const [awesome, setAwesome] = useState()
{
awesome? <h1>Vue is awesome!</h1> : null
// awesome && <h1>Vue is awesome!</h1>
}
在 React 中,一般使用 map 函数来实现列表渲染。
js
const [items, setItems] = useState([{ message: 'Foo' }, { message: 'Bar' }]);
{items.map((item, index) => (
<li key={index}>{item.message}</li>
))}
Vue 的 watch 和 computed
Vue 的 watch 和 computed 都可以监听一些数据的状态,然后做出一些行为。
在 Vue 3 中使用 watch 和 computed 的区别主要在于:
- computed 用来对数据进行转换计算,watch 用来观测数据的变化去执行副作用函数。
- computed 有缓存,多次访问只计算一次结果并缓存,watch 每次数据变化都会执行。
- computed 可以通过 get 和 set 构成双向绑定,watch 只能监听数据变化。
- computed 没有参数传入,watch 可以获取新旧值。
所以:
- 当需要进行数据转换计算并依赖响应式数据时,使用 computed。
- 当需要在响应式数据变化时执行异步或开销较大的操作时,使用 watch。
- 当需要根据参数变化执行不同逻辑时,选用 watch。
- 如果仅需要对数据进行提取转化,选择 computed。
那么在 React 中 想要实现监听一些数据的状态,然后做出一些行为。一般选用 useEffect
js
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
当 deps 依赖项发生变化后,就会执行 useEffect 中的回调函数了。
vue的 v-model
v-model
是 Vue 中的一个指令,用于实现双向数据绑定。它通常用于表单元素,使得表单元素的值与组件中的数据双向绑定,当表单元素的值发生变化时,组件中的数据也会相应变化,反之亦然。
实际上 v-model 是一个语法糖,对于一个 dom 原生元素上的使用方法上:
js
<input v-model="searchText" />
等同于
js
<input
:value="searchText"
@input="searchText = $event.target.value"
/>
在组件上也是同理:
js
<CustomInput
:model-value="searchText"
@update:model-value="newValue => searchText = newValue"
/>
在 React 当中没有类似 v-model
的语法糖,我们只能以绑定事件的方式来实现类似功能。
js
import React, { useState } from 'react';
const App = () => {
const [searchText, setSearchText] = useState('');
const handleInputChange = (event) => {
// 当输入框的值发生变化时,更新状态
setSearchText(event.target.value);
};
return (
<div>
{/* 使用 state 控制输入框的值 */}
<input type="text" value={searchText} onChange={handleInputChange} />
{/* 显示当前输入框的值 */}
<p>Current Value: {searchText}</p>
</div>
);
};
export default App;
组件通信
Vue
在 Vue 3 中实现组件通信的方式:
- Props:
- 父传子(单向数据流): 通过在父组件上使用
v-bind
将数据传递给子组件。
- 父传子(单向数据流): 通过在父组件上使用
vue
<!-- 父组件 -->
<template>
<ChildComponent :data="message" />
</template>
<script>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
export default {
setup() {
const message = ref('Hello from parent');
return { message };
},
components: {
ChildComponent,
},
};
</script>
<!-- ChildComponent.vue -->
<template>
<p>{{ data }}</p>
</template>
<script>
import { defineComponent, PropType } from 'vue';
export default defineComponent({
props: {
data: {
type: String as PropType<string>,
required: true,
},
},
});
</script>
- Custom Events:
- 子传父: 通过在子组件上触发自定义事件,并在父组件上监听这些事件来实现子组件向父组件通信。
vue
<!-- 子组件 -->
<template>
<button @click="handleClick">Click me</button>
</template>
<script>
export default {
methods: {
handleClick() {
this.$emit('child-clicked');
},
},
};
</script>
<!-- 父组件 -->
<template>
<ChildComponent @child-clicked="handleChildClick" />
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
methods: {
handleChildClick() {
console.log('Child component clicked!');
},
},
components: {
ChildComponent,
},
};
</script>
- Provide / Inject:
- 祖先传递给后代: 使用
provide
在祖先组件中提供数据,然后在后代组件中使用inject
接收数据。
- 祖先传递给后代: 使用
vue
<!-- 祖先组件 -->
<template>
<div>
<p>Message from ancestor: {{ message }}</p>
<ChildComponent />
</div>
</template>
<script>
import { ref, provide } from 'vue';
import ChildComponent from './ChildComponent.vue';
export default {
setup() {
const message = ref('Hello from ancestor');
provide('message', message);
return {
message,
};
},
components: {
ChildComponent,
},
};
</script>
<!-- 后代组件 -->
<template>
<p>Message from ancestor: {{ message }}</p>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
const message = inject('message');
return {
message,
};
},
};
</script>
- Teleport:
- 跨组件通信: 使用 Teleport 进行跨组件通信。
vue
<!-- 发送事件的组件 -->
<template>
<button @click="sendMessage">Send Message</button>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const sendMessage = () => {
// 通过 dispatchEvent 发送自定义事件
document.dispatchEvent(new Event('custom-event', { bubbles: true }));
};
return {
sendMessage,
};
},
};
</script>
<!-- 接收事件的组件 -->
<template>
<teleport to="body">
<p v-if="showMessage">Received Message!</p>
</teleport>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue';
export default {
setup() {
const showMessage = ref(false);
// 在 mounted 阶段监听事件
onMounted(() => {
document.addEventListener('custom-event', handleCustomEvent);
});
// 在 unmounted 阶段移除监听
onUnmounted(() => {
document.removeEventListener('custom-event', handleCustomEvent);
});
const handleCustomEvent = () => {
showMessage.value = true;
};
return {
showMessage,
};
},
};
</script>
React
当然,以下是 React 中实现组件通信的一些常见方式:
- Props:
- 父传子(单向数据流): 通过在父组件上使用属性将数据传递给子组件。
jsx
// 父组件
import React from 'react';
import ChildComponent from './ChildComponent';
const ParentComponent = () => {
const data = 'Hello from parent';
return <ChildComponent data={data} />;
};
// 子组件
import React from 'react';
const ChildComponent = ({ data }) => {
return <p>{data}</p>;
};
- Callback 函数:
- 子传父: 父组件将一个回调函数通过
props
传递给子组件,子组件在需要通知父组件时调用这个回调函数。
- 子传父: 父组件将一个回调函数通过
jsx
// 父组件
import React from 'react';
import ChildComponent from './ChildComponent';
const ParentComponent = () => {
const handleChildClick = () => {
console.log('Child component clicked!');
};
return <ChildComponent onClick={handleChildClick} />;
};
// 子组件
import React from 'react';
const ChildComponent = ({ onClick }) => {
return <button onClick={onClick}>Click me</button>;
};
- Context API:
- 跨层级通信: 使用 React 的 Context API 在组件树中共享数据。
jsx
// 创建一个 Context
import { createContext } from 'react';
const MyContext = createContext();
// 父组件
import React from 'react';
import ChildComponent from './ChildComponent';
const ParentComponent = () => {
const sharedData = 'Shared data';
return (
<MyContext.Provider value={sharedData}>
<ChildComponent />
</MyContext.Provider>
);
};
// 子组件
import React, { useContext } from 'react';
import MyContext from './MyContext';
const ChildComponent = () => {
const sharedData = useContext(MyContext);
return <p>{sharedData}</p>;
};
- Redux 状态管理:
- 跨组件通信: 使用 Redux 管理全局状态,实现任意两个组件之间的通信。
jsx
// 创建 Redux store
import { createStore } from 'redux';
import { Provider } from 'react-redux';
const initialState = {
message: 'Hello from Redux!',
};
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case 'UPDATE_MESSAGE':
return { ...state, message: action.payload };
default:
return state;
}
};
const store = createStore(rootReducer);
// 父组件
import React from 'react';
import { useDispatch } from 'react-redux';
const ParentComponent = () => {
const dispatch = useDispatch();
const handleUpdateMessage = () => {
dispatch({ type: 'UPDATE_MESSAGE', payload: 'New message from parent' });
};
return (
<div>
<button onClick={handleUpdateMessage}>Update Message</button>
</div>
);
};
// 子组件
import React from 'react';
import { useSelector } from 'react-redux';
const ChildComponent = () => {
const message = useSelector((state) => state.message);
return <p>{message}</p>;
};
- forwardRef + useImperativeHandle:
- 父组件获取子组件实例: 使用
forwardRef
允许父组件获取子组件的ref
对象,并通过useImperativeHandle
定义需要暴露给父组件的接口。
- 父组件获取子组件实例: 使用
jsx
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
const ChildComponent = forwardRef((props, ref) => {
const childRef = useRef();
useImperativeHandle(ref, () => ({
doSomething: () => {
console.log('Child component is doing something...');
},
childProperty: 'Child Property Value',
}));
return <div>Child Component</div>;
});
const ParentComponent = () => {
const childRef = useRef();
const handleButtonClick = () => {
childRef.current.doSomething();
};
return (
<div>
<ChildComponent ref={childRef} />
<button onClick={handleButtonClick}>Call Child Method</button>
</div>
);
};
Vue2.0 和 Vue3.0
- Vue2.0 只提供了选项式的开发模式。
- Vue3.0 提供了基于组合式的开发模式,同时你也可以使用选项式。(就是这么牛逼。)
js
// Vue2.0
<script>
export default {
data() {
return {
count: 0
}
},
mounted() {
console.log(this.count) // 0
}
}
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
js
// Vue3.0 只用组合式模式
<script setup>
import { onMounted, ref } from 'vue'
const count = ref(0)
onMounted(() => {
console.log('count', count)
})
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
js
// vue 混用
<script>
import { onMounted, ref } from 'vue'
export default {
setup() {
const count = ref(0)
onMounted(() => {
console.log('???')
})
// 返回值会暴露给模板和其他的选项式 API 钩子
return {
count
}
},
mounted() {
console.log(this.count) // 0
}
}
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
之间的区别
- Proxy 替代 Object.defineProperty
- 提供了 组合式 API
第一点带来的改变。 在 Vue 2.0 中,一些操作可能导致对象失去响应性,即修改不会触发视图更新,这主要涉及到 Vue 2.0 使用的是 Object.defineProperty 进行数据劫持。以下是一些常见的操作,可能导致 Vue 2.0 对象失去响应性:
-
直接设置数组索引: 直接使用数组索引设置元素值。
javascript// 在 Vue 2.0 中,以下操作可能导致失去响应性 vm.someArray[0] = 'new value';
-
修改数组长度: 直接修改数组的长度。
javascript// 在 Vue 2.0 中,以下操作可能导致失去响应性 vm.someArray.length = 100;
-
直接设置对象属性: 直接使用对象属性设置值。
javascript// 在 Vue 2.0 中,以下操作可能导致失去响应性 vm.someObject['key'] = 'new value';
-
删除对象属性: 使用
delete
删除对象属性。javascript// 在 Vue 2.0 中,以下操作可能导致失去响应性 delete vm.someObject['key'];
-
通过索引修改嵌套数组: 修改嵌套数组时,直接通过索引修改。
javascript// 在 Vue 2.0 中,以下操作可能导致失去响应性 vm.nestedArray[0][0] = 'new value';
这些操作在 Vue 2.0 中可能导致响应性丢失,需要通过一些特殊的手段来保持响应性,比如使用 Vue.set
或者数组的变异方法。
而在 Vue 3.0 中,采用了 Proxy,这些限制得到了解决。Proxy 可以更细粒度地监听对象的变化,因此不会存在 Vue 2.0 中的一些限制,使得响应式系统更为强大和灵活。在 Vue 3.0 中,你可以更自由地修改数组、对象,而不会失去响应性。
第二点带来的改变: 使得Vue 具备了更好的方式去复用逻辑。
使用 Vue 指令实现的鉴权
自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。
当我们需要涉及到颗粒度级别的权限控制,比如页面上的按钮啥的。那么就可以很好的借助 Vue 的自定义指令来实现访问权限的控制。
一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。
也就是说,在指令当中,我们能获取到我们传递的参数,所绑定的DOM元素,还可以在对应的生命周期当中执行一些逻辑判断。
所以使用 Vue 指令实现的鉴权的大致思路就是,在DOM 挂载时获取所绑定的参数进行权限判断,如果有权限那么展示DOM,如果没有权限那么 remove 掉这个DOM。
举个栗子:
vue
<!-- main.js -->
import { createApp } from 'vue';
import App from './App.vue';
import AuthDirective from './directives/AuthDirective';
const app = createApp(App);
app.directive('auth', AuthDirective);
app.mount('#app');
vue
<!-- AuthDirective.js -->
export default {
mounted(el, binding) {
const { value } = binding;
const hasPermission = checkPermission(value); // 假设 checkPermission 是一个检查权限的函数
if (!hasPermission) {
el.parentNode.removeChild(el) ; // remove
}
},
};
vue
<!-- App.vue -->
<template>
<div>
<h1 v-auth="['admin']">Admin Page</h1>
<p>This is a protected admin page.</p>
<h1 v-auth="['user']">User Page</h1>
<p>This is a protected user page.</p>
</div>
</template>
<script>
export default {
data() {
return {};
},
};
</script>
那么在 React 中,我们如何实现类似功能呢?
确实在 React 中没有类似 Vue 指令这样的功能可以很简便的实现权限控制。
一般是使用一个鉴权的组件,将需要鉴权的组件作为它的 children 。在鉴权组件中做鉴权的判断以及控制是否显示 children。太简单了,代码就不写了。