mixin
- mixins是Vue2 的选项API中常用的代码逻辑抽离手段,在Vue3中同样也可以使用,虽然很方便,但他仍有一些比较显著的缺点.但是vue3中引入的组合API中的自定义hook很好的解决了mixins带来的一些问题
- mixins是什么
- 我们在开发组件的过程中,常常会遇到一些具有相同逻辑和功能的组件
- 如果每个组件各写一套方法会导致代码冗余,后面更改就会很浪费时间
- mixins就是将这些多个相同的逻辑抽离出来.各个组件只需要引入mixins,就能实现一次写代码,多组件收益的效果
- 基本使用步骤
- 用一个js文件将vue的script部分抽离出来
export default {
data(){
return {}
},
methods:{},
computed:{},
filters:{},
created(){},
mounted(){
console.log("我是mixins");
}
}
- 需要引入mixins的组件引入即可
<template>
<div></div>
</template>
<script>
import common from '@/common/mixins/common.js'
export default{
mixins:[common]
mounted(){
console.log('123')
}
}
</script>
mixins的一些特性
- mixins的生命周期会与引入mixins组件的生命周期整合在一起调用
2. 组件的data,methods.filters.会覆盖mixin里的同名data,methods,filters
组件的同名data 会覆盖 mixins的同名data
组件的同名methods 会覆盖 mixins的同名methods
组件的同名filters 会覆盖 mixins的同名filters
虽然是具有相同逻辑的组件,但是每个组件肯定不可能完全100%相同,会有不同的属性或者不同的methods或者filters等。
所以如果组件里没有写data/methods/filters……等的话,
会自动继承mixins里的data/methods/filters……。如果写了就会以组件里定义的data/methods/filters……为准。
- 不同mixin里的同名方法,按照引进的顺序,最后的覆盖前面的同名方法
比如两个文件mixin1.js、mixin2.js
都有同名方法: test()
且我们的引入顺序是:[mixin1,mixin2]
那么最终执行的方法就是mixin2里的 test()
mixix的缺点
- 变量来源不明确,不利于阅读,代码难以维护
组件里可以引入多个mixin,并直接隐式调用mixin里的变量/方法,
这会让我们有时候混乱 这些变量/方法 分别是哪个mixin里的?
- 多个mixins的生命周期会融合到一起执行,但是同名熟悉,同名方法无法融合,可能会导致冲突
比如组件1中的方法要输出属性info,
但是组件2中也有同名属性info,且覆盖了组件1中的属性info,
那么当执行组件1中的方法时,输出的确实组件2中的属性,
这个我们可以避免,但是一不小心就会导致冲突,很容易制造混乱。
- mixins和组件可能出现多对的的关系,复杂度较高.
- 注:VUE3提出的组合API旨在解决这些问题,mixin的缺点是组合API背后的主要动因之一,而组合API又是受到了React Hooks的启发
vue3中的自定义hook函数是什么
- 使用vue3的组合API封装的可复用的功能函数
- 自定义hook的作用类似于vue2的mixin技术
- 自定义hook的优势,很清楚复用功能的来源,便于追溯
mixin与vuex的区别
vuex主要是做状态管理的,里面定义的变量在每个组件中均可以使用和修改,在任一组件修改次变量的值之后,其它组件中此变量的值也会随之修改
mixins 可以定义公用的变量,在每个组件中使用,引入组件中之后各个变量时相互独立的,值的修改在组件中不会互相影响
与公共组件的区别
组件: 在父组件中引入组件,相当于在父组件中给出一片独立的空间供子组件使用,然后根据props来传值,但本质上两者是相互独立的
mixins: 是在引入组件之后与组件中的对象和方法进行合并,相当于扩展了父组件的对象与方法,可以理解为形成了一个新的组件
组件传值
- 父子组件
父=>子在子标签上向子组件传值,子组件用props进行接收.用对象的形式对传递的值做一些要求,比如type类型, required是否必传,validator做一些验证处理子=>父由于vue的单项数据流,不建议子组件直接修改父组件的值,而是在子组件中利用$emit处理,第一个参数是事件名,第二个参数是传递的实参,同样是在子组件的标签上触发父组件的方法让父组件对值进行处理$parent和$children同样也是父子组件的传值,但这种方式不被官方推荐使用
祖孙传值在vue中子孙传值是利用provide和inject进行传值,provide中接受一个key,value键值对,inject中直接输入key参数就可以获取对应的value值,但是这种传值方法会对祖代组件下的所有子孙后代进行传值,因为它是遍历使用js中的Object.create()方法改变原型对象指向来实现的,因此非父子组件传值一般会使用eventbuseventbus我们使用eventbus传值一般会在main.js中使用$bus全局挂载,传值时使用的是发布订阅模式发布者使用$emit进行传值,订阅者使用$on进行接受,一般订阅时会在较早的生命周期就开始订阅,防止遗漏发布者传递的值$attrs和$listeners的使用,下图表示一个多级组件嵌套的情况
如果我们希望A组件与C组件进行通信,有多少中解决方案
- 我们可以使用vuex进行数据管理,但是如果项目中的多个组件共享状态比较少,项目比较小,全局状态比较少,那使用vuex有点大材小用
- 使用B来做中转站,当A组件需要把信息传给C组件时,B组件接受A组件的信息,然后利用属性传给C组件,这是一种解决方案,但是如果嵌套的组件过多,会导致代码繁琐,代码维护比较困难,如果C中状态额改变需要传递给A使用事件系统一级级往上传递
- 自定义一个vue中央数据总线,这个情况是个碰到组件跨级传递消息,但是碰到多人合作,代码的维护性较低,代码可读性低
- 在vue2.4中,为了解决需求,引入了
$attrs和$listeners,新增了inheritAttrs选项.在2.4以前,默认情况下父作用域的不被认作props的属性将会回退且作为普通的HTML特性应用在子组件的跟标签上
// 父组件demo代码
<template>
<div>
<child-dom
:foo = 'foo'
:coo = 'foo'
>
</child-dom>
</div>
</template>
<script>
import childDom from './ChildDom.vue'
export default {
data(){
return {
foo:'Hello,world",
coo:'Hello,rui'
}
}
</script>
<=======================================================>
// 子组件代码demo
<template>
<div>
<p>foo:{{foo}}</p>
</div>
</template>
<script>
export default {
name:'child-dom'
props:["foo"]
}
</script>
<===================================================>
在HTML中显示的结构
<div coo='Hello,rui'>
<p>foo:Hello,world</p>
</div>
- 在2.4中新增了选项
inheritAttrs,inheritAttrs的默认值为true,如果把它的值设为false.这些默认的行为会被禁止,但是通过实例attrs,可以将这些特效生效,且可以通过v-bind绑定到子组件的非根元素上.修改子组件代码如下:
<template>
<div>
<p>foo:{{foo}}</p>
<p>attrs:{{$attrs}}</p>
<childDomChild v-bind="$attrs"></childDomChild>
</div>
</template>
<script>
import childDomChild from './childDomChild';
export default {
name:'child-dom'
props:["foo"]
inheritAttrs:false
}
</script>
<======================================================>
新增子组件 childDomChild
<template>
<div>
<p>coo:{{coo}}</p>
</div>
</template>
<script>
export default {
name:'childDomChild'
props:["coo"]
inheritAttrs:false
}
</script>
- 从上面可以看出,使用
$attrs和ingeritAttrs属性能够使用简洁的代码,将A组件的数据传递给C组件,这种使用场景还是挺广泛的 - 那么我们如果想要把C组件的信息,怎么同步给A组件呢
- 同样的vue2.4新增了
$listeners属性,我们在B组件上绑定v-on='$listeners',在a组件中,监听c组件触发的事件,就可以把c组件发出的数据,传递给a组件
// A组件代码更新如下
<template>
<div>
<child-dom
:foo="foo"
:coo="coo"
v-on:upRocket="reciveRocket"
>
</child-dom>
</div>
</template>
<script>
import childDom from "@/components/ChildDom.vue";
export default {
name:'demoNo',
data(){
return {
foo:'Hello,world",
coo:'Hello,rui'
}
},
components:{childDom},
methods:{
reciveRocket(){
console.log("reciveRocket success")
}
}
}
</script>
<==================================================>
// b组件代码更新如下
<template>
<div>
<p>foo:{{foo}}</p>
<p>attrs:{{$attrs}}</p>
<childDomChild v-bind="$attrs" v-on="$listteners"> </childDomChild>
</div>
</template>
<script>
import childDomChild from './childDomChild';
export default {
name:'child-dom'
props:['foo']
inheritAttrs:false
}
</script>
<=====================================================>
// c组件更新代码如下
<template>
<div>
<p>coo:{{coo}}</p>
<button @click='startUpRocket'>我要发射火箭</button>
</div>
</template>
<script>
export default {
name:'childDomChild',
props:['coo']
methods:{
startUpRocket(){
this.$emit('upRocket'}
console.log('startUpRocket') }
}
}
</script>
- 总结
$attrs可用于隔代子孙传递非props接受的属性值,$liseners可用于传递方法值,但是一旦传递过程中有组件接受了传递的参数,就会终止传递
vuex
- vuex同样也是组件传值的常用的方法,但是他的缺点是一旦刷新后数据就会重置,我们一般会使用一个插件 vuex-persistedstate进行数据持久化,它是和本地存储建立联系来实现的
const store = new Vuex.Store({
state:{},
mutations:{},
actions:{},
getters:{},
modules:{}
})
export default store
- 我们的vuex-persistedstate使用时需要在main.js下进行挂载,同时在src下的store下的根模块中配置plugins,设置需要进行持久化的数据
- 我们在使用vuex时一般也是分模块使用的,不同的页面用到的数据就建议对用的模块,然后在index根模块中挂载,再将根模块在main.js中进行挂载
import store from '@/store'
new Vue({store})
- vuex在对应的模块的基本语法,有namespace声明私有空间,防止命名冲突
// 以前原生js就是这样使用命名空间的
let a = {
name:'aaa'}
let b = {
name:'bbb'}
let c = {
name:'ccc'}
- 然后是state进行数据声明
state:{
userInfo:'zs'
}
// 组件中访问
this.$store.state.userInfo
state
- 在state中设置的数据都要在mutations中设置对应的修改数据的方法,mutation的形参有两个一个是state,一个是value,在组件中调用mutations的语法为
$store.commit('模块名/方法名',传递的实参)
mutations:{
setUserInfo(state,value){
state.userInfo = value
}
}
// 组件中的调用
this.$store.commit('setUserInfo',实参值)
actions
- actions中一般是设置异步代码,多用于调用我们的mutations的方法,actions在组件中的调用语法
$store.dispatch('模块名/方法名',传递的实参)
actions{
refreshUserInfo(store,value){
store.commit('setUserInfo',实参值)
}}
// 组件中调用语法
this.$store.dispatch('refreshUserInfo',实参值)
getters
它就是vuex内部的计算属性,依赖一个值或多个值产生一个新的值,他的值会进行缓存
getters:{
getUserInfo(state){
return '姓名' + state.userInfo
}}
// 组件内访问
this.$store.getters.getUserInfo
vuex中的模块化使用
作用:将vuex中的数据模块化管理,使用方法 store/modules/模块名.js
export default {
state:{
name:'zs'
// 组件内访问 this.$store.state.模块名.属性名
},
mutations:{
setName(state,value){
state.name = value
// 组件内访问 this.$store.commit('setName',实参值)
},
actions:{},
getters:{}
- 在store/index.js中导入与使用
import 模块名 from './modules/模块名.js'
modules:{
模块名
}
- 总结 在modules当中使用的语法 比如 aaa模块
export default {
namespaced:true // 是否启动命名空间,实际开发基本为true,但是默认值为false,也就是必须手动设定
state:{
// 自带了命名空间的
xxx:'zs'
// 访问 this.$store.state.模块名.xxx
},
// 下面的数据是默认不带命名空间的
mutations:{
setXxx(state,value){
state.xxx = value
// 调用mutations:this.$store.commit('模块名/方法名',实参)
}},
actions:{
actionsXxx(store){
**store**:{
// 这里面有什么
state:访问当前模块的state数据
6 访问:state.属性名
**rootState**:访问根模块的state数据
访问根模块下的state:rootState.属性名
访问其他模块下的state:rootState.模块名.属性名
**getters**:用于访问当前模块的getters
访问:getters.getters方法名
**rootGetters**:用于访问根模块的getters,通过根模块能访问到所有模块的getters
访问根模块下的getters:rootGetters.根模块下的getters方法名访问其他模块下的getters:rootGetters['模块名/其它模块的getters方法名']
**commit**:用于调用mutations方法
调用本模块的mutations方法:commit('本模块的mutations方法名',实参值)
调用其它模块的mutations方法:commit('其它模块的模块名/其它模块的mutations方法名',实参,{root:true})
**dispatch**:用于调用actions方法
调用本模块的actions方法:dispatch('本模块的actions方法名',实参值)
调用其它模块的actions方法
dispatch('其它模块的模块名/其它模块的actions方法名',实参.{root:true})
}
modules下的getters
getters:{
方法名(state,getters,rootState,rootGetters){
**state**:访问当前模块的state数据
访问:state.属性名
**rootState**:访问根模块的state数据
访问根模块下的state:rootState.属性名
访问其他模块下的state:rootState.模块名.属性名
**getters**:用于访问当前模块的getters
访问:getters.getters方法名
**rootGetters**:用于访问根模块的getters,通过根模块能访问到所有模块的getters
访问根模块下的getters:rootGetters.根模块下的getters方法名
访问其他模块下的getters:rootGetters['模块名/其他模块的getters方法名']
vuex中map的用法
mapState // 先导入
import {mapState} from 'vuex'
computed:{
...mapState(模块名.['属性名']
}
// 使用: this.属性名
mapMutations
导入 // import {mapMutations} from 'vuex'
methods:{
...mapMutations('模块名',['mutations方法名'])
}
// 调用: this.mutations方法名(实参)
mapActions
导入 // import {mapActions} from 'vuex'
methods:{
...mapActions('模块名',['actions方法名'])
// 调用: this.actions方法名(实参)}
mapGetters
import {mapGetters} from 'vuex'
computed:{
...mapGetters('模块名',['getters方法名'])
}
// 访问: this.getters方法名
路由传参
- path传参<基本路由传参>
1.传
this.$router.push('/home?id=值') // 链式写法
this.$router.push({
path:'/home',
query:{ id=值}})
2.收
对应的组件内
this.$route.query.id
- 动态路由匹配
1.配置路由
{
path:'/home/:参数名' // 这种方法表示必传 可以在后面加个?表示可选
component....
}
2.传
this.$router.push('/home/123')
3.收
this.$route.params.参数名
3.通过name传参
配置路由
{
path:'/home/'
name:'xxx'
...
}
1.传值(query传参)
this.$router.push({
name:'xxx'
query:{
参数名:值}})
2.接受
this.$route.query.参数名
<=============================================>
1.通过(params) 类似于post请求,参数值不在url上体现,
他的值是通过内存传递的,可传递大量数据,但是刷新后数据就没了,可以通过一些方式进行数据存储
this.$router.push({
name:'xxx',
params:{
参数名:值}})
路由生命周期
- 全局路由钩子
beforeEach:每次路由跳转都会执行afterEach:已进入相应路由beforeResolve:全局路由钩子的最后关卡,解析路由所对应组件前组件内的钩子(vue2) vue3中去掉了* * next参数;vue2只适用于路由所对应组件,vue3都可以用wanbeforeRouteEnter(vue3该钩子没有了) 已进入路由所对应组件,但还没有完成组件解析,不能使用this;但是可以在next中的回调函数中使用beforeRouteEnter(to,from,next){next(vm=>vm等同于this))}beforeRouteUpdate:路由更新了,路由所对应组件不变,更新前执行,如果希望更新后执行可在nextTick中使用beforeRouteLeave:离开当前路由前执行beforeEnter:读取配置前执行
- 路由生命周期执行先后
beforeEach=>beforeEnter=>beforeResolve=>afterEach=>beforeRoureEnter - 路由更新时执行先后
beforeEach=>beforeResolve=>afterEach=>beforeRouteUpdate
vue3路由使用模式
- yarn add vue-router
- import {createRouter,createWebHashHistory} from 'vue-router'
- 实例化
const router = createRouter({
mode + routers
history + routes
history:createWebHashHistory(),
routes:[
{
path,component,redirect...
}]
})
- 暴露出去 export default router
- 在main.ts挂载
import router from '@/router'
createApp(App).use(router).mount('#app')
- 路由出口
导航守卫
- 检测是否有token store.state.user.token
- 有token
- 是否所有页面正常进入?
- 如果是登录页 --那就直接跳转到首页
- 如果去的页面非登录页
- 判断是否有用户信息
- 不调用接口正常通过
- 如果没有用户信息
- 调用接口获取用户信息,且在成功后执行正常通过
- 为了验证token是否靠谱(调用接口),一般都是调用获取用户信息接口
- 如果获取到了,正常进入
- 如果没获取到,token有问题,有问题解决方法就是删除token返回到登录页
- vuex定义state用于存储用户信息,同时定义对应的mutation和actions
- actions:调用api获取用户信息,在导航守卫进入页面前触发,正常获取了用户信息放行
- 1.定义api
- 2.在vuex的user模块定义一个userInfo,定义修改他的mutations
- 3.定义一个actions导入api调用api获取用户信息并存储
- 4.在导航守卫中触发actions如果成功获取了用户信息,才正常通过
- 没token 如果在白名单页面就正常放行 如果要去的页面需要登陆,就跳转到登录页
import router from vue-router
router.beforeEach(to,from,next){
if(store.state.userInfo.token){ // 是否携带token
// 表示携带token 如果想回到登录页
if(to.path.toLowerCase() === '/login'{
next('/') // 有token直接到首页
}else{ //去的不是登录页 判断有没有用户信息
if(store.state.userInfo.id){ // 有就正常放行
next()
}else{ 没有就调接口获取
await store.dispatch('方法')
next(to.fullPath) // token过时就避免用户被强制退出
}
}
}else{ 没有token
//去的是白名单就直接放行
if(WhiteList.include(to.path.toLowerCase()){
next()
}else{//不是去白名单就打回首页并携带退出时的参数,再次登陆后回到对应页面
next('/login?redirect=' + to.fullPath)
}
}
})
Vue2响应式原理,对象基础篇
- vue之所以能数据驱动视图发生变更的关键,就是依赖他的响应式系统,响应式系统如果根据数据类型区分,对象和数组它们的实现会有所不同;解释响应式原理,如果只是为了说明响应式原理而说,但不是从整体流程出发,不在vue组件化的整体流程中找到响应式原理的位置,对深刻理解响应式原理并不友好,接下来会从整体流程出发,分别说明对象和数组的实现原理
对象的响应式原理
- 对象响应式数据的创建
- 在组件的初始化阶段,将对传入的状态进行初始化,以下以data为例,会将传入的数据包装为响应式的数据
main.js
new Vue({ // 跟标签
render:h=>h(App)
})
<==========================================================>
app.vue
<template>
<div>{{info.name}}</div> // 只用了info.name属性
</template>
export default { // app组件
data(){
return{
info:{
name:'cc'
sex:'man' // 即使是响应式数据,没被使用就不会进行依赖收集
}
}
}
}
- 接下来的分析将以上面代码为示例,这种结构其实是一个嵌套组件,只不过根组件一般定义的参数比较少而已,这个是很重要的
- 在组件new Vue()后的执行vm._init()初始化过程中,当执行到initState(vm)时就会对内部使用到的一些状态,如props,data,computed,watch,methods分别进行初始化,在对data进行初始化的最后又这么一段代码
function initData(vm){ // 初始化data
.....
observe(data) // info:{name:'cc',sex:'man'}
}
- 这个observe就是将用户定义的data变成响应式的数据,接下来看下它的创建郭恒
export function observe(value){
if(!isObject(value)){ // 不是数组或对象,返回
return
}
return new Observe(value)
}
- 简单理解这个Observe方法就是Observe这个类的工厂方法,所以还是要看下Observe这个类的定义
export class Observe{
constructor(value){
this.value = value
this.walk(value) // 遍历value
}
walk(obj){
const keys = Object.keys(obj)
for(let i =0;i<keys.length;i++){
defineReactive(obj,keys[i]) // 只传入了两个参数
}
}
}
- 当执行new Observer时,首先将传入的对象挂载到当前this下,然后遍历当前对象的没一项,执行defineReactive这个方法,看下他的定义
export function defineReactive(obj,key,val){
const dep = new Dep() // 依赖管理器
val = obj[key] // 计算出对应key的值
observe(val) // 递归包装对象的嵌套属性
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
...收集依赖
},
set(newVal){
...派发依赖
}
})
}
- 这个方法的作用就是使用Object.defineProperty创建响应式数据,首先根据传入的obj和key计算出val具体的值
- 如果val还是对象,那就使用observe方法进行递归创建,在递归的过程中使用Object.defineProperty将对象的没一个属性都变成响应式数据
....
data(){
return{
info:{
name:'cc'
sex:'man'
}
}
}
这段代码有三个响应式数据
info,info.name,info.sex
知识点:Object.defineProperty内的get方法,他的作用就是谁访问到当前keY的值就用defineReactive内的
dep将它收集起来,也就是依赖收集的意思,set方法的作用就是当前key的值被赋值了,就通知dep内收集到的依赖项,key的值发生了变更,视图就变更
- 这个时候get和set只是定义了,并不会触发,什么是依赖我们接下来说明,首先还是用一张图理清响应式数据的创建过程
依赖收集
- 什么是依赖,我们看看mountComponent的定义
function mountComponent(vm,el){
....
const updateComponent = function(){
vm._update(vm.render())
}
new Watcher(vm,updateComponent,noop,{ // 渲染watcher
.....
},true) // true为标志.表示是否渲染watcher
....
}
- 我们首先解释下Watcher类.他类似于之前的VNode类.根据传入的参数不同,可以分别实例化出三种不同的watcher实例;他们分别是watcher,计算watcher以及渲染watcher
用户(user) watcher
- 也就是用户自己定义的,eg:
new Vue({
data{
msg:'hello world'
},
created(){
this.$watch('msg',cb()) // 定义用户watcher
},
watch:{
msg(){...} // 定义用户watcher
}
})
- 这里的两种方式内部都是使用watcher这个类实例化的,只是参数不同,具体实现后续在说,我们只需知道这个是用户watcher即可
计算(computed) watcher
- 顾名思义,这个是当定义计算属性实例化出来的一种
new Vue({
data:{
msg:'hello world'
},
computed(){
sayHi(){ // 计算watcher
return this.msg + 'vue!'
}
}
})
渲染(render) watcher
- 只是用作视图渲染而定义的Watcher实例,再组件执行vm.$mount的最后会实例化Watcher类,这个时候就是以渲染watcher的格式定义的,收集的就是当前渲染watcher的实例,我们看下其内部定义
class Watcher {
constructor(vm,expOrFn,cb,options,isRenderWatcher){
this.vm = vm
if(isRenderWatcher){ // 是否渲染为watcher
vm._watcher = this // 当前组件下挂载vm._watcher属性
}
vm._watchers.push(this) // vm._watchers是之前初始化initState时定义的[]
this.before = options.before // 渲染watcher特有属性
this.getter = expOrFn // 第二个参数
this.get() // 实例化就会执行this.get()方法
}
get(){
pushTarget(this) // 添加
''''
this.getter.call(this.vm,this.vm) // 执行vm._update(vm._render())
.....
popTarget() // 移除
}
addDep(dep){
.....
dep.addSub(this) // 将当前watcher收集到实例中
}
}
- 当执行new Watcher的时候内部都会挂载一些属性,然后执行this.get()这个方法,首先会执行一个全局的方法pushTarget(this)传入当前watcher的实例,我们看下这个方法定义的地方
Dep.target = null
const targetStack = [] // 组件从父到子对于的watcher实例集合
export function pushTarget(_target){ // 添加
if(Dep.target){
targetStack.push(Dep.target) // 添加到集合内
}
Dep.target = _target // 当前的watcher实例
}
export function popTarget(){ // 移除
targetStack.pop() // 移除数组最后一项
Dep.target = targetStack[targetStack.length =1] // 赋值为数组最后一项
}
-
首先会定义一个Dep类的静态属性Dep.target为null.这是一个全局会用到的属性,保存的是当前组件对应渲染watcher的实例,targetStack内存储的是再执行组件化的过程中每个组件对应的渲染watcher实例集合,使用的是一个先进后出的形式来管理数组的数据,这个组件可能有点不太好懂,稍等再看到最后的流程图后自然就明白了,然后将传入的watcher实例赋值给全局属性Dep.target,再之后的依赖收集过程中就是收集的它
-
watcher的get这个方法然后会执行getter这个方法,它是new Watcher 时传入的第二个参数,这个参数就是之前的updateComponent变量:
function mountComponent (vm,el){
...
const updateComponent = function(){ // 第二个参数
vm._update(vm._render())
}
....
}
- 只要一执行就会执行当前组件实例上的
vm._update(vm._render())将render函数转为Vnode,这个时候如果render函数内有使用到data中已经转为了响应式的数据,就会触发get方法进行依赖额收集,补全之前依赖收集的逻辑
export function defineReactive(obj,key,val){
const dep = new Dep() // 依赖管理器
val = obj[key] // 计算出对应的key值
observe(val) // 递归的转化对象的嵌套属性
Object.defineProperty(obj,key,{
enumerable: true,
configurable:true,
get(){ // 触发依赖收集
if(Dep.target){ // 之前赋值的当前watcher实例
dep.depend() // 收集起来,放入到上面的dep依赖管理器内
....
}
return val
},
set(newVal){
...派发更新
}
})
}
-
这个时候我们知道
watcher完整功能了.简单理解就是数据和组件之间一个通信工具的封装,当某个数据被组件读取时,就将依赖数据的组件使用Dep这个类给收集起来 -
当前例子data内的属性是只有一个渲染watcher的,因为没有被其它组件所使用,但如果该属性被其它组件使用到.也会将使用它的组件收集起来,例如作为了props传递给了子组件,在dep的数组内就会存在多个渲染watcher,我们来看下Dep类这个依赖管理器的定义
let uid = 0
export default class Dep{
constructor(){
this.id = uid++
this.subs = [] // 对象某个key的依赖集合
}
addSub(sub){ // 添加watcher实例到数组内
this.sub.push(sub)
}
depend(){
if(Dep.target){ // 已经被赋值为watcher的实例
Dep.target.addDep(this) // 执行watcher的addDep方法
}
}
}
<===================================================================================>
class Watcher{
....
addDep(dep){ // 将当前watcher实例添加到dep内
.....
dep.addSub(this) // 执行dep的addSub方法
}
}
- 这个Dep类的作用就是管理属性对应的watcher,如添加=>删除=>通知,至此依赖收集的过程算是完成了,以下图作为总结
派发更新
- 如果只是收集依赖,那其实是没有任何意义的,将收集到的依赖在数据发生变化时通知到并引起视图变化,这样才有意义,比如现在我们对数据重新赋值
app.vue
export default { // app组件
...
methods:{
changeInfo(){
this.info.name = 'ww'
}
}
}
- 这个时候就会触发创建响应式数据时的set方法了,继续补全逻辑
export function defineReactive(obj,key,val){
const dep = new Dep() // 依赖管理器
val = obj[key] // 计算出对于key的值
observe(val) // 递归转化对象的嵌套属性
Object.defineProperty(0bj,key,{
enumerable:true,
configurable:true,
get(){
...依赖收集
},
set(newVal){ // 派发更新
if(newVal === val){ // 相同
return
}
val = newVal // 赋值
observe(newVal) // 如果新值是对象也递归包装
dep.notify() // 通知更新
}
})
}
- 当赋值触发set时,首先会检测新值和旧值,不能相同,然后将新值赋值给旧值;如果新值是对象则将它变成响应式的.最后让对应属性的依赖管理器使用dep.notify发出更新视图的通知,我们看下他的实现
let uid = 0
class Dep{
constructor(){
this.id = uid++
this.sub = []
}
notify(){ // 通知
const subs = this.subs.slice()
for(let i = 0;i<sub.length;i++){
subs[i].update() // 依次触发update
}
}
}
- 这里做的事情只有一件,将收集起来的watcher挨个遍历触发update方法
class Watch{
...
update(){
queueWatcher(this)
}
}
<======================================>
const queue = []
let has = {}
function queueWatcher(watcher){
const id = watcher.id
if(has[id] == null){
// 如果某个watcher没有被推入队列
....
has[id] = true // 已经推入
queue.push(watcher) // 推入到队列
}
....
nextTick(flushSchedulerQueue) // 下一次更新
- 执行update方法时将当前watcher实例传入到定义的queueWatcher方法内,这个方法的作用是把将要执行更新的watcher收集到一个队列queue之内.保证如果用一个watcher内触发了多次更新.只会更新一次对应的watcher
export default {
data(){
return{ //都被模板引用了
num:0
name:'cc'
sex:'man'
}
},
methods:{
changeNum(){ // 赋值100次
for(let i = 0;i<100;i++){
this.num++
}
},
changeInfo(){ // 一次赋值多个属性的值
this.name = 'ww'
this.sex = 'woman'
}
}
}
- 这里的三个响应式属性它们收集都是同一个渲染watcher.所以当赋值100次的情况出现时.再将当前的渲染watcher推入到队列之后.之后赋值触发的set队列内并不会添加任何渲染watcher;当同时赋值多个属性时也是.因为它们搜集的都是同一个渲染watcher,所以推入到队列一次之后就不会添加了
知识点.通过这两个例子,可以知道派发更新通知的粒度是组件级别,至于组件内是哪个属性赋值了.派发更新并不关心,而且怎么高效更新这个视图,那是之后diff对比做的
- 队列有了,执行nextTick(flushSchedulerQueue)在下一次tick时更新它,这里的nextTick就是我们经常使用的this.$nextTick方法的原始方法,他们作用一致,让我们了解下flushSchedulerQueue的内容
let index = 0
function flushSchedulerQueue(){
let watcher,id
queue.sort((a,b)=>a.id - b.id) //watcher排序
for(index=0;index<queue.length;index++){
watcher = queue[index]
if(watcher.before){//渲染watcher独有属性
watcher.before() // 触发beforeUpdate钩子
}
id = watcher.id
has[id] = null
watcher.run() // 真正的更新方法
....
}
}
- 该参数其实就是个函数,在nextTick方法的内部会执行第一个参数,首先会将queue这个队列进行一次排序,依据是每次new Watcher生成的id,以从小到大的顺序.当前示例只是做渲染,而且队列内只存在了一个渲染watcher,所以是不存在顺序的,但是如果有定义user-watcher和computed-watcher加上render-watcher后他们之间就会存在一个执行顺序的问题
知识点:watcher的执行顺序是先父后子,然后是从computed watcher 到 user watcher 最后 render watcher,这从他们初始化顺序就能看出
- 然后就是遍历这个队列,因为是渲染watcher,所以是有before属性的,执行传入的before方法触发beforeUpdate钩子,最后执行watcher.run()方法,执行真正的派发更新方法,我们了解下run函数的内部
class Watcher{
...
run(){
if(this.active){
this.getAndInvoke(this.cb)
}
}
getAndInvoke(cb){
// 渲染watcher的cb为noop空函数
const value = this.get()
... 后面是用户watcher的逻辑
}
}
- 执行run方法就是执行getAndInvoke方法,因为是渲染watcher,参数cb是noop空函数,其实就是重新执行了一次this.get()方法,让vm._update(vm._render())再执行一次,然后生成新旧VNode,最后执行diff比对
- 最后我们说下vue基于Object.defineProperty响应式系统的一些不足,如果只能监听到数据的变化,所以有时候data中需要定义一堆的初始值,因为加入了响应式系统后才能被感知到,还有就是常规javascript操作对象的方式,并不能监听到增加以及删除,eg:
export default{
data(){
info:{
name:'cc'
}
}
},
methods:{
addInfo(){ // 增加属性
this.info.sex = 'man'
},
delInfo(){ // 删除属性
delete info.name
}
}
}
- 数据是被赋值了,但是视图并不会发生变更,vue为了解决这个问题,提供了两个API: delete
- questions
- 我们知道data中的变量都会被代理到当前this下,所以我们也可以在this下挂载属性,只要不重名即可.而且定义在data中的变量在vue的内部会将它包装成响应式的数据,让它拥有变更即可驱动视图变化的能力.但是如果这个数据不需要驱动视图,定义在created或者mounted钩子中也是可以的,因为不会执行响应式的包装方法,对性能也是一种提升
- 数组更新
- 首先来看下改变数组的两种方式
export default{
data(){
list:[1,2,3]
},
methods:{
changeArr1(){ // 重新赋值
this.list = [4,5,6]
},
changeArr2(){ // 方法改变
this.list.push(7)
}
}
}
- 对于这两种改变数据的方式,vue内部的实现并不相同
方式一:重新赋值
- 实现原理和对象是一样的,在vm._render()时有用到list,就将依赖收集起来,重新赋值后走对象派发更新的那一套
方法二:方法改变
- 走对象的那种不行,因为这并不是重新赋值,虽然 改变了数组自身但并不会触发set,原有的响应式系统根本感知不到,所以我们接下来就分析,vue是如何解决使用数组方法来改变自身触发视图
Dep收集依赖的位置
- Dep类的主要作用就是管理依赖,在响应式系统中会有两个地方要实例化它,当然他们都会进行依赖的收集,首先是之前具体包装的时候
function defineReactive(obj,key,val){
const dep = new Dep() // 自动依赖管理器
....
Object.defineProperty(obj,key,{
get(){...},
set(){...}
})
}
- 这里它会对每个读取到的key都进行依赖收集,无论是对象/数组/原始类型,如果是通过重新赋值触发set就会使用这里收集到的依赖进行更新.我们把它称为自动依赖管理器,还有一个地方也会对他进行实例化就是Observer类中
class Observe{
constructor(value){
this.dep = new Dep() // 手动依赖管理器
....
}
}
- 这个依赖管理器并不能通过set触发,而且是只会收集对象/数组的依赖.也就是说对象的依赖会被收集两次,一次在自动依赖管理器内,一次在这里.而最重要的是数组使用方法改变自身去触发更新的依赖就是在这里收集的
数组的响应式原理
数组响应式数据的创建
数组示例
export default {
data(){
return {
list:[{
name:'cc'
sex:'man'
},{
name:'ww'
sex:'woman'
}]
}
}
}
- 流程开始还是执行observe方法,接下来我们更加详细分析响应式系统
function observe(value){
if(!isObject(value){ // 不是数组或对象.返回
return
}
let ob
if(hasOwn(value,'_ob_') && value._ob_ instanceof Observe){
ob = value._ob_
}else{
ob = new Observer(value)
}
return ob
}
- 只要是响应式的数据都会有一个_ob_的属性,它是在bserve类中挂载的,如果已经有_ob_属性就直接赋值给ob,不会再次去创建Observe实例,避免重复包装,首次肯定没_ob_属性了,所以在重新看下Observer类的定义
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep() // 手动依赖管理器
def(value, '__ob__', this) // 挂载__ob__属性,三个参数
...
}
}
- 首先定义一个手动依赖管理器,然后挂载一个不可枚举的__ob__属性传入到value下,表示是它的一个响应式数据,而且__ob__的值就是当前Observe类的实例,它拥有实例上的所有属性和方法,下面演示def是然后完成属性挂载的
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
- 就是一个简单的封装,如果第四个参数不传,enumerable就是不可枚举的,接着看Observe类的定义
class Observer {
constructor(value) {
...
if (Array.isArray(value)) { // 数组
...
} else { // 对象
this.walk(value) // {list: [{...}, {...}]}
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
- 首次传入还是对象的格式,所以会执行walk遍历的将对象每个熟悉包装为响应式,再来看下defineReactive方法
function defineReactive(obj, key, val) {
const dep = new Dep() // 自动依赖管理器
val = obj[key] // val为数组 [{...}, {...}]
let childOb = observe(val) // 传入到observe里,返回Observer类实例
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() { // 依赖收集
if (Dep.target) {
dep.depend() // 自动依赖管理器收集依赖
if (childOb) { // 只有对象或数组才有返回值
childOb.dep.depend() // 手动依赖管理器收集依赖
if (Array.isArray(val)) { 如果是数组
dependArray(val) // 将数组每一项包装为响应式
}
}
}
return value
},
set(newVal) {
...
}
}
}
- 首先递归执行observe(val)会有一个返回值了,如果是对象或数组的话,childOb就是Observer类的实例,以数组格式在observe内做了什么,我们之后分析。接下来在get内的childOb.dep.depend()执行的就是Observer类里定义的dep进行依赖收集,收集的render-watcher跟自动依赖管理器是一样的。接下来如果是数组就执行dependArray方法:
function dependArray (value) {
for (let e, i = 0, i < value.length; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend() // 是响应式数据
if (Array.isArray(e)) { // 如果是嵌套数组
dependArray(e) // 递归调用自己
}
}
}
this.$set
function set(target,key,val){
if(Array.isArray(target){ // 数组
target.length = Math.max(target.length,key) // 最大值为长度
target.splice(key,1,val) // 移除一位,改写的方法派发更新
return val
}
if( key in target && !(key in Object.prototype)){ // key属于target
target[key] = val // 赋值操作触发set
return val
}
if(!target._ob_){ // 普通对象赋值操作
target[key] = val
return val
}
defineReactive(target,_ob_.value,key,val) // 将新值包装为响应式
target._ob_.dep.notify() // 手动触发通知
return val
}
- 首先判断target是否为数组,是数组的话第二个参数就是数组的长度,设置数组的长度,然后使用splice这个异变方法插入val.然后判断key是否属于target,属于的话就是赋值操作了,这个会触发set去派发更新.接下里如果target并不是响应式数据,那就是普通对象,那就设置一个对应的key,最后以上情况都不满足,说明是在响应式数据上新增了一个熟悉,吧新增的属性转为响应式数据,然后通知手动依赖管理器派发更新
this.$delete
function del(target,key){
if(Array.isArray(target)){ // 数组
target.splice(key,1) // 移除指定下标
return
}
if(!hasOwn(target,key)){ // key不属于target,返回
return
}
delete target[key] // 删除对象指定key
if(!target._ob_){ // 普通对象,返回
return
}
target._ob_.dep.notify() // 手动派发更新
}
- this.$delete就更加简单了,首先如果是数组就是用变异方法splice移除下标值,如果target是对象但key不属于它,返回.然后删除指定key的值,如果target不是响应式对象.删除的就是普通对象一个值,否则通知手动依赖管理器派发更新视图
vue2响应式系统
- 简单来说就是使用Object.defineProperty这个API为数据设置get和set,当读取到某个属性时,触发get将读取它的组件对应的render watcher收集起来;当重置赋值时,触发set通知组件重新渲染页面,如果数据的类型是数组的话,还做了单独的处理.对可以改变数组自身的方法进行重写,因为这些方法不是通过重新赋值改变的数组.不会触发set,所以要单独处理,响应系统也有自身的不足,所以官方给出了delete来弥补