Vue3
Vue3 与 Vue2区别
- 引入了
Composition API:更好的做逻辑划分。查看某个组件功能逻辑的时候,只需要关注这个函数即可。不像options API组合式,同一个功能分散在data,methods,props,computed等。 - 响应式系统优化 ,使用
Proxy替代了Object.defineProperty:不仅能监听对象,还能监听数组。 - 引入了
Fragments:允许组件返回多个根节点。 - 更好的支持
TS,vue3就是用ts写的 diff算法优化,加入了静态标记,标记的节点不会比对。- 编译优化静态提升 ,对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用。
- 对事件处理函数进行缓存,例如'click'等,下次调用的时候直接从缓存里读,减少了不必要的更新操作。
- 对打包体积优化 ,去掉了不常用的API,如
filter,引入了tree-shaking 前提是依赖于ESM,Commonjs是运行时,所以Commonjs下不能使用tree-shaking。还要看标记了副作用。 Vue3全局API名称发生了变化,同时新增了watchEffect、Hooks等功能
Object.defineProperty 和 Proxy 区别
- Object.defineProperty 只能对对象的单个属性进行拦截和操作,需要深度遍历对象的每一个属性,给每个属性添加getter和setter。
- proxy 可以监听数组。直接代理整个对象,而非对象属性,一层代理即可监听结构下所有属性变化,可直接新增/删除属性。
Vue3响应式原理
- 使用
Proxy代替Object.defineProperty实现数据劫持 - 在编译阶段,执行
render,触发get effect代替了watcher- 在
get中收集依赖,将负责渲染的effect存入deps,使用targetMap,depsMap和dep来管理使用到的属性的依赖项 - 当数据变动时触发
set,依次执行该dep下所有effect,进行更新。
reactive 通过在 Proxy get 方法中收集依赖,在 set 方法中触发依赖。整个响应式数据存在变量 targetMap 中
Vite 与 基于Webpack的vue-cli区别
- 打包速度快/快速冷启动 :
- vite :开发模式下,使用原生浏览器支持的
ES Module方式加载模块,通过<script type='module'>加载模块,开发模式下不需要打包项目可直接运行 ,可快速冷启动。 - webpack:对整个项目进行扫描和分析,会打包整个项目,处理loader和plugin,如果项目比较大,速度会特别慢。
- vite :开发模式下,使用原生浏览器支持的
- 按需编译:只有当代码在加载的时候才会编译。vite 会开启一个开发服务器,会拦截浏览器发送的请求,浏览器会向服务器发送请求获取相应的模块, vite会对浏览器不识别的模块进行处理,如当import 后缀为.vue文件时,会在服务器上对.vue文件进行编译,把编译的结果返回给浏览器。
- 模块热更新 :
- vite:浏览器重新请求该模块即可,它只会重新加载变化的模块,而不是整个页面。
- webpack:模块以及模块依赖的模块需重新编译。
- 生产环境打包体积更小 :
- vite:使用Rollup打包,Rollup直接使用原生浏览器的ESM进行打包,不需要使用babel把import转换成require,以及相应的转换函数。打包体积更小
- 构建原理 :
- vite:是用浏览器原生支持的 ES Module(ESM)特性,以模块为单位进行开发。基于esbulid预构建依赖
- webpack:是通过入口文件递归依赖构建。
Composition API
creatApp():用来创建Vue对象。
setup():是 Composition API 的入口。
- 接两个参数 ,第一个参数是外部传入的参数props,并且props是响应式对象,不能被解构。第二个参数是 context,有三个成员 attrs emit slots。
- 返回一个对象,用在templage,methods,computed,和生命周期钩子函数中。
- 执行时机 :在解析props完毕,和创建组件实例之前执行的。所以在
setup内部无法用this获取组件实例 。此时this是undefined。reactive():把一个对象转换成响应式对象,并且嵌套属性也都会变成响应式对象。返回的是一个proxy对象。
toRefs():需要传入一个proxy代理对象 。 内部会创建一个新的对象,然后遍历传入的所有属性,把所有属性都转化成响应式对象,带有.value属性,value属性具有getter/setter,最后挂载到新创建的对象上。
ref():把基本类型(如字符串、数字,布尔值等)转换成响应式对象 ,带有.value属性 ,value属性具有getter/setter。也可以把引用数据类型(对象)转换为响应式,会调用reactive返回一个proxy代理对象。
ref() |
reactive() |
|---|---|
| ✅支持基本数据类型+引用数据类型 | ❌只支持对象和数组(引用数据类型) |
❌在 <script> 和 <template> 使用方式不同(script中要.value) |
✅在 <script> 和 <template> 中无差别使用 |
| ✅重新分配一个新对象不会失去响应 | ❌重新分配一个新对象会丢失响应性 |
| ✅传入函数时,不会失去响应 | ❌将对象传入函数时,失去响应 |
| ✅解构对象时会丢失响应性,需使用toRefs | ❌解构时会丢失响应性,需使用toRefs |
toRef(obj, key): 根据一个响应式对象中的一个属性,创建一个响应式的 ref,并且该 ref 和原对象中的属性保持同步。toRefs(obj): 将一个响应式对象转换成一个普通对象,其中普通对象的每个属性都是响应式的 ref。isRef(value): 判断某个值是否是 ref 对象。unref(value): 用于解除响应式引用shallowRef(value): 创建一个浅层的 ref,只有 value 属性是响应式的,深层的属性不具备响应式。triggerRef(ref): 强制浅层的 ref 发生改变时触发响应式。customRef(factory): 自定义 ref 对象,可以显式地追踪某个值的响应式变化。
Vue3中 HOOKS 的理解
hooks是函数式编程思想,用来封装通用逻辑。
和组件的区别:组件是有DOM或Template的,hooks只有函数。
和utils区别:util里封装的是通用方法,不包含状态。hooks里是包含状态,比如响应式状态。
Vue2
Vue2双向数据绑定原理(响应式原理)
采用数据劫持 结合发布者-订阅者模式 的方式,data数据在初始化的时候,会实例化一个Observe类,在它会将data数据进行递归遍历,并通过Object.defineProperty方法,给每个值添加上一个getter和一个setter。在数据读取的时候会触发getter进行依赖(Watcher)收集,当数据改变时,会触发setter,对刚刚收集的依赖进行触发,并且更新watcher通知视图进行渲染。
通过数据劫持+发布订阅模式实现的。通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
observer劫持监听属性变化->watcher通知变化,更新视图
compile解析指令,订阅数据变化,绑定更新函数->watcher通知变化,更新视图
当数据更新时,通知watcher更新组件

