一、Vue2
1、特点
- 组件化模式,实现代码复用
- 声明式编码,无需直接操作DOM
- Diff 算法和虚拟 DOM
2、底层原理
1)数据劫持
- Vue 实例对象为 data 中的属性添加 getter 和 setter 的行为称为 数据劫持
2)数据监测
(1)监测对象
-
通过 setter 实现监测,且要在 new Vue 时就传入要监测的数据。
-
新增属性,Vue 默认不做响应式处理。但是可以使用 API
- 该API不能给 Vue实例对象的根数据对象添加属性
Vue.set(target,propertyName/index,value)
(2)监测数组
- 通过包裹数组的方法实现,本质是做了两件事
- 调用原生方法对数组进行更新
- 重新解析模板,更新页面
- 数组方法为 shift,unshift,pop,push,reverse,sort,splice
3)数据代理
(1)Vue实例
-
Vue 实例和容器是一对一的关系。
-
el 用于指定当前Vue实例为哪个容器服务
<div id='root'> </div> new Vue({ el:'#root', data:{ name:'zhan', age:18 } })
(2)Object.defineProperty
-
指定配置项
Object.defineProperty( person,'age',{ value:18, enumerable:true, // 控制属性是否可以枚举,默认 false writable:true, // 控制属性是否可以修改,默认 false configurable:true // 控制属性是否可以删除,默认 false })
-
配置 getter 和 setter
Object.defineProperty( person,'age',{ get(){ console.log(" someone 读取 age ") return number } set(value){ number = value } })
(3)图示
-
Vue 实例对象将构造函数的data存放到自身的 _ data 中。
-
为方便开发,Vue实例对象使用数据代理在外层添加 _data中的 name属性和address属性,这一步使用了 Object.defineProperty。
-
此时修改外层的name属性,实际是修改了 _data 中的name属性。
3、响应式原理
1)vue2
-
实现原理
- 对象类型:通过 Object.defineProperty() 对属性的读取、修改进行拦截(数据劫持)。
- 数组类型:对数组的变更方法进行了包裹
-
模拟实现
// 无法实现新增属性、删除属性 let person = { name:'张三', age:18 } let p = {} Object.defineProperty( p,'name',{ get(){ return person.name }, set(value){ person.name = value } }) Object.defineProperty( p,'age',{ get(){ return person.age }, set(value){ person.age = value } })
-
存在问题
-
新增属性、删除属性,界面不会更新;
-
补充:新增属性
this.$set(this.person,'sex','女')
-
补充:删除属性
this.$delete(this.person,'sex')
-
-
直接通过下标修改数组,界面不会自动更新。
-
补充:修改属性
hobby = ['吃饭','学习','学习'] this.$set(this.hobby,0,'学习') console.log(this.hobby) // 学习,学习,学习
-
-
2)vue3
-
实现原理
- 通过 Proxy 代理:拦截对象中任意属性的变化,包括:属性值的读写、添加、删除等。
- 通过 Reflect 反射:对被代理对象的属性进行操作
-
模拟实现
let person = { name:'张三', age:18 } // 建立映射关系 const p = new Proxy( person ,{ //读取 get(target,propName){ return Reflect.get(target,propName) }, //修改 新增 set(target,propName,value){ Reflect.get(target,propName,value) }, //删除 deleteProperty(target,propName){ return Reflect.deleteProperty(target,propName) } })
4、生命周期
- beforeMount 读不到 Dom 元素,返回值是 undefined
- beforeUpdate 获取的是更新之前的 Dom 元素,updated 获取的是 更新之后的 Dom 元素。
1)Vue2
- 此阶段初始化生命周期。
- beforeCreate
- 此时无法访问 data 中的数据和 method 中的方法。
- 此阶段进行数据监测与数据代理。
- created
- 可以访问到 data 中的数据和 method 中的方法。
- 此阶段解析模板,生成虚拟DOM,但是页面还不能显示解析好的内容。
- beforeMount
- 页面呈现的是未经 Vue 编译的DOM结构。
- 所有对DOM的操作,最终都不奏效。
- 此阶段将虚拟DOM转为真实DOM插入页面。
- mounted
- 页面呈现的是经过 Vue 编译的DOM结构。
- 所有对DOM的操作,均奏效。
- 一般在此进行初始化操作:开启定时器、发送网络请求、订阅消息、绑定自定义事件等。
- 此阶段数据发生改变。
- beforeUpdate
- 数据是新的,页面是旧的。
- 此阶段 根据新数据生成新的虚拟DOM,随后与旧的虚拟DOM比较,最终完成页面更新。
- updated
- 数据是新的,页面也是新的。
- beforeDestroy
- 此时的 data、method 都处于可用状态,可以修改但页面不再更新。
- 一般在此进行收尾工作:关闭定时器、取消订阅消息、解绑自定义事件等。
- destroy
- 销毁后自定义事件会失效,但是原生DOM事件依旧有效。
2)Vue3
-
Vue3,setup 代替了 beforecreate 和 created。
-
Vue3 使用 unmount 代替 destroy。
-
Vue3 可以在 setup 中使用组合式生命周期钩子,命名在钩子前面添加 “ on ”。
-
组合式生命周期钩子的执行时机优先于配置项的钩子。
5、methods 事件处理
1)参数
-
默认参数为 $event,但是存在其他参数则需要显式指定 $event。
<div @click='show' ></div> <div @click='show1(a,$event)' ></div> methods:{ show(){ console.log( $event ) }, show1(a,$event){ console.log( a,$event ) } }
2)事件修饰符
修饰符 | 作用 |
---|---|
prevent | 阻止默认行为 |
stop | 阻止事件冒泡 |
once | 事件只触发一次 |
capture | 使用事件的捕获模式 |
3)v-model 修饰符
修饰符 | 作用 |
---|---|
v-model.trim | 移除前后空格 |
v-model.number | 输入字符串转为有效的数字 |
v-model.lazy | 失去焦点后才更新数据 |
6、computed 计算属性
1)原理
-
底层借助了 Object.defineProperty 方法提供的 getter 和 setter
-
优势:内部有缓存机制
2)调用时机
- 初次读取 fullName 时。
- 所依赖的数据发生变化时。
computed:{
fullName:{
get(){
return this.firstName + '-' + this.lastName
},
set(value){
console.log(value)
}
}
}
- 简写形式。
computed:{
fullName(){
return this.firstName + '-' + this.lastName
}
}
7、watch 监视属性
-
可以监视 计算属性
watch:{ isHot:{ deep:true, immediate:true, handler(new,old){ } } }
1)computed 对比
- computed 能完成的功能,watch 都可以完成。
- watch 能完成的功能,computed 不一定能完成,例如:watch 可以进行异步操作。
8、指令
1)内置指令
(1)v-once
-
所在节点在初次渲染后就不再更新,视为静态内容
<h2 v-once> {{n}} </h2>
(2)v-pre
-
可以跳过没有使用指令语法和插值语法的结点,加快编译。
<h2 v-pre> {{n}} </h2>
2)自定义指令
- this 全部指向 window
- 命名方式:多个单词间使用 - ,不遵循驼峰命名法。
- 推荐使用 对象式 写法,函数式在特殊情况下(如获取焦点和获取父元素)存在Bug。
(1)函数式
-
参数1是DOM元素,参数2是绑定的元素
-
执行时机:
- 指令与元素成功绑定时(初始化);
- 指令所在的模板被重新解析时。
<span v-big-number = 'n' ></span> data:{ n:1 }, directives:{ 'big-number'(element,binding){ console.log( binding.value ) } }
(2)对象式
-
bind 和 update 实际组成了 函数式的写法。
<span v-big = 'n' ></span> data:{ n:1 }, directives:{ big:{ // 指令与元素成功绑定时 调用 bind(element,binding){}, // 指令所在元素被插入页面时 调用 inserted(element,binding){ element.focus() }, // 指令所在模板被重新解析时 调用 update(element,binding), } }
(3)全局指令
-
可以在所有 Vue 实例对象内 使用。
Vue.directive('big',{ // 指令与元素成功绑定时 调用 bind(element,binding){}, // 指令所在元素被插入页面时 调用 inserted(element,binding){ element.focus() }, // 指令所在模板被重新解析时 调用 update(element,binding), })
9、页面渲染
1)绑定 class 样式
-
对象写法
<div :class= "{ a1:true, a2:false }"></div>
2)ref 属性
-
应用在 html 标签上获取的是真实的 DOM 结构。
-
应用在 组件标签上获取的是 组件实例对象。
<h1 ref='title'></h1> showDOM(){ console.log( this.$refs.title ) // <h1 ref='title'></h1> }
3)nextTick
-
作用:在下一次 DOM 更新结束后执行其指定的回调。
-
当数据改变后,要基于更新后的新 DOM 进行某些操作时,要在 nextTick 所指定的回调函数中执行。
this.$nextTick( function(){ this.$refs.title.focus() })
4)条件渲染
(1)v-if
- 不展示的 DOM 结构直接被移除
- v-if 可以与 template 标签配套使用,但是 v-show 与该标签配套使用无效。
(2)v-show
- DOM 结构依然存在,但是 display : none
- 频繁切换的推荐使用 v-show。
(3)面试题
-
v-if、v-show、visibility:hidden 的区别
v-if:DOM 结构直接被移除,即打开 F12 没有该结构。
v-show:DOM 结构存在,但是增加了 display : none 属性,不在页面中占位。
visibility:hidden:DOM结构存在,且在页面中占位。
5)列表渲染
(1)key 原理
- key 是虚拟DOM对象的标识,当状态中的数据发生变化时,Vue会根据新数据生成新的虚拟DOM。
- 随后Vue采用 Diff 算法将新虚拟DOM与旧虚拟DOM的差异比较。
(2)Diff 算法
-
旧虚拟DOM中找到了与新虚拟DOM相同的key:
- 若虚拟DOM中内容没变,直接使用之前的真实DOM!
- 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM。
-
旧虚拟DOM中未找到与新虚拟DOM相同的key:
- 创建新的真实DOM,随后渲染到到页面。
-
index作为key可能会引发的问题:
- 若对数据进行逆序添加、逆序删除等破坏顺序操作,会产生没有必要的真实DOM更新。
- 若对数据进行逆序添加、逆序删除等破坏顺序操作,会产生没有必要的真实DOM更新。
(3)源码实现
-
首先是前序一对一循环比较,遇到不一致的跳出循环。
-
然后是后序一对一循环比较,遇到不一致的跳出循环。
-
中间进行一对多遍历比较,尽可能复用。
6)动画与过渡
-
appear属性可以增加进场动画。
<transition name='hello' appear> <h1 v-show="isShow"> </h1> </transition> <style> .hello-enter-active{ animation:a1 1s; } .hello-leave-active{ animation:a1 1s reverse; } @keyframes a1{ from{ transform:translateX( -100% ) } to{ transform:translateX(0) } } </style>
-
tranasition 只能用于一个元素
<transition name='hello' appear> <h1 v-show="isShow"> </h1> </transition> <style> /* 进入的起点,离开的终点 */ .hello-enter, .hello-leave-to{ transform:translateX( -100% ); } .hello-enter-active, .hello-leave-active{ transition: 0,5s linear; } /* 进入的终点,离开的起点 */ .hello-enter-to, .hello-leave{ transform:translateX( 0 ); } </style>
-
多个元素需要使用 transition-group,并且每个元素需要有 key
<transition-group name='hello' appear> <h1 v-show="isShow" key='1'> </h1> <h1 v-show="isShow" key='2'> </h1> </transition-group>
10、组件
1)概述
-
组件的本质是一个名为 VueComponent 的构造函数,由 Vue.extend 生成。
export default { data(){ } } // 等同于 export default Vue.extend({ data(){ } })
-
编写组件标签,Vue 解析模板时,会创建该组件的实例对象,即执行如下代码
new VueComponent( options )
-
new Vue 配置中,this 指向 Vue实例对象。组件配置中, this 指向 VueComponent 组件实例对象。
2)创建
-
el 不用写,所有组件最终都由一个 Vue实例对象管理,由实例对象的 el 决定服务于哪个容器。
-
组件的 data 配置项为什么要写成 函数式?
- 一个组件可能在多个位置被引用,采用对象式的写法,当一个位置修改 data 时,其他位置的 data 也会被修改,即存在引用关系,造成数据混乱。采用函数式写法则不会有这个问题。
<hello></hello> const hello = Vue.extend({ data(){ return { a:1, b:2 } }, template:`<div>{{a}}</div>` }) new Vue({ el:'#root', components:{ hello } })
3)Vue实例与组件的内置关系
-
实现原理:
- VueComponent 构造函数的原型对象的原型对象,即
VueComponent.prototype.__proto__
原本指向 原型链的终点 null,Vue 将之指向了 Vue 实例对象的原型对象。
- VueComponent 构造函数的原型对象的原型对象,即
-
作用:让组件实例对象可以访问到 Vue 原型上的属性和方法。
VueComponent.prototype.__proto__ = Vue.prototype
11、组件间通信
- props 方式与组件自定义事件有相似
- 都需要在编写组件标签时声明所传递的数据或者函数名。
- props 方式需要 props 配置项接收,组件自定义事件需要 this.$emit 函数触发。
- 全局事件总线是组件自定义事件的升级,通过 $bus 统一管理,把各个组件的自定义事件统一绑定到 Vue 实例对象上。
- 消息订阅是全局事件总线的换壳版本,将 $bus 的功能交给第三方库实现。
- 插槽可以传递 html 结构。
1)props
(1)父传子
-
==props 是只读的,可以将 props 的数据赋值给 data。命名冲突时,父组件的优先级更高。
// 父组件 // 传递非字符串类型需要 : <Student name='lisi' sex='女' :age='18' /> // 子组件 方式一 name:'school', data(){ return { myAge:this.age } }, props:{ name:String, sex:String, age:Number }
-
可以设置默认值和限制必要性
// 父组件 <Student name='lisi' sex='女' :age='18' /> // 子组件 方式一 name:'school', props:{ name:String, sex:String, age:Number } // 子组件 方式二 name:'school', props:{ name:{ type:String, required:true // 必需的 }, age:{ type:Number, default:99 // 默认值 } }
(2)子传父
-
子组件向父组件传递数据,需要父组件先定义和传递一个函数
// 父组件 <MyHeader :receive='receive' /> methods:{ receive(todoObj){ console.log('this is parent',todoObj ) } } // 子组件 props:['receive'] methods:{ add(){ this.receive(todoObj) } }
2)组件的自定义事件
-
核心:给哪个组件绑定自定义事件,就需要触发这个组件。
-
当前组件实例对象被销毁,该组件的所有自定义事件都失效,但原生事件不受影响。
(1)子传父
-
通过触发 Student 组件实例身上的 atguigu 事件 传递数据
// 父组件 <Student @atguigu='demo' /> methods:{ demo(name){ console.log('demo 被调用了',name) } } // 子组件 <button @click='sendStudent'> </button> methods:{ sendStudent(){ this.$emit('atguigu',this.name) } }
-
可以解绑组件的自定义事件
// 子组件 <button @click='unbind'> </button> methods:{ unbind(){ this.$off('atguigu') // 解绑一个 this.$off(['atguigu','demo']) // 解绑多个 } }
(2)查看
- 通过图片所示方式可以查看已经触发的自定义事件。
3)全局事件总线
- 可以用于任意组件间通信。
(1)安装
-
安装全局事件总线
new Vue({ el:'#app', render:h => h(App), beforeCreate(){ Vue.prototype.$bus = this } })
(2)使用
-
一般在 mounted 函数中注册。
// 组件A methods(){ demo(data){ ... } } mounted(){ this.$bus.$on('hello',demo) } // 组件B methods(){ show(){ this.$bus.$emit('hello',666) } }
-
解绑
beforeDestroy(){ this.$bus.off('hello') }
4)消息订阅
- 可以用于任意组件间通信。
(1)下载
-
下载第三方库
npm i pubsub-js
(2)订阅发布
-
订阅
- 参数1是消息的名字
- 参数2是传递的数据
import pubsub from 'pubsub-js' methods(){ demo(msgName,data){ ... } } mounted(){ this.pubId = pubsub.subscribe('hello', this.demo ) } beforeDestroy(){ pubsub.unsubscribe( this.pubId ) }
-
发布
import pubsub from 'pubsub-js' pubsub.publish('hello', 666)
5)插槽
- 作用:让父组件可以向子组件指定位置插入 html 结构。
- 支持双向通信。
(1)具名插槽
-
父组件没有填充数据时,子组件显示默认值。
-
样式可以写在父组件也可以写在子组件。
// 父组件 <Category title='food'> <h1 slot='center'>你好啊</h1> <h1 slot='footer'>你好啊!!!</h1> </Category> // 子组件 <div> <slot name='center'>默认值</slot> <slot name='footer'>默认值</slot> </div>
(2)作用域插槽
-
页面结构由父组件决定,数据存放在子组件。
// 父组件 <Category title='food'> <template scope='{game}'> <h1 slot='center'> {{game}}</h1> </template> </Category> // 子组件 <div> <slot name='center' :game='game'>默认值</slot> </div> data(){ return { game:[1,2,3] } }
12、脚手架
1)render 函数
-
vue.js 与 vue.runtime.xxx.js 的区别
- vue.js 是完整版的 Vue,包含:核心功能 + 模板解析器。
- vue.runtime.xxx.js 是运行版的 Vue,只包含 核心功能。
-
Vue-CLI 默认引入的是 vue.runtime.xxx.js,没有模板解析器,不能使用 template 配置项,需要使用 render 函数接收到的 createElement 函数指定具体内容。
render(createElement){ return createElement(App) }
2)mixin 混入
-
可以将公共配置提取到 js 文件中,通过 mixin 引入。
// mixin.js export const a1 = { methods:{ showName(){ alert( this.name ) } } } // 组件 <h2 @click='showName'></h2> import { a1 } from '../mixin' export default { name:'Student', mixins:[ a1 ] }