我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情
前言
大家好,我是前端贰货道士。最近抽空整理了下我对vue2.x
的理解和认知,涵盖了vue2.x
的常用知识
、冷知识
以及一些底层原理
,算是我的vue2世界观
。由于文章比较长,大家可以酌情
根据自身需求选读
,相信各位耐心
看完定会有所收获
。
因为是自己的理解,所以难免会出现错误。如果大家发现了错误,或者有些问题需要交流,欢迎
在评论区下留言。由于最近项目加急
,还有很多事情需要处理,剩下的vue2.x底层原理
会在后续抽空更完,在此向大家说声抱歉。有兴趣继续读下去的朋友们可以先收藏吃灰
,哈哈哈。如果本篇文章对您有帮助,烦请大家一键三连哦, 蟹蟹大家~
1. vue常用知识点总结(vue群演
)
a. $watch和watch的使用
$watch的使用:
Tips
:
$watch
的第二个参数是一个对象,定义handler
方法以及上图四个属性的值。$watch
的第二个参数也可以为一个函数, 此时第三个参数则是一个对象,用于定义上图四个属性的值watch
中监听多个相同属性或者对象,后面会覆盖前面的。因为在里面定义的是对象的key
和value
,最后vue
会遍历这些key
并初始化各个计算属性的watch
监听。$watch
和watch
虽然都是监听方法,但是$watch
中可以定义watch
中定义好的属性或者对象,这两者相互独立。而且,也可以存在多个监听相同对象的$watch
方法,它们之间也是互相独立的。$watch
和watch
的主要区别是:$watch
更加灵活,可以监听任何数据上的变化,也可以写在vue
实例各位置(比如生命周期钩子函数、方法
中等位置中)。而且比较重要的一点是:$watch
可以取消对于自身的监听
`1. 点击添加元素,提示添加成功。再次点击,还是会显示添加成功的提示,因为此时已经取消了对于自身的监听。`
`a. 这个时候的dataArr之所以能监听到,是因为vue重写了数组的push方法,在调用这个响应式方法后,会通知watch进行监听。`
`b. 而当我们将dataArr的值改为[{ a: 100 }],并修改dataArr的值,点击时,this.dataArr[0].a = 1000。`
`这个时候发现并没有触发监听,这是因为在$watch中deep的默认值也是false,同时也没有触发vue的响应式方法。`
`2. vue会监听数据的两种情况:`
`a. 使用$set(包括给对象重新分配地址)或$delete更新数据,触发响应式方法,从而触发监听`
`b. 使用7种重写的响应式数组方法,调用后便会通知watch进行监听`
<template>
<div class="about">
<div class="box">我是watch组件</div>
<el-button class="box" @click="dataArrAdd">向数组中添加元素</el-button>
<div class="box" v-if="showBtn">添加成功</div>
</div>
</template>
<script>
export default {
name: "watch",
data() {
return {
dataArr: [],
showBtn: false,
}
},
created() {
let unDataArr = this.$watch(
() => {
return this.dataArr
},
(newVal, oldVal) => {
this.showBtn = !this.showBtn
unDataArr()
}
)
},
methods: {
dataArrAdd() {
this.dataArr.push("1")
}
}
}
</script>
Tips:
$watch
第一个参数如果为函数形式,一般要return
出需要监听的变量。观察源码,value
其实是$watch
的第一个参数(如果是函数,就是返回值,如果没返回,就是undefined
)。$watch
方法触发的前提是监听的对象发生变化,而需要满足以下三种情况之一: 监听变量的值发生变化、是深度监听或者value
是对象。
Tips:
上图的deep
如果为true
,且收集的依赖value
是对象且有值,就可以进行深度监听。所以在$watch
中的deep
有两个作用:
- a. 为收集的对象依赖进行深度监听
- b. 当
$watch
中监听的对象发生变化时,作为是否触发handler
函数的依据之一存在
总结:
- 会存在一种特殊情况:比如
return a + b + c
, 如果a
、b
、c
单个发生变化,但是整体没变。此时如果不写deep: true
是无法触发监听handler
方法的,所以一般情况下,$watch
中最好写上deep: true
。 $watch
的触发是存在前提的,只有收集的依赖发生变化时,才执行update
方法。- 如果
$watch
中第一个参数是一个函数,且获取的是定义在data
中的对象,但是没有return
出去。此时就算写了deep: true
,改变对象中某个属性的值,也不会触发$wacth
方法。这是因为,收集的依赖是undefined
,因此不会触发$watch
方法。
```传送门(vue3):https://staging-cn.vuejs.org/api/component-instance.html#attrs```
```传送门(vue2):https://cn.vuejs.org/v2/api/#vm-watch```
总结:
```1. 监听一个属性名```
this.$watch('a', (newVal, oldVal) => {})
```2. 监听对象的属性```
this.$watch('a.b', (newVal, oldVal) => {})
```3. 监听getter函数的值```
this.$watch(
// 每一次这个 `this.a + this.b` 表达式生成一个
// 不同的结果,处理函数都会被调用
// 这就好像我们在侦听一个计算属性
// 而不定义计算属性本身。
() => this.a + this.b,
(newVal, oldVal) => {}
)
```4. 停止该侦听器```
const unwatch = this.$watch('a', cb)
unwatch()
```5. 排除对象某些属性的监听```
mounted() {
Object.keys(this.params)
.filter((_) => !["c", "d"].includes(_)) // 排除对c,d属性的监听
.forEach((_) => {
this.$watch((vm) => vm.params[_], handler, {
deep: true,
});
});
},
data() {
return {
params: {
a: 1,
b: 2,
c: 3,
d: 4
},
};
},
watch: {
params: {
deep: true,
handler() {
this.getList;
},
}
}
watch的使用:
`1. 监听:`
watch: {
`不需要深度监听和开始就要立即监听一次的情况下,以需要监听的变量名定义函数就好`
pageSize(newVal, oldVal) {
`newVal表示新的pageSize值,oldVal表示旧的oldVal值`
console.log(newVal, oldVal)
}
}
`2. 深度监听(对引用类型进行深度递归监听):`
watch: {
`需要深度监听或开始就要立即监听一次的情况下,以需要监听监听的变量名作对象`
`分别传入handler方法、immediate属性和deep属性`
person: {
handler(newVal, oldVal) {
console.log(newVal, oldVal)
},
immediate: true,
`是否立即监听一次`
deep: true
}
}
`3. (小技巧)监听的值也可以为对象中的某个属性:`
watch: {
'$route.query.id'() {
...
},
//或者:
'$route.query.id': {
handler(new, old) {
},
immediate: true
...
},
$route(to, from) {
// 监听路由变化
console.log("watch", to.query.id)
}
}
b. 计算属性的用法
`1.不能直接修改计算属性的值,除非为计算属性添加get和set方法:`
sizeCheckAll: {
get() {
return this.selectSizeList?.length == this.allSizes?.length
},
set(bool) {
if(bool) this.selectSizeList = this.allSizes
else this.selectSizeList = []
}
}
`2.计算属性可以解构引用data中定义的变量, 当这些变量或者计算属性中用到的data变量发生变化时,`
`且计算属性有挂载到模板上,则会重新执行计算属性:`
text({ loading }) {
return !loading ? this.$t('page.folder.upload') : this.$t('page.folder.uploading')
}
`3.在计算属性中使用闭包的思想传递参数:`
(1) 需要传递参数的情况不推荐使用计算属性,因为无法回收变量,会造成内存泄漏,需要改成方法;
(2) 除这种情况外,建议使用计算属性,因为有缓存(除非依赖的变量变化才会重新执行)。
当其他变量改变视图更新时, 方法会重新执行,而计算属性不会;
getSku() {
return (type, row) => {
return row.customProductList[row.$sizeIndex].customSpecificProductList?.map((item) => item[type])
}
}
`4.计算属性中的mapState(存储在vuex中的state数据)和mapGetters(对state中的数据进行包装的数据)`
import { mapState,mapGetters } from 'vuex'
computed: {
...mapState(['categoryList','productList']),
...mapGetters(['finalData'])
}
对于解构出来的`mapMutations和mapActions`需要定义在`methods`中,并在`methods`中进行调用
methods: {
...mapMutations(['add','addN']),
...mapActions(['awaitAdd'])
}
`5.计算属性中不能执行异步操作`
`6. 重新执行计算属性的方法:`
(1) 计算属性中可以`解构非关联的属性`,当`非关联属性属性变化`时,会`重新获取`计算属性的值(`小技巧`,但不推荐)
(2) 利用计算属性的原理(`_computedWatchers.计算属性名称.dirty`,不推荐)
a. 计算属性是通过`dirty`来判断是否要进行`重新计算`,默认为`true`,是懒加载,`lazy`为`true`, 而`watch`监听
的`lazy`为`false`,它们走的是不同的逻辑;
b. 在获取当前计算属性的值后,重置为`false`;
c. 当计算属性依赖的值发生变化且对应的计算属性在模板中使用到时,会触发`计算属性watcher`的`update`方法,
将对应计算属性的`dirty`值变为`true`,重新得到计算属性的值,并刷新页面。
1. `dirty`为`false`时, 会`缓存`计算结果,
2. `dirty`为`true`时, 会`重新获取计算属性的值`。
this._computedWatchers.计算属性名称.dirty = true
this.$forceUpdate()
this.$nextTick(()=> {
...
})
计算属性computed
与watch
的区别:
计算属性
和监听属性
, 本质上都是一个watcher实例
, 它们都通过响应式系统与数据、页面建立通信。但它们之间也存在一些差异:
- 执行时机不同:
watch
是在数据变化时立即执行回调函数,而computed
是在属性值被访问时才会执行计算函数。 - 是否具有缓存功能:
watch
没有缓存功能,只要监听的数据发生变化,它就会触发相应的操作,而computed
具有缓存功能,只有属性值被访问且依赖的其它变量发生变化时,才会执行计算函数。 - 是否支持异步:
computed
不支持异步操作,需要返回值,否则计算属性不会生效。而watch
支持异步操作,不需要返回值。
c. 关于路由跳转
1. 使用`$router.push`进行`vue页面`间的`跳转`(会向`vue的history`中添加记录):
(1) 使用`name`进行跳转(推荐,因为`path`可能会移动和变化,但我们一般不会改变路由名称`name`的值)
`在路由上显示传参,可以通过this.$route.query.id获取参数(推荐使用query传参):`
this.$router.push({ name: "detail", query: { id } })
`不会在路由上显示传参,可以通过this.$route.params.id获取参数`
`页面刷新,路由中的params信息会丢失`
`params应用场景是配置动态路由,path: '/user/:id'。此时刷新页面,params不会丢失,因为路由记录了这些信息`
this.$router.push({ name: "detail", params: { id } })
(2) 使用`path`进行跳转(不推荐):
`在路由上显示传参,可以通过this.$route.query.id获取参数:`
this.$router.push(`/product/detail?id=${id}`)
this.$router.push({ path: "/product/detail", query: { id } })
`不会在路由上显示传参,可以通过this.$route.params.id获取参数`
`需要特别注意的是,path和params同时存在的情况下,params无法生效`
this.$router.push({ path: "/product/detail", params: { id } }) ❌
`如果需要使用params进行路由跳转,请使用name进行搭配`
`但使用此种方式进行的路由跳转,刷新页面会丢失路由的params信息`
this.$router.push({ name: 'detail', params: { id } }) √
2. 使用`router.replace`跳转路由(`不会记录路由的history`)
this.$router.replace(`/product/detail?id=${id}`)
this.$router.replace({ name: "detail", query: { id } })
this.$router.replace({ name: "detail", params: { id } })
this.$router.replace({ path: "/product/detail", query: { id } })
3. 使用`router-link`跳转路由:
`默认无样式,如需样式,需要自己手写`
<router-link :to="{ name: 'detail', params: { id } }">
</router-link>
<router-link :to="{ name: 'detail', query: { id } }">
</router-link>
<router-link :to="{ path: '/product/detail', query: { id } }">
</router-link>
4. 新开页面跳转路由:
let routeUrl = this.$router.resolve({
name: 'exportRecords'
})
window.open(routeUrl.href, '_blank')
d. 关于vue中的通讯方式
1. 全局通讯:
a. vueX
b. $root
(获取根组件
,即App.vue
的数据)
c. 听说过eventBus吗
`为避免内存泄漏,需要销毁监听的自定义事件,有两种解决方案:`
(1) 在组件的`beforeDestroy`钩子函数中销毁监听的自定义事件:
beforeDestroy() {
//销毁监听事件
this.$bus.off("addProduct");
}
(2) 每次$on之前$off需要销毁的事件名称;
e. 页面的路由传参
2. 父子组件之间通讯:
a.父组件使用props
向子组件传值, 子组件定义props
接收父组件传递过来的值,prop
在父子组件之间是双向绑定的。特别注意:
如果prop
是基本数据类型
, 那么在子组件中,不能直接修改父组件传递过来的prop
。但是我们可以通过语法糖
: 即给父组件挂载的prop
加上.sync
,子组件通过$emit("update:prop的名称", 需要修改的值)
达到间接修改父组件传递过来的prop
的效果(传递事件给父组件,修改父组件的prop值,因为prop是双向绑定的,从而导致子组件的prop发生变化
);
b.子组件使用$emit
向父组件传递事件和值,父组件使用@事件名
接收事件和参数
1. 父子组件使用`$emit`和`@自定义事件`传递方法:
(1) `父组件中的方法按序接收传递过来的参数`
子组件传递多个参数的情况:
this.$emit('test', 1, 2, 3)
父组件接收子组件的自定义事件:
@test="test"
test(a, b, c) {
console.log('我是接收过来的参数', a, b, c)
}
`或`
test(...params) {
console.log('我是接收过来的参数', params[0], params[1], params[2])
}
(2) `父组件使用函数中内置的arguments伪数组(且必须为这个内置参数),接收传递过来的参数`
this.$emit('test', 1, 2, 3)
@test="test(arguments)"
test(params) {
console.log('我是接收过来的参数', params[0], params[1], params[2])
}
(3) `使用对象的方式组装数据`
this.$emit('test', { age, sex, city })
@test="test"
test(params) {
console.log('我是接收过来的参数', params.age, params.sex, params.city)
}
(4) `自定义事件传递一个参数,自定义事件需要使用子组件的参数和父组件的参数:`
this.$emit('updateProductExternalSkuCode', this.selectData)
<template #productInfo="{ row }">
<productInfo
:isDetail="true"
:data="row"
:canRelation="canEdit"
@updateProductExternalSkuCode="updateProductExternalSkuCode($event, row)"
/>
</template>
(5) `自定义事件传递多个参数,自定义事件需要使用子组件的参数和父组件的参数:`
`使用arguments伪数组接收自定义事件传递过来的多个参数`
this.$emit('updateProductExternalSkuCode', this.selectData, this.data)
<template #productInfo="{ row }">
<productInfo
:isDetail="true"
:data="row"
:canRelation="canEdit"
@updateProductExternalSkuCode="updateProductExternalSkuCode(arguments, row)"
/>
</template>
2. `$emit`的扩展: 使用`$on监听本组件的自定义事件`, 后文会讲到可以`使用$once只监听一次本组件的自定义事件`
mounted() {
`因为不确定事件监听的触发时机,一般会在mounted或created钩子中来监听`
`// 在钩子函数中定义了一个方法,用于closeModal调用时再去执行`
`// 至于$on调用的方法和父组件从子组件接收来的自定义方法执行的快慢就看它们的执行机制
(同步状态下,处理相同条件,父组件更快,一方异步一方同步的状态下,同步的那方先执行,都是异步看谁先执行完)`
this.$on('closeModal',res=>{
console.log(res);
})
},
destoryed() {
`// 使用$off移除事件监听
1)如果没有提供参数,则移除所有的事件监听器;
2)如果只提供了事件,则移除该事件所有的监听器;
3)如果同时提供了事件与回调,则只移除这个回调的监听器。
`
this.$off("closeModal");
},
closeModal(){
this.$emit('closeModal')
}
3. `$emit`的扩展:
`this.$emit('update:visible',false)`, 使用双向绑定的`语法糖`,在父组件中使用`.sync`对传入的`props`进行
双向绑定,更新父组`visible`的`prop`值;
c. 父组件使用this.$refs.子组件的ref名称
获取子组件的vue实例
。$el
是针对组件的dom
元素的,this.$refs.子组件的ref名称.$el
是获取组件的dom
元素。如果this.$refs.名称
不是一个组件,则不用加.$el
,也识别不了。
d. 父组件使用this.$children
(包含所有子组件
(不包含孙子组件)的 VueComponent 对象数组
) 获取子组件的数据,例如this.$children[0].someMethod()
执行子组件的方法。对于子组件,则直接使用this.$parent
获取父组件的值。
e. 插槽
3. 祖先跨级通讯:
a. 祖先组件使用provide
返回需要传递的参数,后代组件使用inject
接收参数
// 祖先组件
provide() {
return {
`// keyName: this, // 把整个vue实例的this对象传过去,因为是同一地址,里面的name变化,值也会响应式变化`
`通过函数的方式也可以[注意,这里是把函数作为value,而不是this.changeValue()]`
keyName: this.changeValue
`// keyName: 'test' value 如果是基本类型,就无法实现响应式`
}
},
data() {
return {
name:'张三'
}
},
methods: {
changeValue(){
this.name = '改变后的名字-李四'
return this.name
}
}
// 后代组件
inject:['keyName']
create() {
`因为是函数,所以得执行才能获取响应式的数据,改变后的名字-李四`
const keyName = this.keyName()
`或者使用const keyName = this.keyName.name`
}
b. 使用$attrs
和$listeners
实现祖先的跨级通讯(详见本文第一点
)
c. 使用$dispatch
和$broadcast
实现祖先跨级通讯的事件传递( Vue.js 1.x中的语法,有点类似provide
和inject
。在现在的vue
版本中,已不再支持)
$dispatch:
主要用于向祖先组件传递事件。而它的祖先级组件,可以在组件内通过$on
监听到,从后代组件中传递过来的自定义事件$broadcast:
主要用于向后代组件广播事件。而它的后代组件,可以在组件内通过$on
监听到,从祖先组件中传递过来的自定义事件- 虽然在高版本的
vue
框架中,这两个api
已经废弃。但是,我们也可以在项目中,为每个组件,创建一个独一无二的名字name
。之后,我们可以通过递归的方式逐级向上或逐级向下,找到需要传递的后代组件或者祖先组件。在它们内部调用$emit
方法,并在组件内部调用$on
监听本组件的$emit
事件即可。而递归的方法,我们可以写在混入里进行封装,方便复用。
`混入文件:`
function broadcast(componentName, eventName, params) {
this.$children.forEach((child) => {
const name = child.$options.name
if (name === componentName) child.$emit(eventName, params)
else broadcast.call(child, componentName, eventName, params)
})
}
export default {
methods: {
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root
let name = parent.$options.name
while (parent && (!name || name !== componentName)) {
parent = parent.$parent
if (parent) name = parent.$options.name
}
if (parent) {
parent.$emit(eventName, params)
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params)
}
}
}
`子组件:`
<template>
<el-button type="primary" size="mini" @click="clickHandler">子组件</el-button>
</template>
<script>
import emitMixin from '@/mixins/emitter'
export default {
name: 'children',
mixins: [emitMixin],
mounted() {
this.$on('handleClick', (data) => {
console.log("我是来自父组件的数据", data)
})
},
methods: {
clickHandler() {
this.dispatch('home', 'clickHandler', '我是cxk,我今年18啦,我的爱好是sing, dance and rap')
}
}
}
</script>
`父组件:`
<template>
<div class="app-container">
<el-button type="primary" size="mini" @click="handleClick">父组件</el-button>
<children />
</div>
</template>
<script>
import children from './module/children'
import emitMixin from '@/mixins/emitter'
export default {
name: 'home',
mixins: [emitMixin],
components: { children },
mounted() {
this.$on('clickHandler', (data) => {
console.log('我是来自子组件的数据', data)
})
},
methods: {
handleClick() {
this.broadcast('children', 'handleClick', '我是来自父组件的数据')
}
}
}
</script>
结果截图:
4. 两个页面之间进行通讯 ( 使用postMessage
或者实时通讯websocket
等):
`前端实时通信的方式: https://www.jb51.net/article/246674.htm`
`postMessage通信方式详解: https://blog.csdn.net/huangpb123/article/details/83692019`
`阮一峰websocket详解:http://www.ruanyifeng.com/blog/2017/05/websocket.html`
`websocket插件:https://github.com/joewalnes/reconnecting-websocket`
`黑马websocket:https://www.bilibili.com/video/BV14K411T7cd?p=6&spm_id_from=pageDriver&vd_source=a540d41ff453db4580db0168b87afe38`
1. 对外mes系统首页index文件:
<template>
<div class="app-container">
<div>欢迎来到对外系统</div>
<el-button class="mt20" type="primary" @click="clickHandler">点击前往对内系统</el-button>
</div>
</template>
<script>
export default {
data() {
return {
data: {
name: 'cxk',
age: 18,
hobby: 'Sing, dance and rap',
descroption: 'Data provided by emes'
}
}
},
methods: {
clickHandler() {
`8084为对内mes系统的端口`
const currentUrl = window.open('http://localhost:8084')
`设置延迟是为了让对外mes系统的数据传输比对内mes设置监听要慢`
`只有对内mes页面先监听数据,对外mes系统才发送数据,好比打电话一样,只有接收方先接电话,才开始交流`
setTimeout(() => {
currentUrl.postMessage(this.data, '*')
}, 1000)
}
}
}
</script>
2. 对内mes系统首页index文件:
`当从对外mes首页点击按钮前往对内mes系统后,对内mes首页就会显示出对外mes传输过来的数据`
<template>
<div class="app-container">
<div>欢迎来到对内系统</div>
<div class="mt20">{{ receivedData }}</div>
</div>
</template>
<script>
export default {
mounted() {
window.addEventListener('message', (e) => {
this.receivedData = e.data
})
},
data() {
return {
receivedData: {}
}
}
}
</script>
e. 插槽
1. 在子组件A中使用`<slot></slot>`进行`占位`,父组件引入子组件A,在A下`添加的内容`会`自动转移`到`插槽占位的地方`;
2. 如果`<slot></slot>`中有内容,如果在父组件不给子组件添加内容,那么就会展示插槽的默认内容;
`需要特别注意的一点是,v-slot是无法挂载在slot上面的,有两种方式给默认插槽加上作用域:`
`a. <slot :row="personObj"></slot>`
`b. <slot v-bind="personObj"></slot>`
`区别在于,a方式传递给父组件的数据,外层由row包裹,而b方式传递给父组件的数据没有`
3. 如果未给`<slot></slot>`提供名称,那么该插槽的默认名称是`default`;
4. 如果需要给`插槽`指定名称,直接对子组件使用`name`命名即可;
`<slot name="footer"></slot>`
如果在父组件中,需要给对应`插槽`添加内容,则可以使用如下三种写法:
`此处如果不加:footer,则表示默认插槽,即vue会将父组件中未命名插槽的html代码隐式加上默认插槽。`
<template v-slot:footer>
<!-- footer 插槽的内容放这里 -->
</template>
<template #footer>
<!-- footer 插槽的内容放这里 -->
</template>
<template slot="footer">
<!-- footer 插槽的内容放这里 -->
</template>
5. `插槽`是有`作用域`的,父组件中的`插槽`内容无法访问子组件的内容,除非通过`作用域插槽`的方式进行传递:
子组件:
`<slot name="footer" :row="row"></slot>`
父组件:
<template v-slot:footer="{ row }">
<!-- footer 插槽的内容放这里 -->
</template>
<template #footer="{ row }">
<!-- footer 插槽的内容放这里 -->
</template>
<template slot="footer" slot-scope="{ row }">
<!-- footer 插槽的内容放这里 -->
</template>
在这里`扩展`一个代码的优化点,`v-if`的代码可以使用`template`包裹,`语义`会更加清晰。
6. 如果组件只有一个`插槽`,则在父组件上,可以直接使用`插槽`语法,而不需要`template`标签嵌套。
7. 自定义组件内部的`$scopedSlots`记载了组件的`作用域插槽信息`,以`key(插槽名)-value(对应函数。指定key值,
执行得到Vnode数组,对应$slots,一般更推荐使用$scopedSlots)`的形式出现。因此,根据这个特性,
当`有多个具有插槽的组件`定义在`一个自定义组件中`时,可以通过`遍历的方式动态添加插槽`
`(1) 使用$scopedSlots封装组件,动态遍历插槽(以多个具有插槽的组件为例):`
<template>
<div class="el-tree-select_component">
<el-select
ref="select"
:class="[defaultText && 'has-default-text']"
v-bind="selectProps"
:value="valueTitle"
:popperClass="concatPopperClass"
>
<template v-for="(val, key) in allSlots.inputScopedSlots" #[key]>
<slot :name="key"></slot>
</template>
<el-option :label="valueTitle" :value="valueId">
<el-tree
ref="selectTree"
:data="options || []"
:node-key="props.value"
:default-expanded-keys="defaultExpandedKey"
v-bind="treeProps"
@node-click="handleNodeClick"
>
<template v-for="(val, key) in allSlots.treeScopedSlots" #[key]="scope">
<slot :name="key" v-bind="scope"></slot>
</template>
</el-tree>
</el-option>
</el-select>
<span class="default-text" v-if="defaultText"> {{ defaultText }} </span>
</div>
</template>
const INPUT_SLOT_LIST = ['prefix', 'empty']
computed: {
// 获取select组件和tree组件的插槽
allSlots({ $scopedSlots }) {
const inputScopedSlots = {}
const treeScopedSlots = {}
for (let key in $scopedSlots) {
const val = $scopedSlots[key]
if (INPUT_SLOT_LIST.includes(key)) inputScopedSlots[key] = val
else treeScopedSlots[key] = val
}
return {
inputScopedSlots,
treeScopedSlots
}
}
}
`(2) 使用$slots封装组件,动态遍历插槽(以el-input组件的二次封装为例):`
`a. 动态插槽:`
<el-input v-bind='$attrs' v-on="$listeners">
<template #[slotName] v-for="(slot, slotName) in $slots">
<slot :name="slotName" />
</template>
</el-input>
`使用el-input中定义好的插槽:`
<customInput placeholder="请输入内容" v-model="value">
<el-button slot="append" icon="el-icon-search"> </el-button>
</customInput>
`如果需要给slot插槽上的点击事件传递本组件的方法,直接绑定点击事件是行不通的,现有两种方法:
方法一:在slot插槽上添加一个div父级容器,并绑定点击事件@click="需要传入的本组件中的方法"
方法二:直接在slot上传递一个:onClick="定义的函数clickHandler"。父组件引入后,在插槽中解构出
clickHandler,然后再绑定点击事件@click="点击事件方法(定义的函数clickHandler)"
`
`children.vue
<template>
<div>
<h3>插槽$slots的用法</h3>
<slot name="header"></slot>
<slot name="main"></slot>
<slot name="footer"></slot>
</div>
</template>
`
`parent.vue
<template>
<children>
<template #[slotName] v-for="(slot, slotName) in $scopedSlots">
<slot :name="slotName" :clickHandler="clickHandler" />
</template>
</children>
</template>
<script>
import children from './children'
export default {
components: { children },
methods: {
clickHandler() {
console.log('插槽被调用了呢')
}
}
}
</script>
`
`index.vue
<template>
<parent>
<template #main>主体区域</template>
<template #footer="{ clickHandler }">
<div @click="click('footer', clickHandler)">尾部区域</div>
</template>
</parent>
</template>
<script>
import parent from './parent.vue'
export default {
components: { parent },
methods: {
click(type, clickHandler) {
console.log(`我是${type}插槽`)
clickHandler()
}
}
}
</script>
`
`b. 动态作用域插槽:(特别注意) $slots无法获取具名作用域插槽, 作用域插槽只能用$scopedSlots获取`
<el-input v-bind='$attrs' v-on="$listeners">
<template #[slotName]="slotProps" v-for="(slot, slotName) in $scopedSlots">
<slot :name="slotName" v-bind="slotProps"/>
</template>
</el-input>
f. vueX
`在平时的项目中,为了代码看上去不是那么臃肿,一般会使用多个store文件来维护vueX,比如product.js, order.js...`
`并可以通过函数的方式拿到vueX中存储的数据`
computed: {
...mapState({
has_image_gallery: (state) => state.customizer.has_image_gallery,
library: (state) => state.myImage.library.list,
meta: (state) => state.myImage.library.pagination,
last_page: (state) => state.myImage.library.pagination.last_page
})
}
g. 指令(以回到顶部组件说明)
`自定义指令中的第三个参数vnode的context记载了组件的一些信息,这个是我们比较需要关注的`
`1. 使用自定义指令,实现回到顶部的效果:`
`添加全局公共样式:`
.scroll-top-class {
position: fixed;
bottom: 120px;
right: 30px;
opacity: 0;
height: 40px;
width: 40px;
line-height: 40px;
font-size: 30px;
text-align: center;
color: #ddd;
opacity: 0;
z-index: 2021;
cursor: pointer;
border-radius: 50%;
box-shadow: 0px 0px 8px 1px #ccc;
background-color: rgba($color: #666, $alpha: 0.5);
transition: all 1s;
}
`指令挂载方法:在有滚动条的容器上,添加v-scrollTop指令, 并提供相应的值即可。如果不提供,则使用默认值`
<div class="topic-page" v-scrollTop> </div>
`指令注册方法: 同第k点, 先install, 在本文件暴露出去。然后在main.js文件中引入,并使用vue.use(引入的名称)全局注册`
`第一种方法:直接使用binding.value判断回到顶部图标出现的位置(相对推荐)`
Vue.directive('scrollTop', {
inserted(el, binding) {
`如果未设置binding.value的值,则默认为200`
`滚动条移动超过200px的距离就显示,反之则隐藏`
if (!binding.value) binding.value = 200
el.style.scrollBehavior = 'smooth'
const backEl = document.createElement('div')
backEl.className = 'scroll-top-class el-icon-top'
el.appendChild(backEl)
backEl.addEventListener('click', () => (el.scrollTop = 0))
el.addEventListener('scroll', () => {
if (el.scrollTop >= binding.value) backEl.style.opacity = 1
else backEl.style.opacity = 0
})
}
})
`第二种方法:使用binding.value,根据滚动条的总高度所占比例,间接判断回到顶部图标出现的位置`
`(不推荐,因为在产品列表无限滚动情况下,滚动条高度是动态变化的,无法适用,而且倍数不好控制)`
// 滚动条指令
Vue.directive('scrollTop', {
inserted(el, binding) {
if (binding.value >= 1 || binding.value <= 0) return new Error('v-scrollTop的绑定值需要介于0到1之间')
`获取元素的整体高度`
const elH = el.offsetHeight
`也可以给visibilityHeight定值(不推荐,无法兼容所有需要滚动条的页面)`
let visibilityHeight = 0
if (binding.value) visibilityHeight = binding.value * elH
`阈值默认为滚动区域整体高度的0.2倍`
else visibilityHeight = 0.2 * elH
`为滚动条返回顶部添加平滑的过渡效果`
el.style.scrollBehavior = 'smooth'
const backEl = document.createElement('div')
backEl.className = 'scroll-top-class el-icon-top'
`将创建的回到顶部图标作为孩子插入到el中`
el.appendChild(backEl)
backEl.addEventListener('click', () => (el.scrollTop = 0))
el.addEventListener('scroll', () => {
if (el.scrollTop >= visibilityHeight) backEl.style.opacity = 1
else backEl.style.opacity = 0
})
}
})
`2. 自定义组件,实现回到顶部的效果:`
`使用这种方式,需要在每个有回到顶部需求的文件中引入该自定义组件,并指定高度阈值及滚动条dom容器对应的字符串`
<template>
<el-button v-show="visible" class="back" @click="backTop">top</el-button>
</template>
<script>
export default {
props: {
height: {
required: false,
type: Number,
default: 200
},
target: {
required: false,
type: String,
default: '.topic-page'
}
},
data() {
return {
container: false,
visible: false
}
},
mounted() {
this.container = document.querySelector(this.target)
if (!this.container) throw new Error('target is not existed: ' + this.target)
this.container.style.scrollBehavior = 'smooth'
`最保险的做法是,使用this.$nextTick包裹下面的代码,因为vue是异步更新机制,dom可能还未更新`
this.container.addEventListener('scroll', this.scrollToTop)
this.$once('hook:beforeDestory', () => {
this.container.removeEventListener('scroll', this.scrollToTop)
})
},
methods: {
backTop() {
this.container.scrollTo({
top: 0,
behavior: 'smooth'
})
},
scrollToTop() {
this.visible = this.container.scrollTop > this.height ? true : false
}
}
}
</script>
<style lang="scss" scoped>
.back {
position: fixed;
bottom: 100px;
right: 100px;
}
</style>
点击当前dom
区域以外的地方进行的操作, 常见应用如点击模态框、下拉列表关闭等
`第一种方法:引入element-ui自带的指令`
<template>
<div class="select-container" v-clickoutside="() => handleClickOutside(row)">
</div>
</template>
<script>
import Clickoutside from 'element-ui/src/utils/clickoutside'
export default {
directives: { Clickoutside },
props: {
row: Object
},
methods: {
handleClickOutside(row) {
row.showUpIcon = false
}
}
}
</script>
`第二种方法:手动编写指令,然后挂载到dom上,此处是本人封装的指令,非element官方封装`
Vue.directive('clickoutside', {
bind(el, binding) {
function handleClickOutside(e) {
if (!el.contains(e.target)) typeof binding.value === 'function' && binding.value(e)
}
el.clickOutsideFunction = handleClickOutside
document.addEventListener('click', el.clickOutsideFunction)
},
unbind(el) {
document.removeEventListener('click', el.clickOutsideFunction)
delete el.clickOutsideFunction
}
})
h. 使用install和use进行全局注册: 使用vue.use(xx)
,就会调用xx
里面的install
方法
`lodopPrintPdf.js`
import Vue from 'vue'
import PrintBtn from './printBtn'
import merge from 'element-ui/src/utils/merge'
`手动将PrintBtn这个js对象转换为vue实例,这也是vue内部将对象文件转换为vue实例的过程`
export default async function lodopPrintPdf(option) {
`使用vue构造器,创建一个vue的子类,及子类构造器`
const ExtendPrintBtn = Vue.extend(PrintBtn)
`继承打印组件并初始化vue实例`
const vm = new ExtendPrintBtn({})
`合并option,等价于Object.assign(vm, option)`
`相当于遍历添加传入vm的prop参数`
merge(vm, option)
`调用实例的方法,js动态加载完成`
return vm.printHandler()
}
`globalConst.js`
import lodopPrintPdf from '@/components/lodopPrint/lodopPrintPdf.js'
export default {
install(Vue) {
`在vue的原型对象上挂载$lodopPrintPdf,并暴露出去`
Vue.prototype.$lodopPrintPdf = lodopPrintPdf //lodop打印pdf
}
}
`main.js`
`Vue.use的用法: 安装Vue插件。
如果插件是一个对象,必须提供 install 方法。
如果插件是一个函数,它会被作为 install 方法。`
import globalConst from '@/commons/globalConst'
Vue.use(globalConst)
`$lodopPrintPdf方法的使用`
`传入的五个参数就是前面定义的函数所接收的option值,相当于调用打印组件,传入对应的五个props`
this.$lodopPrintPdf({
type: 'html',
printable: this.$refs.label.$el,
paperSize: [841.89, 595.28],
onSuccess: this.resetLoading,
onError: this.resetLoading
})
`需要在main.js中引入该js,并使用vue.use(xx), 就可以将该组件注册为全局组件`
import CrudInput from '../crud/src/crud-input'
/* istanbul ignore next */
CrudInput.install = function (Vue) {
Vue.component(CrudInput.name, CrudInput)
}
export default CrudInput
i. 混入 && 继承(可以继承vue
公共组件)
混入: 对于具有相同逻辑的vue
文件,其实可以抽取成一个混入
,存放公共的js
代码。在使用混入
的vue
文件中,可以定义相同的变量或者方法来覆盖
混入中的变量或者方法。
类: 在混入中定义的变量和方法,很容易与vue
文件中定义的变量和方法冲突,从而被vue
文件中定义的变量和方法覆盖掉。而相比混入,类中定义的变量和方法不容易被污染,因此开发过程中,尽量多使用类来代替混入。
继承: 相比混入
,继承
更加霸道,可以继承
整个vue
文件。同时在继承
文件中,可以添加一些额外的js
代码。如果在被继承
的组件中存在这些js
变量和方法,那么继承
组件就会覆盖这些变量和方法,如果不存在则为添加。如果在继承
组件中添加html
和css
代码,不管这些代码之前是否和被继承
组件的html
和css
代码冲突,继承
组件的html
和css
代码都会以自身代码为主,不会继承被继承
组件的html
和css
代码。
<script>
import dialog from '@/extend/components/dialog/index'
export default {
extends: dialog
}
</script>
j. $props三兄弟和inherits属性
$props:当前组件接收到的 props 对象。Vue 实例代理了对其 props 对象属性的访问。
$attrs: 包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。
$listeners:包含了父作用域中的(不含.native 修饰器的)v-on事件监听器。
inherits属性
的作用是禁止传入的属性添加到组件的根元素上。默认为true,即将传入的属性添加到组件的根元素上。
应用:v-bind="$attrs"
和v-on="$listeners"
一般用于组件封装上,必须得绑定在组件上。
-
v-bind="$attrs"
相当于一个展开运算符,将从父组件传来的props
且未被当前组件props
接收的prop
挂载到组件上,使得组件具有可扩展性。如果未绑定,孙子组件可以通过this.$attrs
拿到子组件的props
,但是无法拿到父组件的props
。如果要拿到父组件的props
,则需要在子组件上绑定v-bind="$attrs"
,这样孙子组件中的this.$attrs
就指向父组件的props
(孙子组件和子组件中定义的props会排除在外)。 -
v-on="$listeners"
是把父组件的事件绑定在子组件上。因此,会有一种减少代码的小技巧。假定有这么一种情形:有祖先组件A
, 父组件B
和子组件C
,在组件c
中某个元素被点击时,需要将事件逐级向上传递到组件组件A
中。常规解决思路是,逐级向上$emit
事件。但有一种简便的思路是,我们可以利用v-on="$listeners"
,将它绑定在父组件B
上,这样就可以不用在父组件B
上再监听子组件C
传递而来的事件。
Tips:
v-bind="$attrs"
的扩展: 如果父组件传递过来的$attrs
和子组件上的props
重复,以子组件挂载的props
为基准。在我们封装组件时,一般会在子组件中给定默认配置项defaultOption
, 合并$attrs
,并将最终的合并结果挂载到子组件上。v-on="$listeners"
的扩展: 如果父组件传递过来的事件和子组件绑定的事件重复,会同时调用子组件和父组件上绑定的事件,且优先调用子组件上触发的事件(谁先调用结束看异步情况)
k. v-model语法糖
1. 在未加`.sync`的情形下,`:props`是单向绑定。在不自定义`v-model`的前提下,`v-model`其实是`v-model:value`的简写
2. <input v-model="searchText" />
`等价于`
<input :value="searchText" @input="searchText = $event.target.value" />
3. `二次封装组件`时,如果需要对双向绑定的值做处理,可以将v-model拆开:
`比如有这么一种需求,需要el-input-number组件,在给null默认值或空字符串时,会显示为0`
<template>
<el-input-number
v-bind="$attrs"
v-on="$listeners"
:value='num'
@input="$emit('input', $event)">
</el-input-number>
</template>
<script>
export default {
props: {
`value就是父组件v-model绑定的值`
value: [String, Number]
},
computed: {
num() {
return typeof this.value === 'number' ? this.value : undefined
}
}
}
</script>
`使用:num和子组件的是value双向绑定的`
<cz-input-number placeholder='请输入数量' @change="change" v-model="num"></cz-input-number>
l. 修饰符的顺序及理解
m. render函数 && 函数式组件
原创——深度剖析render函数、函数式组件与JSX之间的爱恨情仇
n. 递归组件 && 动态组件
- 递归组件:
何为递归组件
? 递归组件
就是通过调用组件自身
来实现递归。因此递归组件需要提供name
属性和递归条件
(比如是否为数组),方便自己调用。这种组件主要用于处理一些需要递归的数据,最普遍的比如树状结构
。
`利用递归组件实现el-tree的基础功能:https://juejin.cn/post/7056922161788747789`
`1. 子组件:`
<template>
<div class="tree-item">
<div v-for="item in treeData" :key="item.id">
<div class="item-title" @click="nodeClick(item)">
<span>{{ item.name }}</span>
<i
v-if="isArray(item.children)"
:class="['ml5', isOpen(item.id) ? 'el-icon-arrow-up' : 'el-icon-arrow-down']"
>
</i>
</div>
<div v-if="isArray(item.children) && isOpen(item.id)" class="item-childen">
<my-tree :treeData="item.children" @node-click="$emit('node-click', $event)"></my-tree>
</div>
</div>
</div>
</template>
<script>
import {isArray} from 'lodash'
export default {
name: 'myTree',
props: {
treeData: {
type: Array,
default: () => []
}
},
data() {
return {
expandedKeys: [] // 当前展开的节点id组成的数组
}
},
methods: {
isArray,
isOpen(id) {
return this.expandedKeys.includes(id)
},
nodeClick(item) {
`判断展开节点id组成的数组是否包含当前id。如果包含,此时点击,就是取消展开;如果不包含,此时点击,就是展开`
this.$emit('node-click', item)
`之所以要做这层判断,是为了减少不必要的逻辑,提高代码执行效率`
if(!this.isArray(item.children)) return
let index = this.expandedKeys.indexOf(item.id)
if (index > -1) this.expandedKeys.splice(index, 1)
else this.expandedKeys.push(item.id)
}
}
}
</script>
<style lang="scss" scoped>
.tree-item {
cursor: pointer;
.item-title {
padding: 4px 8px;
&:hover {
background: #eee;
}
.ml5 {
margin-left: 5px;
}
}
.item-childen {
padding-left: 20px;
}
}
</style>
`2. 父组件:`
<template>
<my-tree :tree-data="treeData" @node-click="nodeClick"></my-tree>
</template>
<script>
`静态变量,直接定义在data外就好`
const treeData = [
{ id: 1, name: '一级1' },
{
id: 2,
name: '一级2',
children: [
{ id: 3, name: '二级2-1' },
{ id: 4, name: '二级2-2' }
]
},
{
id: 5,
name: '一级3',
children: [
{
id: 6,
name: '二级3-1',
children: [
{ id: 7, name: '三级3-1-1' },
{ id: 8, name: '三级3-1-2' }
]
},
{ id: 9, name: '二级3-2' },
{ id: 10, name: '二级3-3' }
]
}
]
import myTree from './module/myTree.vue'
export default {
components: {
myTree
},
data() {
return {
treeData: treeData
}
},
methods: {
nodeClick(data) {
console.log('data', data)
}
}
}
</script>
效果浏览:
- 动态组件:
任意标签上添加:is
属性,就会成为一个动态组件
。此时,给is
属性添加上引入的组件名称,就会根据is
的当前值来动态切换组件。但为了语义化,我们最好将这个标签命名为component
。值得注意的是,组件切换的过程中会销毁上一个组件,每次进入新组件,都会触发新组件的生命周期。为了解决这一问题,我们可以使用keep-alive
对动态组件进行缓存。
<keep-alive>
<component :is="component"></component>
</keep-alive>
o. 路由守卫
在此,只对vue
组件内的路由守卫进行讨论:
- beforeRouteEnter:
(1) 进入组件前调用,此时组件还未渲染,所以该路由守卫中不存在this
;
(2) 但是如果我们一定要获取vue
的实例,我们可以给该路由守卫中的参数next
,添加一个函数回调,这个函数回调的参数就是当前vue
的实例,也就是this
。
beforeRouteEnter(to, from, next) {
next((vm)=>{
console.log(vm);
})
}
- beforeRouteLeave: 离开当前组件后触发,此时存在
this
beforeRouteLeave(to, from, next) {
// console.log(this);
next();
}
- beforeRouteUpdate: 同一组件路由传参发生变化时才触发, 也存在
this
beforeRouteUpdate(to, from, next) {
// console.log(this);
next();
}
p. 关于vue中this的基础知识(特别基础)
vue
中的this
来源:
vue scrip
脚本export default
区域以外的this
指向undefined
, 我们也可以在这个区域中定义一些变量,将这些变量挂载到data
中并使用。但是在export default
区域中,是不支持定义变量的。我们可以将它理解成一个对象,在这个对象上挂载了很多属性。而有些属性,比如created
生命周期钩子函数之所以能使用指向vue
实例的this
,是因为vue
在处理created
生命周期时,通过apply
方法,将该钩子函数指向了本文件的vue
实例,所以就可以通过this
拿到本文件的vue
实例了。
vue
中的this
理解:
一个vue
文件分为html模板
、js文件
以及css文件
,这些文件是相互独立
的。如果script
下有引入方法,且该方法没有挂载
到methods
中。因为vue
中的this
指向的是该组件的vue实例本身
,所以就无法通过this.someMethods()
来调用方法
, this.someMethods()
会得到undefined
。但是由于它们位于同一个script
文件中,因此可以直接通过someMethods()
直接调用方法。html模板
中使用到的变量
和方法
其实省略了this
, 它们分别来源于挂载到data
中的响应式变量和methods
中的方法。倘若未挂载就使用,会报错。
q. 关于引入的component
组件的理解
我们在vue
文件中引入的component
组件都是以对象的形式存在,只不过vue
帮助我们进行了处理,将这个对象转换成了vue
实例,这点也可以从引入的函数式组件或者render
函数中体现出来。
r. 如何写出可维护的vue
代码?
vue
的核心思想是数据驱动视图。非必要情况下忌讳父组件直接使用$refs
去操作子组件的dom
,这种方式可能会在子组件中添加一些本组件未使用到而在父组件中使用到的方法。一种比较好的解决方法是,通过在子组件中定义props,监听到props的变化后再去子组件中执行对应的逻辑,然后在父组件中绑定props- 需要判断很多种条件的情况下,其实可以使用
js对象配置的方式
去操作。对于不同特殊情况,可以通过配置,调用不同的接口,定义不同的方法,传不同的参数去解决问题 - 忌讳使用很多变量去定义某些特殊状态。这些变化其实和代码中定义的某些变量挂钩,此时
使用计算属性
去解决即可 - 不要在代码中出现类似
this.变量 == 1
的代码,需要定义常量
、注释常量
并引入常量
去解决,这样做的好处是有助于后续的开发和维护 - 一个方法忌讳写很大一串不同逻辑的代码,不同逻辑的代码需要抽成不同方法,然后定义在这个方法中,即一个方法处理一个逻辑
- 使用项目中封装好的全局样式、颜色常量以及字体常量等公共资源,方便维护
- 使用
组件化的思想
封装vue
文件 - 公共
js
抽出来放到混入中 - 变量的命名要合理,不能出现很奇葩的命名干扰开发
2. vue的冷知识(vue间谍
,大部分内容来自Sunshine_Lin
的掘金博客。部分内容有自己的思考和扩展, 以扩展
两字进行标注)
a. 为什么不建议v-for和v-if同时存在?
<div v-for="item in [1, 2, 3, 4, 5, 6, 7]" v-if="item !== 3">
{{item}}
</div>
`拓展:`
`vue2中的v-for优先级高于v-if, vue3则相反。首先会把7个元素都遍历出来,然后再一个个判断是否为3,并把3的dom给隐藏掉,
这样的坏处就是,渲染了无用的3节点,增加无用的dom操作,建议使用computed来解决这个问题:`
`有一个小技巧就是,给模态框组件加上v-if,当控制模态框的visible值为true时,会重新触发模态框的生命周期。
这点和给组件绑定:key值很像,会刷新组件`
`template标签上不能使用v-if, 因为指令需要挂载到元素上`
<div v-for="item in list">
{{item}}
</div>
computed() {
list() {
return [1, 2, 3, 4, 5, 6, 7].filter(item => item !== 3)
}
}
b. 为什么不建议用index做key,为什么不建议用随机数做key?
<div v-for="(item, index) in list" :key="index">{{item.name}}</div>
list: [
{ name: '小明', id: '123' },
{ name: '小红', id: '124' },
{ name: '小花', id: '125' }
]
渲染为
<div key="0">小明</div>
<div key="1">小红</div>
<div key="2">小花</div>
现在我执行 list.unshift({ name: '小林', id: '122' })
渲染为
<div key="0">小林</div>
<div key="1">小明</div>
<div key="2">小红</div>
<div key="3">小花</div>
新旧对比
<div key="0">小明</div> <div key="0">小林</div>
<div key="1">小红</div> <div key="1">小明</div>
<div key="2">小花</div> <div key="2">小红</div>
<div key="3">小花</div>
可以看出,如果用index做key的话,其实是更新了原有的三项,并新增了小花,虽然达到了渲染目的,但是损耗性能
现在我们使用id来做key,渲染为
<div key="123">小明</div>
<div key="124">小红</div>
<div key="125">小花</div>
现在我执行 list.unshift({ name: '小林', id: '122' }),渲染为
<div key="122">小林</div>
<div key="123">小明</div>
<div key="124">小红</div>
<div key="125">小花</div>
新旧对比
<div key="122">小林</div>
<div key="123">小明</div> <div key="123">小明</div>
<div key="124">小红</div> <div key="124">小红</div>
<div key="125">小花</div> <div key="125">小花</div>
可以看出,原有的三项都不变,只是新增了小林这个人,这才是最理想的结果
用index
和用随机数
都是同理,随机数
每次都在变,做不到专一性,很渣男
,也很消耗性能,所以,拒绝渣男
,选择老实人
c. 为什么data是个函数并且返回一个对象呢?
`data`之所以是一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行`data函数`并返回新的数据对象。
这样,可以避免多处调用的`数据污染`。
d. vue2的内部指令
`拓展:`
1. `v-text`的用法:
本质其实和`插值表达式`一样。不同的是,`v-text`后面必须得`赋值`,它只与`赋予的变量的值`有关。比如:
<div v-text="123"> {{ unsignedCountries }} </div>
此处只`渲染123`, 而不会渲染`计算属性`的值。
2. `v-once`:指令所绑定的`html标签`和`组件`中的变量只会渲染一次,`不会随着变量的变化而变化`,利用好这一点可以优化
e. vue2的生命周期
`拓展:`
`1. vue生命周期钩子函数的组成:`
`vue的生命周期`是一个`组件/实例`从`创建到销毁`的过程中自动执行的函数,主要分为:`创建、挂载、更新、销毁`四个模块。
`挂载`:`$el`和`$mount`都是为了将`实例化后的vue挂载`到指定的`dom元素`中,但是`$el`的优先级要高于`$mount`。
如果`实例化vue`的时候指定`el`,则`vue`将会渲染到`此el对应的dom`中,
反之,若没有指定`el`,则`vue实例`会处于一种`未挂载`的状态,此时可以通过`$mount`来手动`执行挂载`。
`2. 被keep-alive缓存的组件的生命周期:`
第一次进入,`created` -> `mounted` -> `activated`,
退出时触发`deactivated`。当再次进入时,只触发`activated`。
`3. 创建期间的生命周期函数:`
`beforeCreate`:`vue实例`刚在内存中被创建出来,此时还没有初始化好`data`、计算属性以及`methods`等。
`虽然在该钩子函数中,可以获取到this,但无法获取到该vue文件中的data数据和methods方法。`
`created`:实例已经在内存中创建完毕,此时的`data`和`methods`也已经创建好了,但是还没开始编译模板。
`beforeMount`:此时已经完成了模板的`编译`,但是还没有挂载到页面中。
`换句话说,此时页面中的类似 {{ msg }} 这样的语法还没有被替换成真正的数据。`
`mounted`:此时已经将编译好的模板,挂载到了页面指定的容器中显示,可以获取`DOM`节点, 发起异步请求,
用户已经可以看到渲染好的页面了。
`4. 运行期间的生命周期函数beforeUpdate(需要特别注意,该钩子函数中的data值都是最新的数据)与updated:`
`beforeUpdate钩子`: 数据发生变化,`dom`更新之前执行此函数, 此时`data`中的值是最新的,但是界面上显示的还是旧数据,
因为此时还`没有重新渲染DOM`节点。
`updated钩子`: `dom`更新完毕之后调用此函数,此时`data`中的值也是最新的,而且`dom`已经被重新渲染好了。
`5. 销毁期间的生命周期函数:
`
`beforeDestroy`: `vue实例销毁`之前调用。在这一步,`实例`仍然完全可用。
`destroyed`:`vue实例`销毁后调用。调用后,`vue实例`指示的所有东西都会解绑,所有的事件监听器会被移除,
`所有的子实例也会被销毁`。
f. 如何设置动态class,动态style?
(1) 动态class
(对象形式):
<div :class="{ 'is-active': true, 'red': isRed }"></div>
(2) 动态class
(数组形式):
拓展: <div :class="['is-active', isRed && 'red' ]"></div>
(3) 动态style
对象(其中的css属性
必须使用驼峰命名
):
<div :style="{ color: textColor, fontSize: '18px' }"></div>
(4) 动态style
数组(使用多个对象
包裹,对象
之间的css属性
使用逗号
隔开):
<div :style="[{ color: textColor, fontSize: '18px' }, { fontWeight: '300' }]"></div>
g. 如何处理非响应式数据?
在我们的Vue开发中,会有一些数据,从始至终都未曾改变过
,这种死数据
,既然不改变
,那也就不需要对他做响应式处理
了,不然只会做一些无用功消耗性能,比如一些写死的下拉框,写死的表格数据,这些数据量大的死数据
,如果都进行响应式处理,那会消耗大量性能。
// 方法一:将数据定义在data之外
data () {
`拓展:这种非响应式的数据可以直接定义,包括在methods方法里。
但是这类数据如果在template中使用,后续也使用$set动态更新,虽然数据会发生变化,但是视图不变,
因为并未挂载到data函数中的return对象中,无法收集依赖。
`
this.list1 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list2 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list3 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list4 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list5 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
return {}
}
// 方法二:Object.freeze()
data () {
return {
list1: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list2: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list3: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list4: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list5: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
}
}
h. 父子组件的生命周期?
挂载阶段: 父beforeCreate
>父created
> 父beforeMount
> 子beforeCreate
>子created
> 子beforeMount
> 子mounted
>父mounted
扩展:
子组件更新过程: 父beforeUpdate
> 子beforeUpdate
> 子updated
> 父updated
父组件更新过程(影响到子组件): 父beforeUpdate
> 子beforeUpdate
> 子updated
> 父updated
父组件更新过程(不影响子组件): 父beforeUpdate
> 父updated
销毁过程: 父beforeDestroy
> 子beforeDestroy
> 子destroyed
> 父destroyed
h. Vue采用的是异步更新
的策略,通俗点说就是,同一事件循环内
多次修改,会统一
进行一次视图更新
,这样才会节省性能。
i. 关于props的自定义校验
props: {
num: {
default: 1,
`// type:Number, 指定props的类型`
`// required: true, 指定props是否为必填项`
validator: function (value) {
// 返回值为false则验证不通过,报错
return [1, 2, 3, 4, 5].indexOf(value) !== -1
}
}
}
`拓展:`
`如果props没有默认值,一般的写法是:`
props: {
num: Number
}
`如果props有默认值,一般的写法是:`
props: {
num: {
type: Number,
default: 1
}
}
`特别地,如果需要给对象或者数组指定默认值,一般的写法是:`
props: {
selectList: {
type: Array,
default: () => []
}
}
默认值使用`函数的形式`返回的原因,其实和`data为什么需要返回一个函数`大同小异。
多个`父组件`引用`同一个子组件`时,引用类型`props`要`相互隔离`。
`如果需要为props的类型指定多个值,可以使用数组包裹多种类型`
props: {
selectList: {
type: [Array, Object]
}
}
i. 使用this.$options.data()获取vue文件
data函数返回的初始对象
状态
vue
实例属性$options
是一个对象, 可以调用vue
各个组件下的方法和数据。即new vue({})
大括号内的东西,统称为$options
。因此,如果我们需要在模态框关闭时,初始化模态框中的数据, 可以使用如下代码:
`扩展:`
onClose() {
const data = this.$options.data()
Object.keys(data).map((prop) => {
this[prop] = data[prop]
})
}
j. 自定义v-model
默认情况下,v-model
是 @input 事件侦听器
和 :value 属性
上的语法糖。但是,你可以在你的Vue组件
中指定一个模型属性
来定义使用什么事件
和value属性
——非常棒!
export default: {
model: {
event: 'change',
prop: 'checked'
}
}
k. 给组件绑定动态key值
,当key
值变化时,可以刷新组件
,重新走组件的生命周期
l. 动态指令和动态参数的使用
`拓展: 可以理解为,在template中使用[]传递动态参数,
用 @[响应式变量名] 实现动态自定义事件, 使用 :[响应式变量名] 实现动态props
`
<template>
...
<aButton @[someEvent]="handleSomeEvent()" :[someProps]="1000" />...
</template>
<script>
...
data(){
return{
...
someEvent: someCondition ? "click" : "dbclick",
someProps: someCondition ? "num" : "price"
}
},
methods: {
handleSomeEvent(){
// handle some event
}
}
</script>
m. (拓展) hook和$once的使用
`1. $once的介绍:
**$once先订阅,触发发布时机时,才能发布**
**如果先发布,$once再去订阅,此时是不会触发发布的**
(1) $once是一个函数,可以为Vue组件实例绑定一个自定义事件,但该事件只能被触发一次,触发之后随即被移除。
(2) $once有两个参数,第一个参数为字符串类型,用来指定绑定的事件名称,第二个参数设置事件的回调函数。
(3) $once可以多次为同一个事件绑定多个回调,触发时,回调函数按照绑定顺序依次执行。
(4) once可以作为修饰符,.once只会触发一次 `
<template>
<div>
<button @click="$emit('clickHander')">按钮</button>
</div>
</template>
<script>
export default {
mounted() {
`在按钮第一次点击时,会先后调用两次回调函数`
`此后再点击按钮,不会触发回调函数`
this.$once('clickHander', () => {
console.log('第一次:该事件只能够被触发一次,触发后立刻被移除');
});
this.$once('clickHander', () => {
console.log('第二次:该事件只能够被触发一次,触发后立刻被移除');
});
}
}
</script>
`2. 使用$once清除定时器:`
通常的代码:`使用这种方式会多定义一个响应式变量timer,而且需要分别在两个生命周期里定义定时器以及清除定时器。`
export default{
data(){
timer: null
},
mounted(){
this.timer = setInterval(() => {
//具体执行内容
console.log('1')
},1000)
}
beforeDestory(){
clearInterval(this.timer)
this.timer = null
}
}
`使用$once的方法解决问题会更加优雅:`
export default{
mounted(){
let timer = setInterval(() => {
//具体执行内容
console.log('1')
},1000);
this.$once('hook:beforeDestroy',() => {
clearInterval(timer)
timer = null
})
}
}
`3. 使用监听生命周期钩子的hook进行父子组件之间的事件传递:`
通常的代码:
//父组件
<rl-child @childMounted="childMountedHandle"
/>
method () {
childMountedHandle() {
// do something...
}
},
// 子组件
mounted () {
this.$emit('childMounted')
},
`使用hook写出的优雅代码:`
//父组件
<rl-child @hook:mounted="childMountedHandle"
/>
method () {
childMountedHandle() {
// do something...
}
}
n. (拓展) v-for的使用 ( 非必须,指定key
值方便diff算法
对新旧虚拟dom
进行比较,提升效率)
1. `v-for在数组中的遍历(item 是数组的每一项,index 对应数组的索引,同时支持解构)`
<div v-for="({name, id}, index) in cityArr" :key="id"> </div>
2. `v-for在对象中的遍历(value, key, index分别对应值、键、索引)`
<div v-for="(value, key, index) in form" :key="key"> </div>
3. `v-for迭代数字(从1开始打印,1 2 3 4)`
<div v-for="count in 4" :key="count"> {{ count }} </div>
4. `v-for迭代数字(输出每一个字符)`
<div v-for="str in 'hello'"> {{ str }} </li>
o. (拓展) vue2组件为什么只能有一个根节点?
`结论`: `vue2`组件只能有一个根节点,但是在`vue3组件`中,可以有`多个根节点`。
`原因:`
(1) `vue2`的`虚拟dom`是一颗`单根树形结构`,`patch方法`在遍历的时候`从根节点开始遍历和比较`,
它要求组件只有一个`根节点`,组件会转换为一个`虚拟dom`。
(2) `vue3`引入了`Fragment`的概念。这是一个抽象的节点,如果发现组件有多个根,就创建一个`Fragment节点`,
把`多个根节点`作为它的`children`。
p. (拓展) 使用vue.config
全局配置,在开发阶段
获取组件的错误信息
`vue.config是一个对象,包含vue的很多全局配置,这里不一一展开介绍,有兴趣的朋友请移步vue官网`
`不过vue已经对组件名称进行处理,我们并不清楚具体指代,所以这个扩展我们看看就好,知道就行,最好的是sentry配置`
.在`main.js`中进行`全局配置`:
Vue.config.errorHandler = function(err, vm, info) {
console.log(`组件${vm.$vnode.tag}发生错误:${err.message},${info}`)
}
`sentry上注册一个账号,并创建一个vue项目`
1.`在main.js中,引入配置sentry的index文件, 获取对应的dsn地址`
import '@/sentry'
2.`index.js配置(参照官网)`
import Vue from 'vue'
import router from '../router'
import * as Sentry from '@sentry/vue'
import { BrowserTracing } from '@sentry/tracing'
const needSentry = ['pro', 'localdev', 'pet', 'test', 'sit'].includes(process.env.VUE_APP_ENV_STAGE)
const isProduction = process.env.NODE_ENV === 'production'
if (needSentry && isProduction) {
Sentry.init({
Vue,
dsn: '请填写sentry对应的dsn地址',
integrations: [
new BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(router),
tracingOrigins: [process.env.VUE_APP_BASE_URL]
})
],
beforeSend(event, hint) {
const error = hint.originalException
Sentry.configureScope(function (scope) {
scope.setLevel('error')
})
if (
error &&
error.message &&
error.detailMessage
) {
event.exception.values[0].value = `message: ${error.message}\n detailMessage: ${error.detailMessage}`
}
return event
},
tracesSampleRate: 1.0
})
}
q. 不同组件使用相同ref
的注意事项
3.vue2.x
的底层原理(vue刺客
)
a. vue2.x
在初始化的过程中做了什么事?
`原文传送门:https://blog.csdn.net/weixin_37517329/article/details/121861153`
`总结:`
1. `选项合并`,处理组件的配置内容,将`传入的options`与`构造函数本身的options`进行合并
(`用户选项和系统默认的选项进行合并`,与本文第一点:`vue常用知识点总结`中的`第k点`中的`merge`类似)
(初始化做了一些`性能优化`,将组件配置对象上的一些深层次属性放到`vm.$options`选项中,以提高代码的执行效率)
2. `初始化组件实例的关系属性`,如`$parent`、`$children`、`$root`、`$refs`等
3.`初始化自定义组件事件的监听`, 若存在父监听事件, 则添加到该实例上
4. 初始化`render渲染`所需的`slots`、`渲染函数`等。
(其实就两件事:`插槽的处理`和`$createElm的声明`,也就是`render函数中的h的声明`)
5. `调用beforeCreate钩子函数`,在这里就能看出一个组件在创建前和后分别做了哪些初始化
6. `初始化注入数据`,隔代传参时先`inject`。
(作为一个组件,在要给后辈组件提供数据之前,需要先把`祖辈传下来的数据注入`进来)
7. 对`props`,`methods`,`data`,`computed`,`watch`进行初始化,包括`响应式的处理`
8. 在`把祖辈传下来的数据注入`进来以后, 再`初始化provide`
9. 调用`created`钩子函数,初始化完成后,就可以执行挂载,进入挂载阶段
b. vue2.x
在挂载的过程中做了什么?
c. vue2.x
的响应式原理?
(1) vue2.x
重写了数组
的部分原生方法
:
响应式方法(会改变原数组,简略复习下js)
unshift:
向数组
的头部增加一条记录数据
, 返回值是增加的数据
,会改变原数组
。
var a = [1, 2, 3]
var b = a.unshift(4)
a `结果是 [4, 1, 2, 3]`
b `结果是4`
push:
向数组
的末尾增加一条记录数据
, 返回值是增加的数据
,会改变原数组
。
var a = [1, 2, 3]
var b = a.push(4)
a `结果是[1, 2, 3, 4]`
b `结果是4`
shift:
删除数组
中的第一条记录, 返回值是删除的记录
,会改变原数组
。
var a = [1, 2, 3]
var b = a.shift()
a `结果是[2, 3]`
b `结果是1`
pop:
删除数组
中的最后一条记录, 返回值是删除的记录
,会改变原数组
。
var a = [1, 2, 3]
var b = a.pop()
a `结果是[1, 2]`
b `结果是3`
sort:
排序
方法,会改变原数组
。
`a. 如果未指定函数,则默认根据元素按照转换为的字符串的各个字符的Unicode位点进行排序:`
var arr = [12, 13, 24, 46, 49, 32, 34]
arr.sort() // (7) [12, 13, 24, 32, 34, 46, 49]
['Javascript','Vue','React','Node','Webpack'].sort() //
`b. 指定函数的情况下:`
如果想按照其他标准进行排序,就需提供比较函数compareFunction(a,b),数组会按照调用该函数的返回值排序,
即a和b是两个将要比较的元素:
1.如果compareFunction(a,b)小于0,则a排列到b之前;
2.如果 compareFunction(a, b)等于0,a和b的相对位置不变(并不保证);
3.如果 compareFunction(a, b)大于0,b排列到a之前;
let Users = [
{name:'鸣人', age:16},
{name:'卡卡西', age:28},
{name:'自来也', age:50},
{name:'佐助', age:17}
];
Users.sort((a, b) => {
return a.age - b.age
})
// => 鸣人、佐助、卡卡西、自来也的对象数组
reverse:
对原数组
进行翻转
,会改变原数组
。因为原数组
的引用地址未发生变化,所以原数组
和翻转后的数组
结果一样。
var a = [1,2,3,4]
var b = a.reverse()
a `结果是[4, 3, 2, 1]`
a === b `true, 因为翻转后的原数组和原数组都指向用一个内存地址`
splice:
向数组添加
或者删除
元素,返回删除的元素
,会改变原数组
。
`参数说明:
(1) 第一个参数为插入元素或者删除元素的位置(从0开始)
(2) 第二个参数为要删除的元素数量
(3) (从删除的位置开始)后面的参数都会依次添加到数组中
`
const arr1 = [1, 2, 3, 4, 5]
const arr2 = [1, 2, 3, 4, 5]
const arr3 = [1, 2, 3, 4, 5]
const a = arr1.splice(0, 2) `输出: [1, 2]; 原数组: [3, 4, 5]`
const b = arr2.splice(3) `输出: [4, 5]; 原数组: [1, 2, 3]`
const c = arr3.splice(3, 1, "a", "b", "c") `输出: [4]; 原数组: [1, 2, 3, "a", "b", "c", 5]`
import { def } from './utils'
const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)
const methodsNeedChange = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
`重写数组原型链上的方法,先将数组转换为响应式数组,触发watcher监听,并返回数组方法的执行结果`
methodsNeedChange.forEach(methodName => {
const original = arrayPrototype[methodName]
def(
arrayMethods,
methodName,
function () {
const result = original.apply(this, arguments)
const arr = [...arguments]
const ob = this['__ob__']
let inserted = []
switch (methodName) {
`对可能会增加数组索引的操作,找到新增的数据,并重新手动Observer`
case 'push':
case 'unshift':
inserted = arr
break
`splice插入的数据是第三个参数,找到新增的数据, 并重新手动Observer`
case 'splice':
inserted = arr.slice(2)
break
}
`判断是因为inserted可能为空数组,只有数组有值才手动Observer`
if (inserted.length) {
ob.observeArray(inserted)
}
`重点:每次使用数组的响应式方法,都会触发watcher监听,更新数组的值,并在vue的下一个周期渲染dom。`
ob.dep.notify()
return result
},
false
)
})
非响应式方法举例(不会改变原数组,简略复习下js)
slice
: 用于字符串
或者数组
的截取
。
`参数说明:
(1) 第一个参数为截取的起始位置(从0开始,包含)
(2) 第二个参数为截取的结束位置(不包含,如果第二个参数不取值,则从起始位置截取到数组末尾)
`
const array = [1, 2, 3, 4, 5]
const array1 = array.slice(0, 2) `输出: [1, 2]`
const array2 = array.slice(2, 3) `输出: [3]`
const array3 = array.slice(3) `输出: [4, 5]`
const array4 = array.slice(-4, -3) `输出: [2], 从倒数第四个取到倒数第三个,但不包含倒数第三个`
const array4 = array.slice(-3, -4) `输出: [], 只能从前往后取,不能从后往前取,故为空数组`
`特别说明:
因为slice是非响应式方法,切记不能直接对响应式数组变量使用slice方法,页面无法获取最新的数据
`
`举个栗子,对于定义在data中的响应式变量arr:`
this.arr.slice(0, 2)
`这种做法是不可取的,无法达到响应式的效果,但是可以通过定义一个中间引用类型变量去接收,并将地址重新赋值给响应式变量`
const tempArr = this.arr.slice(0, 2)
this.arr = tempArr
`这样就可以达到响应式效果了`
(2)非响应式操作情况:
- 对于数组:为空数组新增索引并赋值,比如为第一项新增
name
字段,并赋值为cxk
(可以使用this.$set(this.list[0], 'name', 'cxk')
实现响应式)。 - 对于对象:直接给对象新增
key
并赋值(可以使用类似this.$set(this.person, 'name', 'cxk')
的写法实现响应式)或者直接使用delete
删除已有的key
, 都无法达到响应式的效果(结果是key
对应的value
还是显示在网页上,更不会因为后续value
的变化而变化。 可以使用vue
内置api
属性$delete
就可以在页面上删除已有的value
,如this.$delete(this.person, 'name')
)。
Tips
:当然,对于第一种情况,我们也可以使用重写的splice
方法实现响应式:this.idList.splice(this.currentIndex, 1, this.modifiedId)
给对象新增key
并赋值的vue
案例说明:
- 假定一个
vue
文件的结构:
<template>
<div class="app-container">
{{ obj.name }}
{{ obj.age }}
<el-button type="primary" size="mini" @click="clickHandler">change</el-button>
</div>
</template>
<script>
export default {
data() {
return {
obj: {
name: 'cxk'
}
}
},
methods: {
clickHandler() {
this.obj.age = 20
}
}
}
</script>
- 如果我们使用
created
钩子函数给obj
添加age
字段
created() {
`由于在created生命周期中,页面dom还未渲染完毕,新增字段后,在mounted生命周期渲染dom,所以初始化能显示age的值`
`此时如果点击按钮,虽然obj中的age发生了变化,但由于这种方法不是响应式,所以能获取但无法渲染最新的数据`
this.obj.age = 18
`用vue内置的api方法$set可以达到响应式的效果,在点击按钮后,能获取并渲染最新的数据`
this.$set(this.obj, 'age', 18)
}
- 如果我们使用
mounted
钩子函数给obj
添加age
字段
`注意与created钩子的区别`
mounted() {
`页面初始化就能获取但无法渲染age的值,这是因为mounted生命周期已经完成dom的渲染,此时再新增字段,由于不是响应式`
`所以无法在页面初始化时,通知页面重新实时渲染age的值,同样由于非响应式的特点,在点击按钮时也无法渲染最新的age值`
this.obj.age = 18
`而使用$set则可以达到响应式的效果`
this.$set(this.obj, 'age', 18)
}
给对象重新分配地址,也是响应式的:
`页面初始化时,name和age字段并没有显示。而在点击按钮,给对象重新分配地址后,此时页面就能正常显示name和age字段`
<template>
<div class="app-container">
{{ obj.name }}
{{ obj.age }}
<el-button type="primary" @click="clickHandler">change</el-button>
</div>
</template>
<script>
export default {
data() {
return {
obj: {}
}
},
methods: {
clickHandler() {
this.obj = {
name: 'test',
age: 20
}
}
}
}
</script>
关于$set
的补充:$set
无法对对象上已经存在的key
做响应式处理
<template>
<div class="app-container">
<div class="mb10">{{ obj.sex }}</div>
<el-button size="small" type="primary" @click="changeHandler">change</el-button>
</div>
</template>
<script>
export default {
data() {
return {
obj: {
name: 'cxk'
}
}
},
mounted() {
`直接给对象增加sex属性是非响应式的`
this.obj.sex = 'male'
},
methods: {
changeHandler() {
`$set无法对对象上已经存在的key做响应式处理`
this.$set(this.obj, 'sex', 'female')
`此时obj虽然发生了变化,但无法在dom上响应式变化`
`但是,如果我们在mounted阶段,去掉给对象增加sex属性的操作,再用$set增加字段,是可以达到响应式效果的`
`为什么vue要做判断对象上是否已存在key值的判断呢?这是为了提高vue性能所作出的举措`
`只要这个key已经存在在对象上,则默认vue已经收集过了,已经双向绑定过了,无需再次收集`
console.log("this.obj", this.obj)
}
}
}
</script>
(3) 个人理解
的vue2.x
的响应式原理:
在data
中return
的变量都是响应式变量。一个组件a在template模板中
(在js代码中使用到的data中return的变量,也是响应式变量,只是变化后不会更新dom)使用到这个定义好的响应式变量(简而言之,data中return的变量如果有在template模板中使用,就会收集依赖。特别注意,在js中对未定义在template中的变量进行修改,是不会触发依赖收集的),就会收集依赖。那什么是依赖呢?打印data中定义的变量,会展示subs
及subs
下对应的watcher
,这个watcher
就是依赖。一个vue
组件a如果在template
中使用到data
中定义的变量val
,那么val
就会收集到一个本组件的依赖watcher
。而在a组件的子组件及孙子组件中,如果有n
个组件在template模板中
,有使用到a组件定义好的响应式变量val
,就会收集到n
个watcher
对象。换句话说,watcher
是和组件挂钩的,每个组件有且只有一个对应的全局渲染watcher
去监听。
- 当组件
a
中在template
中使用的val
发生变化,就会触发set
方法,刷新a组件。同时通知a组件的后代组件的template模板
中有使用到val的watcher
对象,包含template、computed以及watch里面的watcher
, 执行update
方法来更新组件的dom
。 - 当在
a
的后代组件b
中,template方法
有使用到的val
发生改变时,就会触发set
方法,刷新a
组件及b
组件。
(4) vue2
与vue3
响应式原理的对比:
vue2
响应式底层原理使用的是Object.defineProperty
vue2
无法实现响应式的情况和原因分析:
1. 为对象新增属性不能实现响应式的原因其实很简单:因为vue2
的响应式是针对属性进行劫持的,这一操作在vue2
的初始化阶段就已经完成。如果需要给对象新增的属性也实现响应式效果,需要手动observer
。
2. 使用delete
删除对象属性不能实现响应式的原因同样也很简单:因为这一操作并没有触发defineProperty
的set
方法,所以同样不会实现响应式。
3. 使用Object.defineProperty
可以监控到数组中存在的key
对应value
的变化,因为对于数组来说,key
就是索引,value
就是数组对应索引的值。而对于为数组新增的索引值,该方法是无法监测到的,除非再次手动observe
。
Tips: 重新为data
中定义的响应式对象分配新的内存地址,也会达到响应式的效果,这是因为触发了响应式对象的setter
方法。
vue2
响应式的缺点分析:
1. vue2
初始化时的递归遍历会造成性能损失;
2. 新增或删除属性需要使用$set
或者$delete
这些vue
内置的api
才能达到响应式效果;
3. 响应式效果不适用于es6
新增的Map
、Set
这些数据结构;
`简单实现下vue2的响应式原理:`
`1. 依赖收集方法:`
function defineReactive(obj, key, value) {
Object.defineProperty(obj, key, {
get() {
console.log(`获取了${key}属性`)
return value
},
set(val) {
if (value === val) return
console.log(`${key}属性的值被设置为${val}`)
value = val
}
})
}
`递归响应式方法:`
function observer(obj) {
for (const [key, value] of Object.entries(obj)) {
defineReactive(obj, key, value)
if (typeof value === 'object' && value !== null) observer(value)
}
}
`2. 对于对象:`
const obj = { name: 'cxk', age: 18, info: { sex: 'male' } }
observer(obj)
obj.name // 获取了name属性 'cxk'
obj.name = 'cxk1' // name属性的值被设置为cxk1 'cxk1'
obj.sex = 'male' // male,此时并没有触发set方法
`如果我们对更新后的obj, 手动observe。此时再获取sex的值,是会触发set方法的`
`此时只会提示: 获取了name属性 获取了age属性。这是因为:我们开始支队obj的name和age属性挂载了get和set方法`
observer(obj) // 手动observer
obj.sex // 获取了sex属性 'male'
obj.sex = 'female' // sex属性的值被设置为female 'female'
obj.info `打印:获取了info属性,并显示对应的value`
obj.info.sex `打印:获取了info属性 获取了sex属性 并显示对应的value` // 此处触发了两次get方法
obj.info.hobby = 'rap' `此处只打印获取了info属性,触发了一次set方法,因为info是响应式而非新增的hobby属性`
obj.info.hobby `此处同样只打印获取了info属性,触发一次get方法,因为info是响应式而非新增的hobby属性`
`3. 对于数组:`
const arr = [1, 2, 3]
observe(arr)
arr[0] // 获取了0属性 1
arr[2] // 获取了2属性 3
arr[0] = 4 // 0属性的值被设置为4 4
arr[0] // 获取了0属性 4
arr[3] = 5 // 5 同样可以发现未触发set方法,用push方法也同样不会触发,因为新增了属性
delete arr[0] // true, 删除成功返回true,此时同样未触发set方法
arr[0] // undefined, arr[0]此时已不再是响应式
`
defineProperty监测数组下标变化的情况总结:
a. 对于存在的索引,通过索引访问或者设置对应元素的值时,可以触发getter和setter方法;
b. 通过原生数组的push或unshift方法会为数组增加索引。对于新增的索引,需要手动observe才能触发getter和setter方法;
c. 通过原生数组的pop或shift删除元素,会删除并更新索引,对于存在的索引,也可以触发getter和setter方法;
`
vue3
是直接通过Proxy
代理目标对象,且代理的是最外层的对象,可以监听到新增的属性,性能自然会更好。
`使用递归方法封装简单实现下vue3的响应式原理:`
`考虑到对象中可能嵌套对象属性的情况,获取值的时候,如果值是对象类型,则继续获取,直到它不再为对象为止`
`所以递归的出口为不是对象类型,且不为null,并返回对应的值`
function observer(target) {
`递归的出口`
if (typeof target !== 'object' || target == null) return target
`配置代理`
const proxy = {
get(target, key, receiver) {
`Reflect.get中的第一个参数为源对象,第二个参数为源对象的key,第三个参数可以省略,为当前this的指向`
`在这个方法中,receiver参数代表代理后的对象,其实源对象和代理后的对象指向是一样的,那为什么不省略呢`
`这是因为,源对象可能也是另一个代理的代理对象,为了避免污染,就将this指向到代理后的对象,及receiver上`
const result = Reflect.get(target, key, receiver)
console.log(`获取了${key}属性`)
`深度代理,递归循环,直到value不再为对象类型且不为null就返回`
return observer(result)
},
set(target, key, val, receiver) {
if (val === target[key]) return
console.log(`设置了${key}属性,值为${val}`)
return Reflect.set(target, key, val, receiver)
},
deleteProperty(target, key) {
console.log('delete property', key)
return Reflect.deleteProperty(target, key)
}
}
// 生成代理对象
return new Proxy(target, proxy)
}
可以发现,使用proxy
作为响应式原理的实现方法,可以监测到新增的属性,这是defineProperty
所不具备的特性。与此同时,我们还可以在代理中对传入的数据做一系列譬如删除之类的拦截操作
。
(5) vue2.x
底层响应式原理解析(从控制台打印中get
响应式原理):
vue
框架MVVM
模式的理解:
总结: 在MVVM
框架下视图和模型是不能直接通信的,但是它们可以通过ViewModel
来通信, 而MVVM
中的View
和ViewModel
却可以互相通信。ViewModel
要实现一个Observer
观察者,当数据发生变化,ViewModel
能够监听到数据的这种变化,然后通知到对应的视图做自动更新;而当用户操作视图,ViewModel
也能监听到视图的变化,然后通知数据做改动,这实际上就实现了数据的双向绑定。
- 视图变化,对应数据会随之变化的效果是通过事件监听的方式实现的。在此重点讨论数据变化是怎么引起视图的变化:
- 实现一个监听器
Observer
,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者; - 实现一个订阅器
Dep
,用来收集订阅者,对监听器Observer
和 订阅者Watcher
进行统一管理; - 实现一个订阅者
Watcher
,可以收到属性的变化通知并执行相应的方法,从而更新视图; - 实现一个解析器
Compile
,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化。
- 从打印截图上的整体概览:
`index.vue 父vue文件`
<template>
<div class="app-container">
{{ obj.name }}
{{ obj1.name }}
{{ obj.hobby }}
{{ finalName }}
<test :data="obj1" />
</div>
</template>
<script>
import test from './module/test'
export default {
components: { test },
mounted() {
console.dir(this)
},
data() {
return {
name: 'cxk',
obj: {
name: 'cxk',
age: 18,
hobby: {
hobbyName: 'rap'
}
},
obj1: {
name: '1',
sex: 'male'
}
}
},
watch: {
'obj.name': {
handler(newVal, oldVal) {},
immediate: true,
deep: true
}
},
computed: {
finalName({ obj1, obj }) {
return obj.name + obj1.name
}
}
}
</script>
`test.vue 子vue文件`
<template>
<div>{{ data }}</div>
</template>
<script>
export default {
props: {
data: Object
}
}
</script>
<style lang="scss" scoped></style>
在上述截图中,忘解释了全局渲染watcher
为什么会有9个deps
,在此给出文字说明:template
模板上挂载了obj.name
(2 + 1 = 3)、obj1.name
(2 + 1 = 3)以及obj.hobby
(obj
之前已经收集过了,不会再次收集。所以,会收集obj
下的hobby
对象。hobby
对象占2个,hobbyName
占1个,2 + 1 = 3)。所以,最终会收集到3 + 3 + 3 = 9
个依赖。
扩展tips:
1. 只要watch
方法有监听data
中定义的响应式变量(哪怕这个变量未挂载到template
模板),也同样会在全局watchers
和监听响应式变量的Observer
中观察到监听watcher
这类watcher
。
2. 如果定义的计算属性未挂载到template
模板上,虽然在全局watchers
上还是会存在这类计算属性watcher
,但是与之依赖关联的变量是不会挂载这一类计算属性的。
3. 单独一个计算属性或者单独一个watch
方法对应单独的一个watcher
,并挂载到全局watchers
中。
4. 如何理解watcher
和dep
互相收集?
a. 从截图可以看出:Observes
下的dep.subs
存放的其实是这个属性的所有watcher
b. 从截图可以看出:dep.subs
中的每个watcher
都存放了这个watcher
下收集到的所有dep
实例
5. vue
的设计思想用到了那些设计模式?
发布订阅模式
又叫观察者模式
,它定义对象之间一种一对多的依赖关系
。当一个对象的状态改变时,所有依赖于它的对象都将得到通知。在vue
中,将所有依赖收集起来作为订阅者
。数据变化就通知所有依赖者
,更新dom
渲染页面,而数据的变化这个过程就称为发布者
。
6. 为什么一个对象收集两次依赖?
这是因为对象本身收集一次,对象上的__ob__
(即对象上的Observer
也收集一次)
- 监听器
Observer
Observer,是通过在Object.defineProperty
方法上定义get
和set
方法,循环遍历响应式数据的每一个属性,使响应式数据对象变得可观测。简单来说,就是我们之前封装的Observer
方法:
function defineReactive(obj, key, value) {
Object.defineProperty(obj, key, {
get() {
console.log(`获取了${key}属性`)
return value
},
set(val) {
if (value === val) return
console.log(`${key}属性的值被设置为${val}`)
value = val
}
})
}
`递归响应式方法:`
function observer(obj) {
for (const [key, value] of Object.entries(obj)) {
defineReactive(obj, key, value)
if (typeof value === 'object' && value !== null) observer(value)
}
}
- 订阅器Dep
我们需要创建一个依赖收集容器,也就是消息订阅器Dep
,用来容纳所有的订阅者
。订阅器Dep
主要负责收集订阅者
,然后当数据变化的时候后执行对应订阅者
的更新函数。
function Dep () {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
})
}
};
Dep.target = null; // 全局变量,防止重复push相同的watcher
和Observer
监听器结合到一起,重构下来就是:
function defineReactive(obj, key, value) {
var dep = new Dep()
Object.defineProperty(obj, key, {
get() {
console.log(`获取了${key}属性`)
`dep.subs收集watcher`
if(Dep.target) dep.addSub(Dep.target)
return value
},
set(val) {
if (value === val) return
console.log(`${key}属性的值被设置为${val}`)
value = val
`通知watcher执行notify方法`
dep.notify()
}
})
}
`递归响应式方法:`
function observer(obj) {
for (const [key, value] of Object.entries(obj)) {
defineReactive(obj, key, value)
if (typeof value === 'object' && value !== null) observer(value)
}
}
- 订阅者Watcher
function Watcher(vm, exp, cb) {
```
vm:Vue的实例对象;
exp:是node节点v-model等指令的属性值 或者插值符号中的属性。如:v-model="name", exp 就是name;
cb:是 Watcher 绑定的更新函数;
```
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get(); // 将自己添加到订阅器中
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
`获取挂载到data上的响应式变量的值`
var value = this.vm.data[this.exp];
var oldVal = this.value;
`如果值发生变化,就执行方法,更新dom`
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal); // 调用更新函数重新渲染dom
}
},
get: function() {
Dep.target = this; // 将watcher收集到dep.subs中
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数,触发get方法
Dep.target = null; // watcher收集完使用到的响应式变量后,释放Dep.target,防止重复push
return value;
}
}
- 解析器Compile
解析模板指令,替换模板数据,初始化视图。将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器Dep
。
compileText: function(node, exp) {
var self = this;
`获取data中响应式变量的值:`
var initText = this.vm[exp];
`更新dom节点中文本的值:`
this.updateText(node, initText);
`将这个指令初始化为一个订阅者,后续变量改变时,就会触发这个更新回调,从而更新视图`
new Watcher(this.vm, exp, function (value) {
self.updateText(node, value);
});
}
初始化:
1.`Observer`对数据进行响应式绑定;
2.`Compiler`编译解析模块指令,初始化渲染页面,将每个指令的节点绑上更新函数,并实例化监听监听数据的订阅者`Watcher`;
3.数据`getter`时,执行对应数据的`dep`收集所有`watcher`依赖;
更新:
1.更新时触发`dep.notify()`,派发通知所有订阅者`watcher`;
2.订阅者`watcher`执行`update()`回调函数;
3.调用对应`Compiler`编译解析模块,重新更新视图;
d. vue2.x
的diff算法?
结语
就这样吧~ 以上内容是对学习vue2.x
框架的一个小结。最近比较忙,后续如果有空会学习下react。