单页面应用
加载单个 HTML 页面,局限于一个 Web 页面中,仅在该 Web 页面初始化时加载相应的 HTML 、 JavaScript 、 CSS 。一旦页面加载完成,就不会再有整页刷新, SPA 不会因为用户的操作而进行页面的重新加载或跳转,而是利用 JavaScript 动态的变换 HTML(采用的是 div 切换显示和隐藏),从而实现UI与用户的交互。
优点:
-
用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
-
基于上面一点,SPA 相对对服务器压力小;
-
前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
缺点:
-
初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
-
前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
-
SEO(搜索引擎优化) 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。
v-show 和v-if的区别
-
v-if
是“真正的”条件渲染,因为它确保在切换期间被正确销毁和重新创建。懒惰的:如果初始渲染时条件为假,它不会做任何事情 - 条件块不会被渲染,直到条件变为真 -
v-show
无论初始条件如何,元素总是被渲染,使用基于 CSS 的display切换 -
v-if
切换成本较高,适用于条件不太可能在运行时更改,v-show初始渲染成本较高,适用于经常切换某些内容
为什么v-for和v-if不建议用在一起
1.当 v-for 和 v-if 处于同一个节点时,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费
2.这种场景建议使用 computed,先对数据进行过滤
Class 与 Style 如何动态绑定?(v-bind处理)
-
Class 可以通过对象语法和数组语法进行动态绑定:
- 对象语法:[类已经确定了,根据对象的value值条件判断是否渲染当前类】
-
<div class="static" v-bind:class="{ active: isActive, 'text-danger': hasError }" ></div>
-
<div v-bind:class="classObject"></div> data: { isActive: true, error: null }, computed: { classObject: function () { return { active: this.isActive && !this.error, 'text-danger': this.error && this.error.type === 'fatal' } } }
数组语法: 【类不确定,根据数组元素设置类】
<div v-bind:class="[activeClass, errorClass]"></div> data: { activeClass: 'active', errorClass: 'text-danger' }
使用三元表达式:
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
-
Style 也可以通过对象语法和数组语法进行动态绑定:
- CSS 属性名称可以使用驼峰式或 kebab-case(在 kebab-case 中使用引号):
- 对象语法:
-
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div> data: { activeColor: 'red', fontSize: 30 }
-
直接绑定到样式对象通常是一个好主意,以便模板更清晰:
<div v-bind:style="styleObject"></div>
data: { styleObject: { color: 'red', fontSize: '13px' } }
- 数组语法:
-
v-bind:style
允许您将多个样式对象应用于同一元素:<div v-bind:style="[baseStyles, overridingStyles]"></div>
-
<div v-bind:style="[styleColor, styleSize]"></div> <div v-bind:style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
怎样理解 Vue 的单向数据流?
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。
这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。
computed 和 watch 的区别和运用的场景?(异步,缓存)
computed: 计算属性,1.依赖其它属性值(data,props),有缓存,只有它依赖的属性值发生改变,才会重新计算,数据没有改变的话多次访问将立即返回先前计算的结果,相比之下,只要发生重新渲染,方法调用将始终运行该函数。2. 不支持异步,当computed内有异步操作时无效,无法监听数据的变化。它可以帮助我们将在模板中的一些稍微复杂的逻辑计算放回到js代码中
-
默认情况下,计算属性仅限 getter,但您也可以在需要时提供 setter:
-
computed: { fullName: { // getter get: function () { 、、 return this.firstName + ' ' + this.lastName }, // setter
//监视当前属性值的变化,当属性值发生变化时执行,更新相关的属性数据 //newValue 就是fullName的最新属性值
set: function (newValue) { var names = newValue.split(' ') this.firstName = names[0] this.lastName = names[names.length - 1] } } } // ...
现在,当你运行
vm.fullName = 'John Doe'
,二传手将被调用,vm.firstName
并且vm.lastName
将相应地更新。 -
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;一般用来监听属性的变化(也可以用来监听计算属性函数)1. 不支持缓存,数据变,直接会触发相应的操作。2.watch支持异步;监听的函数接收两个参数,第一个参数是最新的值;第二个参数是输入之前的值;3.当一个属性发生变化时,需要执行对应的操作;一对多;4.无缓存性,页面重新渲染时值不变化也会执行
- 监听数据必须是data中声明过或者父组件传递过来的props中的数据,当数据变化时,触发其他操作,函数有两个参数:
-
首先确认 watch是一个对象,一定要当成对象来用。 对象就有键,有值。
键:就是你要监控的那个家伙,比如说$route,这个就是要监控路由的变化。或者是data中的某个变量。
值可以是函数:就是当你监控的家伙变化时,需要执行的函数,这个函数有两个形参,第一个是当前值,第二个是变化后的值。
值也可以是函数名:不过这个函数名要用单引号来包裹。
值是包括选项的对象:选项包括有三个,如下第一个handler:其值是一个回调函数。即监听到变化时应该执行的函数。
第二个是deep:其值是true或false;确认是否深入监听。(一般监听时是不能监听到对象属性值的变化的,数组的值变化可以听到。)
第三个是immediate:其值是true或false;确认是否以当前的初始值执行handler的函数。 -
如果不设置immediate,或者将immediate设为false的话,则刷新页面后不会立即监听此对象,也就是控制台不会有输出,必须要监听的对象有值改变时控制台才会有输出。
-
immediate: 组件加载立即触发回调函数执行
watch: { firstName: { handler(newName, oldName) { this.fullName = newName + ' ' + this.lastName; }, // 代表在wacth里声明了firstName这个方法之后立即执行handler方法 immediate: true } }
-
deep: deep的意思就是深入观察,监听器会一层层的往下遍历,给对象的所有属性都加上这个监听器,但是这样性能开销就会非常大了,任何修改obj里面任何一个属性都会触发这个监听器里的 handler
watch: { obj: { handler(newName, oldName) { console.log('obj.a changed'); }, immediate: true, deep: true } }
-
进行优化,给对象的指定属性添加侦听,减少性能开销,这样vue.js会一层一层解析直到遇到属性a,才给a设置监听函数
-
注意:在变更 (不是替换) 对象或数组时,旧值将与新值相同,因为它们的引用指向同一个对象/数组。Vue 不会保留变更之前值的副本。
-
监听对象单个属性
方法一:可以直接对用对象.属性的方法拿到属性
data(){ return{ 'first':{ second:0 } } }, watch:{ first.second:function(newVal,oldVal){ console.log(newVal,oldVal); } },
方法二:watch如果想要监听对象的单个属性的变化,必须用computed作为中间件转化,因为computed可以取到对应的属性值
data(){ return{ 'first':{ second:0 } } }, computed:{ secondChange(){ return this.first.second } }, watch:{ secondChange(){ console.log('second属性值变化了') } },
-
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
-
大概总结一下,computed 和 watch 的使用场景并不一样,computed 的话是通过几个数据的变化,来影响一个数据,而 watch,则是可以一个数据的变化,去影响多个数据。
-
watch 和computed区别那里,如果watch的值不发生变化重新渲染的时候也不会执行watch。可以是使用 $forceUpdate()方法验证,调用它会强制刷新页面。
-
通俗来讲,既能用 computed 实现又可以用 watch 监听来实现的功能,推荐用 computed
<template>
<div>
<p>{{ this.number }}</p>
</div>
</template>
<script>
export default {
name: 'test1',
data () {
return {
number: 1
}
},
created () {
setTimeout(() => {
this.number = 100
}, 2000)
},
watch: {
number (newVal, oldVal) {
console.log('number has changed: ', newVal)
}
}
}
</script>
Vue.set( target, propertyName/index, value )
受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。
由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新 property,因为 Vue 无法探测普通的新增 property (比如 this.myObject.newProperty = 'hi'
)
方案一:利用Vue.set(object,key,val)
例:Vue.set(vm.obj,'k1','v1')
方案二:利用this.$set(this.obj,key,val)
例:this.$set(this.obj,'k1','v1')
方案三:利用Object.assign({},this.obj)创建新对象
直接给一个数组项赋值,Vue 能检测到变化吗?
-
由于 JavaScript 的限制,Vue 不能检测到以下数组的变动:
-
当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
-
当你修改数组的长度时,例如:vm.items.length = newLength
-
为了解决第一个问题,Vue 提供了以下操作方法:
// Vue.set Vue.set(vm.items, indexOfItem, newValue) // Array.prototype.splice vm.items.splice(indexOfItem, 1, newValue) 你也可以使用 vm.$set 实例方法,该方法是全局方法 Vue.set 的一个别名: vm.$set(vm.items, indexOfItem, newValue)
为了解决第二个问题,Vue 提供了以下操作方法:
vm.items.splice(newLength)
数组考虑性能原因没有用defineProperty对数组的每一项进行拦截,而是选择重写数组 方法以进行重写。当数组调用到这 7 个方法的时候,执行 ob.dep.notify() 进行派发通知 Watcher 更新;
重写数组方法:push/pop/shift/unshift/splice/reverse/sort
补充回答:
在Vue中修改数组的索引和长度是无法监控到的。需要通过以下7种变异方法修改数组才会触发数组对应的wacther进行更新。数组中如果是对象数据类型也会进行递归劫持。
说明:那如果想要改索引更新数据怎么办?
可以通过Vue.set()来进行处理 =》 核心内部用的是 splice 方法。
// 取出原型方法;
const arrayProto = Array.prototype
// 拷贝原型方法;
export const arrayMethods = Object.create(arrayProto)
// 重写数组方法;
def(arrayMethods, method, function mutator (...args) { }
ob.dep.notify() // 调用方法时更新视图;
Vue.set 方法是如何实现的?
核心答案:
为什么$set可以触发更新,我们给对象和数组本身都增加了dep属性,当给对象新增不存在的属性则触发对象依赖的watcher去更新,当修改数组索引时我们调用数组本身的splice方法去更新数组。
补充回答:
官方定义Vue.set(object, key, value)
1) 如果是数组,调用重写的splice方法 (这样可以更新视图 )
代码:target.splice(key, 1, val)
2) 如果不是响应式的也不需要将其定义成响应式属性。
3) 如果是对象,将属性定义成响应式的 defineReactive(ob.value, key, val)
通知视图更新 ob.dep.notify()
export function set (target: Array<any> | Object, key: any, val: any): any {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// key 已经存在,直接修改属性值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// target 本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val
return val
}
// 对属性进行响应式处理
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
vm.$set 的实现原理是:
- 如果目标是数组,直接使用数组的 splice 方法触发相应式;
- 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
-
有时你想向已有对象上添加一些属性,例如使用 Object.assign() 或 _.extend() 方法来添加属性。但是,添加到对象上的新属性不会触发更新。在这种情况下可以创建一个新的对象,让它包含原对象的属性和新的属性:
// 代替
Object.assign(this.obj, { a: 1, e: 2 })
this.obj= Object.assign({}, this.obj, { a: 1, e: 2 })
React/Vue 项目中 key 的作用
-
key的作用是为了在diff算法执行时更快的找到对应的节点,
提高diff速度,更高效的更新虚拟DOM
;vue和react都是采用diff算法来对比新旧虚拟节点,从而更新节点。在vue的diff函数中,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点。如果没找到就认为是一个新增节点。而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个map映射,另一种是遍历查找。相比而言。map映射的速度更快。
-
为了在数据变化时强制更新组件,以避免
“就地复用”
带来的副作用。当 Vue.js 用
v-for
更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。重复的key会造成渲染错误。
Vue自定义指令:
vue 中常用的一些内置 v- 指令
-
v-text:元素的 innerText 属性,只能用在双标签中, 和{{ }}效果是一样的,使用较少
-
v-html:元素的 innerHTML,其实就是给元素的 innerHTML 赋值
-
v-show
-
v-if
-
v-else-if:前一个相邻元素必须有 v-if 或 v-else-if
-
v-else:前一个相邻元素必须有 v-if 或 v-else-if,如果 v-if 和 v-else-if 都有对应的表达式,则 v-else 可以直接写
-
v-for:用于循环渲染一组数据(数组或对象)。必须使用特定语法:v-for="alias in expression"。注:当 v-for 和 v-if 同处于一个节点时,v-for 的优先级比 v-if 更高。即 v-if 将运行在每个 v-for循环中
-
v-on:
-
v-model:用于 input/textarea 等表单控件上创建双向数据绑定。
-
v-bind:动态的绑定一个或多个属性,常用于绑定 class,style,href 等。
-
v-once:组件和元素只渲染一次,当数据发生变化,也不会重新渲染
对普通 DOM 元素进行底层操作,这时候就会用到自定义指令
-
Vue 提供了自定义指令的5个钩子函数:
- bind:指令第一次绑定到元素时调用,只执行一次。在这里可以进行一次性的初始化设置。
-
inserted:被绑定的元素,插入到父节点的 DOM 中时调用(仅保证父节点存在)。
-
update:组件更新时调用(当指令绑定的元素状态/样式、内容(这里指元素绑定的 vue 数据) 发生改变时触发)。
-
componentUpdated:组件与子组件更新时调用(当 update() 执行完毕之后触发)。
-
unbind:指令与元素解绑时调用,只执行一次。
-
注意:
-
-
除 update 与 componentUpdated 钩子函数之外,每个钩子函数都含有 el、binding、vnode (Vue 编译生成的虚拟节点)这三个参数
-
在每个函数中,第一个参数永远是 el, 表示被绑定了指令的那个 dom 元素,这个el 参数,是一个原生的 JS 对象,所以 Vue 自定义指令可以用来直接和 DOM 打交道
-
binding 是一个对象,它包含以下6个属性:name(指令名,不包括
v-
前缀)、value(指令的绑定值=》vue data中的值)、oldValue、expression(字符串形式的指令表达式)、arg(传给指令的参数,可选。例如v-my-directive:foo
中,参数为"foo"
)、modifiers(一个包含修饰符的对象。例如:v-my-directive.foo.bar
中,修饰符对象为{ foo: true, bar: true }
。)除了el
之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。 -
oldVnode 只有在 update 与 componentUpdated 钩子中生效
-
除了 el 之外,binding、vnode 属性都是只读的
-
实列
// clickOutside.js export default { bind (el, binding) { const self = {} // 定义一个私有变量,方便在unbind中可以解除事件监听 self.documentHandler = (e) => { if (el.contains(e.target)) { // 这里判断绑定的元素是否包含点击元素, //如果包含则返回 return false } if (binding.value) { // 判断指令中是否绑定了值 binding.value(e) // 如果绑定了函数则调用那个函数,此处 //binding.value就是handleClose方法 } return true } document.addEventListener('click', self.documentHandler) }, unbind (el) { // 解除事件监听 document.removeEventListener('click', self.documentHandler) delete self.documentHandler } } 在组件中使用: <template> <div> <div v-show="isShow" v-clickoutside="handleClickOutside" @click="showOrHide"> 子菜单 ... </div> </div> </template> <script> import clickoutside from './js/clickOutside' export default { ... ... directives: { clickoutside }, data() { return { isShow: true, }; }, methods: { handleOutsideClick () { this.isShow = false } } } </script>
-
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
如果想注册局部指令,组件中也接受一个 directives 的选项:
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}
然后你可以在模板中任何元素上使用新的 v-focus property,如下:
<input v-focus>
Vue.nextTick( [callback, context] )
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
使用场景:
- 在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作都应该放进
Vue.nextTick()
的回调函数中。
在Vue生命周期的created()
钩子函数进行的DOM操作一定要放在Vue.nextTick()
的回调函数中
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
})
// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})
因为 $nextTick()
返回一个 Promise
对象,所以你可以使用新的 ES2017 async/await 语法完成相同的事情
methods: {
updateMessage: async function () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
await this.$nextTick()
console.log(this.$el.textContent) // => '已更新'
}
}
原理:
Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue
将开启1个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher
被多次触发,只会被推入到队列中-次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM
操作是非常重要的。nextTick
方法会在队列中加入一个回调函数,确保该函数在前面的dom操作完成后才调用;
nextTick主要使用了宏任务和微任务。 根据执行环境分别尝试采用Promise、MutationObserver、setImmediate,如果以上都不行则采用setTimeout定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。
使用过插槽么?用的是具名插槽还是匿名插槽或作用域插槽
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
后备内容:插槽默认内容
<slot>Submit</slot>
Vue 实现了一套内容分发的 API,就是一个占位的。
没有插槽的情况下在组件标签内写一些内容是不起任何作用的,当我在组件中声明了slot元素后,在组件元素内写的内容,就会跑到它这里了!
vue当中插槽包含三种一种是默认插槽(匿名)一种是具名插槽还有一种就是作用域插槽 匿名插槽就是没有名字的只要默认的都填到这里具名插槽指的是具有名字的
具名插槽:(<slot name="header"></slot> <tempate v-slot:header></template>)
跟 v-on
和 v-bind
一样,v-slot
也有缩写,即把参数之前的所有内容 (v-slot:
) 替换为字符 #
。例如 v-slot:header
可以被重写为 #header
:
<slot>
元素有一个特殊的 attribute:name
。这个 attribute 可以用来定义额外的插槽:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
一个不带 name
的 <slot>
出口会带有隐含的名字“default”。
在向具名插槽提供内容的时候,我们可以在一个 <template>
元素上使用 v-slot
指令,并以 v-slot
的参数的形式提供其名称:
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
作用域插槽:(<slot v-bind:user="user"></slot> <template v-slot:(插槽名称) ="getuser")
为了让 user
在父级的插槽内容中可用,我们可以将 user
作为 <slot>
元素的一个 attribute 绑定上去:
<span>
<slot v-bind:user="user">
{{ user.lastName }}
</slot>
</span>
绑定在 <slot>
元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot
来定义我们提供的插槽 prop 的名字:
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
常考-生命周期
Vue的生命周期方法有哪些?一般在哪一步发起请求及原因
核心答案:
总共分为8个阶段:创建前/后,载入前/后,更新前/后,销毁前/后。
1、创建前/后:
1) beforeCreate阶段:vue实例的挂载元素el和数据对象data都为undefined,还未初始化。
说明:在当前阶段data、methods、computed以及watch上的数据和方法都不能被访问。
2) created阶段:vue实例的数据对象data有了,el(也就是Vue实例挂载的元素节点)
el : '#app', 在实例挂在之后可以用vm.$el访问
)还没有。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算watch/event 事件回调。
说明:可以做一些初始数据的获取,在当前阶段无法与Dom进行交互,如果非要想,可以通过vm.$nextTick来访问Dom。
2、载入前/后:
1)beforeMount阶段:vue实例的$el和data都初始化了,但还是挂载之前为虚拟的dom节点。
说明:当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。相关的 render 函数首次被调用
2)mounted阶段:vue实例挂载完成,data.message成功渲染。
说明:在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。
3、更新前/后:
1)beforeUpdate阶段:响应式数据更新时调用,发生在虚拟DOM打补丁之前,适合在更新之前访问现有的DOM,比如手动移除已添加的事件监听器。
说明:可以在当前阶段进行更改数据,不会造成重渲染。
2) updated阶段:虚拟DOM重新渲染和打补丁之后调用,组成新的DOM已经更新,避免在这个钩子函数中操作数据,防止死循环。
说明:当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。
4、销毁前/后:
1)beforeDestroy阶段:实例销毁前调用,实例还可以用,this能获取到实例,常用于销毁定时器,解绑事件。
说明:在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。
2) destroyed阶段:实例销毁后调用,调用后所有事件监听器会被移除,所有的子实例都会被销毁。
说明:当前阶段组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。
异步请求在哪一步发起:
可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
能更快获取到服务端数据,减少页面 loading 时间;
ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性性
第一次页面加载时会触发:beforeCreate, created, beforeMount, mounted。
1)created 实例已经创建完成,因为它是最早触发的原因可以进行一些数据,资源的请求。(服务器渲染支持created方法)
2)mounted实例已经挂载完成,可以进行一些DOM操作。(接口请求)
activited | keep-alive 专属,组件被激活时调用 |
deactivated | keep-alive 专属,组件被销毁时调用 |
生命周期钩子是如何实现的?
核心答案:
Vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法。
补充回答:
内部主要是使用callHook方法来调用对应的方法。核心是一个发布订阅模式,将钩子订阅好(内部采用数组的方式存储),在对应的阶段进行发布。
Vue 的父组件和子组件生命周期钩子执行顺序
核心答案:
第一次页面加载时会触发 beforeCreate, created, beforeMount, mounted 这几个钩子。
渲染过程:
父组件挂载完成一定是等子组件都挂载完成后,才算是父组件挂载完,所以父组件的mounted在子组件mouted之后
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
随后-->父组件beforeUpdate -->子组件beforeDestroy--> 子组件destroyed --> 父组件updated
子组件更新过程:
影响到父组件:父beforeUpdate -> 子beforeUpdate->子updated -> 父updted
不影响父组件:子beforeUpdate -> 子updated
父组件更新过程:
影响到子组件:父beforeUpdate -> 子beforeUpdate->子updated -> 父updted
不影响子组件:父beforeUpdate -> 父updated
销毁过程:
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
重要:父组件等待子组件完成后,才会执行自己对应完成的钩子。
挂载阶段 父created->子created->子mounted->父mounted
父组件监听到子组件的生命周期
1、使用on和on和emit (在子组件的生命周期中$emit一个事件,父组件@on接收)
子组件emit触发一个事件,父组件on监听相应事件。
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
this.$emit("mounted");
}
2、hook钩子函数
使用vue hook生命周期钩子函数
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
Vue中的组件的data 为什么是一个函数?
每次使用组件时都会对组件进行实例化操作,并且调用data函数返回一个对象作为组件的数据源。这样可以保证多个组件间数据互不影响。
如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。
因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。
1.一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。 2.如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数
<script>
const obj = {
name:"a",
age:22
}
function person() {
return obj;
};
var obj1 = person();
var obj2 = person();
obj1.name = 'b';
console.log(obj1);
console.log(obj2);
</script>
总结:第一个示例中每调用一次函数都返回一个新的对象,而在第二个示例中所共用一个对象。所以,在改变其中一个对象某一属性的值时,第一个示例中的两个对象不会相互影响,而第二个示例中则是相互影响的。
Vue 组件间通信有哪几种方式?
Vue 组件间通信只要指以下 3 类通信:父子、隔代、兄弟
父子组件通信:
1.props / $emit 父子组件通信
父组件A通过props的方式向子组件B传递,B to A 通过在 B 组件中 $emit, A 组件中 v-on 的方式实现。
2.$emit
/$on
中央事件总线EventBus
的方式(兄弟组件的通信)
EventBus
通过新建一个Vue
事件bus
对象,通过bus.$emit
触发事件,bus.$on监听触发的事件。
// 定义中央事件总线
let bus = new Vue();
// 将中央事件总线赋值给Vue.prototype中,这样所有组件都能访问到了
Vue.prototype.$bus = bus;
let vm = new Vue({
el: '#app',
template:
Event Bus
实现跨组件通信 Vue.prototype.$bus = new Vue()
自定义事件
var Event=new Vue(); Event.$emit(事件名,数据); Event.$on(事件名,data => {});
3.$attrs和$listeners【在中间组件上面添加属性v-bind=$attrs【传不被props接受的属性往下传值。】在中间组件上面增加v-on=$listeners,上级组件可以直接调用下级组件$emit的方法】
$attrs
包含了父作用域中不被认为 props 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 props 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind=”$attrs” 传入内部组件——在创建更高层次的组件时非常有用。
$listeners
包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on=”$listeners” 传入内部组件——在创建更高层次的组件时非常有用。
inheritAttrs
默认情况下父作用域的不被认作 props 的特性绑定 (attribute bindings) 将会“回退”且作为普通的 HTML 特性应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 inheritAttrs 到 false,这些默认行为将会被去掉。而通过 (同样是 2.4 新增的) 实例属性 $attrs 可以让这些特性生效,且可以通过 v-bind 显性的绑定到非根元素上。
Vue.component('A', {
template: `
<div>
<p>我是A组件</p>
<B :msg='msg' @getCData='getCData'></B>
</div>
`,
methods: {
getCData(val) {
alert(val)
}
},
data() {
return {
msg: 'hello 小马哥'
}
},
})
Vue.component('B', {
template: `
<div>
<p>我是B组件</p>
<!-- C组件中能直接触发 getCData 的原因在于:B组件调用 C组件时,使用 v-on 绑定了 $listeners 属性 -->
<!-- 通过v-bind 绑定 $attrs 属性,C组件可以直接获取到 A组件中传递下来的 props(除了 B组件中 props声明的) -->
<C v-bind='$attrs' v-on='$listeners'></C>
</div>
`,
// props: ['msg'],
data() {
return {
}
}
})
Vue.component('C', {
template: `
<div>
<p>我是C组件</p>
<p>{{$attrs.msg}}</p>
<button @click='handleClick'>传递数据</button>
</div>
`,
methods: {
handleClick() {
this.$emit('getCData', 'C组件的数据')
}
},
data() {
return {
}
}
})
let vm = new Vue({
el: '#app',
template: `
<div>
<A></A>
</div>
`,
})
4.ref 与 $parent / $children 适用 父子组件通信
ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
修改属性或者方法
let name = this.$refs.myRef.name
this.$refs.myRef.getSonMsg()
$parent:获取父组件实例,同样,获取之后我们可以使用它的属性和方法。如果当前实例没有父实例,此实例将会是其自己。
this.$parent.num = 1
this.$parent.funa("xx")
console.log(this.$parent.total);
注意:如果子组件是公共组件,会被多个父组件调用,那么$parent会怎么获取?改变他们的属性将会怎么变化?父组件中没有这个属性怎么办?
- 针对不同父组件调用,子组件会每次都会生成一个实例,这也是Vue的重要机制。$parent会获取每个调用它的父组件实例。
- 子组件中通过$parent会改变每个调用它的父组件中的对应属性。
$children为当前组件的所有直接子组件,是一个无序的数组 。儿子里面有个 _uid 属性,可以知道他是第几个元素,是元素的唯一标识符。不是响应式的。
this.$children[0]就能获取相应的组件
注意:
1、如果你在mounted里获取this.$refs,因为dom还未完全加载,所以你是拿不到的, update阶段则是完成了数据更新到 DOM 的阶段(对加载回来的数据进行处理),此时,就可以使用this.$refs了
2、如果写在method中,那么可以使用 this.$nextTick(() => {}) 等页面渲染好再调用,这样就可以了
root 和parent 都能够实现访问父组件的属性和方法,两者的区别在于,如果存在多级子组件,通过parent 访问得到的是它最近一级的父组件,通过root 访问得到的是根父组件
总结父子组件
父组件传值给子组件(props)
子组件传值给父组件($emit)
父组件调用子组件的方法(this.$refs.child.sing())
子组件调用父组件的方法($emit)
父组件改变子组件的值 (通过this.$refs.son.a = "xx"或者this.$refs.son.b())
子组件改变父组件的值(通过$emit,让父组件自己去改)
5.vueX
[
Vuex 是一个专为 Vue 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。
- Vuex 的状态存储是响应式的;当 Vue 组件从 store 中读取状态的时候,
若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新 2. 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation, 这样使得我们可以方便地跟踪每一个状态的变化 Vuex主要包括以下几个核心模块:
- State:定义了应用的状态数据
- Getter:在 store 中定义“getter”(可以认为是 store 的计算属性),
就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来, 且只有当它的依赖值发生了改变才会被重新计算 3. Mutation:是唯一更改 store 中状态的方法,且必须是同步函数 4. Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作 5. Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中
]
状态管理模式,当我们的应用遇到多个组件共享状态时
- 多个视图依赖于同一状态。
- 来自不同视图的行为需要变更同一状态。
每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。
你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。
Vuex 通过 store
选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex)
), 子组件能通过 this.$store
访问到
在 Vue 组件中获得 Vuex 状态:
由于 store 中的状态是响应式的,在组件中调用 store 中的状态简单到仅需要在计算属性中返回即可。触发变化也仅仅是在组件的 methods 中提交 mutation。
// 创建一个 Counter 组件
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}
每当 store.state.count
变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM。
mapState
辅助函数
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState
辅助函数帮助我们生成计算属性
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
// ...
//computed: {
// count () {
// return this.$store.state.count
//}
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组
computed: mapState([
// 映射 this.count 为 store.state.count
'count'
])
mapState
函数返回的是一个对象。我们如何将它与局部计算属性混合使用对象扩展运算符
computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})
}
Getter (参数为state,getters)
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
Getter 接受 state 作为其第一个参数
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
访问getters:
通过属性访问(store.getters.doneTodos)
Getter 会暴露为 store.getters
对象,你可以以属性的形式访问这些值:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getter 也可以接受其他 getter 作为第二个参数:
getters: {
// ...
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
store.getters.doneTodosCount // -> 1
通过方法访问
你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。
mapGetters
辅助函数
mapGetters
辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
如果你想将一个 getter 属性另取一个名字,使用对象形式:
...mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})
Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
你需要以相应的 type 调用 store.commit 方法:
store.commit('increment') //mutation 都是同步事务:
// 任何由 "increment" 导致的状态变更都应该在此刻完成。
可以向 store.commit
传入额外的参数,即 mutation 的 载荷(payload):
// ...
mutations: {
increment (state, n) {
state.count += n
}
}
store.commit('increment', 10)
在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:
// ...
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
store.commit('increment', {
amount: 10
})
提交 mutation 的另一种方式是直接使用包含 type
属性的对象:
store.commit({
type: 'increment',
amount: 10
})
Mutation 需遵守 Vue 的响应规则
Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:
-
最好提前在你的 store 中初始化好所有所需属性。
-
当需要在对象上添加新属性时,你应该
-
使用
Vue.set(obj, 'newProp', 123)
, 或者 -
以新对象替换老对象。例如,利用对象展开运算符 (opens new window)我们可以这样写:
state.obj = { ...state.obj, newProp: 123 }
使用常量替代 Mutation 事件类型
使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然:
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = new Vuex.Store({
state: { ... },
mutations: {
// 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
[SOME_MUTATION] (state) {
// mutate state
}
}
})
在组件中提交 Mutation
你可以在组件中使用 this.$store.commit('xxx')
提交 mutation,或者使用 mapMutations
辅助函数将组件中的 methods 映射为 store.commit
调用(需要在根节点注入 store
)。
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// `mapMutations` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
}
Mutation 必须是同步函数
Action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
-
const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state) { state.count++ } }, actions: { increment (context) { context.commit('increment') } } })
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用
context.commit
提交一个 mutation,或者通过context.state
和context.getters
来获取 state 和 getters。 -
实践中,我们会经常用到 ES2015 的 参数解构 (opens new window)来简化代码(特别是我们需要调用
commit
很多次的时候):actions: { increment ({ commit }) { commit('increment') } }
分发 Action
Action 通过 store.dispatch
方法触发:
store.dispatch('increment')
乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
Actions 支持同样的载荷方式和对象方式进行分发:
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})
在组件中分发 Action
你在组件中使用 this.$store.dispatch('xxx')
分发 action,或者使用 mapActions
辅助函数将组件的 methods 映射为 store.dispatch
调用(需要先在根节点注入 store
):
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
// `mapActions` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
}
组合 Action
store.dispatch
可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch
仍旧返回 Promise:
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
现在你可以:
store.dispatch('actionA').then(() => {
// ...
})
在另外一个 action 中也可以:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
最后,如果我们利用 async / await (opens new window),我们可以如下组合 action:
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
一个
store.dispatch
在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。
Module
Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
** 路由刷新
路由刷新是无刷新跳转,表面看起来就像是一个app应用,表现效果就像你写的 tab 选项卡,所有的数据都还存在内存里,页面是无重载的。
F5****页面刷新
F5刷新做了什么事呢,重新载入页面,销毁之前所有的数据。
sessionStorage(以防请求数据量过大页面加载时拿不到返回的数据)
本方法选择的是sessionStorage,选择的原因是由于vue是单页面应用,操作都是在一个页面跳转路由,另一个原因是sessionStorage可以保证打开页面时sessionStorage的数据为空,而如果是localStorage则会读取上一次打开页面的数据。
解决办法:
存储到localStorage
通过监听页面的刷新操作,即beforeunload前存入本地localStorage,页面加载时再从本地localStorage读取信息【sessionStorage一样的】
在App.vue中加入下面代码
1 2 3 4 5 6 7 8 9 |
|
使用vuex-persistedstate 插件
安装插件:npm install vuex-persistedstate --save
配置:
在store的index.js中,手动引入插件并配置
1 2 3 4 5 |
|
该插件默认持久化所有state,当然也可以指定需要持久化的state:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
组件中写 name选项有哪些好处及作用?
1、可以通过名字找到对应的组件( 递归组件 )组件递归操作
vue允许组件模板调用自身,这在日常需求中也时有出现,此时我们就可以根据组件的name,来进行操作。
<!-- 父组件 -->
<div class="container">
<ul>
<child-tree :list="comRecursive" v-if="comRecursive.length"></child-tree>
</ul>
</div>
data(){
return {
comRecursive :[
{
name:'第一层内容1',
childArr:[
{
name:'第二层内容1'
},
{
name:'第二层内容2',
childArr:[
{name:'第三层内容1'}
]
}
]
},
{
name:'第一层内容2'
},
{
name:'第一层内容3'
}
]
}
}
<!-- 子组件 -->
<template>
<div class="container">
<li v-for="(item,index) in list" :key="index">
{{item.name}}
<template v-if="item.childArr">
<ul>
<ChildTreeName :list="item.childArr"/>
</ul>
</template>
</li>
</div>
</template>
<script>
export default {
name:'ChildTreeName',
props:{
list:{
type:Array,
default:[]
}
}
}
<script>
当我们需要组件嵌套自身的时候,此时在组件内部就是通过name值来调用。值得注意的时候,在做组件递归的时候一定要处理好出口,避免造成死循环
2、配合keep-alive对组件缓存做限制(include/exclude="name")
我们知道 keep-alive的 include和exclude 允许有条件的对组件进行缓存,其中include和exclude所匹配的就是组件的name值。
实例:
<!-- 把除了组件名是 Liantong,Dianxin 的组件缓存起来 -->
<keep-alive exclude="Liantong,Dianxin">
<router-view></router-view>
</keep-alive>
3、在dev-tools中使用 可以通过name来识别组件(跨级组件通信时非常重要)
在开发中我们经常需要对代码进行调试,在dev-tools中组件是以组件name进行显示的,
VUE动态组件
让多个组件使用同一个挂载点,并动态切换,这就是动态组件
通过使用保留的 <component>
元素,动态地绑定到它的 is 特性,可以实现动态组件。
keep-alive平时在哪里使用?原理是?
场景:tabs标签页 后台导航,vue性能优化
<transition>
相似,<keep-alive>
是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们主要用于保留组件状态或避免重新渲染。注意:所有的匹配都是匹配组件的 name,没有设置 name 的组件或者路由则不会被匹配。<keep-alive>
要求被切换到的组件都有自己的名字,不论是通过组件的 name
选项还是局部/全局注册。
keep-alive用法
- 在动态组件中的应用
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
<component :is="currentComponent"></component>
</keep-alive>
- 在vue-router中的应用
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
//router.js
export default new Router({
routes: [
{
path: '/',
name: 'Hello',
component: Hello,
meta: {
keepAlive: false // 不需要缓存
}
},
{
path: '/page1',
name: 'Page1',
component: Page1,
meta: {
keepAlive: true // 需要被缓存
}
}
]
})
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
<router-view></router-view>
</keep-alive>
<!-- 多个条件判断的子组件 -->
<keep-alive>
<comp-a v-if="a > 1"></comp-a>
<comp-b v-else></comp-b>
</keep-alive>
<!-- 和 `<transition>` 一起使用 -->
<transition>
<keep-alive>
<component :is="view"></component>
</keep-alive>
</transition>
注意,<keep-alive>
是用在其一个直属的子组件被开关的情形。如果你在其中有 v-for
则不会工作。如果有上述的多个条件性的子元素,<keep-alive>
要求同时只有一个子元素被渲染。
include定义缓存白名单,keep-alive会缓存命中的组件;exclude定义缓存黑名单,被命中的组件将不会被缓存;max定义缓存组件上限,超出上限使用LRU的策略置换缓存数据。
- activated: 页面第一次进入的时候,钩子触发的顺序是created->mounted->activated(,可以将逻辑放到这里面去做)
- deactivated: 页面退出的时候会触发deactivated,当再次前进或者后退的时候只触发activated
- 同理:离开缓存组件的时候,
beforeDestroy和destroyed
并不会触发,可以使用deactivated
离开缓存组件的钩子来代替。
事件挂载的方法等,只执行一次的放在 mounted 中;组件每次进去执行的方法放在 activated 中;
原理:Vue.js
内部将DOM
节点抽象成了一个个的VNode
节点,keep-alive
组件的缓存也是基于VNode
节点的而不是直接存储DOM
结构。它将满足条件(pruneCache与pruneCache)
的组件在cache
对象中缓存起来,在需要重新渲染的时候再将vnode
节点从cache
对象中取出并渲染。
v-model 的原理?
vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的,那么vue是如果进行数据劫持的,我们可以先来看一下通过控制台输出一个定义在vue初始化数据上的对象是个什么东西。
vue是通过Object.defineProperty()来实现数据劫持的
data如何更新view,因为view更新data其实可以通过事件监听即可,比如input标签监听 'input' 事件就可以实现了。所以我们着重来分析下,当数据改变,如何更新视图的
数据更新视图的重点是如何知道数据变了,只要知道数据变了,那么接下去的事都好处理。如何知道数据变了,其实上文我们已经给出答案了,就是通过Object.defineProperty( )对属性设置一个set函数,当数据改变了就会来触发这个函数,所以我们只要将一些需要更新的方法放在这里面就可以实现data更新view了。
首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图
实现数据的双向绑定:
1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。(Object.defineProperty( ))
2.实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。(订阅器Dep添加一个订阅者设计在getter里面,这是为了让Watcher初始化进行触发)
3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
-
text 和 textarea 元素使用 value 属性和 input 事件;
-
checkbox 和 radio 使用 checked 属性和 change 事件;
-
select 字段将 value 作为 prop 并将 change 作为事件。
以 input 表单元素为例:
<input v-model='something'>
如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:
什么是 MVVM
视图模型双向绑定,把MVC
中的Controller
演变成ViewModel
Model–View–ViewModel (MVVM)
1)View 层
View 是视图层UI组件,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。
(2)Model 层
Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。
(3)ViewModel 层
ViewModel 是视图数据层。View
和Model
层的桥梁,数据会绑定到viewModel
层并自动将数据渲染到页面中,视图变化的时候会通知viewModel
层更新数据。
我们以下通过一个 Vue 实例来说明 MVVM 的具体实现,有 Vue 开发经验的同学应该一目了然:
(1)View 层
<div id="app">
(2)ViewModel 层
var app = new Vue({
(3) Model 层
{
优点:
1.双向绑定技术,当Model变化时,View-Model会自动更新,View也会自动变化,能很好的做到数据一致性。
2.View的功能进一步的强化,具有控制的部分功能。
3.UI和逻辑的开发解耦
Object.defineProperty()
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
writable: false
});
obj
要定义属性的对象。
prop
要定义或修改的属性的名称或 Symbol 。
descriptor
要定义或修改的属性描述符。
描述符都是对象
configurable
当且仅当该属性的 configurable
键值为 true
时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
默认为 false
。
enumerable
当且仅当该属性的 enumerable
键值为 true
时,该属性才会出现在对象的枚举属性中。
默认为 false
。
数据描述符还具有以下可选键值:
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。
默认为 undefined。
writable
当且仅当该属性的 writable
键值为 true
时,属性的值,也就是上面的 value
,才能被赋值运算符 (en-US)改变。
默认为 false
。
存取描述符还具有以下可选键值:
get
属性的 getter 函数,如果没有 getter,则为 undefined
。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this
对象(由于继承关系,这里的this
并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
默认为 undefined。
set
属性的 setter 函数,如果没有 setter,则为 undefined
。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this
对象。
默认为 undefined。
描述符默认值汇总
- 拥有布尔值的键
configurable
、enumerable
和writable
的默认值都是false
。 - 属性值和函数的键
value
、get
和set
字段的默认值为undefined
。
var o = {}; // 创建一个新对象
// 在对象中添加一个属性与数据描述符的示例
Object.defineProperty(o, "a", {
value : 37,
writable : true,
enumerable : true,
configurable : true
});
// 对象 o 拥有了属性 a,值为 37
// 在对象中添加一个设置了存取描述符属性的示例
var bValue = 38;
Object.defineProperty(o, "b", {
// 使用了方法名称缩写(ES2015 特性)
// 下面两个缩写等价于:
// get : function() { return bValue; },
// set : function(newValue) { bValue = newValue; },
get() { return bValue; },
set(newValue) { bValue = newValue; },
enumerable : true,
configurable : true
});
o.b; // 38
// 对象 o 拥有了属性 b,值为 38
// 现在,除非重新定义 o.b,o.b 的值总是与 bValue 相同
// 数据描述符和存取描述符不能混合使用
Object.defineProperty(o, "conflict", {
value: 0x9f91102,
get() { return 0xdeadbeef; }
});
// 抛出错误 TypeError: value appears only in data descriptors, get appears only in accessor descriptors
添加多个属性和默认值
var o = {};
o.a = 1;
// 等同于:
Object.defineProperty(o, "a", {
value: 1,
writable: true,
configurable: true,
enumerable: true
});
// 另一方面,
Object.defineProperty(o, "a", { value : 1 });
// 等同于:
Object.defineProperty(o, "a", {
value: 1,
writable: false,
configurable: false,
enumerable: false
});
Proxy(代理器)
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
var proxy = new Proxy(target, handler);
new Proxy()
表示生成一个Proxy
实例,target
参数表示所要拦截的目标对象,handler
参数也是一个对象,用来定制拦截行为。
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title //
get
方法的两个参数分别是目标对象和所要访问的属性
注意,要使得Proxy
起作用,必须针对Proxy
实例(上例是proxy
对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。
如果handler
没有设置任何拦截,那就等同于直接通向原对象。
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
上面代码中,handler
是一个空对象,没有任何拦截效果,访问proxy
就等同于访问target
。
Proxy 实例也可以作为其他对象的原型对象。
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
上面代码中,proxy
对象是obj
对象的原型,obj
对象本身并没有time
属性,所以根据原型链,会在proxy
对象上读取该属性,导致被拦截。
var handler = {
get: function(target, name) {
if (name === 'prototype') {
return Object.prototype;
}
return 'Hello, ' + name;
},
apply: function(target, thisBinding, args) {
return args[0];
},
construct: function(target, args) {
return {value: args[1]};
}
};
var fproxy = new Proxy(function(x, y) {
return x + y;
}, handler);
fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true
对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
- get(target, propKey, receiver):拦截对象属性的读取,比如
proxy.foo
和proxy['foo']
。 - set(target, propKey, value, receiver):拦截对象属性的设置,比如
proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。 - has(target, propKey):拦截
propKey in proxy
的操作,返回一个布尔值。 - deleteProperty(target, propKey):拦截
delete proxy[propKey]
的操作,返回一个布尔值。 - ownKeys(target):拦截
Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。 - getOwnPropertyDescriptor(target, propKey):拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。 - defineProperty(target, propKey, propDesc):拦截
Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。 - preventExtensions(target):拦截
Object.preventExtensions(proxy)
,返回一个布尔值。 - getPrototypeOf(target):拦截
Object.getPrototypeOf(proxy)
,返回一个对象。 - isExtensible(target):拦截
Object.isExtensible(proxy)
,返回一个布尔值。 - setPrototypeOf(target, proto):拦截
Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 - apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。 - construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)
。 -
get()
get
方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。 -
get
方法可以继承。let proto = new Proxy({}, { get(target, propertyKey, receiver) { console.log('GET ' + propertyKey); return target[propertyKey]; } }); let obj = Object.create(proto); obj.foo // "GET foo"
上面代码中,拦截操作定义在
Prototype
对象上面,所以如果读取obj
对象继承的属性时,拦截会生效 -
ES6 Proxy 与 Object.defineProperty 的优劣对比?
-
Proxy 的优势如下:
Proxy 可以直接监听数组的变化
Proxy 可以直接监听对象而非属性
Proxy 有 13 种拦截方法,比 Object.defineProperty 要更加丰富的多
Object.defineProperty 的优势如下:兼容性好
Object.defineProperty (obj, prop, descriptor) 的问题主要有三个:无法监听数组的变化
必须遍历对象的每个属性
必须深层遍历嵌套的对象 -
(1)无法监听数组的变化
Vue 把会修改原来数组的方法定义为变异方法。
变异方法例如 push、pop、shift、unshift、splice、sort、reverse等,是无法触发 set 的。
非变异方法,例如 filter,concat,slice 等,它们都不会修改原始数组,而会返回一个新的数组。
Vue 的做法是把这些变异方法重写来实现监听数组变化。
const aryMethods = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
]
const arrayAugmentations = {}
aryMethods.forEach((method) => {
// 这里是原生 Array 的原型方法
let original = Array.prototype[method]
// 将 push, pop 等封装好的方法定义在对象 arrayAugmentations 的属性上
// 注意:是实例属性而非原型属性
arrayAugmentations[method] = function () {
console.log('我被改变啦!')
// 调用对应的原生方法并返回结果
return original.apply(this, arguments)
}
})
let list = ['a', 'b', 'c']
// 将我们要监听的数组的原型指针指向上面定义的空数组对象
// 这样就能在调用 push, pop 这些方法时走进我们刚定义的方法,多了一句 console.log
list.__proto__ = arrayAugmentations
list.push('d') // 我被改变啦!
// 这个 list2 是个普通的数组,所以调用 push 不会走到我们的方法里面。
let list2 = ['a', 'b', 'c']
list2.push('d') // 不输出内容
(2)必须遍历对象的每个属性使用 Object.defineProperty 多数情况下要配合 Object.keys 和遍历,于是就多了一层嵌套。
并且由于遍历的原因,假如对象上的某个属性并不需要“劫持”,但此时依然会对其添加“劫持”。
Object.keys(obj).forEach((key) => {
Object.defineProperty(obj, key, {
// ...
})
})
(3)必须深层遍历嵌套的对象当一个对象为深层嵌套的时候,必须进行逐层遍历,直到把每个对象的每个属性都调用 Object.defineProperty() 为止。
-
Object.defineProperty let obj = { name: '测试', age: 89 }; // 这个API没办法兼容 IE8 // Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。 let p = Object.defineProperty(obj, 'name', { value: '默认的', // 给 obj.name 赋值,没有默认是undefined configurable: true, // 设置该属性是否可被删除 默认是false enumerable: true, // 该属性是否可枚举 默认是false writable: true, // 该属性是否可以进行赋值操作(更改value的值) 默认是false }); // 返回的还是原来的对象(原来的堆地址) console.log(p === obj); // true let newVal = ''; // Object.defineProperty() 一次只能监听一个属性 Object.defineProperty(obj, 'sex', { // 如果监控对象没有改属性,则给对象加上 get(){ // 每当获取 obj.sex 值的时候会触发这个函数 // 获取到的值是 get 函数的返回值 console.log('get'); // return 'get'; // 记得要return // 如果返回的是一个固定的值,那么obj.sex的值就一直是固定的,改不了 // 想要给 sex 动态赋值,需要借助第三方变量 return newVal; }, set(val){ // 每当给 obj.sex 进行赋值的时候会触发这个函数,val是赋值的新值 console.log('set',val); newVal = val; // obj.sex = val; // 千万不能这样写,是死递归(一直在触发set函数) } }); console.log(obj.sex); // 获取的是 get函数返回值
Proxy 语法:const p = new Proxy(target, handler) let obj2 = { name: 'proxy', age: 89, sex: 11 }; let pp = new Proxy(obj2, { // 凡是获取 pp 对象的任意一个属性值,都会触发这个函数 get(obj, key){ // obj就是被代理的对象 obj2;key是操作的属性名 // console.log(arguments); // console.log(obj === obj2); // true console.log(obj, key); // 记得要return return obj[key]; }, // 凡是给 pp 对象的任意一个属性进行赋值,都会触发这个函数 set(obj, key, newVal){ // console.log('set'); obj[key] = newVal; // 这样写不会造成死递归的原因:代理的是整个对象(堆内存),只是改变其中的一个属性值,不会改变整个堆地址 } }); // 返回的是一个新对象(跟原来的堆地址不一样),但是属性名跟目标对象obj的还是一样的 console.log(obj2 === pp); // false
vue的响应式原理:
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data
选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty
是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。
对于对象Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data
对象上存在才能让 Vue 将它转换为响应式的。
对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value)
方法向嵌套对象添加响应式 property。例如,对于:
Vue.set(vm.someObject, 'b', 2)
您还可以使用 vm.$set
实例方法,这也是全局 Vue.set
方法的别名:
this.$set(this.someObject,'b',2)
有时你可能需要为已有对象赋值多个新 property,比如使用 Object.assign()
或 _.extend()
。但是,这样添加到对象上的新 property 不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。
// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
Vue 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
举个例子:
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的
为了解决第一类问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue
相同的效果,同时也将在响应式系统内触发状态更新:
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
你也可以使用 vm.$set 实例方法,该方法是全局方法 Vue.set
的一个别名:
vm.$set(vm.items, indexOfItem, newValue)
为了解决第二类问题,你可以使用 splice
:
vm.items.splice(newLength)
Vue.Router(这里的路由就是SPA(单页应用)的路径管理器)
页面应用(SPA)的核心之一是: 更新视图而不重新请求页面
至于我们为啥不能用a标签,这是因为用Vue做的都是单页应用,就相当于只有一个主的index.html页面,所以你写的<a></a>标签是不起作用的,你必须使用vue-router来进行管理
路由全局守卫
// GOOD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
})
router.afterEach路由独享守卫
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
组件内的守卫:
const Foo = {
template: `...`,
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例(此时无法获取实例)
})
}
参数或查询的改变并不会触发进入/离开的导航守卫
将组件 (components) 映射到路由 (routes),然后告诉 Vue Router 在哪里渲染它们
HTML
<!-- 使用 router-link 组件来导航. -->
<!-- 通过传入 `to` 属性指定链接. -->
<!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
JavaScript
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)
// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
routes // (缩写) 相当于 routes: routes
})
// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
router
}).$mount('#app')
// 现在,应用已经启动了!
omponent
组件,就是咱们在最上面引入的 import ...
了,当然这个组件的写法还有一种懒加载
懒加载的方式,我们就不需要再用import
去引入组件了,直接如下即可。懒加载的好处是当你访问到这个页面的时候才会去加载相关资源,这样的话能提高页面的访问速度。component: resolve => require(['./page/linkParamsQuestion.vue'], resolve)
通过注入路由器,我们可以在任何组件内通过 this.$router
访问路由器,也可以通过 this.$route
访问当前路由:
// Home.vue
export default {
computed: {
username() {
// 我们很快就会看到 `params` 是什么
return this.$route.params.username
}
},
methods: {
goBack() {
window.history.length > 1 ? this.$router.go(-1) : this.$router.push('/')
}
}
}
要注意,当 <router-link>
对应的路由匹配成功,将自动设置 class 属性值 .router-link-active
。查
动态路由匹配(动态路径参数 以冒号开头,this.$route.params.)
某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User
组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染
const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头
{ path: '/user/:id', component: User }
]
})
像 /user/foo
和 /user/bar
都将映射到相同的路由。
一个“路径参数”使用冒号 :
标记。当匹配到一个路由时,参数值会被设置到 this.$route.params
,可以在每个组件内使用。于是,我们可以更新 User
的模板,输出当前用户的 ID:
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
你可以在一个路由中设置多段“路径参数”,对应的值都会设置到 $route.params
中。例如:
模式 | 匹配路径 | $route.params |
---|---|---|
/user/:username | /user/evan | { username: 'evan' } |
/user/:username/post/:post_id | /user/evan/post/123 | { username: 'evan', post_id: '123' } |
除了 $route.params
外,$route
对象还提供了其它有用的信息,例如,$route.query
(如果 URL 中有查询参数)、$route.hash
响应路由参数的变化
提醒一下,当使用路由参数时,例如从 /user/foo
导航到 /user/bar
,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。
复用组件时,想对路由参数的变化作出响应的话,你可以简单地 watch (监测变化) $route
对象:
const User = {
template: '...',
watch: {
$route(to, from) {
// 对路由变化作出响应...
}
}
}
或者使用 2.2 中引入的 beforeRouteUpdate
导航守卫:
const User = {
template: '...',
beforeRouteUpdate(to, from, next) {
// react to route changes...
// don't forget to call next()
}
}
捕获所有路由或 404 Not found 路由
常规参数只会匹配被 /
分隔的 URL 片段中的字符。如果想匹配任意路径,我们可以使用通配符 (*
):
{
// 会匹配所有路径
path: '*'
}
{
// 会匹配以 `/user-` 开头的任意路径
path: '/user-*'
}
当使用通配符路由时,请确保路由的顺序是正确的,也就是说含有通配符的路由应该放在最后。路由 { path: '*' }
通常用于客户端 404 错误。如果你使用了History 模式,请确保正确配置你的服务器
当使用一个通配符时,$route.params
内会自动添加一个名为 pathMatch
参数。它包含了 URL 通过通配符被匹配的部分:
// 给出一个路由 { path: '/user-*' }
this.$router.push('/user-admin')
this.$route.params.pathMatch // 'admin'
// 给出一个路由 { path: '*' }
this.$router.push('/non-existing')
this.$route.params.pathMatch // '/non-existing'
嵌套路由
要在嵌套的出口中渲染组件,需要在 VueRouter
的参数中使用 children
配置:
const router = new VueRouter({
routes: [
{
path: '/user/:id',
component: User,
children: [
{
// 当 /user/:id/profile 匹配成功,
// UserProfile 会被渲染在 User 的 <router-view> 中
path: 'profile',
component: UserProfile
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 会被渲染在 User 的 <router-view> 中
path: 'posts',
component: UserPosts
}
]
}
]
})
要注意,以 /
开头的嵌套路径会被当作根路径。 这让你充分的使用嵌套组件而无须设置嵌套的路径
此时,基于上面的配置,当你访问 /user/foo
时,User
的出口是不会渲染任何东西,这是因为没有匹配到合适的子路由。如果你想要渲染点什么,可以提供一个 空的 子路由:
const router = new VueRouter({
routes: [
{
path: '/user/:id',
component: User,
children: [
// 当 /user/:id 匹配成功,
// UserHome 会被渲染在 User 的 <router-view> 中
{ path: '', component: UserHome }
// ...其他子路由
]
}
]
})
编程式的导航
router.push(location, onComplete?, onAbort?)
注意:在 Vue 实例内部,你可以通过 $router
访问路由实例。因此你可以调用 this.$router.push
。
想要导航到不同的 URL,则使用 router.push
方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。
当你点击 <router-link>
时,这个方法会在内部调用,所以说,点击 <router-link :to="...">
等同于调用 router.push(...)
。
声明式 | 编程式 |
---|---|
<router-link :to="..."> | router.push(...) |
该方法的参数可以是一个字符串路径,或者一个描述地址的对象。例如:
// 字符串
router.push('home')
// 对象
router.push({ path: 'home' })
// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})
// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})
注意:如果提供了 path
,params
会被忽略,上述例子中的 query
并不属于这种情况。取而代之的是下面例子的做法,你需要提供路由的 name
或手写完整的带有参数的 path
:
const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123
// 这里的 params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user
注意: 如果目的地和当前路由相同,只有参数发生了改变 (比如从一个用户资料到另一个 /users/1
-> /users/2
),你需要使用 beforeRouteUpdate 来响应这个变化 (比如抓取用户信息)
vue 路由传参 params 与 query两种方式的区别
1、用法上的
刚才已经说了,query要用path来引入,params要用name来引入,接收参数都是类似的,分别是this.$route.query.name和this.$route.params.name。
this.$router.push({
name:"detail",
params:{
name:'nameValue',
code:10011
}
});
2、展示上的
query更加类似于我们ajax中get传参,params则类似于post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示
query:
params:
router.replace(location, onComplete?, onAbort?)
跟 router.push
很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。
声明式 | 编程式 |
---|---|
<router-link :to="..." replace> | router.replace(...) |
router.go(n)
这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)
。
例子
// 在浏览器记录中前进一步,等同于 history.forward()
router.go(1)
// 后退一步记录,等同于 history.back()
router.go(-1)
// 前进 3 步记录
router.go(3)
// 如果 history 记录不够用,那就默默地失败呗
router.go(-100)
router.go(100)
命名路由
有时候,通过一个名称来标识一个路由显得更方便一些,特别是在链接一个路由,或者是执行一些跳转的时候。你可以在创建 Router 实例的时候,在 routes
配置中给某个路由设置名称。
const router = new VueRouter({
routes: [
{
path: '/user/:userId',
name: 'user',
component: User
}
]
})
要链接到一个命名路由,可以给 router-link
的 to
属性传一个对象:
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>
这跟代码调用 router.push()
是一回事:
router.push({ name: 'user', params: { userId: 123 } })
这两种方式都会把路由导航到 /user/123
路径。
命名视图
有时候想同时 (同级) 展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar
(侧导航) 和 main
(主内容) 两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view
没有设置名字,那么默认为 default
。
<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>
一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用 components
配置 (带上 s):
const router = new VueRouter({
routes: [
{
path: '/',
components: {
default: Foo,
a: Bar,
b: Baz
}
}
]
})
重定向也是通过 routes
配置来完成,下面例子是从 /a
重定向到 /b
:
const router = new VueRouter({
routes: [
{ path: '/a', redirect: '/b' }
]
})
重定向的目标也可以是一个命名的路由:
const router = new VueRouter({
routes: [
{ path: '/a', redirect: { name: 'foo' }}
]
})
甚至是一个方法,动态返回重定向目标:
const router = new VueRouter({
routes: [
{ path: '/a', redirect: to => {
// 方法接收 目标路由 作为参数
// return 重定向的 字符串路径/路径对象
}}
]
})
注意导航守卫并没有应用在跳转路由上,而仅仅应用在其目标上。在下面这个例子中,为 /a
路由添加一个 beforeEnter
守卫并不会有任何效果。
#别名
/a
的别名是 /b
,意味着,当用户访问 /b
时,URL 会保持为 /b
,但是路由匹配则为 /a
,就像用户访问 /a
一样。
上面对应的路由配置为:
const router = new VueRouter({
routes: [
{ path: '/a', component: A, alias: '/b' }
]
})
HTML5 History 模式(vue-router 路由模式有几种)
供了两种方式:Hash模式和History模式;根据mode参数来决定采用哪一种方式
hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说 #是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中也不会不包括#;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据
vue-router
默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。
hash值的改变会触发hashchange事件
如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState
API 来完成 URL 跳转而无须重新加载页面。
const router = new VueRouter({
mode: 'history',
routes: [...]
})
当你使用 history 模式时,URL 就像正常的 url,例如 http://yoursite.com/user/id
,也好看!
不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id
就会返回 404,这就不好看了。
所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html
页面,这个页面就是你 app 依赖的页面
1.vue-router如何参数传递 (利用name和params或者path 后面加:)
1.通过<router-link>
标签中的to传参
这种传参方法的基本语法:
<router-link :to="{name:xxx,params:{key:value}}">valueString</router-link>
比如先在src/App.vue文件中
<router-link :to="{name:'hi1',params:{username:'jspang',id:'555'}}">Hi页面1</router-link>
然后把src/router/index.js文件里给hi1配置的路由起个name,就叫hi1.
{path:'/hi1',name:'hi1',component:Hi1}
最后在模板里(src/components/Hi1.vue)用$route.params.username
进行接收.
{{$route.params.username}}-{{$route.params.id}}
2.vue-router 利用url传递参数----在配置文件里以冒号的形式设置参数 1.路由匹配参数。
我们在/src/router/index.js文件里配置路由
{ path:'/params/:newsId/:newsTitle', component:Params }
代码中获取name
的方式如下:
let name = this.$route.params.name; // 链接里的name被封装进了 this.$route.params let age = this.$route.query.age; //问号后面参数会被封装进 this.$route.query;
在App.vue文件里加入我们的<router-view>
标签。这时候我们可以直接利用url传值了
<router-link to="/params/198/jspang website is very good">params</router-link>
|
#导航守卫
“导航”表示路由正在发生改变
记住参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate
的组件内守卫。
全局前置守卫(以直接在路由配置文件router.js
里编写代码逻辑。可以做一些全局性的路由拦截。)
你可以使用 router.beforeEach
注册一个全局前置守卫:
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})
当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。
每个守卫方法接收三个参数:
-
to: Route
: 即将要进入的目标 路由对象 -
from: Route
: 当前导航正要离开的路由 -
next: Function
: 一定要调用该方法来 resolve 这个钩子。执行效果依赖next
方法的调用参数。-
next()
: 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。 -
next(false)
: 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到from
路由对应的地址。 -
next('/')
或者next({ path: '/' })
: 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向next
传递任意位置对象,且允许设置诸如replace: true
、name: 'home'
之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的选项。 -
next(error)
: (2.4.0+) 如果传入next
的参数是一个Error
实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。
-
确保 next
函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。这里有一个在用户未能验证身份时重定向到 /login
的示例:
// BAD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
// 如果用户未能验证身份,则 `next` 会被调用两次
next()
})
// GOOD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
})
#全局解析
在 2.5.0+ 你可以用 router.beforeResolve
注册一个全局守卫。这和 router.beforeEach
类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
#全局后置钩子
你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next
函数也不会改变导航本身:
router.afterEach((to, from) => {
// ...
})
路由独享的守卫
你可以在路由配置上直接定义 beforeEnter
守卫:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
这些守卫与全局前置守卫的方法参数是一样的。
#组件内的守卫
最后,你可以在路由组件内直接定义以下路由导航守卫:
beforeRouteEnter
beforeRouteUpdate
(2.2 新增)beforeRouteLeave
const Foo = {
template: `...`,
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
beforeRouteEnter
守卫 不能 访问 this
,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。
不过,你可以通过传一个回调给 next
来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
注意 beforeRouteEnter
是支持给 next
传递回调的唯一守卫。对于 beforeRouteUpdate
和 beforeRouteLeave
来说,this
已经可用了,所以不支持传递回调,因为没有必要了。
beforeRouteUpdate (to, from, next) {
// just use `this`
this.name = to.params.name
next()
}
这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false)
来取消。
beforeRouteLeave (to, from, next) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()
} else {
next(false)
}
完整的导航解析流程
在页面上添加以下代码,可以显示这些路由对象的属性:
<div> <p>当前路径:{{$route.path}}</p>
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫 (2.2+)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫 (2.5+)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入。 -
路由对象暴露了以下属性:
- $route.path
字符串,等于当前路由对象的路径,会被解析为绝对路径,如"/home/news"
。 - $route.params
对象,包含路由中的动态片段和全匹配片段的键值对 - $route.query
对象,包含路由中查询参数的键值对。例如,对于/home/news/detail/01?favorite=yes
,会得到$route.query.favorite == 'yes'
。 - $route.router
路由规则所属的路由器(以及其所属的组件)。 - $route.matched
数组,包含当前匹配的路径中所包含的所有片段所对应的配置参数对象。 - $route.name
当前路径的名字,如果没有使用具名路径,则名字为空。
数据获取
导航完成后获取数据【监听路由变化获取数据 watch:{'$route':'fetchData}】
当你使用这种方式时,我们会马上导航和渲染组件,然后在组件的 created
钩子中获取数据。这让我们有机会在数据获取期间展示一个 loading 状态,还可以在不同视图间展示不同的 loading 状态。
假设我们有一个 Post
组件,需要基于 $route.params.id
获取文章数据:
<template>
<div class="post">
<div v-if="loading" class="loading">
Loading...
</div>
<div v-if="error" class="error">
{{ error }}
</div>
<div v-if="post" class="content">
<h2>{{ post.title }}</h2>
<p>{{ post.body }}</p>
</div>
</div>
</template>
export default {
data () {
return {
loading: false,
post: null,
error: null
}
},
created () {
// 组件创建完后获取数据,
// 此时 data 已经被 observed 了
this.fetchData()
},
watch: {
// 如果路由有变化,会再次执行该方法
'$route': 'fetchData'
},
methods: {
fetchData () {
this.error = this.post = null
this.loading = true
// replace getPost with your data fetching util / API wrapper
getPost(this.$route.params.id, (err, post) => {
this.loading = false
if (err) {
this.error = err.toString()
} else {
this.post = post
}
})
}
}
}
在导航完成前获取数据
通过这种方式,我们在导航转入新的路由前获取数据。我们可以在接下来的组件的 beforeRouteEnter
守卫中获取数据,当数据获取成功后只调用 next
方法。
export default {
data () {
return {
post: null,
error: null
}
},
beforeRouteEnter (to, from, next) {
getPost(to.params.id, (err, post) => {
next(vm => vm.setData(err, post))
})
},
// 路由改变前,组件就已经渲染完了
// 逻辑稍稍不同
beforeRouteUpdate (to, from, next) {
this.post = null
getPost(to.params.id, (err, post) => {
this.setData(err, post)
next()
})
},
methods: {
setData (err, post) {
if (err) {
this.error = err.toString()
} else {
this.post = post
}
}
}
}
在为后面的视图获取数据时,用户会停留在当前的界面,因此建议在数据获取期间,显示一些进度条或者别的指示。如果数据获取失败,同样有必要展示一些全局的错误提醒。
滚动行为
vue-router
能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。
注意: 这个功能只在支持 history.pushState
的浏览器中可用。
当创建一个 Router 实例,你可以提供一个 scrollBehavior
方法:
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {
// return 期望滚动到哪个的位置
}
})
滚动行为
在利用vue-router
去做跳转的时候,到了新页面如果对页面的滚动条位置有要求的话,可以利用下面这个方法。
const router = new VueRouter({ routes: [...], scrollBehavior (to, from, savedPosition) { // return 期望滚动到哪个的位置 } })
scrollBehavior
方法接收 to
和 from
路由对象。
第三个参数 savedPosition
当且仅当 popstate
导航 (mode
为 history
通过浏览器的 前进/后退 按钮触发) 时才可用。
这里就不细致的讲了,文档都有也非常简单,记住有这个东西就行。
//所有路由新页面滚动到顶部: scrollBehavior (to, from, savedPosition) { return { x: 0, y: 0 } } //如果有锚点 scrollBehavior (to, from, savedPosition) { if (to.hash) { return { selector: to.hash } } }
路由懒加载
像vue这种单页面应用,如果没有应用懒加载,运用webpack打包后的文件将会异常的大,造成进入首页时,需要加载的内容过多,时间过长,会出啊先长时间的白屏,即使做了loading也是不利于用户体验,而运用懒加载则可以将页面进行划分,需要的时候加载页面,可以有效的分担首页所承担的加载压力,减少首页加载用时
第一( vue异步组件技术 ==== 异步加载 ),可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):
vue-router配置路由 , 使用vue的异步组件技术 , 可以实现按需加载 .
但是,这种情况下一个组件生成一个js文件
/* vue异步组件技术 */
{ path: '/home', name: 'home', component: resolve => require(['@/components/home'],resolve) },
{ path: '/index', name: 'Index', component: resolve => require(['@/components/index'],resolve) },
{ path: '/about', name: 'about', component: resolve => require(['@/components/about'],resolve) }
const Foo = () =>
Promise.resolve({
/* 组件定义对象 */
})
第二,在 Webpack 2 中,我们可以使用动态 import (opens new window)语法来定义代码分块点 (split point):
import('./Foo.vue') // 返回 Promise
注意
如果您使用的是 Babel,你将需要添加 syntax-dynamic-import (opens new window)插件,才能使 Babel 可以正确地解析语法。
结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件。
const Foo = () => import('./Foo.vue')
在路由配置中什么都不需要改变,只需要像往常一样使用 Foo
:
const router = new VueRouter({
routes: [{ path: '/foo', component: Foo }]
})
#把组件按组分块
有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用 命名 chunk (opens new window),一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)。
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
双向数据绑定:
MVVM
数据双向绑定主要是指:数据变化更新视图,视图变化更新数据
讨论如何根据 Data
变化更新 View
。
1、实现一个监听器 Observer
,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者;
2、实现一个订阅器 Dep
,用来收集订阅者,对监听器 Observer
和 订阅者 Watcher
进行统一管理;
3、实现一个订阅者 Watcher
,可以收到属性的变化通知并执行相应的方法,从而更新视图;
4、实现一个解析器 Compile
,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化。
监听器 Observer 实现
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
/**
* 循环遍历数据对象的每个属性
*/
function observable(obj) {
if (!obj || typeof obj !== 'object') {
return;
}
let keys = Object.keys(obj);
keys.forEach((key) => {
defineReactive(obj, key, obj[key])
})
return obj;
}
/**
* 将对象的属性用 Object.defineProperty() 进行设置
*/
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`${key}属性被读取了...`);
return val;
},
set(newVal) {
console.log(`${key}属性被修改了...`);
val = newVal;
}
})
}
订阅器 Dep 实现
发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态改变时,所有依赖于它的对象都将得到通知。
可以在数据被读或写的时候通知那些依赖该数据的视图更新了,为了方便,我们需要先将所有依赖收集起来,一旦数据发生变化,就统一通知更新。其实,这就是前一节所说的“发布订阅者”模式,数据变化为“发布者”,依赖对象为“订阅者”。
我们需要创建一个依赖收集容器,也就是消息订阅器 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;
有了订阅器,我们再将 defineReactive
函数进行改造一下,向其植入订阅器:
defineReactive: function(data, key, val) {
var dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function getter () {
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set: function setter (newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify();
}
});
}
订阅者 Watcher 实现
我们已经知道监听器Observer
是在 get 函数执行了添加订阅者 Wather 的操作的,所以我们只要在订阅者 Watcher
初始化的时候触发对应的 get
函数去执行添加订阅者操作即可,那要如何触发 get
的函数,再简单不过了,只要获取对应的属性值就可以触发了,核心原因就是因为我们使用了 Object.defineProperty( )
进行数据监听
function Watcher(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get(); // 将自己添加到订阅器的操作
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
Dep.target = this; // 全局变量 订阅者 赋值
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 全局变量 订阅者 释放
return value;
}
};
订阅者 Watcher
分析如下:
订阅者 Watcher
是一个 类,在它的构造函数中,定义了一些属性:
- **vm:**一个 Vue 的实例对象;
- **exp:**是
node
节点的v-model
等指令的属性值 或者插值符号中的属性。如v-model="name"
,exp
就是name
; - **cb:**是
Watcher
绑定的更新函数;
当我们去实例化一个渲染 watcher
的时候,首先进入 watcher
的构造函数逻辑,就会执行它的 this.get()
方法,进入 get
函数,首先会执行:
Dep.target = this; // 将自己赋值为全局的订阅者
复制代码
实际上就是把 Dep.target
赋值为当前的渲染 watcher
,接着又执行了:
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
复制代码
在这个过程中会对 vm
上的数据访问,其实就是为了触发数据对象的 getter
。
每个对象值的 getter
都持有一个 dep
,在触发 getter
的时候会调用 dep.depend()
方法,也就会执行this.addSub(Dep.target)
,即把当前的 watcher
订阅到这个数据持有的 dep
的 watchers
中,这个目的是为后续数据变化时候能通知到哪些 watchers
做准备。
这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了吗?其实并没有,完成依赖收集后,还需要把 Dep.target
恢复成上一个状态,即:
Dep.target = null; // 释放自己
复制代码
而 update()
函数是用来当数据发生变化时调用 Watcher
自身的更新函数进行更新的操作。先通过 let value = this.vm.data[this.exp];
获取到最新的数据,然后将其与之前 get()
获得的旧数据进行比较,如果不一样,则调用更新函数 cb
进行更新。
至此,简单的订阅者 Watcher
设计完毕。
解析器 Compile 实现
解析器 Compile
实现步骤:
- 解析模板指令,并替换模板数据,初始化视图;
- 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器;
-
compileText: function(node, exp) { var self = this; var initText = this.vm[exp]; // 获取属性值 this.updateText(node, initText); // dom 更新节点文本值 // 将这个指令初始化为一个订阅者,后续 exp 改变时,就会触发这个更新回调,从而更新视图 new Watcher(this.vm, exp, function (value) { self.updateText(node, value); }); }