1、数据代理概括
数据代理过程相当于是进行了 vm 代理 vm_data中的属性,vm._data 是与 我们vue文件中写的 data是全等的
//创建Vue实例
let data = { //data中用于存储数据,数据供el所指定的容器去使用,值我们暂时先写成一个对象。
name:'atguigu',
address:'北京'
}
new Vue({
el:'#demo', //el用于指定当前Vue实例为哪个容器服务,值通常为css选择器字符串。
data
})
Object.defineProperty(vm, 'name', {
get(){
return vm._data.name
},
set(value){
vm._data.name = value
}
})
2、v-on:事件
只有data里面的东西才会做数据劫持,代理,方法其实也可以写在data,但是会使vue实例变得冗余,因为对方法做了数据代理及劫持,这是没意义的操作
事件的基本使用:
1.使用v-on:xxx 或 @xxx 绑定事件,其中xxx是事件名;
2.事件的回调需要配置在methods对象中,最终会在vm上;
3.methods中配置的函数,不要用箭头函数!否则this就不是vm了;
4.methods中配置的函数,都是被Vue所管理的函数,this的指向是vm 或 组件实例对象;
5.@click="demo" 和 @click="demo($event)" 效果一致,但后者可以传参;
2.1、事件修饰符
Vue中的事件修饰符:
1.prevent:阻止默认事件(常用);
2.stop:阻止事件冒泡(常用);
3.once:事件只触发一次(常用);
4.capture:使用事件的捕获模式,让事件在捕获阶段执行(默认事件是在冒泡阶段执行的);
5.self:只有event.target是当前操作的元素时才触发事件;
6.passive:事件的默认行为立即执行,无需等待事件回调执行完毕;
2.2、键盘事件
1.Vue中常用的按键别名:
回车 => enter
删除 => delete (捕获“删除”和“退格”键)
退出 => esc
空格 => space
换行 => tab (特殊,必须配合keydown去使用)
上 => up
下 => down
左 => left
右 => right
2.Vue未提供别名的按键,可以使用按键原始的key值去绑定,但注意要转为kebab-case(短横线命名)
3.系统修饰键(用法特殊):ctrl、alt、shift、meta
(1).配合keyup使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才被触发。
(2).配合keydown使用:正常触发事件。
4.也可以使用keyCode去指定具体的按键(不推荐)
5.Vue.config.keyCodes.自定义键名 = 键码,可以去定制按键别名
3、列表渲染 key
最终效果图
操作步骤:
复制张三-18到其对应的右侧输入框
李四-19、王五-20同上操作
然后点击添加老刘就会得到上述的效果,具体原因在 下面 3.1 有解释
demo代码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>key的原理</title>
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<!-- 准备好一个容器-->
<div id="root">
<!-- 遍历数组 -->
<h2>人员列表(遍历数组)</h2>
<button @click.once="add">添加一个老刘</button>
<ul>
<!-- 分别通过 index 与 id作为 key -->
<li v-for="(p,index) of persons" :key="index">
{{p.name}}-{{p.age}}
<input type="text">
</li>
</ul>
</div>
<script type="text/javascript">
Vue.config.productionTip = false
new Vue({
el:'#root',
data:{
persons:[
{id:'001',name:'张三',age:18},
{id:'002',name:'李四',age:19},
{id:'003',name:'王五',age:20}
]
},
methods: {
add(){
const p = {id:'004',name:'老刘',age:40}
this.persons.unshift(p)
}
},
})
</script>
</html>
3.1、index作为key
解释:
- 当index作为key的时候(没设置key时,默认也是以index作为key的)
- 我们一开始渲染的是左侧的三条数据,会生成三个虚拟dom节点,key 分别为0、1、2,
- 然后vue根据这三虚拟dom生成真正的三个dom节点,渲染到页面上去
- 当我们通过 unshift 逆序添加一个元素到列表时,破坏了原列表的顺序,生成四个虚拟节点
- vue会通过diff算法 将左侧(原本的)和右侧(新生成)的虚拟节点进行对比,主要是通过key,去对比
- 发现右侧key = 0的地方 文本节点与左侧key = 0的地方文本节点不一样,则在生成真实dom节点的时候,就会重新生成dom节点,对比input虚拟节点的时候,发现是一样的,则会复用已有的dom节点而此时,由于复用的是左侧key = 0 节点下的input,所以老刘对应的输入框本应是无值的变成了 张三-18,以此类推最终就会得到 右图所示的效果,出现数据错乱
- 而且如果是类似上述情况,涉及逆序破坏列表顺序的情况,使用idnex作为key,会造成效率问题,让本该复用的节点,得不到复用,重复创建dom节点,所以当是类似上述情况,使用唯一标识很重要,下面解释为啥使用唯一标识重要
3.2、唯一标识作为key
解释:
- 当唯一标识(这里是id)作为key的时候
- 我们一开始渲染的是左侧的三条数据,会生成三个虚拟dom节点,key 分别为001、002、003,
- 然后vue根据这三虚拟dom生成真正的三个dom节点,渲染到页面上去
- 当我们通过 unshift 逆序添加一个元素到列表时,破坏了原列表的顺序,生成四个虚拟节点
- vue会通过diff算法 将左侧(原本的)和右侧(新生成)的虚拟节点进行对比,主要是通过key,去对比
- 发现右侧key = 001的地方 文本节点与左侧key = 001的地方文本节点与input节点是一样的,则会复用已有的dom节点,此时就能正确显示,以此类推 002、003对应的节点杜能正确配对显示,所以就能得到我们想要的效果
4、vue监测数据改变的原理
引出问题
demo代码,注意要引入 vue.js (vue 2.xx.xx版本)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>更新时的一个问题</title>
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<!-- 准备好一个容器-->
<div id="root">
<h2>人员列表</h2>
<button @click="updateMei">更新马冬梅的信息</button>
<ul>
<li v-for="(p,index) of persons" :key="p.id">
{{p.name}}-{{p.age}}-{{p.sex}}
</li>
</ul>
</div>
<script type="text/javascript">
Vue.config.productionTip = false
const vm = new Vue({
el:'#root',
data:{
persons:[
{id:'001',name:'马冬梅',age:30,sex:'女'},
{id:'002',name:'周冬雨',age:31,sex:'女'},
{id:'003',name:'周杰伦',age:18,sex:'男'},
{id:'004',name:'温兆伦',age:19,sex:'男'}
]
},
methods: {
updateMei(){
// this.persons[0].name = '马老师' //奏效
// this.persons[0].age = 50 //奏效
// this.persons[0].sex = '男' //奏效
this.persons[0] = {id:'001',name:'马老师',age:50,sex:'男'} //不奏效
// this.persons.splice(0,1,{id:'001',name:'马老师',age:50,sex:'男'}) // 奏效
}
}
})
</script>
</html>
一个属性一个属性的更改是可以检测到的,但是当是数组的整个对象更新的时候,就不行
临时添加的对象属性,也不具有响应式(因为没有通过Object.defineProperty 进行劫持),除非使用$set添加的 ($set就相当于是重新使用 Object.defineProperty 进行数据劫持)
数组通过索引更改值,vue也检测不到,页面也不会实时更新(当然也可以使用set,就可以检测到了),必须通过数组的七个方法更新数组(push、pop、unshift、shift、splice、sort、reverse),vue才能检测到,因为vue对上述方法进行了重写
$set局限性不能vue实例/根数据对象身上使用 set, 会报错:避免添加响应式属性到xxx
5、其他
5.1、v-model 相关的三个修饰符
v-model的三个修饰符:
lazy:失去焦点再收集数据
number:输入字符串转为有效的数字
trim:输入首尾空格过滤
5.2、过滤器
过滤器:
定义:对要显示的数据进行特定格式化后再显示(适用于一些简单逻辑的处理)。
语法:
1.注册过滤器:Vue.filter(name,callback) 或 new Vue{filters:{}}
2.使用过滤器:{{ xxx | 过滤器名}} 或 v-bind:属性 = "xxx | 过滤器名"
备注:
1.过滤器也可以接收额外参数、多个过滤器也可以串联
2.并没有改变原本的数据, 是产生新的对应的数据
5.3、内置指令
v-text指令:
1.作用:向其所在的节点中渲染文本内容。
2.与插值语法的区别:v-text会替换掉节点中的内容,{{xx}}则不会。
v-html(慎用)
因为有了可能会在指令后面的内容中,插入一些获取 cookie的操作(document.cookie),这样就会导致xss攻击,也就是类似仿冒的操作,如果服务器返回的cookie 没有设置成 httpOnly ,则可以通过document.cookie 获取到,攻击者就可以获取到cookie,v-html 就有可能会带有,类似获取cookie,然后发送到攻击者的服务器的操作,所以如果要使用v-html 一定要确保是可信的,并进行相应的校验
v-once 只加载一次,后续相关值改变, 此处值不作变更
v-once指令:
1.v-once所在节点在初次动态渲染后,就视为静态内容了。
2.以后数据的改变不会引起v-once所在结构的更新,可以用于优化性能。
v-pre
v-pre指令:
1.跳过其所在节点的编译过程。
2.可利用它跳过:没有使用指令语法、没有使用插值语法的节点,会加快编译。
5.4、自定义指令
自定义指令总结:
一、定义语法:
(1).局部指令:
new Vue({
directives{指令名:回调函数}
})
(2).全局指令:
Vue.directive(指令名,配置对象) 或 Vue.directive(指令名,回调函数)
二、配置对象中常用的3个回调:
(1).bind:指令与元素成功绑定时调用。
(2).inserted:指令所在元素被插入页面时调用。
(3).update:指令所在模板结构被重新解析时调用。
三、备注:
1.指令定义时不加v-,但使用时要加v-;
2.指令名如果是多个单词,要使用kebab-case命名方式,不要用camelCase命名。
指令可以分为函数式写法与对象式写法
new Vue({
el:'#root',
data:{
name:'尚硅谷',
n:1
},
directives:{
big(element,binding){
console.log('big',this) //注意此处的this是window
// console.log('big')
element.innerText = binding.value * 10
},
fbind:{
//指令与元素成功绑定时(一上来)
bind(element,binding){
element.value = binding.value
},
//指令所在元素被插入页面时
inserted(element,binding){
element.focus()
},
//指令所在的模板被重新解析时
update(element,binding){
element.value = binding.value
}
}
}
})
函数式写法就相当于只写了bind 与 update, 向上面这样bind、inserted、update都写,一些小细节的东西才能处理
5.5、Vm 与 Vc
关于this指向:
(1).组件配置中:
data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【VueComponent实例对象】。
(2).new Vue(options)配置中:
data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【Vue实例对象】。
Vm是指Vue实例, Vc是指VueComponent实例,一般Vm中的$children 会有相应的Vc实例,如果是嵌套组件,则其下也会有$children 也会有Vc实例, Vm 与 Vc基本长一个样 ,且Vm创建的时候又el配置项用于指定服务于那个容器,而Vc则没有
1.一个重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype
2.为什么要有这个关系:让组件实例对象(vc)可以访问到 Vue原型上的属性、方法。
5.6、插件
写法 plugins.js
export default {
install(Vue,x,y,z){
console.log(x,y,z)
//全局过滤器
Vue.filter('mySlice',function(value){
return value.slice(0,4)
})
//定义全局指令
Vue.directive('fbind',{
//指令与元素成功绑定时(一上来)
bind(element,binding){
element.value = binding.value
},
//指令所在元素被插入页面时
inserted(element,binding){
element.focus()
},
//指令所在的模板被重新解析时
update(element,binding){
element.value = binding.value
}
})
//定义混入
Vue.mixin({
data() {
return {
x:100,
y:200
}
},
})
//给Vue原型上添加一个方法(vm和vc就都能用了),但是一般不这么做,需要慎重考虑
Vue.prototype.hello = ()=>{alert('你好啊')}
}
}
在项目入口文件中引入安装一下
import Vue from 'vue'
//引入App
import App from './App.vue'
//引入插件
import plugins from './plugins'
//关闭Vue的生产提示
Vue.config.productionTip = false
//应用(使用)插件
Vue.use(plugins,1,2,3)
//创建vm
new Vue({
el:'#app',
render: h => h(App)
})
经过上面操作,这些定义好的过滤器、指令、mixin就可以全局使用了
5.7、npm 查看依赖版本
5.8、解决跨域方法
- 服务端设置cors
- jsonp,需要前后端配置,且只能处理get请求
- 脚手架/打包工具 代理
- nginx代理
6、生命周期
6.1、原理图
常用的生命周期钩子:
1.mounted: 发送ajax请求、启动定时器、绑定自定义事件、订阅消息等【初始化操作】。
2.beforeDestroy: 清除定时器、解绑自定义事件、取消订阅消息等【收尾工作】。
关于销毁Vue实例
1.销毁后借助Vue开发者工具看不到任何信息。
2.销毁后自定义事件会失效,但原生DOM事件依然有效。
3.一般不会在beforeDestroy操作数据,因为即便操作数据,也不会再触发更新流程了。
7、事件总线
1、Vue原型对象上包含事件处理的方法
1)$on(eventName,listener):绑定自定义事件监听
2)$emit(eventName,data):分发自定义事件
3)$off(eventName):解绑自定义事件监听4)
$once(eventName,listener):绑定事件监听,但只能处理一次
2、所有组件实例对象的原型对象的原型对象就是Vue的原型对象
1)所有组件对象都能看到Vue原型对象上的属性和方法
2)Vue.prototype.$bus=newVue(),所有的组件对象都能看到$bus这个属性对象
事件总线其实大概就是这样一个东西
new Vue({
el:'#app',
render: h => h(App),
beforeCreate() {
Vue.prototype.$bus = this //安装全局事件总线
},
})
8、Vuex
8.1、基础使用
vuex官网 都是使用
但是需要注意是,vue开发者工具只能检测到mutation 的动作
mapState、mapGetters使用
computed:{
//靠程序员自己亲自去写计算属性
/* sum(){
return this.$store.state.sum
},
school(){
return this.$store.state.school
},
subject(){
return this.$store.state.subject
}, */
//借助mapState生成计算属性,从state中读取数据。(对象写法)
// ...mapState({he:'sum',xuexiao:'school',xueke:'subject'}),
//借助mapState生成计算属性,从state中读取数据。(数组写法)
...mapState(['sum','school','subject']),
/* ******************************************************************** */
/* bigSum(){
return this.$store.getters.bigSum
}, */
//借助mapGetters生成计算属性,从getters中读取数据。(对象写法)
// ...mapGetters({bigSum:'bigSum'})
//借助mapGetters生成计算属性,从getters中读取数据。(数组写法)
...mapGetters(['bigSum'])
},
mapMutations、mapActions
methods: {
//程序员亲自写方法
/* increment(){
this.$store.commit('JIA',this.n)
},
decrement(){
this.$store.commit('JIAN',this.n)
}, */
//借助mapMutations生成对应的方法,方法中会调用commit去联系mutations(对象写法)
...mapMutations({increment:'JIA',decrement:'JIAN'}),
//借助mapMutations生成对应的方法,方法中会调用commit去联系mutations(数组写法)
// ...mapMutations(['JIA','JIAN']),
/* ************************************************* */
//程序员亲自写方法
/* incrementOdd(){
this.$store.dispatch('jiaOdd',this.n)
},
incrementWait(){
this.$store.dispatch('jiaWait',this.n)
}, */
//借助mapActions生成对应的方法,方法中会调用dispatch去联系actions(对象写法)
...mapActions({incrementOdd:'jiaOdd',incrementWait:'jiaWait'})
//借助mapActions生成对应的方法,方法中会调用dispatch去联系actions(数组写法)
// ...mapActions(['jiaOdd','jiaWait'])
},
模块化开发
模块化的作用,顾名思义就是模块化,减少在同事间在协作的过程中可能会出现的冲突,代码结构更清晰,方便维护
9、vue-router
官网
9.1、params参数
携带 params 参数进行路由跳转,必须使用 name,不能使用path
若路由定义了 props: true 则会将params 作为组件的props,可以在组件里面通过prosp获取
若props 直接返回对象也可以,也可以写成函数形式
路由跳转方式:声明式导航 router-link、编程式导航,router.push/replace
9.2、路由守卫
全局路由守卫
//全局前置路由守卫————初始化的时候被调用、每次路由切换之前被调用
router.beforeEach((to,from,next)=>{
console.log('前置路由守卫',to,from)
if(to.meta.isAuth){ //判断是否需要鉴权
if(localStorage.getItem('school')==='atguigu'){
next()
}else{
alert('学校名不对,无权限查看!')
}
}else{
next()
}
})
//全局后置路由守卫————初始化的时候被调用、每次路由切换之后被调用
router.afterEach((to,from)=>{
console.log('后置路由守卫',to,from)
document.title = to.meta.title || '管理系统'
})
路由独享守卫
{
name:'zhuye',
path:'/home',
component:Home,
meta:{title:'主页'},
children:[
{
name:'xinwen',
path:'news',
component:News,
meta:{isAuth:true,title:'新闻'},
beforeEnter: (to, from, next) => {
console.log('独享路由守卫',to,from)
if(to.meta.isAuth){ //判断是否需要鉴权
if(localStorage.getItem('school')==='xxxx'){
next()
}else{
alert('学校名不对,无权限查看!')
}
}else{
next()
}
}
}
}
组件内路由守卫
export default {
name:'About',
/* beforeDestroy() {
console.log('About组件即将被销毁了')
},*/
/* mounted() {
console.log('About组件挂载完毕了',this)
window.aboutRoute = this.$route
window.aboutRouter = this.$router
}, */
mounted() {
// console.log('%%%',this.$route)
},
//通过路由规则,进入该组件时被调用
beforeRouteEnter (to, from, next) {
console.log('About--beforeRouteEnter',to,from)
if(to.meta.isAuth){ //判断是否需要鉴权
if(localStorage.getItem('school')==='xxxxx'){
next()
}else{
alert('学校名不对,无权限查看!')
}
}else{
next()
}
},
//通过路由规则,离开该组件时被调用
beforeRouteLeave (to, from, next) {
console.log('About--beforeRouteLeave',to,from)
next()
}
}
路由模式
hash 与 history
history 会将路由地址,当做请求资源的地址,所以会出现404问题,需要后端去做适配,区别那些前段路由,那些是后端路由才行 ,而hash就不会出现这个问题