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
单向数据流,需要手动setState
Vue template
结构表现分离,React
用jsx
结构表现融合,html/css都可以写到js里- 都可以通过
props
进行父子组件数据传递,只是Vue props
要声明,React
不用声明可能直接使用 Vue
可以用插槽,React
是万物皆可props
Vue2
利用基本都是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
统一存放,方便理解;redux
state依赖所有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中的方法,所以观察者模式中的发布者和订阅者之间是存在依赖的。
- 发布订阅模式:由统一的事件中心调度。因此发布者与订阅者不需要知道对方的存在。