Vue路由hash模式和history模式
1. hash
模式
早期的前端路由的实现就是基于 location.hash
来实现的。其实现原理很简单,location.hash
的值就是 URL
中 #
后面的内容。比如下面这个网站,它的 location.hash
的值为 '#search'
https://interview2.poetries.top#search
hash 路由模式的实现主要是基于下面几个特性
URL
中hash
值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash
部分不会被发送;hash
值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash
的切换;- 可以通过
a
标签,并设置href
属性,当用户点击这个标签后,URL
的hash
值会发生改变;或者使用JavaScript
来对loaction.hash
进行赋值,改变URL
的hash
值; - 我们可以使用
hashchange
事件来监听hash
值的变化,从而对页面进行跳转(渲染)
window.addEventListener("hashchange", funcRef, false);
每一次改变 hash
(window.location.hash
),都会在浏览器的访问历史中增加一个记录利用 hash
的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了
特点 :兼容性好但是不美观
2. history
模式
history
采用HTML5
的新特性;且提供了两个新方法: pushState()
, replaceState()
可以对浏览器历史记录栈进行修改,以及popState
事件的监听到状态变更
window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);
这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL
改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。
history 路由模式的实现主要基于存在下面几个特性:
pushState
和repalceState
两个API
来操作实现URL
的变化 ;- 我们可以使用
popstate
事件来监听url
的变化,从而对页面进行跳转(渲染); history.pushState()
或history.replaceState()
不会触发popstate
事件,这时我们需要手动触发页面跳转(渲染)。
特点 :虽然美观,但是刷新会出现 404
需要后端进行配置
Vue.set的实现原理
- 给对应和数组本身都增加了
dep
属性 - 当给对象新增不存在的属性则触发对象依赖的
watcher
去更新 - 当修改数组索引时,我们调用数组本身的
splice
去更新数组(数组的响应式原理就是重新了splice
等方法,调用splice
就会触发视图更新)
基本使用
以下方法调用会改变原始数组:
push()
,pop()
,shift()
,unshift()
,splice()
,sort()
,reverse()
,Vue.set( target, key, value )
- 调用方法:
Vue.set(target, key, value )
target
:要更改的数据源(可以是对象或者数组)key
:要更改的具体数据value
:重新赋的值
<div id="app">{{user.name}} {{user.age}}</div>
<div id="app"></div>
<script>
// 1. 依赖收集的特点:给每个属性都增加一个dep属性,dep属性会进行收集,收集的是watcher
// 2. vue会给每个对象也增加一个dep属性
const vm = new Vue({
el: '#app',
data: { // vm._data
user: {name:'poetry'}
}
});
// 对象的话:调用defineReactive在user对象上定义一个age属性,增加到响应式数据中,触发对象本身的watcher,ob.dep.notify()更新
// 如果是数组 通过调用 splice方法,触发视图更新
vm.$set(vm.user, 'age', 20); // 不能给根属性添加,因为给根添加属性 性能消耗太大,需要做很多处理
// 修改肯定是同步的 -> 更新都是一步的 queuewatcher
</script>
相关源码
// src/core/observer/index.js 44
export class Observer { // new Observer(value)
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep() // 给所有对象类型增加dep属性
}
}
// src/core/observer/index.js 201
export function set (target: Array<any> | Object, key: any, val: any): any {
// 1.是开发环境 target 没定义或者是基础类型则报错
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// 3.如果是对象本身的属性,则直接添加即可
if (key in target && !(key in Object.prototype)) {
target[key] = val // 直接修改属性值
return val
}
// 4.如果是Vue实例 或 根数据data时 报错,(更新_data 无意义)
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 5.如果不是响应式的也不需要将其定义成响应式属性
if (!ob) {
target[key] = val
return val
}
// 6.将属性定义成响应式的
defineReactive(ob.value, key, val)
// 通知视图更新
ob.dep.notify()
return val
}
我们阅读以上源码可知,vm.$set 的实现原理是:
- 如果目标是数组 ,直接使用数组的
splice
方法触发相应式; - 如果目标是对象 ,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用
defineReactive
方法进行响应式处理(defineReactive
方法就是Vue
在初始化对象时,给对象属性采用Object.defineProperty
动态添加getter
和setter
的功能所调用的方法)
Vue组件之间通信方式有哪些
Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。 Vue 组件间通信只要指以下 3 类通信 :
父子组件通信
、隔代组件通信
、兄弟组件通信
,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信
组件传参的各种方式
组件通信常用方式有以下几种
props / $emit
适用 父子组件通信- 父组件向子组件传递数据是通过
prop
传递的,子组件传递数据给父组件是通过$emit
触发事件来做到的
- 父组件向子组件传递数据是通过
ref
与$parent / $children(vue3废弃)
适用 父子组件通信ref
:如果在普通的DOM
元素上使用,引用指向的就是DOM
元素;如果用在子组件上,引用就指向组件实例$parent / $children
:访问访问父组件的属性或方法 / 访问子组件的属性或方法
EventBus ($emit / $on)
适用于 父子、隔代、兄弟组件通信- 这种方法通过一个空的
Vue
实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件
- 这种方法通过一个空的
$attrs / $listeners(vue3废弃)
适用于 隔代组件通信$attrs
:包含了父作用域中不被prop
所识别 (且获取) 的特性绑定 (class
和style
除外 )。当一个组件没有声明任何prop
时,这里会包含所有父作用域的绑定 (class
和style
除外 ),并且可以通过v-bind="$attrs"
传入内部组件。通常配合inheritAttrs
选项一起使用$listeners
:包含了父作用域中的 (不含.native
修饰器的)v-on
事件监听器。它可以通过v-on="$listeners"
传入内部组件
provide / inject
适用于 隔代组件通信- 祖先组件中通过
provider
来提供变量,然后在子孙组件中通过inject
来注入变量。provide / inject
API 主要解决了跨级组件间的通信问题, 不过它的使用场景,主要是子组件获取上级组件的状态 ,跨级组件间建立了一种主动提供与依赖注入的关系
- 祖先组件中通过
$root
适用于 隔代组件通信 访问根组件中的属性或方法,是根组件,不是父组件。$root
只对根组件有用Vuex
适用于 父子、隔代、兄弟组件通信Vuex
是一个专为Vue.js
应用程序开发的状态管理模式。每一个Vuex
应用的核心就是store
(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 (state
)Vuex
的状态存储是响应式的。当Vue
组件从store
中读取状态的时候,若store
中的状态发生变化,那么相应的组件也会相应地得到高效更新。- 改变
store
中的状态的唯一途径就是显式地提交 (commit
)mutation
。这样使得我们可以方便地跟踪每一个状态的变化。
根据组件之间关系讨论组件通信最为清晰有效
- 父子组件:
props
/$emit
/$parent
/ref
- 兄弟组件:
$parent
/eventbus
/vuex
- 跨层级关系:
eventbus
/vuex
/provide+inject
/$attrs + $listeners
/$root
下面演示组件之间通讯三种情况: 父传子、子传父、兄弟组件之间的通讯
1. 父子组件通信
使用
props
,父组件可以使用props
向子组件传递数据。
父组件vue
模板father.vue
:
<template>
<child :msg="message"></child>
</template>
<script>
import child from './child.vue';
export default {
components: {
child
},
data () {
return {
message: 'father message';
}
}
}
</script>
子组件vue
模板child.vue
:
<template>
<div>{{msg}}</div>
</template>
<script>
export default {
props: {
msg: {
type: String,
required: true
}
}
}
</script>
回调函数(callBack)
父传子:将父组件里定义的method
作为props
传入子组件
// 父组件Parent.vue:
<Child :changeMsgFn="changeMessage">
methods: {
changeMessage(){
this.message = 'test'
}
}
// 子组件Child.vue:
<button @click="changeMsgFn">
props:['changeMsgFn']
子组件向父组件通信
父组件向子组件传递事件方法,子组件通过
$emit
触发事件,回调给父组件
父组件vue
模板father.vue
:
<template>
<child @msgFunc="func"></child>
</template>
<script>
import child from './child.vue';
export default {
components: {
child
},
methods: {
func (msg) {
console.log(msg);
}
}
}
</script>
子组件vue
模板child.vue
:
<template>
<button @click="handleClick">点我</button>
</template>
<script>
export default {
props: {
msg: {
type: String,
required: true
}
},
methods () {
handleClick () {
//........
this.$emit('msgFunc');
}
}
}
</script>
2. provide / inject 跨级访问祖先组件的数据
父组件通过使用provide(){return{}}
提供需要传递的数据
export default {
data() {
return {
title: '我是父组件',
name: 'poetry'
}
},
methods: {
say() {
alert(1)
}
},
// provide属性 能够为后面的后代组件/嵌套的组件提供所需要的变量和方法
provide() {
return {
message: '我是祖先组件提供的数据',
name: this.name, // 传递属性
say: this.say
}
}
}
子组件通过使用inject:[“参数1”,”参数2”,…]
接收父组件传递的参数
<template>
<p>曾孙组件</p>
<p>{{message}}</p>
</template>
<script>
export default {
// inject 注入/接收祖先组件传递的所需要的数据即可
//接收到的数据 变量 跟data里面的变量一样 可以直接绑定到页面 {{}}
inject: [ "message","say"],
mounted() {
this.say();
},
};
</script>
3. $parent + $children 获取父组件实例和子组件实例的集合
this.$parent
可以直接访问该组件的父实例或组件- 父组件也可以通过
this.$children
访问它所有的子组件;需要注意$children
并不保证顺序,也不是响应式的
<!-- parent.vue -->
<template>
<div>
<child1></child1>
<child2></child2>
<button @click="clickChild">$children方式获取子组件值</button>
</div>
</template>
<script>
import child1 from './child1'
import child2 from './child2'
export default {
data(){
return {
total: 108
}
},
components: {
child1,
child2
},
methods: {
funa(e){
console.log("index",e)
},
clickChild(){
console.log(this.$children[0].msg);
console.log(this.$children[1].msg);
}
}
}
</script>
<!-- child1.vue -->
<template>
<div>
<button @click="parentClick">点击访问父组件</button>
</div>
</template>
<script>
export default {
data(){
return {
msg:"child1"
}
},
methods: {
// 访问父组件数据
parentClick(){
this.$parent.funa("xx")
console.log(this.$parent.total);
}
}
}
</script>
<!-- child2.vue -->
<template>
<div>
child2
</div>
</template>
<script>
export default {
data(){
return {
msg: 'child2'
}
}
}
</script>
4. $attrs + $listeners多级组件通信
$attrs
包含了从父组件传过来的所有props
属性
// 父组件Parent.vue:
<Child :name="name" :age="age"/>
// 子组件Child.vue:
<GrandChild v-bind="$attrs" />
// 孙子组件GrandChild
<p>姓名:{{$attrs.name}}</p>
<p>年龄:{{$attrs.age}}</p>
$listeners
包含了父组件监听的所有事件
// 父组件Parent.vue:
<Child :name="name" :age="age" @changeNameFn="changeName"/>
// 子组件Child.vue:
<button @click="$listeners.changeNameFn"></button>
5. ref 父子组件通信
// 父组件Parent.vue:
<Child ref="childComp"/>
<button @click="changeName"></button>
changeName(){
console.log(this.$refs.childComp.age);
this.$refs.childComp.changeAge()
}
// 子组件Child.vue:
data(){
return{
age:20
}
},
methods(){
changeAge(){
this.age=15
}
}
6. 非父子, 兄弟组件之间通信
vue2
中废弃了broadcast
广播和分发事件的方法。父子组件中可以用props
和$emit()
。如何实现非父子组件间的通信,可以通过实例一个vue
实例Bus
作为媒介,要相互通信的兄弟组件之中,都引入Bus
,然后通过分别调用Bus事件触发和监听来实现通信和参数传递。Bus.js
可以是这样:
// Bus.js
// 创建一个中央时间总线类
class Bus {
constructor() {
this.callbacks = {}; // 存放事件的名字
}
$on(name, fn) {
this.callbacks[name] = this.callbacks[name] || [];
this.callbacks[name].push(fn);
}
$emit(name, args) {
if (this.callbacks[name]) {
this.callbacks[name].forEach((cb) => cb(args));
}
}
}
// main.js
Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上
// 另一种方式
Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能
<template>
<button @click="toBus">子组件传给兄弟组件</button>
</template>
<script>
export default{
methods: {
toBus () {
this.$bus.$emit('foo', '来自兄弟组件')
}
}
}
</script>
另一个组件也在钩子函数中监听on
事件
export default {
data() {
return {
message: ''
}
},
mounted() {
this.$bus.$on('foo', (msg) => {
this.message = msg
})
}
}
7. $root 访问根组件中的属性或方法
- 作用:访问根组件中的属性或方法
- 注意:是根组件,不是父组件。
$root
只对根组件有用
var vm = new Vue({
el: "#app",
data() {
return {
rootInfo:"我是根元素的属性"
}
},
methods: {
alerts() {
alert(111)
}
},
components: {
com1: {
data() {
return {
info: "组件1"
}
},
template: "<p>{{ info }} <com2></com2></p>",
components: {
com2: {
template: "<p>我是组件1的子组件</p>",
created() {
this.$root.alerts()// 根组件方法
console.log(this.$root.rootInfo)// 我是根元素的属性
}
}
}
}
}
});
8. vuex
- 适用场景: 复杂关系的组件数据传递
- Vuex作用相当于一个用来存储共享变量的容器
state
用来存放共享变量的地方getter
,可以增加一个getter
派生状态,(相当于store
中的计算属性),用来获得共享变量的值mutations
用来存放修改state
的方法。actions
也是用来存放修改state的方法,不过action
是在mutations
的基础上进行。常用来做一些异步操作
小结
- 父子关系的组件数据传递选择
props
与$emit
进行传递,也可选择ref
- 兄弟关系的组件数据传递可选择
$bus
,其次可以选择$parent
进行传递 - 祖先与后代组件数据传递可选择
attrs
与listeners
或者Provide
与Inject
- 复杂关系的组件数据传递可以通过
vuex
存放共享的变量
Vue-router 路由模式有几种
vue-router
有 3
种路由模式:hash
、history
、abstract
,对应的源码如下所示
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
其中,3 种路由模式的说明如下:
hash
: 使用URL hash
值来作路由,支持所有浏览器history
: 依赖HTML5 History API
和服务器配置abstract
: 支持所有JavaScript
运行环境,如Node.js
服务器端。如果发现没有浏览器的API
,路由会自动强制进入这个模式.
vue-router守卫
导航守卫
router.beforeEach
全局前置守卫
to: Route
: 即将要进入的目标(路由对象)from: Route
: 当前导航正要离开的路由next: Function
: 一定要调用该方法来resolve
这个钩子。(一定要用这个函数才能去到下一个路由,如果不用就拦截)- 执行效果依赖 next 方法的调用参数。
next()
: 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。next(false)
:取消进入路由,url地址重置为from路由地址(也就是将要离开的路由地址)
// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next) => {
next();
});
router.beforeResolve((to, from, next) => {
next();
});
router.afterEach((to, from) => {
console.log('afterEach 全局后置钩子');
});
路由独享的守卫 你可以在路由配置上直接定义
beforeEnter
守卫
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
组件内的守卫你可以在路由组件内直接定义以下路由导航守卫
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用,我们用它来禁止用户离开
// 可以访问组件实例 `this`
// 比如还未保存草稿,或者在用户离开前,
将setInterval销毁,防止离开之后,定时器还在调用。
}
}
Vue要做权限管理该怎么做?控制到按钮级别的权限怎么做?
分析
- 综合实践题目,实际开发中经常需要面临权限管理的需求,考查实际应用能力。
- 权限管理一般需求是两个:页面权限和按钮权限,从这两个方面论述即可。
思路
- 权限管理需求分析:页面和按钮权限
- 权限管理的实现方案:分后端方案和前端方案阐述
- 说说各自的优缺点
回答范例
-
权限管理一般需求是页面权限和按钮权限的管理
-
具体实现的时候分后端和前端两种方案:
-
前端方案 会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个
asyncRoutes
数组,需要认证的页面在其路由的meta
中添加一个roles
字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过router.addRoutes(accessRoutes)
方式动态添加路由即可 -
后端方案 会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过
addRoutes
动态添加路由信息 -
按钮权限的控制通常会
实现一个指令
,例如v-permission
,将按钮要求角色通过值传给v-permission
指令,在指令的moutned
钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮
- 纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!
可能的追问
- 类似
Tabs
这类组件能不能使用v-permission
指令实现按钮权限控制?
<el-tabs>
<el-tab-pane label="⽤户管理" name="first">⽤户管理</el-tab-pane>
<el-tab-pane label="⻆⾊管理" name="third">⻆⾊管理</el-tab-pane>
</el-tabs>
- 服务端返回的路由信息如何添加到路由器中?
// 前端组件名和组件映射表
const map = {
//xx: require('@/views/xx.vue').default // 同步的⽅式
xx: () => import('@/views/xx.vue') // 异步的⽅式
}
// 服务端返回的asyncRoutes
const asyncRoutes = [
{ path: '/xx', component: 'xx',... }
]
// 遍历asyncRoutes,将component替换为map[component]
function mapComponent(asyncRoutes) {
asyncRoutes.forEach(route => {
route.component = map[route.component];
if(route.children) {
route.children.map(child => mapComponent(child))
}
})
}
mapComponent(asyncRoutes)
你知道哪些Vue3新特性?
官网列举的最值得注意的新特性:v3-migration.vuejs.org(opens new window)
Composition API
SFC Composition API
语法糖Teleport
传送门Fragments
片段Emits
选项- 自定义渲染器
SFC CSS
变量Suspense
以上这些是api相关,另外还有很多框架特性也不能落掉
回答范例
api
层面Vue3
新特性主要包括:Composition API
、SFC Composition API
语法糖、Teleport
传送门、Fragments
片段、Emits
选项、自定义渲染器、SFC CSS
变量、Suspense
- 另外,
Vue3.0
在框架层面也有很多亮眼的改进:
- 更快
- 虚拟
DOM
重写,diff
算法优化 - 编译器优化:静态提升、
patchFlags(静态标记)
、事件监听缓存 - 基于
Proxy
的响应式系统 SSR
优化
- 虚拟
- 更小 :更好的摇树优化
tree shaking
、Vue3
移除一些不常用的API
- 更友好 :
vue3
在兼顾vue2
的options API
的同时还推出了composition API
,大大增加了代码的逻辑组织和代码复用能力 - 更容易维护 :
TypeScript
+ 模块化 - 更容易扩展
- 独立的响应化模块
- 自定义渲染器
Vue3有了解过吗?能说说跟vue2的区别吗?
1. 哪些变化
从上图中,我们可以概览Vue3
的新特性,如下:
- 速度更快
- 体积减少
- 更易维护
- 更接近原生
- 更易使用
1.1 速度更快
vue3
相比vue2
- 重写了虚拟
Dom
实现 - 编译模板的优化
- 更高效的组件初始化
undate
性能提高1.3~2倍SSR
速度提高了2~3倍
1.2 体积更小
通过webpack
的tree-shaking
功能,可以将无用模块“剪辑”,仅打包需要的
能够tree-shaking
,有两大好处:
- 对开发人员,能够对
vue
实现更多其他的功能,而不必担忧整体体积过大 - 对使用者,打包出来的包体积变小了
vue
可以开发出更多其他的功能,而不必担忧vue
打包出来的整体体积过多
1.3 更易维护
compositon Api
- 可与现有的
Options API
一起使用 - 灵活的逻辑组合与复用
Vue3
模块可以和其他框架搭配使用
更好的Typescript支持
VUE3
是基于typescipt
编写的,可以享受到自动的类型定义提示
1.4 编译器重写
1.5 更接近原生
可以自定义渲染 API
1.6 更易使用
响应式 Api
暴露出来
轻松识别组件重新渲染原因
2. Vue3新增特性
Vue 3 中需要关注的一些新功能包括:
framents
Teleport
composition Api
createRenderer
2.1 framents
在 Vue3.x
中,组件现在支持有多个根节点
<!-- Layout.vue -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
2.2 Teleport
Teleport
是一种能够将我们的模板移动到 DOM
中 Vue app
之外的其他位置的技术,就有点像哆啦A梦的“任意门”
在vue2
中,像 modals
,toast
等这样的元素,如果我们嵌套在 Vue
的某个组件内部,那么处理嵌套组件的定位、z-index
和样式就会变得很困难
通过Teleport
,我们可以在组件的逻辑位置写模板代码,然后在 Vue
应用范围之外渲染它
<button @click="showToast" class="btn">打开 toast</button>
<!-- to 属性就是目标位置 -->
<teleport to="#teleport-target">
<div v-if="visible" class="toast-wrap">
<div class="toast-msg">我是一个 Toast 文案</div>
</div>
</teleport>
2.3 createRenderer
通过createRenderer
,我们能够构建自定义渲染器,我们能够将 vue
的开发模型扩展到其他平台
我们可以将其生成在canvas
画布上
关于createRenderer
,我们了解下基本使用,就不展开讲述了
import { createRenderer } from '@vue/runtime-core'
const { render, createApp } = createRenderer({
patchProp,
insert,
remove,
createElement,
// ...
})
export { render, createApp }
export * from '@vue/runtime-core'
2.4 composition Api
composition Api,也就是组合式api
,通过这种形式,我们能够更加容易维护我们的代码,将相同功能的变量进行一个集中式的管理
关于compositon api
的使用,这里以下图展开
简单使用:
export default {
setup() {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => console.log('component mounted!'))
return {
count,
double,
increment
}
}
}
3. 非兼容变更
3.1 Global API
- 全局
Vue API
已更改为使用应用程序实例 - 全局和内部
API
已经被重构为可tree-shakable
3.2 模板指令
- 组件上
v-model
用法已更改 <template v-for>
和 非v-for
节点上key
用法已更改- 在同一元素上使用的
v-if
和v-for
优先级已更改 v-bind="object"
现在排序敏感v-for
中的ref
不再注册ref
数组
3.3 组件
- 只能使用普通函数创建功能组件
functional
属性在单文件组件(SFC)
- 异步组件现在需要
defineAsyncComponent
方法来创建
3.4 渲染函数
- 渲染函数
API
改变 $scopedSlots
property 已删除,所有插槽都通过$slots
作为函数暴露- 自定义指令 API 已更改为与组件生命周期一致
- 一些转换
class
被重命名了:v-enter
->v-enter-from
v-leave
->v-leave-from
- 组件
watch
选项和实例方法$watch
不再支持点分隔字符串路径,请改用计算函数作为参数 - 在
Vue 2.x
中,应用根容器的outerHTML
将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。VUE3.x
现在使用应用程序容器的innerHTML
。
3.5 其他小改变
destroyed
生命周期选项被重命名为unmounted
beforeDestroy
生命周期选项被重命名为beforeUnmount
[prop default
工厂函数不再有权访问this
是上下文- 自定义指令 API 已更改为与组件生命周期一致
data
应始终声明为函数- 来自
mixin
的data
选项现在可简单地合并 attribute
强制策略已更改- 一些过渡
class
被重命名 - 组建 watch 选项和实例方法
$watch
不再支持以点分隔的字符串路径。请改用计算属性函数作为参数。 <template>
没有特殊指令的标记 (v-if/else-if/else
、v-for
或v-slot
) 现在被视为普通元素,并将生成原生的<template>
元素,而不是渲染其内部内容。- 在
Vue 2.x
中,应用根容器的outerHTML
将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。Vue 3.x
现在使用应用容器的innerHTML
,这意味着容器本身不再被视为模板的一部分。
3.6 移除 API
keyCode
支持作为v-on
的修饰符$on
,$off
和$once
实例方法- 过滤
filter
- 内联模板
attribute
$destroy
实例方法。用户不应再手动管理单个Vue
组件的生命周期。
watch 原理
watch
本质上是为每个监听属性 setter
创建了一个 watcher
,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deep
和 immediate
,对应原理如下
deep
:深度监听对象,为对象的每一个属性创建一个watcher
,从而确保对象的每一个属性更新时都会触发传入的回调函数。主要原因在于对象属于引用类型,单个属性的更新并不会触发对象setter
,因此引入deep
能够很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,避免性能浪费。immediate
:在初始化时直接调用回调函数,可以通过在created
阶段手动调用回调函数实现相同的效果
谈一谈对Vue组件化的理解
- 组件化开发能大幅提高开发效率、测试性、复用性等
- 常用的组件化技术:属性、自定义事件、插槽
- 降低更新频率,只重新渲染变化的组件
- 组件的特点:高内聚、低耦合、单向数据流
说说你对slot的理解?slot使用场景有哪些
一、slot是什么
在HTML中 slot
元素 ,作为 Web Components
技术套件的一部分,是Web组件内的一个占位符
该占位符可以在后期使用自己的标记语言填充
举个栗子
<template id="element-details-template">
<slot name="element-name">Slot template</slot>
</template>
<element-details>
<span slot="element-name">1</span>
</element-details>
<element-details>
<span slot="element-name">2</span>
</element-details>
template
不会展示到页面中,需要用先获取它的引用,然后添加到DOM
中,
customElements.define('element-details',
class extends HTMLElement {
constructor() {
super();
const template = document
.getElementById('element-details-template')
.content;
const shadowRoot = this.attachShadow({mode: 'open'})
.appendChild(template.cloneNode(true));
}
})
在Vue
中的概念也是如此
Slot
艺名插槽,花名“占坑”,我们可以理解为solt
在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot
位置),作为承载分发内容的出口
二、使用场景
通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理
如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情
通过slot
插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用
比如布局组件、表格列、下拉选、弹框显示内容等
你觉得vuex有什么缺点
分析
相较于redux
,vuex
已经相当简便好用了。但模块的使用比较繁琐,对ts
支持也不好。
体验
使用模块:用起来比较繁琐,使用模式也不统一,基本上得不到类型系统的任何支持
const store = createStore({
modules: {
a: moduleA
}
})
store.state.a // -> 要带上 moduleA 的key,内嵌模块的话会很长,不得不配合mapState使用
store.getters.c // -> moduleA里的getters,没有namespaced时又变成了全局的
store.getters['a/c'] // -> 有namespaced时要加path,使用模式又和state不一样
store.commit('d') // -> 没有namespaced时变成了全局的,能同时触发多个子模块中同名mutation
store.commit('a/d') // -> 有namespaced时要加path,配合mapMutations使用感觉也没简化
回答范例
vuex
利用响应式,使用起来已经相当方便快捷了。但是在使用过程中感觉模块化这一块做的过于复杂,用的时候容易出错,还要经常查看文档- 比如:访问
state
时要带上模块key
,内嵌模块的话会很长,不得不配合mapState
使用,加不加namespaced
区别也很大,getters
,mutations
,actions
这些默认是全局,加上之后必须用字符串类型的path来匹配,使用模式不统一,容易出错;对ts的支持也不友好,在使用模块时没有代码提示。 - 之前
Vue2
项目中用过vuex-module-decorators
的解决方案,虽然类型支持上有所改善,但又要学一套新东西,增加了学习成本。pinia
出现之后使用体验好了很多,Vue3 + pinia
会是更好的组合
原理
下面我们来看看vuex
中store.state.x.y
这种嵌套的路径是怎么搞出来的
首先是子模块安装过程:父模块状态
parentState
上面设置了子模块名称moduleName
,值为当前模块state
对象。放在上面的例子中相当于:store.state['x'] = moduleX.state
。此过程是递归的,那么store.state.x.y
安装时就是:store.state['x']['y'] = moduleY.state
//源码位置 https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115
if (!isRoot && !hot) {
// 获取父模块state
const parentState = getNestedState(rootState, path.slice(0, -1))
// 获取子模块名称
const moduleName = path[path.length - 1]
store._withCommit(() => {
// 把子模块state设置到父模块上
parentState[moduleName] = module.state
})
}
Vuex 为什么要分模块并且加命名空间
- 模块 : 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,
store
对象就有可能变得相当臃肿。为了解决以上问题,Vuex
允许我们将store
分割成模块(module
)。每个模块拥有自己的state
、mutation
、action
、getter
、甚至是嵌套子模块 - 命名空间 :默认情况下,模块内部的
action
、mutation
和getter
是注册在全局命名空间的——这样使得多个模块能够对同一mutation
或action
作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加namespaced: true
的方式使其成为带命名空间的模块。当模块被注册后,它的所有getter
、action
及mutation
都会自动根据模块注册的路径调整命名
构建的 vue-cli 工程都到了哪些技术,它们的作用分别是什么
vue.js
:vue-cli
工程的核心,主要特点是 双向数据绑定 和 组件系统。vue-router
:vue
官方推荐使用的路由框架。vuex
:专为Vue.js
应用项目开发的状态管理器,主要用于维护vue
组件间共用的一些 变量 和 方法。axios
( 或者fetch
、ajax
):用于发起GET
、或POST
等http
请求,基于Promise
设计。vuex
等:一个专为vue
设计的移动端UI组件库。- 创建一个
emit.js
文件,用于vue
事件机制的管理。 webpack
:模块加载和vue-cli
工程打包器。
Vue3.2 setup 语法糖汇总
提示:vue3.2
版本开始才能使用语法糖!
在 Vue3.0
中变量必须 return
出来, template
中才能使用;而在 Vue3.2
中只需要在 script
标签上加上 setup
属性,无需 return
, template
便可直接使用,非常的香啊!
1. 如何使用setup语法糖
只需在 script
标签上写上 setup
<template>
</template>
<script setup>
</script>
<style scoped lang="less">
</style>
2. data数据的使用
由于 setup
不需写 return
,所以直接声明数据即可
<script setup>
import {
ref,
reactive,
toRefs,
} from 'vue'
const data = reactive({
patternVisible: false,
debugVisible: false,
aboutExeVisible: false,
})
const content = ref('content')
//使用toRefs解构
const { patternVisible, debugVisible, aboutExeVisible } = toRefs(data)
</script>
3. method方法的使用
<template >
<button @click="onClickHelp">帮助</button>
</template>
<script setup>
import {reactive} from 'vue'
const data = reactive({
aboutExeVisible: false,
})
// 点击帮助
const onClickHelp = () => {
console.log(`帮助`)
data.aboutExeVisible = true
}
</script>
4. watchEffect的使用
<script setup>
import {
ref,
watchEffect,
} from 'vue'
let sum = ref(0)
watchEffect(()=>{
const x1 = sum.value
console.log('watchEffect所指定的回调执行了')
})
</script>
5. watch的使用
<script setup>
import {
reactive,
watch,
} from 'vue'
//数据
let sum = ref(0)
let msg = ref('hello')
let person = reactive({
name:'张三',
age:18,
job:{
j1:{
salary:20
}
}
})
// 两种监听格式
watch([sum,msg],(newValue,oldValue)=>{
console.log('sum或msg变了',newValue,oldValue)
},
{immediate:true}
)
watch(()=>person.job,(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{deep:true})
</script>
6. computed计算属性的使用
computed
计算属性有两种写法(简写和考虑读写的完整写法)
<script setup>
import {
reactive,
computed,
} from 'vue'
// 数据
let person = reactive({
firstName:'poetry',
lastName:'x'
})
// 计算属性简写
person.fullName = computed(()=>{
return person.firstName + '-' + person.lastName
})
// 完整写法
person.fullName = computed({
get(){
return person.firstName + '-' + person.lastName
},
set(value){
const nameArr = value.split('-')
person.firstName = nameArr[0]
person.lastName = nameArr[1]
}
})
</script>
7. props父子传值的使用
父组件代码如下(示例):
<template>
<child :name='name'/>
</template>
<script setup>
import {ref} from 'vue'
// 引入子组件
import child from './child.vue'
let name= ref('poetry')
</script>
子组件代码如下(示例):
<template>
<span>{{props.name}}</span>
</template>
<script setup>
import { defineProps } from 'vue'
// 声明props
const props = defineProps({
name: {
type: String,
default: 'poetries'
}
})
// 或者
//const props = defineProps(['name'])
</script>
8. emit子父传值的使用
父组件代码如下(示例):
<template>
<AdoutExe @aboutExeVisible="aboutExeHandleCancel" />
</template>
<script setup>
import { reactive } from 'vue'
// 导入子组件
import AdoutExe from '../components/AdoutExeCom'
const data = reactive({
aboutExeVisible: false,
})
// content组件ref
// 关于系统隐藏
const aboutExeHandleCancel = () => {
data.aboutExeVisible = false
}
</script>
子组件代码如下(示例):
<template>
<a-button @click="isOk">
确定
</a-button>
</template>
<script setup>
import { defineEmits } from 'vue';
// emit
const emit = defineEmits(['aboutExeVisible'])
/**
* 方法
*/
// 点击确定按钮
const isOk = () => {
emit('aboutExeVisible');
}
</script>
9. 获取子组件ref变量和defineExpose暴露
即vue2
中的获取子组件的ref
,直接在父组件中控制子组件方法和变量的方法
父组件代码如下(示例):
<template>
<button @click="onClickSetUp">点击</button>
<Content ref="content" />
</template>
<script setup>
import {ref} from 'vue'
// content组件ref
const content = ref('content')
// 点击设置
const onClickSetUp = ({ key }) => {
content.value.modelVisible = true
}
</script>
<style scoped lang="less">
</style>
子组件代码如下(示例):
<template>
<p>{{data }}</p>
</template>
<script setup>
import {
reactive,
toRefs
} from 'vue'
/**
* 数据部分
* */
const data = reactive({
modelVisible: false,
historyVisible: false,
reportVisible: false,
})
defineExpose({
...toRefs(data),
})
</script>
10. 路由useRoute和useRouter的使用
<script setup>
import { useRoute, useRouter } from 'vue-router'
// 声明
const route = useRoute()
const router = useRouter()
// 获取query
console.log(route.query)
// 获取params
console.log(route.params)
// 路由跳转
router.push({
path: `/index`
})
</script>
11. store仓库的使用
<script setup>
import { useStore } from 'vuex'
import { num } from '../store/index'
const store = useStore(num)
// 获取Vuex的state
console.log(store.state.number)
// 获取Vuex的getters
console.log(store.state.getNumber)
// 提交mutations
store.commit('fnName')
// 分发actions的方法
store.dispatch('fnName')
</script>
12. await的支持
setup
语法糖中可直接使用await
,不需要写async
,setup
会自动变成async setup
<script setup>
import api from '../api/Api'
const data = await Api.getData()
console.log(data)
</script>
13. provide 和 inject 祖孙传值
父组件代码如下(示例):
<template>
<AdoutExe />
</template>
<script setup>
import { ref,provide } from 'vue'
import AdoutExe from '@/components/AdoutExeCom'
let name = ref('py')
// 使用provide
provide('provideState', {
name,
changeName: () => {
name.value = 'poetries'
}
})
</script>
子组件代码如下(示例):
<script setup>
import { inject } from 'vue'
const provideState = inject('provideState')
provideState.changeName()
</script>
Vue-router跳转和location.href有什么区别
- 使用
location.href= /url
来跳转,简单方便,但是刷新了页面; - 使用
history.pushState( /url )
,无刷新页面,静态跳转; - 引进 router ,然后使用
router.push( /url )
来跳转,使用了diff
算法,实现了按需加载,减少了 dom 的消耗。其实使用 router 跳转和使用history.pushState()
没什么差别的,因为vue-router就是用了history.pushState()
,尤其是在history模式下。
delete和Vue.delete删除数组的区别?
delete
只是被删除的元素变成了empty/undefined
其他的元素的键值还是不变。Vue.delete
直接删除了数组 改变了数组的键值。
var a=[1,2,3,4]
var b=[1,2,3,4]
delete a[0]
console.log(a) //[empty,2,3,4]
this.$delete(b,0)
console.log(b) //[2,3,4]
vue-loader是什么?它有什么作用?
回答范例
vue-loader
是用于处理单文件组件(SFC
,Single-File Component
)的webpack loader
- 因为有了
vue-loader
,我们就可以在项目中编写SFC
格式的Vue
组件,我们可以把代码分割为<template>
、<script>
和<style>
,代码会异常清晰。结合其他loader
我们还可以用Pug
编写<template>
,用SASS
编写<style>
,用TS
编写<script>
。我们的<style>
还可以单独作用当前组件 webpack
打包时,会以loader
的方式调用vue-loader
vue-loader
被执行时,它会对SFC
中的每个语言块用单独的loader
链处理。最后将这些单独的块装配成最终的组件模块
原理
vue-loader
会调用@vue/compiler-sfc
模块解析SFC
源码为一个描述符(Descriptor
),然后为每个语言块生成import
代码,返回的代码类似下面
// source.vue被vue-loader处理之后返回的代码
// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'
script.render = render
export default script
我们想要script
块中的内容被作为js
处理(当然如果是<script lang="ts">
被作为ts
理),这样我们想要webpack
把配置中跟.js
匹配的规则都应用到形如source.vue?vue&type=script
的这个请求上。例如我们对所有*.js
配置了babel-loader
,这个规则将被克隆并应用到所在Vue SFC
import script from 'source.vue?vue&type=script
将被展开为:
import script from 'babel-loader!vue-loader!source.vue?vue&type=script'
类似的,如果我们对.sass
文件配置了style-loader + css-loader + sass-loader
,对下面的代码
<style scoped lang="scss">
vue-loader
将会返回给我们下面结果:
import 'source.vue?vue&type=style&index=1&scoped&lang=scss'
然后webpack
会展开如下:
import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
- 当处理展开请求时,
vue-loader
将被再次调用。这次,loader
将会关注那些有查询串的请求,且仅针对特定块,它会选中特定块内部的内容并传递给后面匹配的loader
- 对于
<script>
块,处理到这就可以了,但是<template>
和<style>
还有一些额外任务要做,比如- 需要用
Vue
模板编译器编译template
,从而得到render
函数 - 需要对
<style scoped>
中的CSS
做后处理(post-process
),该操作在css-loader
之后但在style-loader
之前
- 需要用
实现上这些附加的loader
需要被注入到已经展开的loader
链上,最终的请求会像下面这样:
// <template lang="pug">
import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'
// <style scoped lang="scss">
import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
Vue中的过滤器了解吗?过滤器的应用场景有哪些?
过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,我们也可以理解其为一个纯函数
Vue 允许你自定义过滤器,可被用于一些常见的文本格式化
ps: Vue3
中已废弃filter
如何用
vue中的过滤器可以用在两个地方:双花括号插值和 v-bind
表达式,过滤器应该被添加在 JavaScript表达式的尾部,由“管道”符号指示:
<!-- 在双花括号中 -->
{ message | capitalize }
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
定义filter
在组件的选项中定义本地的过滤器
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
定义全局过滤器:
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
new Vue({
// ...
})
注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器
过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。在上述例子中,capitalize
过滤器函数将会收到 message
的值作为第一个参数
过滤器可以串联:
{ message | filterA | filterB }
在这个例子中,filterA
被定义为接收单个参数的过滤器函数,表达式 message
的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 filterB
,将 filterA
的结果传递到 filterB
中。
过滤器是 JavaScript
函数,因此可以接收参数:
{{ message | filterA('arg1', arg2) }}
这里,filterA
被定义为接收三个参数的过滤器函数。
其中 message
的值作为第一个参数,普通字符串 'arg1'
作为第二个参数,表达式 arg2
的值作为第三个参数
举个例子:
<div id="app">
<p>{{ msg | msgFormat('疯狂','--')}}</p>
</div>
<script>
// 定义一个 Vue 全局的过滤器,名字叫做 msgFormat
Vue.filter('msgFormat', function(msg, arg, arg2) {
// 字符串的 replace 方法,第一个参数,除了可写一个 字符串之外,还可以定义一个正则
return msg.replace(/单纯/g, arg+arg2)
})
</script>
小结:
- 部过滤器优先于全局过滤器被调用
- 一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。其执行顺序从左往右
应用场景
平时开发中,需要用到过滤器的地方有很多,比如单位转换
、数字打点
、文本格式化
、时间格式化
之类的等
比如我们要实现将30000 => 30,000
,这时候我们就需要使用过滤器
Vue.filter('toThousandFilter', function (value) {
if (!value) return ''
value = value.toString()
return .replace(str.indexOf('.') > -1 ? /(\d)(?=(\d{3})+\.)/g : /(\d)(?=(?:\d{3})+$)/g, '$1,')
})
原理分析
使用过滤器
{{ message | capitalize }}
在模板编译阶段过滤器表达式将会被编译为过滤器函数,主要是用过parseFilters
,我们放到最后讲
_s(_f('filterFormat')(message))
首先分析一下_f
:
_f
函数全名是:resolveFilter
,这个函数的作用是从this.$options.filters
中找出注册的过滤器并返回
// 变为
this.$options.filters['filterFormat'](message) // message为参数
关于resolveFilter
import { indentity,resolveAsset } from 'core/util/index'
export function resolveFilter(id){
return resolveAsset(this.$options,'filters',id,true) || identity
}
内部直接调用resolveAsset
,将option
对象,类型,过滤器id
,以及一个触发警告的标志作为参数传递,如果找到,则返回过滤器;
resolveAsset
的代码如下:
export function resolveAsset(options,type,id,warnMissing){ // 因为我们找的是过滤器,所以在 resolveFilter函数中调用时 type 的值直接给的 'filters',实际这个函数还可以拿到其他很多东西
if(typeof id !== 'string'){ // 判断传递的过滤器id 是不是字符串,不是则直接返回
return
}
const assets = options[type] // 将我们注册的所有过滤器保存在变量中
// 接下来的逻辑便是判断id是否在assets中存在,即进行匹配
if(hasOwn(assets,id)) return assets[id] // 如找到,直接返回过滤器
// 没有找到,代码继续执行
const camelizedId = camelize(id) // 万一你是驼峰的呢
if(hasOwn(assets,camelizedId)) return assets[camelizedId]
// 没找到,继续执行
const PascalCaseId = capitalize(camelizedId) // 万一你是首字母大写的驼峰呢
if(hasOwn(assets,PascalCaseId)) return assets[PascalCaseId]
// 如果还是没找到,则检查原型链(即访问属性)
const result = assets[id] || assets[camelizedId] || assets[PascalCaseId]
// 如果依然没找到,则在非生产环境的控制台打印警告
if(process.env.NODE_ENV !== 'production' && warnMissing && !result){
warn('Failed to resolve ' + type.slice(0,-1) + ': ' + id, options)
}
// 无论是否找到,都返回查找结果
return result
}
下面再来分析一下_s
:
_s
函数的全称是 toString
,过滤器处理后的结果会当作参数传递给 toString
函数,最终 toString
函数执行后的结果会保存到Vnode
中的text属性中,渲染到视图中
function toString(value){
return value == null
? ''
: typeof value === 'object'
? JSON.stringify(value,null,2)// JSON.stringify()第三个参数可用来控制字符串里面的间距
: String(value)
}
最后,在分析下parseFilters
,在模板编译阶段使用该函数阶段将模板过滤器解析为过滤器函数调用表达式
function parseFilters (filter) {
let filters = filter.split('|')
let expression = filters.shift().trim() // shift()删除数组第一个元素并将其返回,该方法会更改原数组
let i
if (filters) {
for(i = 0;i < filters.length;i++){
experssion = warpFilter(expression,filters[i].trim()) // 这里传进去的expression实际上是管道符号前面的字符串,即过滤器的第一个参数
}
}
return expression
}
// warpFilter函数实现
function warpFilter(exp,filter){
// 首先判断过滤器是否有其他参数
const i = filter.indexof('(')
if(i<0){ // 不含其他参数,直接进行过滤器表达式字符串的拼接
return `_f("${filter}")(${exp})`
}else{
const name = filter.slice(0,i) // 过滤器名称
const args = filter.slice(i+1) // 参数,但还多了 ‘)’
return `_f('${name}')(${exp},${args}` // 注意这一步少给了一个 ')'
}
}
小结:
- 在编译阶段通过
parseFilters
将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的结果是后一个过滤器函数的参数) - 编译后通过调用
resolveFilter
函数找到对应过滤器并返回结果 - 执行结果作为参数传递给
toString
函数,而toString
执行后,其结果会保存在Vnode
的text
属性中,渲染到视图
Vue3速度快的原因
Vue3.0 性能提升体现在哪些方面
- 代码层面性能优化主要体现在全新响应式
API
,基于Proxy
实现,初始化时间和内存占用均大幅改进; - 编译层面做了更多编译优化处理,比如
静态标记pachFlag
(diff
算法增加了一个静态标记,只对比有标记的dom
元素)、事件增加缓存
、静态提升
(对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用)等,可以有效跳过大量diff
过程; - 打包时更好的支持
tree-shaking
,因此整体体积更小,加载更快 ssr
渲染以字符串方式渲染
一、编译阶段
试想一下,一个组件结构如下图
<template>
<div id="content">
<p class="text">静态文本</p>
<p class="text">静态文本</p>
<p class="text">{ message }</p>
<p class="text">静态文本</p>
...
<p class="text">静态文本</p>
</div>
</template>
可以看到,组件内部只有一个动态节点,剩余一堆都是静态节点,所以这里很多 diff
和遍历其实都是不需要的,造成性能浪费
因此,Vue3在编译阶段,做了进一步优化。主要有如下:
diff
算法优化- 静态提升
- 事件监听缓存
SSR
优化
1. diff 算法优化
Vue 2x
中的虚拟dom
是进行全量的对比。Vue 3x
中新增了静态标记(PatchFlag
):在与上次虚拟结点进行对比的时候,值对比 带有patch flag
的节点,并且可以通过flag
的信息得知当前节点要对比的具体内容化
Vue2.x的diff算法
vue2.x
的diff
算法叫做全量比较
,顾名思义,就是当数据改变的时候,会从头到尾的进行vDom
对比,即使有些内容是永恒固定不变的
Vue3.0的diff算法
vue3.0
的diff
算法有个叫静态标记(PatchFlag
)的小玩意,啥是静态标记呢?简单点说,就是如果你的内容会变,我会给你一个flag
,下次数据更新的时候我直接来对比你,我就不对比那些没有标记的了
已经标记静态节点的p
标签在diff
过程中则不会比较,把性能进一步提高
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("p", null, "'HelloWorld'"),
_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
//上面这个1就是静态标记
]))
}
关于静态类型枚举如下
TEXT = 1 // 动态文本节点
CLASS=1<<1,1 // 2//动态class
STYLE=1<<2,// 4 //动态style
PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
FULLPR0PS=1<<4,// 16 //具有动态key属性,当key改变时,需要进行完整的diff比较。
HYDRATE_ EVENTS = 1 << 5,// 32 //带有监听事件的节点
STABLE FRAGMENT = 1 << 6, // 64 //一个不会改变子节点顺序的fragment
KEYED_ FRAGMENT = 1 << 7, // 128 //带有key属性的fragment 或部分子字节有key
UNKEYED FRAGMENT = 1<< 8, // 256 //子节点没有key 的fragment
NEED PATCH = 1 << 9, // 512 //一个节点只会进行非props比较
DYNAMIC_SLOTS = 1 << 10 // 1024 // 动态slot
HOISTED = -1 // 静态节点
// 指示在diff算法中退出优化模式
BALL = -2
2. hoistStatic 静态提升
Vue 2x
: 无论元素是否参与更新,每次都会重新创建。Vue 3x
: 对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用。这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用
<p>HelloWorld</p>
<p>HelloWorld</p>
<p>{ message }</p>
开启静态提升前
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("p", null, "'HelloWorld'"),
_createVNode("p", null, "'HelloWorld'"),
_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
开启静态提升后编译结果
const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_hoisted_1,
_hoisted_2,
_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
可以看到开启了静态提升后,直接将那两个内容为helloworld
的p
标签声明在外面了,直接就拿来用了。同时 _hoisted_1
和_hoisted_2
被打上了 PatchFlag
,静态标记值为 -1
,特殊标志是负整数表示永远不会用于 Diff
3. cacheHandlers 事件监听缓存
- 默认情况下 绑定事件会被视为动态绑定 ,所以每次都会去追踪它的变化
- 但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可
<div>
<button @click = 'onClick'>点我</button>
</div>
开启事件侦听器缓存之前:
export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])
// PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
]))
})
这里有一个8
,表示着这个节点有了静态标记,有静态标记就会进行diff
算法对比差异,所以会浪费时间
开启事件侦听器缓存之后:
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", {
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
}, "点我")
]))
}
上述发现开启了缓存后,没有了静态标记。也就是说下次diff
算法的时候直接使用
4. SSR优化
当静态内容大到一定量级时候,会用createStaticVNode
方法在客户端去生成一个static node
,这些静态node
,会被直接innerHtml
,就不需要创建对象,然后根据对象渲染
<div>
<div>
<span>你好</span>
</div>
... // 很多个静态属性
<div>
<span>{{ message }}</span>
</div>
</div>
编译后
import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"
export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
const _cssVars = { style: { color: _ctx.color }}
_push(`<div${
_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
}><div><span>你好</span>...<div><span>你好</span><div><span>${
_ssrInterpolate(_ctx.message)
}</span></div></div>`)
}
二、源码体积
相比Vue2
,Vue3
整体体积变小了,除了移出一些不常用的API
,再重要的是Tree shanking
任何一个函数,如ref
、reactive
、computed
等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小
import { computed, defineComponent, ref } from 'vue';
export default defineComponent({
setup(props, context) {
const age = ref(18)
let state = reactive({
name: 'test'
})
const readOnlyAge = computed(() => age.value++) // 19
return {
age,
state,
readOnlyAge
}
}
});
三、响应式系统
vue2
中采用 defineProperty
来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加getter
和setter
,实现响应式
vue3
采用proxy
重写了响应式系统,因为proxy
可以对整个对象进行监听,所以不需要深度遍历
- 可以监听动态属性的添加
- 可以监听到数组的索引和数组
length
属性 - 可以监听删除属性
Vue 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)
相关代码如下
export function callHook(vm, hook) {
// 依次执行生命周期对应的方法
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
handlers[i].call(vm); //生命周期里面的this指向当前实例
}
}
}
// 调用的时候
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = mergeOptions(vm.constructor.options, options);
callHook(vm, "beforeCreate"); //初始化数据之前
// 初始化状态
initState(vm);
callHook(vm, "created"); //初始化数据之后
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
Vue.set的实现原理
- 给对应和数组本身都增加了
dep
属性 - 当给对象新增不存在的属性则触发对象依赖的
watcher
去更新 - 当修改数组索引时,我们调用数组本身的
splice
去更新数组(数组的响应式原理就是重新了splice
等方法,调用splice
就会触发视图更新)
基本使用
以下方法调用会改变原始数组:
push()
,pop()
,shift()
,unshift()
,splice()
,sort()
,reverse()
,Vue.set( target, key, value )
- 调用方法:
Vue.set(target, key, value )
target
:要更改的数据源(可以是对象或者数组)key
:要更改的具体数据value
:重新赋的值
<div id="app">{{user.name}} {{user.age}}</div>
<div id="app"></div>
<script>
// 1. 依赖收集的特点:给每个属性都增加一个dep属性,dep属性会进行收集,收集的是watcher
// 2. vue会给每个对象也增加一个dep属性
const vm = new Vue({
el: '#app',
data: { // vm._data
user: {name:'poetry'}
}
});
// 对象的话:调用defineReactive在user对象上定义一个age属性,增加到响应式数据中,触发对象本身的watcher,ob.dep.notify()更新
// 如果是数组 通过调用 splice方法,触发视图更新
vm.$set(vm.user, 'age', 20); // 不能给根属性添加,因为给根添加属性 性能消耗太大,需要做很多处理
// 修改肯定是同步的 -> 更新都是一步的 queuewatcher
</script>
相关源码
// src/core/observer/index.js 44
export class Observer { // new Observer(value)
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep() // 给所有对象类型增加dep属性
}
}
// src/core/observer/index.js 201
export function set (target: Array<any> | Object, key: any, val: any): any {
// 1.是开发环境 target 没定义或者是基础类型则报错
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// 3.如果是对象本身的属性,则直接添加即可
if (key in target && !(key in Object.prototype)) {
target[key] = val // 直接修改属性值
return val
}
// 4.如果是Vue实例 或 根数据data时 报错,(更新_data 无意义)
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 5.如果不是响应式的也不需要将其定义成响应式属性
if (!ob) {
target[key] = val
return val
}
// 6.将属性定义成响应式的
defineReactive(ob.value, key, val)
// 通知视图更新
ob.dep.notify()
return val
}
我们阅读以上源码可知,vm.$set 的实现原理是:
- 如果目标是数组 ,直接使用数组的
splice
方法触发相应式; - 如果目标是对象 ,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用
defineReactive
方法进行响应式处理(defineReactive
方法就是Vue
在初始化对象时,给对象属性采用Object.defineProperty
动态添加getter
和setter
的功能所调用的方法)
nextTick 使用场景和原理
nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法
相关代码如下
let callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false; //把标志还原为false
// 依次执行回调
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
}
let timerFunc; //定义异步方法 采用优雅降级
if (typeof Promise !== "undefined") {
// 如果支持promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== "undefined") {
// MutationObserver 主要是监听dom变化 也是一个异步方法
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else if (typeof setImmediate !== "undefined") {
// 如果前面都不支持 判断setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 最后降级采用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
export function nextTick(cb) {
// 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组
callbacks.push(cb);
if (!pending) {
// 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false
pending = true;
timerFunc();
}
}
computed 的实现原理
computed 本质是一个惰性求值的观察者。
computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。
其内部通过 this.dirty 属性标记计算属性是否需要重新求值。
当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,
computed watcher 通过 this.dep.subs.length 判断有没有订阅者,
有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)
没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)
Vue为什么没有类似于React中shouldComponentUpdate的生命周期
- 考点:
Vue
的变化侦测原理 - 前置知识: 依赖收集、虚拟
DOM
、响应式系统
根本原因是
Vue
与React
的变化侦测方式有所不同
- 当React知道发生变化后,会使用
Virtual Dom Diff
进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要shouldComponentUpdate
进行手动操作来减少diff
,从而提高程序整体的性能 Vue
在一开始就知道那个组件发生了变化,不需要手动控制diff
,而组件内部采用的diff
方式实际上是可以引入类似于shouldComponentUpdate
相关生命周期的,但是通常合理大小的组件不会有过量的diff,手动优化的价值有限,因此目前Vue
并没有考虑引入shouldComponentUpdate
这种手动优化的生命周期
Vue模版编译原理知道吗,能简单说一下吗?
简单说,Vue的编译过程就是将template
转化为render
函数的过程。会经历以下阶段:
- 生成AST树
- 优化
- codegen
首先解析模版,生成AST语法树
(一种用JavaScript对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对
,对运行时的模板起到很大的优化作用。
编译的最后一步是将优化后的AST树转换为可执行的代码
。
谈谈你对SPA单页面的理解
SPA
( single-page application )仅在Web
页面初始化时加载相应的HTML
、JavaScript
和CSS
。一旦页面加载完成,SPA
不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现HTML
内容的变换,UI
与用户的交互,避免页面的重新加载
优点:
- 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
- 基于上面一点,
SPA
相对对服务器压力小; - 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理
缺点:
- 初次加载耗时多:为实现单页
Web
应用功能及显示效果,需要在加载页面的时候将JavaScript
、CSS
统一加载,部分页面按需加载; - 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
SEO
难度较大:由于所有的内容都在一个页面中动态替换显示,所以在SEO
上其有着天然的弱势
单页应用与多页应用的区别
单页面应用(SPA) | 多页面应用(MPA) | |
---|---|---|
组成 | 一个主页面和多个页面片段 | 多个主页面 |
刷新方式 | 局部刷新 | 整页刷新 |
url 模式 |
哈希模式 | 历史模式 |
SEO 搜索引擎优化 |
难实现,可使用SSR方式改善 | 容易实现 |
数据传递 | 容易 | 通过url 、cookie 、localStorage 等传递 |
页面切换 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差 |
维护成本 | 相对容易 | 相对复杂 |
实现一个SPA
- 监听地址栏中
hash
变化驱动界面变化 - 用
pushsate
记录浏览器的历史,驱动界面发送变化
- hash 模式 :核心通过监听
url
中的hash
来进行路由跳转
// 定义 Router
class Router {
constructor () {
this.routes = {}; // 存放路由path及callback
this.currentUrl = '';
// 监听路由change调用相对应的路由回调
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
route(path, callback){
this.routes[path] = callback;
}
push(path) {
this.routes[path] && this.routes[path]()
}
}
// 使用 router
window.miniRouter = new Router();
miniRouter.route('/', () => console.log('page1'))
miniRouter.route('/page2', () => console.log('page2'))
miniRouter.push('/') // page1
miniRouter.push('/page2') // page2
- history模式 :
history
模式核心借用HTML5 history api
,api
提供了丰富的router
相关属性先了解一个几个相关的api
history.pushState
浏览器历史纪录添加记录history.replaceState
修改浏览器历史纪录中当前纪录history.popState
当history
发生变化时触发
// 定义 Router
class Router {
constructor () {
this.routes = {};
this.listerPopState()
}
init(path) {
history.replaceState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
route(path, callback){
this.routes[path] = callback;
}
push(path) {
history.pushState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
listerPopState () {
window.addEventListener('popstate' , e => {
const path = e.state && e.state.path;
this.routers[path] && this.routers[path]()
})
}
}
// 使用 Router
window.miniRouter = new Router();
miniRouter.route('/', ()=> console.log('page1'))
miniRouter.route('/page2', ()=> console.log('page2'))
// 跳转
miniRouter.push('/page2') // page2
题外话:如何给SPA做SEO
- SSR服务端渲染
将组件或页面通过服务器生成html
,再返回给浏览器,如nuxt.js
- 静态化
目前主流的静态化主要有两种:
-
一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中
-
另外一种是通过WEB服务器的
URL Rewrite
的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现URL静态化的效果
- 使用
Phantomjs
针对爬虫处理
原理是通过Nginx
配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server
,再通过PhantomJS
来解析完整的HTML
,返回给爬虫。下面是大致流程图
Vue中封装的数组方法有哪些,其如何实现页面更新
在Vue中,对响应式处理利用的是Object.defineProperty对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行hack,让Vue能监听到其中的变化。 那Vue是如何实现让这些数组方法实现元素的实时更新的呢,下面是Vue中对这些方法的封装:
// 缓存数组原型
const arrayProto = Array.prototype;
// 实现 arrayMethods.__proto__ === Array.prototype
export const arrayMethods = Object.create(arrayProto);
// 需要进行功能拓展的方法
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse"
];
/** * Intercept mutating methods and emit events */
methodsToPatch.forEach(function(method) {
// 缓存原生数组方法
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
// 执行并缓存原生数组功能
const result = original.apply(this, args);
// 响应式处理
const ob = this.__ob__;
let inserted;
switch (method) {
// push、unshift会新增索引,所以要手动observer
case "push":
case "unshift":
inserted = args;
break;
// splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。
case "splice":
inserted = args.slice(2);
break;
}
//
if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听
// notify change
ob.dep.notify();// 通知依赖更新
// 返回原生数组方法的执行结果
return result;
});
});
简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它的Observer对象,如果有新的值,就调用observeArray继续对新的值观察变化(也就是通过target__proto__ == arrayMethods
来改变了数组实例的型),然后手动调用notify,通知渲染watcher,执行update。
子组件可以直接改变父组件的数据么,说明原因
这是一个实践知识点,组件化开发过程中有个单项数据流原则
,不在子组件中修改父组件是个常识问题
思路
- 讲讲单项数据流原则,表明为何不能这么做
- 举几个常见场景的例子说说解决方案
- 结合实践讲讲如果需要修改父组件状态应该如何做
回答范例
- 所有的
prop
都使得其父子之间形成了一个单向下行绑定:父级prop
的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。另外,每次父级组件发生变更时,子组件中所有的prop
都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变prop
。如果你这样做了,Vue
会在浏览器控制台中发出警告
const props = defineProps(['foo'])
// ❌ 下面行为会被警告, props是只读的!
props.foo = 'bar'
- 实际开发过程中有两个场景会想要修改一个属性:
这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data
,并将这个 prop
用作其初始值:
const props = defineProps(['initialCounter'])
const counter = ref(props.initialCounter)
这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop
的值来定义一个计算属性:
const props = defineProps(['size'])
// prop变化,计算属性自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())
- 实践中如果确实想要改变父组件属性应该
emit
一个事件让父组件去做这个变更。注意虽然我们不能直接修改一个传入的对象或者数组类型的prop
,但是我们还是能够直接改内嵌的对象或属性
delete和Vue.delete删除数组的区别
delete
只是被删除的元素变成了empty/undefined
其他的元素的键值还是不变。Vue.delete
直接删除了数组 改变了数组的键值。
Vue的diff算法详细分析
1. 是什么
diff
算法是一种通过同层的树节点进行比较的高效算法
其有两个特点:
- 比较只会在同层级进行, 不会跨层级比较
- 在diff比较的过程中,循环从两边向中间比较
diff
算法在很多场景下都有应用,在 vue
中,作用于虚拟 dom
渲染成真实 dom
的新旧 VNode
节点比较
2. 比较方式
diff
整体策略为:深度优先,同层比较
- 比较只会在同层级进行, 不会跨层级比较
- 比较的过程中,循环从两边向中间收拢
下面举个vue
通过diff
算法更新的例子:
新旧VNode
节点如下图所示:
第一次循环后,发现旧节点D与新节点D相同,直接复用旧节点D作为diff
后的第一个真实节点,同时旧节点endIndex
移动到C,新节点的 startIndex
移动到了 C
第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,diff
后创建了 C 的真实节点插入到第一次创建的 D 节点后面。同时旧节点的 endIndex
移动到了 B,新节点的 startIndex
移动到了 E
第三次循环中,发现E没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex
移动到了 A。旧节点的 startIndex
和 endIndex
都保持不动
第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 diff
后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex
移动到了 B,新节点的startIndex
移动到了 B
第五次循环中,情形同第四次循环一样,因此 diff
后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex
移动到了 C,新节点的 startIndex 移动到了 F
新节点的 startIndex
已经大于 endIndex
了,需要创建 newStartIdx
和 newEndIdx
之间的所有节点,也就是节点F,直接创建 F 节点对应的真实节点放到 B 节点后面
3. 原理分析
当数据发生改变时,set
方法会调用Dep.notify
通知所有订阅者Watcher
,订阅者就会调用patch
给真实的DOM
打补丁,更新相应的视图
源码位置:src/core/vdom/patch.js
function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) { // 没有新节点,直接执行destory钩子函数
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue) // 没有旧节点,直接用新节点生成dom元素
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 判断旧节点和新节点自身一样,一致执行patchVnode
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 否则直接销毁及旧节点,根据新节点生成dom元素
if (isRealElement) {
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
}
}
oldVnode = emptyNodeAt(oldVnode)
}
return vnode.elm
}
}
}
patch
函数前两个参数位为oldVnode
和 Vnode
,分别代表新的节点和之前的旧节点,主要做了四个判断:
- 没有新节点,直接触发旧节点的
destory
钩子 - 没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用
createElm
- 旧节点和新节点自身一样,通过
sameVnode
判断节点是否一样,一样时,直接调用patchVnode
去处理这两个节点 - 旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点
下面主要讲的是patchVnode
部分
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 如果新旧节点一致,什么都不做
if (oldVnode === vnode) {
return
}
// 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
const elm = vnode.elm = oldVnode.elm
// 异步占位符
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 如果新旧都是静态节点,并且具有相同的key
// 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上
// 也不用再有其他操作
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 如果vnode不是文本节点或者注释节点
if (isUndef(vnode.text)) {
// 并且都有子节点
if (isDef(oldCh) && isDef(ch)) {
// 并且子节点不完全一致,则调用updateChildren
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
// 如果只有新的vnode有子节点
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// elm已经引用了老的dom节点,在老的dom节点上添加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// 如果老节点是文本节点
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
// 如果新vnode和老vnode是文本节点或注释节点
// 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
patchVnode
主要做了几个判断:
- 新节点是否是文本节点,如果是,则直接更新
dom
的文本内容为新节点的文本内容 - 新节点和旧节点如果都有子节点,则处理比较更新子节点
- 只有新节点有子节点,旧节点没有,那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新
DOM
,并且添加进父节点 - 只有旧节点有子节点而新节点没有,说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把
DOM
删除
子节点不完全一致,则调用updateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // 旧头索引
let newStartIdx = 0 // 新头索引
let oldEndIdx = oldCh.length - 1 // 旧尾索引
let newEndIdx = newCh.length - 1 // 新尾索引
let oldStartVnode = oldCh[0] // oldVnode的第一个child
let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
let newStartVnode = newCh[0] // newVnode的第一个child
let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
// 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果oldVnode的第一个child不存在
if (isUndef(oldStartVnode)) {
// oldStart索引右移
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
// 如果oldVnode的最后一个child不存在
} else if (isUndef(oldEndVnode)) {
// oldEnd索引左移
oldEndVnode = oldCh[--oldEndIdx]
// oldStartVnode和newStartVnode是同一个节点
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// patch oldStartVnode和newStartVnode, 索引左移,继续循环
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// oldEndVnode和newEndVnode是同一个节点
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// patch oldEndVnode和newEndVnode,索引右移,继续循环
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// oldStartVnode和newEndVnode是同一个节点
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// patch oldStartVnode和newEndVnode
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// oldStart索引右移,newEnd索引左移
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 如果oldEndVnode和newStartVnode是同一个节点
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// patch oldEndVnode和newStartVnode
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// oldEnd索引左移,newStart索引右移
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 如果都不匹配
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果未找到,说明newStartVnode是一个新的节点
if (isUndef(idxInOld)) { // New element
// 创建一个新Vnode
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
// 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
} else {
vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
// 比较两个具有相同的key的新节点是否是同一个节点
//不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
if (sameVnode(vnodeToMove, newStartVnode)) {
// patch vnodeToMove和newStartVnode
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
// 清除
oldCh[idxInOld] = undefined
// 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
// 移动到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
// 如果key相同,但是节点不相同,则创建一个新的节点
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
}
// 右移
newStartVnode = newCh[++newStartIdx]
}
}
while
循环主要处理了以下五种情景:
- 当新老
VNode
节点的start
相同时,直接patchVnode
,同时新老VNode
节点的开始索引都加 1 - 当新老
VNode
节点的end
相同时,同样直接patchVnode
,同时新老VNode
节点的结束索引都减 1 - 当老
VNode
节点的start
和新VNode
节点的end
相同时,这时候在patchVnode
后,还需要将当前真实dom
节点移动到oldEndVnode
的后面,同时老VNode
节点开始索引加 1,新VNode
节点的结束索引减 1 - 当老
VNode
节点的end
和新VNode
节点的start
相同时,这时候在patchVnode
后,还需要将当前真实dom
节点移动到oldStartVnode
的前面,同时老VNode
节点结束索引减 1,新VNode
节点的开始索引加 1 - 如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况:
- 从旧的
VNode
为key
值,对应index
序列为value
值的哈希表中找到与newStartVnode
一致key
的旧的VNode
节点,再进行patchVnode
,同时将这个真实dom
移动到oldStartVnode
对应的真实dom
的前面 - 调用
createElm
创建一个新的dom
节点放到当前newStartIdx
的位置
- 从旧的
小结
- 当数据发生改变时,订阅者
watcher
就会调用patch
给真实的DOM
打补丁 - 通过
isSameVnode
进行判断,相同则调用patchVnode
方法 patchVnode
做了以下操作:- 找到对应的真实
dom
,称为el
- 如果都有都有文本节点且不相等,将
el
文本节点设置为Vnode
的文本节点 - 如果
oldVnode
有子节点而VNode
没有,则删除el
子节点 - 如果
oldVnode
没有子节点而VNode
有,则将VNode
的子节点真实化后添加到el
- 如果两者都有子节点,则执行
updateChildren
函数比较子节点
- 找到对应的真实
updateChildren
主要做了以下操作:- 设置新旧
VNode
的头尾指针 - 新旧头尾指针进行比较,循环向中间靠拢,根据情况调用
patchVnode
进行patch
重复流程、调用createElem
创建一个新节点,从哈希表寻找key
一致的VNode
节点再分情况操作
- 设置新旧
简述 mixin、extends 的覆盖逻辑
(1)mixin 和 extends mixin 和 extends均是用于合并、拓展组件的,两者均通过 mergeOptions 方法实现合并。
- mixins 接收一个混入对象的数组,其中混入对象可以像正常的实例对象一样包含实例选项,这些选项会被合并到最终的选项中。Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。
- extends 主要是为了便于扩展单文件组件,接收一个对象或构造函数。
(2)mergeOptions 的执行过程
- 规范化选项(normalizeProps、normalizelnject、normalizeDirectives)
- 对未合并的选项,进行判断
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm);
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
}
- 合并处理。根据一个通用 Vue 实例所包含的选项进行分类逐一判断合并,如 props、data、 methods、watch、computed、生命周期等,将合并结果存储在新定义的 options 对象里。
- 返回合并结果 options。
Vue中修饰符.sync与v-model的区别
sync
的作用
.sync
修饰符可以实现父子组件之间的双向绑定,并且可以实现子组件同步修改父组件的值,相比较与v-model
来说,sync
修饰符就简单很多了- 一个组件上可以有多个
.sync
修饰符
<!-- 正常父传子 -->
<Son :a="num" :b="num2" />
<!-- 加上sync之后的父传子 -->
<Son :a.sync="num" :b.sync="num2" />
<!-- 它等价于 -->
<Son
:a="num"
:b="num2"
@update:a="val=>num=val"
@update:b="val=>num2=val"
/>
<!-- 相当于多了一个事件监听,事件名是update:a, -->
<!-- 回调函数中,会把接收到的值赋值给属性绑定的数据项中。 -->
v-model
的工作原理
<com1 v-model="num"></com1>
<!-- 等价于 -->
<com1 :value="num" @input="(val)=>num=val"></com1>
- 相同点
- 都是语法糖,都可以实现父子组件中的数据的双向通信
- 区别点
- 格式不同:
v-model="num"
,:num.sync="num"
v-model
:@input + value
:num.sync
:@update:num
v-model
只能用一次;.sync
可以有多个
- 格式不同:
什么是 MVVM?
Model–View–ViewModel (MVVM) 是一个软件架构设计模式,由微软 WPF 和 Silverlight 的架构师 Ken Cooper 和 Ted Peters 开发,是一种简化用户界面的事件驱动编程方式。由 John Gossman(同样也是 WPF 和 Silverlight 的架构师)于2005年在他的博客上发表
MVVM 源自于经典的 Model–View–Controller(MVC)模式 ,MVVM 的出现促进了前端开发与后端业务逻辑的分离,极大地提高了前端开发效率,MVVM 的核心是 ViewModel 层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用
(1)View 层
View 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。
(2)Model 层
Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。
(3)ViewModel 层
ViewModel 是由前端开发人员组织生成和维护的视图数据层。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。需要注意的是 ViewModel 所封装出来的数据模型包括视图的状态和行为两部分,而 Model 层的数据模型是只包含状态的,比如页面的这一块展示什么,而页面加载进来时发生什么,点击这一块发生什么,这一块滚动时发生什么这些都属于视图行为(交互),视图状态和行为都封装在了 ViewModel 里。这样的封装使得 ViewModel 可以完整地去描述 View 层。
MVVM 框架实现了双向绑定,这样 ViewModel 的内容会实时展现在 View 层,前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维护 ViewModel,更新数据视图就会自动得到相应更新。这样 View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的重要一环。
我们以下通过一个 Vue 实例来说明 MVVM 的具体实现,有 Vue 开发经验的同学应该一目了然:
(1)View 层
<div id="app">
<p>{{message}}</p>
<button v-on:click="showMessage()">Click me</button>
</div>
(2)ViewModel 层
var app = new Vue({
el: '#app',
data: { // 用于描述视图状态
message: 'Hello Vue!',
},
methods: { // 用于描述视图行为
showMessage(){
let vm = this;
alert(vm.message);
}
},
created(){
let vm = this;
// Ajax 获取 Model 层的数据
ajax({
url: '/your/server/data/api',
success(res){
vm.message = res;
}
});
}
})
(3) Model 层
{
"url": "/your/server/data/api",
"res": {
"success": true,
"name": "IoveC",
"domain": "www.cnblogs.com"
}
}
为什么要使用异步组件
- 节省打包出的结果,异步组件分开打包,采用
jsonp
的方式进行加载,有效解决文件过大的问题。 - 核心就是包组件定义变成一个函数,依赖
import()
语法,可以实现文件的分割加载。
components:{
AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([])
}
原理
export function ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void {
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默认调用此函数时返回 undefiend
// 第二次渲染时Ctor不为undefined
if (Ctor === undefined) {
return createAsyncPlaceholder( // 渲染占位符 空虚拟节点
asyncFactory,
data,
context,
children,
tag
)
}
}
}
function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void {
if (isDef(factory.resolved)) {
// 3.在次渲染时可以拿到获取的最新组件
return factory.resolved
}
const resolve = once((res: Object | Class<Component>) => {
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender(true) //2. 强制更新视图重新渲染
} else {
owners.length = 0
}
})
const reject = once(reason => {
if (isDef(factory.errorComp)) {
factory.error = true forceRender(true)
}
})
const res = factory(resolve, reject)// 1.将resolve方法和reject方法传入,用户调用 resolve方法后
sync = false
return factory.resolved
}
Vue 中 computed 和 watch 有什么区别?
计算属性 computed:
(1)支持缓存,只有依赖数据发生变化时,才会重新进行计算函数;
(2)计算属性内不支持异步操作;
(3)计算属性的函数中都有一个 get(默认具有,获取计算属性)和 set(手动添加,设置计算属性)方法;
(4)计算属性是自动监听依赖值的变化,从而动态返回内容。
侦听属性 watch:
(1)不支持缓存,只要数据发生变化,就会执行侦听函数;
(2)侦听属性内支持异步操作;
(3)侦听属性的值可以是一个对象,接收 handler 回调,deep,immediate 三个属性;
(3)监听是一个过程,在监听的值变化时,可以触发一个回调,并做一些其他事情。
请说出vue cli项目中src目录每个文件夹和文件的用法
assets
文件夹是放静态资源;components
是放组件;router
是定义路由相关的配置;view
视图;app.vue
是一个应用主组件;main.js
是入口文件
谈谈Vue和React组件化的思想
- 1.我们在各个页面开发的时候,会产生很多重复的功能,比如element中的xxxx。像这种纯粹非页面的UI,便成为我们常用的UI组件,最初的前端组件也就仅仅指的是UI组件
- 2.随着业务逻辑变得越来多是,我们就想要我们的组件可以处理很多事,这就是我们常说的组件化,这个组件就不是UI组件了,而是包具体业务的业务组件
- 3.这种开发思想就是分而治之。最大程度的降低开发难度和维护成本的效果。并且可以多人协作,每个人写不同的组件,最后像撘积木一样的把它构成一个页面
Vue 3.0 中的 Vue Composition API?
在 Vue2 中,代码是 Options API 风格的,也就是通过填充 (option) data、methods、computed 等属性来完成一个 Vue 组件。这种风格使得 Vue 相对于 React极为容易上手,同时也造成了几个问题:
- 由于 Options API 不够灵活的开发方式,使得Vue开发缺乏优雅的方法来在组件间共用代码。
- Vue 组件过于依赖
this
上下文,Vue 背后的一些小技巧使得 Vue 组件的开发看起来与 JavaScript 的开发原则相悖,比如在methods
中的this
竟然指向组件实例来不指向methods
所在的对象。这也使得 TypeScript 在Vue2 中很不好用。
于是在 Vue3 中,舍弃了 Options API,转而投向 Composition API。Composition API本质上是将 Options API 背后的机制暴露给用户直接使用,这样用户就拥有了更多的灵活性,也使得 Vue3 更适合于 TypeScript 结合。
如下,是一个使用了 Vue Composition API 的 Vue3 组件:
<template>
<button @click="increment">
Count: {{ count }} </button>
</template>
<script>
// Composition API 将组件属性暴露为函数,因此第一步是导入所需的函数
import { ref, computed, onMounted } from 'vue'
export default { setup() {
// 使用 ref 函数声明了称为 count 的响应属性,对应于Vue2中的data函数
const count = ref(0)
// Vue2中需要在methods option中声明的函数,现在直接声明
function increment() { count.value++ } // 对应于Vue2中的mounted声明周期
onMounted(() => console.log('component mounted!')) return { count, increment } }}
</script>
显而易见,Vue Composition API 使得 Vue3 的开发风格更接近于原生 JavaScript,带给开发者更多地灵活性
请说明Vue中key的作用和原理,谈谈你对它的理解
key
是为Vue
中的VNode
标记的唯一id
,在patch
过程中通过key
可以判断两个虚拟节点是否是相同节点,通过这个key
,我们的diff
操作可以更准确、更快速diff
算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key
与旧节点进行比对,然后检出差异- 尽量不要采用索引作为
key
- 如果不加
key
,那么vue
会选择复用节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug
- 更准确 :因为带
key
就不是就地复用了,在sameNode
函数a.key === b.key
对比中可以避免就地复用的情况。所以会更加准确。 - 更快速 :
key
的唯一性可以被Map
数据结构充分利用,相比于遍历查找的时间复杂度O(n)
,Map
的时间复杂度仅仅为O(1)
,比遍历方式更快。
源码如下:
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
回答范例
分析
这是一道特别常见的问题,主要考查大家对虚拟DOM
和patch
细节的掌握程度,能够反映面试者理解层次
思路分析:
- 给出结论,
key
的作用是用于优化patch
性能 key
的必要性- 实际使用方式
- 总结:可从源码层面描述一下
vue
如何判断两个节点是否相同
回答范例:
key
的作用主要是为了更高效的更新虚拟DOM
vue
在patch
过程中 判断两个节点是否是相同节点是key
是一个必要条件 ,渲染一组列表时,key
往往是唯一标识,所以如果不定义key
的话,vue
只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch
过程比较低效,影响性能- 实际使用中在渲染一组列表时
key
必须设置,而且必须是唯一标识,应该避免使用数组索引作为key
,这可能导致一些隐蔽的bug
;vue
中在使用相同标签元素过渡切换时,也会使用key
属性,其目的也是为了让vue
可以区分它们,否则vue
只会替换其内部属性而不会触发过渡效果 - 从源码中可以知道,
vue
判断两个节点是否相同时主要判断两者的key
和标签类型(如div)
等,因此如果不设置key
,它的值就是undefined
,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom
更新操作,明显是不可取的
如果不使用
key
,Vue
会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key
是为Vue
中vnode
的唯一标记,通过这个key
,我们的diff
操作可以更准确、更快速
diff程可以概括为:
oldCh
和newCh
各有两个头尾的变量StartIdx
和EndIdx
,它们的2
个变量相互比较,一共有4
种比较方式。如果4
种比较都没匹配,如果设置了key
,就会用key
进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx
表明oldCh
和newCh
至少有一个已经遍历完了,就会结束比较,这四种比较方式就是首
、尾
、旧尾新头
、旧头新尾
相关代码如下
// 判断两个vnode的标签和key是否相同 如果相同 就可以认为是同一节点就地复用
function isSameVnode(oldVnode, newVnode) {
return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;
}
// 根据key来创建老的儿子的index映射表 类似 {'a':0,'b':1} 代表key为'a'的节点在第一个位置 key为'b'的节点在第二个位置
function makeIndexByKey(children) {
let map = {};
children.forEach((item, index) => {
map[item.key] = index;
});
return map;
}
// 生成的映射表
let map = makeIndexByKey(oldCh);
标签:面试题,vue,const,19,Vue,key,组件,节点
From: https://www.cnblogs.com/bbxiaxia1998/p/16724942.html