页面首次加载的时候会调用Compiler解析指令/解析差值表达式,会调用其中的方法,更新视图。
首次加载是Compiler更新视图。
数据变化都是通过watcher更新视图。
订阅数据变化:创建watcher对象,订阅数据变化,当数据变化时Dep会通知watcher。
绑定更新函数:当创建watcher对象的时候,会传入一个回调函数,回调函数中更新视图。
Vuex原理
- vuex是
响应式的全局状态管理工具,状态会保存在state内,一个状态改变,全局都会跟着变化。 getter是从state的派发状态,相当于state的计算属性- 改变
state状态的唯一途径是提交(commit)mutation,且mutation是同步操作。 action像一个装饰器,可以包裹提交mutation,可以提交多个,action可以包含任意异步操作。mocules模块化vuex,是store分割的模块,每个模块拥有自己的state、getters、mutations、actions。
为什么 Vuex 的 mutation 中不能做异步操作?
Vuex 中所有的状态更新的唯一途径都是mutation,异步操作通过Action 来提交 mutation 实现,这样可以方便地跟踪每一个状态的变化。如果 mutation 支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。
页面刷新后Vuex 状态丢失怎么解决?
Vuex 只是在内存中保存状态,刷新后就会丢失,如果要持久化就需要保存起来。
localStorage就很合适,提交mutation的时候同时存入localStorage,在store中把值取出来作为state的初始值即可。
也可以使用第三方插件,推荐使用vuex-persist插件,它是为 Vuex 持久化储存而生的一个插件,不需要你手动存取storage,而是直接将状态保存至 cookie 或者 localStorage中。
Pinia 和 Vuex
Vuex : State、Gettes、Mutations(同步)、Actions(异步)
Pinia : State、Gettes、Actions(同步异步都支持)
-
Pinia 没有
Mutations -
Actions支持同步和异步 -
没有模块的嵌套结构:Pinia 通过设计提供扁平结构 ,就是说每个 store 都是互相独立的,谁也不属于谁,也就是扁平化了,更好的代码分割且没有命名空间。当然你也可以通过在一个模块中导入另一个模块来隐式嵌套 store,甚至可以拥有 store 的循环依赖关系
-
更好的
TypeScript支持:不需要再创建自定义的复杂包装器来支持 TypeScript 所有内容都类型化,并且 API 的设计方式也尽可能的使用 TS 类型推断 -
不需要注入、导入函数、调用它们,享受自动补全,让我们开发更加方便
-
无需手动添加 store,它的模块默认情况下创建就自动注册的
-
Vue2 和 Vue3 都支持:除了初始化安装和SSR配置之外,两者使用上的API都是相同的
-
支持
Vue DevTools- 跟踪 actions, mutations 的时间线
- 在使用了模块的组件中就可以观察到模块本身
- 支持 time-travel 更容易调试
- 在 Vue2 中 Pinia 会使用 Vuex 的所有接口,所以它俩不能一起使用
- 但是针对 Vue3 的调试工具支持还不够完美,比如还没有 time-travel 功能
-
模块热更新
- 无需重新加载页面就可以修改模块
- 热更新的时候会保持任何现有状态
-
支持使用插件扩展 Pinia 功能
-
支持服务端渲染
vue 和 react区别
Vue API多,React API少Vue双向绑定,修改数据自动更新视图,而React单向数据流,需要手动setStateVue template结构表现分离,React用jsx结构表现融合,html/css都可以写到js里- 都可以通过
props进行父子组件数据传递,只是Vue props要声明,React不用声明可能直接使用 Vue可以用插槽,React是万物皆可propsVue2利用基本都是Mixin,React可以用高阶函数、自定义hook实现Vue的fragment、hook到Vue3才有,Vue还有丰富的指令
vue 和 react 如何选型?
- 看公司基建,公司的基建能更好的支持哪个框架
- 团队技术储备
- 历史代码
- 效率和产出是最高的
Vuex 和 Redux异同
相同:
- state 共享数据,单一状态树的概念
- 流程一致:定义全局state,触发,修改state
- 原理相似,通过全局注入store。
不同:
vuex定义了state、getter、mutation、action四个对象;redux定义了store、reducer、action。vuex触发方式有两种commit同步和dispatch异步;redux同步和异步都使用dispatchvuex中state统一存放,方便理解;reduxstate依赖所有reducer的初始值vuex有getter,目的是快捷得到state;redux没有这层,react-redux mapStateToProps参数做了这个工作。vuex中mutation只是单纯赋值(很浅的一层);redux中reducer只是单纯设置新state(很浅的一层)。他俩作用类似,但书写方式不同Redux使用的是不可变数据 ,而Vuex的数据是可变的 。Redux每次都是用新的state替换旧的state ,而Vuex是直接修改Redux在检测数据变化的时候,是通过diff的方式比较差异的,而Vuex其实和Vue的原理一样,是通过getter/setter来比较的
虚拟DOM
虚拟DOM是表示真实DOM的JS对象。包含 TagName标签名、props标签属性 和Children子标签名,子属性,文本节点。
diff算法原理:更高效地更新视图
Vue2 是同层比较新老 vnode ,新的不存在老的存在就删除,新的存在老的不存在就创建,子节点采用双指针头对尾两端对比的方式,全量diff,然后移动节点时通过 splice 进行数组操作
只比较同级节点。采用首尾指针法。新旧虚拟DOM都有首尾两个元素。
按下图1234的顺序依次进行比较,比较成功的两个节点的指针向中间靠拢移动,当新旧节点中有一个首指针跑到尾指针后时,结束比较。更新真实DOM。
视频链接www.bilibili.com/video/BV1JR...
Vue3中加入了静态标记。被标记的节点不会比较,标记值为-1。
采用 Map 数据结构以及动静结合的方式,在编译阶段提前标记静态节点,Diff 过程中直接跳过有静态标记的节点,并且子节点对比会使用一个 source 数组来记录节点位置及最长递增子序列算法优化了对比流程,快速 Diff,需要处理的边际条件会更少
vue-router模式和原理
-
hash :URL 中的路由信息以
#符号开始,后面跟随着路由路径。浏览器不会将哈希值的变化发送到服务器 ,因此不会触发页面的重新加载 。因为只将 hash 前面的部分当作地址 ,服务端仍然会返回 index.html。 原理:Vue Router 会监听浏览器hashchange事件。当哈希值发生变化时,会在浏览器的访问历史 中增加一个记录。Vue Router 根据新的哈希值切换到对应的组件 ,更新视图。哈希值的变化不会使浏览器向服务器发送请求 ,因此切换路由时只是在客户端内部进行的操作。Hash 模式适用于不依赖于服务器的情况。 -
history :URL上没有
#。更美观。地址栏中的地址全部看作请求地址。原理:Vue Router 会调用浏览器的
pushState或replaceState方法,将新的路由信息添加或替换到浏览器历史记录 中。历史记录发生变化时被触发,Vue Router 监听popstate事件,并根据新的路由信息切换到对应的组件,更新视图。切换路由时浏览器会向服务器发送请求。
$nextTick原理
$nextTick是vue异步操作工具,能在DOM更新后执行回调函数。本质是js的事件循环机制。
vue更新DOM是异步的 。当有数据变化时,会将变化塞到任务队列中 ,在组件更新时,Vue 会将这个队列中的变化应用到虚拟 DOM 中。$nextTick 的回调函数会被添加到一个微任务队列中。任务队列执行后会检查微任务队列是否有任务,如果有则执行。
computed和watch有什么不同?
-
computed :计算结果并返回,只有当被计算的属性发生改变时才会触发(即:计算属性的结果会被缓存 ,除非依赖的响应属性变化才会重新计算)
原理:基于vue的响应式原理。响应式数据发生改变时,会通知存储computed的Dep对象,标记该computed为dirty 。当下次访问该computed的属性值时 ,computed会检查 依赖的响应式数据是否发生了变更 ,如果没有 则直接返回已缓存的属性值 ;如果依赖的数据发生了变更,则重新计算 。并将计算后的结果缓存起来 。此时,该computed会将dirty标记重置为false,等待下次依赖项发生变化时再次重新计算。
-
watch:监听某一个值,当被监听的值发生变化时,执行相关操作。
原理:对watch每个属性创建一个watcher ,watcher在初始化时会将监听的目标值缓存到watcher.value中,因此触发 data[key]的get方法,被对应的dep进行依赖收集;当data[key]发生变动时触发set方法,执行
dep.notify方法,通知所有收集的依赖watcher,触发收集的watch watcher,执行watcher.cb,也就是watch中的监听函数
vue生命周期中异步加载在mouted还是create里实现
最常用的是在 created 钩子函数中调用异步请求 ,此时data已经挂载到vue实例了(有数据了,但是还没有真实的DOM)
有两个优点:
1、能更快获取到服务端数据,减少页面 loading 时间;
2、放在 created 中有助于一致性,因为ssr 不支持 beforeMount 、mounted 钩子函数。
Vue2 为什么要用 vm.$set() 解决对象新增属性不能响应的问题 ?
- Vue使用了Object.defineProperty实现双向数据绑定
- 在初始化实例时对属性执行 getter/setter 转化
- 属性必须在data对象上存在才能让Vue将它转换为响应式的 (这也就造成了Vue无法检测到对象属性的添加或删除)无法检测实例被创建时不存在于
data中的 属性。
所以Vue提供了Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)
vm.$set 的实现原理是:
- 如果目标是数组 ,直接使用数组的 splice 方法触发响应式;
- 如果目标是对象 ,会先判读属性是否存在、对象是否是响应式,
- 最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理
delete 和 Vue.delete
delete:会删除数组的值,但是它依然会在内存中占位置 Vue.delete:直接删除数组,改变数组的键值,删除数组在内存中的占位
js
let arr1 = [1,2,3]
let arr2 = [1,2,3]
delete arr1[1]
this.$delete(arr2,2)
console.log(arr1) // [1, empty, 3]
console.log(arr2) // [1,2]
生命周期
- beforeCreate(创建前):data属性还没有赋值,也没有DOM。
- created(创建后):data属性有值了,但是DOM还没有生成,$el属性还不存在。
- beforeMount(挂载前):this.$el有值,但是数据还没有挂载到页面上。
- mounted(挂载后):模板编译完成,数据挂载完毕。
- beforeUpdate(更新前):只有数据更新后,才能调用(触发)beforeUpdate。
- updated(更新后):组件更新之后执行的函数。
- activated(组件激活):keep-alive组件激活时调用。
- deactivated(组件停用):keep-alive组件停用时调用。
- beforeDestroy(销毁前):vue(组件)对象销毁之前。
- destroyed(销毁后):vue组件销毁后。
Vue 父子组件生命周期执行顺序
加载渲染阶段:父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted更新阶段:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated销毁阶段:父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
Vue.js基础结构
创建Vue实例,传入
el和data选项,会把data中的数据填充到el指向的模板中,并把模版渲染到浏览器 。

