1. 数据双向绑定原理
vue的双向绑定是由数据劫持结合发布者-订阅者模式实现的,那么什么是数据劫持?就是通过Object.defineProperty()来劫持对象属性的setter和getter操作,在数据变动时做你想要做的事情。
数据劫持set和get函数起到什么作用?
回顾一下Object.defineProperty:
- 语法:Object.defineProperty(obj, prop, descriptor)
- 参数:
obj:目标对象 prop:需要定义的属性或方法的名称 descriptor:目标属性所拥有的特性 - 可供定义的特性列表
value:属性的值 writable:如果为false,属性的值就不能被重写。 get:一旦目标属性被访问就会调用此方法,并将此方法的运算结果返回用户。 set:一旦目标属性被赋值,就会调回此方法。 configurable:如果为false,则任何尝试删除目标属性或修改属性以下特性(writable, configurable, enumerable)的行为将被无效化。 enumerable:是否能在for…in循环中遍历出来或在Object.keys中列举出来。
拓展:js中遍历一个对象的属性的方法
- Object.keys() 仅仅返回自身的可枚举属性,不包括继承来的,更不包括Symbol属性
- Object.getOwnPropertyNames() 返回自身的可枚举和不可枚举属性。但是不包括Symbol属性
- Object.getOwnPropertySymbols() 返回自身的Symol属性
- for…in 可以遍历对象的自身的和继承的可枚举属性,不包含Symbol属性
- Reflect.ownkeys() 返回对象自身的所有属性,不管是否可枚举,也不管是否是Symbol。注意不包括继承的属性
2. v-for 和 v-if 谁先执行?
当它们处于同一节点,v-for的优先级比v-if更高,这意味着 v-if将分别重复运行于每个 v-for循环中。
Vue 会先执行 v-for 得到 item 然后执行 v-if 判断是否展示。
官网并不推荐同一节点使用,原因是会影响性能增加渲染时长,我们可以在渲染之前把数据处理好。
3. v-html会导致哪些问题?
1.可能会导致 xss 攻击。用v-html一定要保证你的内容是可以依赖的,不要用在用户提交的内容上 // 因为用户输入的信息不可信,这样输入什么就会放入什么,v-html就相当于一个innerHTML 2.v-html 会替换掉标签内部的子元素
4. vue中的 watch、computed 和 methods之间的差别以及应用场景:
computed 和 watch,一个是计算,一个是观察,在语义上是有区别的。 计算是通过变量计算来得出数据。而观察是观察一个特定的值,根据被观察者的变动进行相应的变化,在特定的场景下不能相互混用,所以还是需要注意api运用的合理性和语义性。
computer
当页面中有某些数据依赖其他数据进行变动的时候,可以使用计算属性。 需要注意的是,就算在data中没有直接声明出要计算的变量,也可以直接在computed中写入。 computed比较适合对多个变量或者对象进行处理后返回一个结果值,也就是多个变量中的某一个值发生了变化则我们监控的这个值也就会发生变化。(举例:购物车里面的商品列表和总金额之间的关系,只要商品列表里面的商品数量发生变化,或减少或增多或删除商品,总金额都应该发生变化。这里的这个总金额使用computed属性来进行计算是最好的选择。) 计算属性默认只有getter,可以在需要的时候自己设定setter:
computed: {
fullName: {
get: function () {
return this.firstName + ' ' + this.lastName
},
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
适用场景:
watch
watch和computed很相似,watch用于观察和监听页面上的vue实例,当然在大部分情况下我们都会使用computed,但如果要在数据变化的同时进行异步操作或者是比较大的开销,那么watch为最佳选择。 watch一般用于监控路由、input输入框的值特殊处理等等,它比较适合的场景是一个数据影响多个数据。 watch为一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。直接引用文档例子:
var vm = new Vue({
el: '#app',
data: {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
},
watch: {
firstName: function (val) {
this.fullName = val + ' ' + this.lastName
},
lastName: function (val) {
this.fullName = this.firstName + ' ' + val
}
}
})
如果在data中没有相应的属性的话,是不能watch的,这点和computed不一样。 适用场景:
methods方法
跟前面的都不一样,我们通常在这里面写入方法,只要调用就会重新执行一次,相应的有一些触发条件,在某些时候methods和computed看不出来具体的差别,但是一旦在运算量比较复杂的页面中,就会体现出不一样。 需要注意的是,computed是具有缓存的,这就意味着只要计算属性的依赖没有进行相应的数据更新,那么computed会直接从缓存中获取值,多次访问都会返回之前的计算结果。
5. Vue-router相关:
5.1 vue-router的几种模式?JS是如何监听HistoryRouter的变化的?
通过浏览器的地址栏来改变切换页面,前端实现主要有两种方式:
1. 通过hash改变,利用window.onhashchange 监听。
2. 通过history的改变,进行js操作加载页面。然而history并不像hash那样简单, 因为history的改变,除了浏览器的几个前进后退(使用 history.back(),history.forward() 和 history.go() 方法来完成在用户历史记录中向后和向前的跳转)等操作会主动触发popstate 事件,pushState、replaceState 并不会触发popstate事件,要解决history监听的问题,方法是:
首先完成一个订阅-发布模式,然后重写history.pushState、history.replaceState 并添加消息通知,这样一来只要history的无法实现监听函数就被我们加上了事件通知,只不过这里用的不是浏览器原生事件,而是通过我们创建的event-bus 来实现通知,然后触发事件订阅函数的执行。
5.2 HashRouter 和 HistoryRouter:
vue-router是Vue官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。 vue-router 默认 hash 模式,还有一种是history模式。
原理:
-
hash路由:hash模式的工作原理是hashchange事件,可以在window监听hash的变化。我们在 url后面随便添加一个#xx触发这个事件。vue-router默认的是hash模式—使用URL的hash来模拟一个完整的URL,于是当URL改变的时候页面不会重新加载,也就是单页应用了,当#后面的hash发生变化,不会导致浏览器向服务器发出请求,浏览器不发出请求就不会刷新页面,并且会触发hashChange 事件,通过监听hash值的变化来实现更新页面部分内容的操作。 对于hash模式会创建hashHistory对象,在访问不同的路由的时候,会发生两件事: HashHistory.push()将新的路由添加到浏览器访问历史的栈顶,和HasHistory.replace()替换到当前栈顶的路由。 -
history路由: 主要使用HTML5的pushState()和replaceState()这两个api结合window.popstate事件(监听浏览器前进后退)实现的。pushState()可以改变url地址且不会发送请求,replaceState()可以读取历史记录栈,还可以对浏览器记录进行修改。
区别:
-
hash模式较丑,history模式较优雅; -
pushState设置的新URL可以是与当前URL同源的任意URL;而hash只可修改#后面的部分,故只可设置与当前同文档的URL -
pushState设置的新URL可以与当前URL一模一样,这样也会把记录添加到栈中;而hash设置的新值必须与原来不一样才会触发记录添加到栈中; -
pushState通过stateObject可以添加任意类型的数据到记录中,而hash只可添加短字符串; -
pushState可额外设置title属性供后续使用; -
hash兼容IE8以上,history兼容IE10以上; -
history模式需要后端配合将所有访问都指向index.html,否则用户刷新页面,会导致404错误。
window.addEventListener('hashchange', () => {
div.innerHTML = location.hash.slice(1)
})
function routerChange (pathname){
history.pushState(null, null, pathname)
div.innerHTML = location.pathname
}
window.addEventListener('popstate', () => {
div.innerHTML = location.pathname
})
5.3 Vue router 原理哪个模式不会请求服务器?
Vue router 的两种方法,hash模式不会请求服务器。
- url的hash,就是通常所说的锚点#,javascript通过hashChange事件来监听url的变化,IE7以下 需要轮询。比如这个 URL: http://www.abc.com/#/hello ,hash 的值为 #/hello 。它的特点 在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。
- HTML5的History模式,它使url看起来像普通网站那样,以“/”分割,没有#,单页面并没有跳转。 不过使用这种模式需要服务端支持,服务端在接收到所有请求后,都只想同一个html文件,不然 会出现404。因此单页面应用只有一个html,整个网站的内容都在这一个html里,通过js来处理。
5.4 Vue-router 实现懒加载:
懒加载:当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
实现:结合 Vue 的异步组件和 Webpack 的代码分割功能,实现路由组件的懒加载
-
首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身): const Foo = () => Promise.resolve({ })
-
在 Webpack 2 中,我们可以使用动态 import 语法来定义代码分块点 (split point): import('./Foo.vue')
结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件: const Foo = () => import('./Foo.vue')
在路由配置中什么都不需要改变,只需要像往常一样使用 Foo : const router = new VueRouter({
routes: [
{ path: '/foo', component: Foo }
]
})
6. Vue-alive 干嘛的?
keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染。 prop: include: 字符串或正则表达式。只有匹配的组件会被缓存。 exclude: 字符串或正则表达式。任何匹配的组件都不会被缓存。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fVXyC8E1-1636901017942)(/Users/apple/Library/Application Support/typora-user-images/image-20210908183344689.png)]
7. Vue组件通信相关:
7.1 组件通信的8种方法:
1. props 和 $emit
这是最常用的父子组件通信方式,父组件通过props向子组件传递数据,子组件通过$emit触发事件传递数据给父组件。
2. $attrs 和 $listeners
第一种方式处理父子组件之间的数据传输有一个问题:如果多层嵌套,父组件A下面有子组件B,组件B下面有组件C,这时如果组件A想传递数据给组件C怎么办呢?如果采用第一种方法,我们必须让组件A通过props传递消息给组件B,组件B再通过props传递消息给组件C;要是组件A和组件C之间有更多的组件,那采用这种方式就很复杂了。从Vue 2.4开始,提供了$attrs 和 $listeners来解决这个问题,能够让组件A直接传递消息给组件C。
3. v-model
父组件通过v-model传递值给子组件时,会自动传递一个value的prop属性,在子组件中通过 this.$emit(‘input', val) 自动修改v-model绑定的值.
4. provider 和 inject
父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量。不论子组件有多深, 只要调用了inject那么就可以注入provider中的数据。而不是局限于只能从当前父组件的props属性来获取数据,只要在父组件的生命周期内,子组件都可以调用。
5. 中央事件总线
上面方式都是处理的父子组件之间的数据传递,那如果两个组件不是父子关系,是兄弟组件如何通信?这种情况下可以使用中央事件总线的方式。新建一个Vue事件bus对象,然后通过bus.$emit 触发事件,通过bus.$on 监听触发的事件。
6. parent 和 children
7. boradcast 和 dispatch
vue1.0中提供了这种方式,但vue2.0中没有,但很多开源软件都自己封装了这种方式,比如min ui、element ui和iview等。 比如如下代码一般都作为一个mixins去使用,broadcast是向特定的父组件触发事件,dispatch是向特定的子组件触发事件,本质上这种方式还是on和emit的封装,但在一些基础组件中却很实用。
8. vuex处理组件之间的数据交互
如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候上面这一些方法可能不利于项目的维护,vuex的做法就是将这一些公共的数据抽离出来,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。
7.2 组件间传值 => attrs 和 listeners
$attrs 和 $listeners的作用:解决多层嵌套情况下,父组件A下面有子组件B,组件B下面有组件C,组件A传递数据给组件C的问题,这个方法是在Vue 2.4提出的。
实现方式:
Vue.component('C', {
template:
`<div>
<input type="text" v-model="$attrs.messageC" @input="passCData($attrs.messageC)">
</div>`,
methods: {
passCData(val) {
this.$emit('getCData', val)
}
}
})
Vue.component('B', {
data() {
return {
myMessage: this.message
}
},
template:
`<div>
<input type="text" v-model="myMessage" @input="passData(myMessage)">
<C v-bind="$attrs" v-on="$listeners"></C>
</div>`,
props: ['message'],
methods: {
passData(val){
this.$emit('getChildData', val)
}
}
})
Vue.component('C', {
template:
`<div>
<input type="text" v-model="$attrs.messageC" @input="passCData($attrs.messageC)">
</div>`,
methods: {
passCData(val) {
this.$emit('getCData', val)
}
}
})
Vue.component('A', {
template:
`<div>
<p>this is parent compoent!</p>
<B :messageC="messageC" :message="message" v-on:getCData="getCData"
v-on:getChildData="getChildData(message)"></B>
</div>`,
data() {
return {
message:'Hello',
messageC:'Hello c'
}
},
methods: {
getChildData(val) {
console.log('这是来自B组件的数据')
},
getCData(val) {
console.log("这是来自C组件的数据:" + val)
}
}
})
var app = new Vue({
el: '#app',
template: `<div><A></A></div>`
})
C组件中能直接触发getCData的原因在于 B组件调用C组件时 使用 v-on 绑定了$listeners 属性;
通过v-bind 绑定$attrs属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props 声明的)
7.3 组件传值 => 事件总线
中央事件总线:主要用来解决兄弟组件之间的通信问题。
实现方式:
新建一个Vue事件bus对象,然后通过bus.
e
m
i
t
触
发
事
件
,
b
u
s
.
emit 触发事件,bus.
emit触发事件,bus.on监听触发的事件。
Vue.component('brother1', {
data() {
return {
myMessage:'Hello brother1'
}
},
template:
`<div>
<p>this is brother1 compoent!</p>
<input type="text" v-model="myMessage" @input="passData(myMessage)">
</div>`,
methods: {
passData(val) {
bus.$emit('globalEvent',val)
}
}
})
Vue.component('brother2', {
template:
`<div>
<p>this is brother2 compoent!</p> <p>brother1传递过来的数据:{{brothermessage}}</p>
</div>`,
data() {
return {
myMessage:'Hello brother2',
brothermessage:''
}
},
mounted() {
bus.$on('globalEvent', (val) => {
this.brothermessage=val;
})
}
})
var bus = new Vue();
var app = new Vue({
el:'#app',
template:
`<div>
<brother1></brother1>
<brother2></brother2>
</div>`
})
8. Vuex相关
8.1 存储刷新:刷新页面保持数据不被清空
- 监听beforeunload,在刷新之前把(this.$store.state)存入localStorage;
- 刷新完页面初始化时在created中取出重新存入vuex中;
localStorage.getItem("publicTit") && this.$store.replaceState(Object.assign(this.$store.state,JSON.parse(localStorage.getItem("publicTit"))));
9. Vue 自定义指令:
一、注册自定义指令
? Vue自定义指令和组件一样存在着全局注册和局部注册两种方式。
注册全局指令的方式,通过 Vue.directive( id, [definition] ) ,第一个参数为自定义指令名称(指令名称不需要加 v- 前缀,默认是自动加上前缀的,使用指令的时候一定要加上前缀),第二个参数可以是对象数据,也可以是一个指令函数。
<div id="app" class="demo">
<input type="text" placeholder="我是全局自定义指令" v-focus>
</div>
<script>
Vue.directive("focus", {
inserted: function(el){
el.focus();
}
})
new Vue({
el: "#app"
})
</script>
注册局部自定义指令,通过在Vue实例中添加 directives 对象数据注册局部自定义指令。
<div id="app" class="demo">
<input type="text" placeholder="我是局部自定义指令" v-focus2>
</div>
<script>
new Vue({
el: "#app",
directives: {
focus2: {
inserted: function(el){
el.focus();
}
}
}
})
</script>
这个简单案例当中,我们通过注册一个 v-focus 指令,实现了在页面加载完成之后自动让输入框获取到焦点的小功能。
二、钩子函数
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
bind :只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。inserted :被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。update :所在组件的 VNode 更新时调用。- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind :只调用一次,指令与元素解绑时调用。
指令钩子函数会被传入以下参数:
- el: 指令所绑定的元素,可以用来直接操作 DOM,就是放置指令的那个元素。
- binding: 一个对象,里面包含了几个属性,这里不多展开说明,官方文档上都有很详细的描述。
- vnode:Vue 编译生成的虚拟节点。
- oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
<script>
Vue.directive('demo', function (el, binding) {
console.log(binding.value.color)
console.log(binding.value.text)
})
</script>
10. vue 其它相关:
1. Vue强制刷新组件的方法:
使用vue进行开发时,如果要刷新当前路由,则调用router.go(0)方法即可。但是某些情况下,我们可能要求仅仅刷新某个组件,而不是路由,那么我们应该怎么做呢?
① 使用this.$forceUpdate强制重新渲染
如果要在组件内部中进行强制刷新,则可以调用**this.$forceUpdate()**强制重新渲染组件,从而达到更新目的。
<template>
<button @click="reload()">刷新当前组件</button>
</template>
<script>
export default {
name: 'comp',
methods: {
reload() {
this.$forceUpdate()
}
}
}
</script>
② 使用watch监测值的变化,v-if以及this.$nextTick配合使用
如果是刷新某个子组件,则可以通过v-if指令实现。我们知道,当v-if的值发生变化时,组件都会被重新渲染一遍。因此,利用v-if指令的特性,可以达到强制刷新组件的目的。
2.Vue 插槽:
在构建页面过程中一般会把用的比较多的公共的部分抽取出来作为一个单独的组件,但是在实际使用这个组件的时候却又不能完全的满足需求,可能希望在这个组件中添加一点东西,这时候我们就需要用到插槽来分发内容。
官方文档对于插槽的应用场景是这样描述的:我们经常需要向一个组件传递内容,Vue 自定义的 <slot> 元素让这变得非常简单,只要在需要的地方加入插槽即可。
插槽是子组件中提供给父组件使用的一个占位符,用<slot></slot> 表示,父组件可以在这个占位符中填充任何模板代码,如 HTML、组件等,填充的内容会替换子组件的<slot></slot> 标签。
插槽的作用:让用户可以拓展组件,更好地复用组件和对其做定制化处理。
插槽的分类:
1.默认插槽
2.具名插槽
3.作用域插槽(主要解决的是父组件在向子组件插槽传递模板内容时存在访问子组件数据的问题)
11. vue生命周期、钩子函数
vue每个组件都是独立的,每个组件都有一个属于它的生命周期。
vue生命周期在项目中的执行顺序: 初始化、创建、挂载、更新、销毁
beforeCeate() => data() => created() => beforeMount() => mounted() 当更新会在created之后插入: beforeUpdate => updated 关闭页面组件最终都会销毁: beforeDestroy() => destroyed()
强调几点:
- beforeCeate在事件和生命周期钩子初始化前调用
- data的初始化是在created前完成数据观测(data observer)
vue中内置方法属性的运行顺序: (methods、computed、data、watch、props)
从源码可以知道: props => methods =>data => computed => watch
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true )
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
|