什么是 mixin ?
- Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。
- 如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。
- 然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。
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>
v-show 与 v-if 有什么区别?
v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
vue3中 watch、watchEffect区别
watch
是惰性执行,也就是只有监听的值发生变化的时候才会执行,但是watchEffect
不同,每次代码加载watchEffect
都会执行(忽略watch
第三个参数的配置,如果修改配置项也可以实现立即执行)watch
需要传递监听的对象,watchEffect
不需要watch
只能监听响应式数据:ref
定义的属性和reactive
定义的对象,如果直接监听reactive
定义对象中的属性是不允许的(会报警告),除非使用函数转换一下。其实就是官网上说的监听一个getter
watchEffect
如果监听reactive
定义的对象是不起作用的,只能监听对象中的属性
看一下watchEffect
的代码
<template>
<div>
请输入firstName:
<input type="text" v-model="firstName">
</div>
<div>
请输入lastName:
<input type="text" v-model="lastName">
</div>
<div>
请输入obj.text:
<input type="text" v-model="obj.text">
</div>
<div>
【obj.text】 {{obj.text}}
</div>
</template>
<script>
import {ref, reactive, watch, watchEffect} from 'vue'
export default {
name: "HelloWorld",
props: {
msg: String,
},
setup(props,content){
let firstName = ref('')
let lastName = ref('')
let obj= reactive({
text:'hello'
})
watchEffect(()=>{
console.log('触发了watchEffect');
console.log(`组合后的名称为:${firstName.value}${lastName.value}`)
})
return{
obj,
firstName,
lastName
}
}
};
</script>
改造一下代码
watchEffect(()=>{
console.log('触发了watchEffect');
// 这里我们不使用firstName.value/lastName.value ,相当于是监控整个ref,对应第四点上面的结论
console.log(`组合后的名称为:${firstName}${lastName}`)
})
watchEffect(()=>{
console.log('触发了watchEffect');
console.log(obj);
})
稍微改造一下
let obj = reactive({
text:'hello'
})
watchEffect(()=>{
console.log('触发了watchEffect');
console.log(obj.text);
})
再看一下watch的代码,验证一下
let obj= reactive({
text:'hello'
})
// watch是惰性执行, 默认初始化之后不会执行,只有值有变化才会触发,可通过配置参数实现默认执行
watch(obj, (newValue, oldValue) => {
// 回调函数
console.log('触发监控更新了new', newValue);
console.log('触发监控更新了old', oldValue);
},{
// 配置immediate参数,立即执行,以及深层次监听
immediate: true,
deep: true
})
- 监控整个
reactive
对象,从上面的图可以看到deep
实际默认是开启的,就算我们设置为false
也还是无效。而且旧值获取不到。 - 要获取旧值则需要监控对象的属性,也就是监听一个
getter
,看下图
总结
- 如果定义了
reactive
的数据,想去使用watch
监听数据改变,则无法正确获取旧值,并且deep
属性配置无效,自动强制开启了深层次监听。 - 如果使用
ref
初始化一个对象或者数组类型的数据,会被自动转成reactive
的实现方式,生成proxy
代理对象。也会变得无法正确取旧值。 - 用任何方式生成的数据,如果接收的变量是一个
proxy
代理对象,就都会导致watch
这个对象时,watch
回调里无法正确获取旧值。 - 所以当大家使用
watch
监听对象时,如果在不需要使用旧值的情况,可以正常监听对象没关系;但是如果当监听改变函数里面需要用到旧值时,只能监听 对象.xxx`属性 的方式才行
watch和watchEffect异同总结
体验
watchEffect
立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
count.value++
// -> logs 1
watch
侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
回答范例
watchEffect
立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。watch
侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数watchEffect(effect)
是一种特殊watch
,传入的函数既是依赖收集的数据源,也是回调函数。如果我们不关心响应式数据变化前后的值,只是想拿这些数据做些事情,那么watchEffect
就是我们需要的。watch
更底层,可以接收多种数据源,包括用于依赖收集的getter
函数,因此它完全可以实现watchEffect
的功能,同时由于可以指定getter
函数,依赖可以控制的更精确,还能获取数据变化前后的值,因此如果需要这些时我们会使用watch
watchEffect
在使用时,传入的函数会立刻执行一次。watch
默认情况下并不会执行回调函数,除非我们手动设置immediate
选项- 从实现上来说,
watchEffect(fn)
相当于watch(fn,fn,{immediate:true})
watchEffect
定义如下
export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle {
return doWatch(effect, null, options)
}
watch
定义如下
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
return doWatch(source as any, cb, options)
}
很明显watchEffect
就是一种特殊的watch
实现。
那vue中是如何检测数组变化的呢?
数组就是使用 object.defineProperty
重新定义数组的每一项,那能引起数组变化的方法我们都是知道的, pop
、 push
、 shift
、 unshift
、 splice
、 sort
、 reverse
这七种,只要这些方法执行改了数组内容,我就更新内容就好了,是不是很好理解。
- 是用来函数劫持的方式,重写了数组方法,具体呢就是更改了数组的原型,更改成自己的,用户调数组的一些方法的时候,走的就是自己的方法,然后通知视图去更新。
- 数组里每一项可能是对象,那么我就是会对数组的每一项进行观测,(且只有数组里的对象才能进行观测,观测过的也不会进行观测)
vue3:改用 proxy
,可直接监听对象数组的变化。
了解nextTick吗?
异步方法,异步渲染最后一步,与JS事件循环联系紧密。主要使用了宏任务微任务(setTimeout
、promise
那些),定义了一个异步方法,多次调用nextTick
会将方法存入队列,通过异步方法清空当前队列。
参考 前端进阶面试题详细解答
Vue中的key到底有什么用?
key
是为Vue中的vnode标记的唯一id,通过这个key,我们的diff操作可以更准确、更快速
diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key
与旧节点进行比对,然后超出差异.
diff程可以概括为:oldCh和newCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一个已经遍历完了,就会结束比较,这四种比较方式就是首、尾、旧尾新头、旧头新尾.
- 准确: 如果不加
key
,那么vue会选择复用节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug. - 快速: key的唯一性可以被Map数据结构充分利用,相比于遍历查找的时间复杂度O(n),Map的时间复杂度仅仅为O(1).
Vue 组件间通信有哪几种方式?
Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。
(1)props / $emit
适用 父子组件通信 这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。
(2)ref 与 $parent / $children
适用 父子组件通信
ref
:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例$parent / $children
:访问父 / 子实例
(3)EventBus ($emit / $on)
适用于 父子、隔代、兄弟组件通信 这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。
(4)$attrs/$listeners
适用于 隔代组件通信
$attrs
:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何prop
时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过v-bind="$attrs"
传入内部组件。通常配合inheritAttrs
选项一起使用。$listeners
:包含了父作用域中的 (不含 .native 修饰器的)v-on
事件监听器。它可以通过v-on="$listeners"
传入内部组件
(5)provide / inject
适用于 隔代组件通信 祖先组件中通过 provider
来提供变量,然后在子孙组件中通过 inject
来注入变量。 provide / inject API
主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。 (6)Vuex
适用于 父子、隔代、兄弟组件通信 Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
Vue.extend 作用和原理
官方解释:Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并
相关代码如下
export default function initExtend(Vue) {
let cid = 0; //组件的唯一标识
// 创建子类继承Vue父类 便于属性扩展
Vue.extend = function (extendOptions) {
// 创建子类的构造函数 并且调用初始化方法
const Sub = function VueComponent(options) {
this._init(options); //调用Vue初始化方法
};
Sub.cid = cid++;
Sub.prototype = Object.create(this.prototype); // 子类原型指向父类
Sub.prototype.constructor = Sub; //constructor指向自己
Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options
return Sub;
};
}
mixin 和 mixins 区别
mixin
用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。
Vue.mixin({
beforeCreate() {
// ...逻辑 // 这种方式会影响到每个组件的 beforeCreate 钩子函数
},
});
虽然文档不建议在应用中直接使用 mixin
,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的 ajax
或者一些工具函数等等。
mixins
应该是最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins
混入代码,比如上拉下拉加载数据这种逻辑等等。
另外需要注意的是 mixins
混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。
为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?
Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组
push();
pop();
shift();
unshift();
splice();
sort();
reverse();
由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。
Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。
Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
如何理解Vue中模板编译原理
Vue
的编译过程就是将template
转化为render
函数的过程
- 解析生成AST树 将
template
模板转化成AST
语法树,使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理 - 标记优化 对静态语法做静态标记
markup
(静态节点如div
下有p
标签内容不会变化)diff
来做优化 静态节点跳过diff
操作Vue
的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM
也不会变化。那么优化过程就是深度遍历AST
树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用- 等待后续节点更新,如果是静态的,不会在比较
children
了
- 代码生成 编译的最后一步是将优化后的
AST
树转换为可执行的代码
回答范例
思路
- 引入
vue
编译器概念 - 说明编译器的必要性
- 阐述编译器工作流程
回答范例
Vue
中有个独特的编译器模块,称为compiler
,它的主要作用是将用户编写的template
编译为js
中可执行的render
函数。- 之所以需要这个编译过程是为了便于前端能高效的编写视图模板。相比而言,我们还是更愿意用
HTML
来编写视图,直观且高效。手写render
函数不仅效率底下,而且失去了编译期的优化能力。 - 在
Vue
中编译器会先对template
进行解析,这一步称为parse
,结束之后会得到一个JS
对象,我们称为 抽象语法树AST ,然后是对AST
进行深加工的转换过程,这一步成为transform
,最后将前面得到的AST
生成为JS
代码,也就是render
函数
可能的追问
Vue
中编译器何时执行?
在
new Vue()
之后。Vue
会调用_init
函数进行初始化,也就是这里的 init
过程,它会初始化生命周期、事件、props
、methods
、data
、computed
与watch
等。其中最重要的是通过Object.defineProperty
设置setter
与getter
函数,用来实现「响应式」以及「依赖收集」
-
初始化之后调用
$mount
会挂载组件,如果是运行时编译,即不存在render function
但是存在template
的情况,需要进行「编译」步骤 -
compile
编译可以分成parse
、optimize
与generate
三个阶段,最终需要得到render function
React
有没有编译器?
react
使用babel
将JSX
语法解析
<div id="app"></div>
<script>
let vm = new Vue({
el: '#app',
template: `<div>
// <span>hello world</span> 是静态节点
<span>hello world</span>
// <p>{{name}}</p> 是动态节点
<p>{{name}}</p>
</div>`,
data() {
return { name: 'test' }
}
});
</script>
源码分析
export function compileToFunctions(template) {
// 我们需要把html字符串变成render函数
// 1.把html代码转成ast语法树 ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法
// 很多库都运用到了ast 比如 webpack babel eslint等等
let ast = parse(template);
// 2.优化静态节点:对ast树进行标记,标记静态节点
if (options.optimize !== false) {
optimize(ast, options);
}
// 3.通过ast 重新生成代码
// 我们最后生成的代码需要和render函数一样
// 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
// _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本
let code = generate(ast);
// 使用with语法改变作用域为this 之后调用render函数可以使用call改变this 方便code里面的变量取值
let renderFn = new Function(`with(this){return ${code}}`);
return renderFn;
}
为什么Vue采用异步渲染呢?
Vue
是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能, Vue
会在本轮数据更新后,在异步更新视图。核心思想 nextTick
。
dep.notify()
通知 watcher进行更新, subs[i].update
依次调用 watcher 的 update
, queueWatcher
将watcher 去重放入队列, nextTick( flushSchedulerQueue
)在下一tick中刷新watcher队列(异步)。
Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题 ?
受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)
来实现为对象添加响应式属性,那框架本身是如何实现的呢?
我们查看对应的 Vue 源码:vue/src/core/instance/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// key 已经存在,直接修改属性值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// target 本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val
return val
}
// 对属性进行响应式处理
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
我们阅读以上源码可知,vm.$set 的实现原理是:
- 如果目标是数组,直接使用数组的 splice 方法触发相应式;
- 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
谈谈Vue和React组件化的思想
- 1.我们在各个页面开发的时候,会产生很多重复的功能,比如element中的xxxx。像这种纯粹非页面的UI,便成为我们常用的UI组件,最初的前端组件也就仅仅指的是UI组件
- 2.随着业务逻辑变得越来多是,我们就想要我们的组件可以处理很多事,这就是我们常说的组件化,这个组件就不是UI组件了,而是包具体业务的业务组件
- 3.这种开发思想就是分而治之。最大程度的降低开发难度和维护成本的效果。并且可以多人协作,每个人写不同的组件,最后像撘积木一样的把它构成一个页面
Vue2.x 响应式数据原理
整体思路是数据劫持+观察者模式
对象内部通过 defineReactive
方法,使用 Object.defineProperty
来劫持各个属性的 setter
、getter
(只会劫持已经存在的属性),数组则是通过重写数组7个方法
来实现。当页面使用对应属性时,每个属性都拥有自己的 dep
属性,存放他所依赖的 watcher
(依赖收集),当属性变化后会通知自己对应的 watcher
去更新(派发更新)
Object.defineProperty基本使用
function observer(value) { // proxy reflect
if (typeof value === 'object' && typeof value !== null)
for (let key in value) {
defineReactive(value, key, value[key]);
}
}
function defineReactive(obj, key, value) {
observer(value);
Object.defineProperty(obj, key, {
get() { // 收集对应的key 在哪个方法(组件)中被使用
return value;
},
set(newValue) {
if (newValue !== value) {
observer(newValue);
value = newValue; // 让key对应的方法(组件重新渲染)重新执行
}
}
})
}
let obj1 = { school: { name: 'poetry', age: 20 } };
observer(obj1);
console.log(obj1)
源码分析
class Observer {
// 观测值
constructor(value) {
this.walk(value);
}
walk(data) {
// 对象上的所有属性依次进行观测
let keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
let value = data[key];
defineReactive(data, key, value);
}
}
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
observe(value); // 递归关键
// --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
// 思考?如果Vue数据嵌套层级过深 >>性能会受影响
Object.defineProperty(data, key, {
get() {
console.log("获取值");
//需要做依赖收集过程 这里代码没写出来
return value;
},
set(newValue) {
if (newValue === value) return;
console.log("设置值");
//需要做派发更新过程 这里代码没写出来
value = newValue;
},
});
}
export function observe(value) {
// 如果传过来的是对象或者数组 进行属性劫持
if (
Object.prototype.toString.call(value) === "[object Object]" ||
Array.isArray(value)
) {
return new Observer(value);
}
}
说一说你对vue响应式理解回答范例
- 所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制
MVVM
框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理- 以
vue
为例说明,通过数据响应式加上虚拟DOM
和patch
算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的DOM操作,从而大大提升开发效率,降低开发难度 vue2
中的数据响应式会根据数据类型来做不同处理,如果是 对象则采用Object.defineProperty()
的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是数组则通过覆盖数组对象原型的7个变更方法 ,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete
这样特殊的api
才能生效;对于es6
中新产生的Map
、Set
这些数据结构不支持等问题- 为了解决这些问题,
vue3
重新编写了这一部分的实现:利用ES6
的Proxy
代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊api
,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity
包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了
computed和watch有什么区别?
computed:
computed
是计算属性,也就是计算值,它更多用于计算值的场景computed
具有缓存性,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算computed
适用于计算比较消耗性能的计算场景
watch:
- 更多的是「观察」的作用,类似于某些数据的监听回调,用于观察
props
$emit
或者本组件的值,当数据变化时来执行回调进行后续操作 - 无缓存性,页面重新渲染时值不变化也会执行
小结:
- 当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为computed
- 如果你需要在某个数据变化时做一些事情,使用watch来观察这个数据变化
Vue.js的template编译
简而言之,就是先转化成AST树,再得到的render函数返回VNode(Vue的虚拟DOM节点),详细步骤如下:
首先,通过compile编译器把template编译成AST语法树(abstract syntax tree 即 源代码的抽象语法结构的树状表现形式),compile是createCompiler的返回值,createCompiler是用以创建编译器的。另外compile还负责合并option。
然后,AST会经过generate(将AST语法树转化成render funtion字符串的过程)得到render函数,render的返回值是VNode,VNode是Vue的虚拟DOM节点,里面有(标签名、子节点、文本等等)
父组件可以监听到子组件的生命周期吗
比如有父组件 Parent
和子组件 Child
,如果父组件监听到子组件挂载 mounted
就做一些逻辑处理,可以通过以下写法实现:
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
this.$emit("mounted");
}
以上需要手动通过 $emit
触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook
来监听即可,如下所示:
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
当然 @hook
方法不仅仅是可以监听 mounted
,其它的生命周期事件,例如:created
,updated
等都可以监听
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
节点再分情况操作
- 设置新旧