第二种使用vue-cli创建使用
render和$mount
h函数:用来创建虚拟DOM
render函数:把虚拟DOM返回
$mount:虚拟DOM转成真实DOM,渲染到浏览器

Vue.use()
用来注册插件,并调用传入插件的install方法。
$route 和 $router
-
$route:是一个Object对象,里面存放路由规则、路径。 -
$router:是一个VueRouter实例,实例里提供跟路由相关的方法,和路由模式mode等,currentRoute当前路由规则。
发布订阅模式
vue中的自定义事件 eventbus 和 node的事件机制都是发布订阅模式。
js
// 构造函数是一个对象,同一事件可以定义多个注册方法。
// { 'click': [fn1, fn2], 'change': [fn] }
class EventBus {
constructor() {
this.eventObj = Object.create(null); // 定义一个无原型的空对象
}
// 订阅事件,类似监听事件$on('key',()=>{}) 所以两个参数分别是 事件名 和 回调函数
$on(eventName, callback) {
// 如果同一事件已经有方法了,就获取出来,否则为空数组
this.eventObj[eventName] = this.eventObj[eventName] || [];
// 将订阅方法 push 进该事件中
this.eventObj[eventName].push(callback);
}
// 发布事件,类似于触发事件$emit('key', params) 参数是事件名 和 参数
$emit(eventName, ...args) {
// 如果找到该注册的事件,则遍历事件中的方法 并 执行
if(this.eventObj[eventName]) {
const eventList = this.eventObj[eventName];
for(let callback of eventList) {
callback(...args);
}
}
}
}
// 测试验证
const bus = new EventBus();
bus.$on('myEvent111',(name, age)=>{
console.log('myEvent111 第一个', name, age);
})
bus.$on('myEvent111',(name, age)=>{
console.log('myEvent111 第二个', name, age);
})
bus.$on('myEvent222',(age)=>{
console.log('myEvent222', age);
})
bus.$emit('myEvent111', '张三', 18);
bus.$emit('myEvent222', 18);
观察者模式
观察者与发布订阅的区别是没有事件中心,
- 观察者(订阅者 )- Watcher
- update():当事件发生变化的时候,具体要做的事。
- 目标(发布者)- Dep (依赖)
- sub数组:用来存储所有的
Watcher。 - addSub():用来添加
Watcher。 - notify():当事件发生时,调用所有观察者
Watcher的update方法。
- sub数组:用来存储所有的
- 没有事件中心
当事件发生变化的时候,目标(发布者)
Dep会调用所有观察者Watcher里的update方法,用来更新视图。
js
// 目标(依赖)- 发布者
class Dep {
constructor() {
this.subs = []; // 记录所有 watcher
}
// 添加 watcher
addSub(sub) {
if(sub && sub.update){ // 有 update 方法的才是 watcher
this.subs.push(sub);
}
}
// 发布通知
notify() {
for(let sub of this.subs) { // 遍历调用所有 watcher 的 update
sub.update();
}
}
}
// 观察者 - 订阅者
class Watcher {
update() {
console.log('观察到了,哈哈哈')
}
}
// 测试验证
let dep = new Dep();
let watcher = new Watcher();
dep.addSub(watcher);
dep.notify();
发布订阅模式 与 观察者模式 区别
- 观察者模式:是由具体目标调度。比如事件触发,Dep会调用watcher中的方法,所以观察者模式中的发布者和订阅者之间是存在依赖的。
- 发布订阅模式:由统一的事件中心调度。因此发布者与订阅者不需要知道对方的存在。
