9.前端框架vue-3
1.13 js是如何监听HistoryRouter的变化的
参考答案:
通过浏览器的地址栏来改变切换页面,前端实现主要有两种方式:
通过hash改变,利用window.onhashchange 监听。
HistoryRouter:通过history的改变,进行js操作加载页面,然而history并不像hash那样简单,因为history的改变,除了浏览器的几个前进后退(使用 history.back(), history.forward()和 history.go() 方法来完成在用户历史记录中向后和向前的跳转。)等操作会主动触发popstate 事件,pushState,replaceState 并不会触发popstate事件,要解决history监听的问题,方法是:
首先完成一个订阅-发布模式,然后重写history.pushState, history.replaceState,并添加消息通知,这样一来只要history的无法实现监听函数就被我们加上了事件通知,只不过这里用的不是浏览器原生事件,而是通过我们创建的event-bus 来实现通知,然后触发事件订阅函数的执行。
具体操作如下:
- 订阅-发布模式示例
class Dep { // 订阅池 constructor(name){ this.id = new Date() //这里简单的运用时间戳做订阅池的ID this.subs = [] //该事件下被订阅对象的集合 } defined(){ // 添加订阅者 Dep.watch.add(this); } notify() { //通知订阅者有变化 this.subs.forEach((e, i) => { if(typeof e.update === 'function'){ try { e.update.apply(e) //触发订阅者更新函数 } catch(err){ console.warr(err) } } }) } } Dep.watch = null; class Watch { constructor(name, fn){ this.name = name; //订阅消息的名称 this.id = new Date(); //这里简单的运用时间戳做订阅者的ID this.callBack = fn; //订阅消息发送改变时->订阅者执行的回调函数 } add(dep) { //将订阅者放入dep订阅池 dep.subs.push(this); } update() { //将订阅者更新方法 var cb = this.callBack; //赋值为了不改变函数内调用的this cb(this.name); } }
- 重写history方法,并添加window.addHistoryListener事件机制。
var addHistoryMethod = (function(){ var historyDep = new Dep(); return function(name) { if(name === 'historychange'){ return function(name, fn){ var event = new Watch(name, fn) Dep.watch = event; historyDep.defined(); Dep.watch = null; //置空供下一个订阅者使用 } } else if(name === 'pushState' || name === 'replaceState') { var method = history[name]; return function(){ method.apply(history, arguments); historyDep.notify(); } } } }()) window.addHistoryListener = addHistoryMethod('historychange'); history.pushState = addHistoryMethod('pushState'); history.replaceState = addHistoryMethod('replaceState');
1.14 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发生变化,不会导致浏览器向服务器发出请求,浏览器不发出请求就不会刷新页面,并且会触发hasChange这个事件,通过监听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错误
使用方法:
<script> // hash路由原理*************************** // 监听hashchange方法 window.addEventListener('hashchange',()=>{ div.innerHTML = location.hash.slice(1) }) // history路由原理************************ // 利用html5的history的pushState方法结合window.popstate事件(监听浏览器前进后退) function routerChange (pathname){ history.pushState(null,null,pathname) div.innerHTML = location.pathname } window.addEventListener('popstate',()=>{ div.innerHTML = location.pathname }) </script>
1.15 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来处理。
1.16 组件通信的方式
参考答案:
组件通信的方式的方式有以下8种方法:
props和$emit
这是最最常用的父子组件通信方式,父组件向子组件传递数据是通过prop传递的,子组件传递数据给父组件是通过$emit触发事件来做到的
listeners
第一种方式处理父子组件之间的数据传输有一个问题:如果多层嵌套,父组件A下面有子组件B,组件B下面有组件C,这时如果组件A想传递数据给组件C怎么办呢?
如果采用第一种方法,我们必须让组件A通过prop传递消息给组件B,组件B在通过prop传递消息给组件C;要是组件A和组件C之间有更多的组件,那采用这种方式就很复杂了。从Vue 2.4开始,提供了listeners来解决这个问题,能够让组件A之间传递消息给组件C。
v-model
父组件通过v-model传递值给子组件时,会自动传递一个value的prop属性,在子组件中通过this.$emit(‘input',val)自动修改v-model绑定的值
provide和inject
父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量。不论子组件有多深,只要调用了inject那么就可以注入provider中的数据。而不是局限于只能从当前父组件的prop属性来获取数据,只要在父组件的生命周期内,子组件都可以调用。
中央事件总线
上面方式都是处理的父子组件之间的数据传递,那如果两个组件不是父子关系呢?也就是兄弟组件如何通信?
这种情况下可以使用中央事件总线的方式。新建一个Vue事件bus对象,然后通过bus.on监听触发的事件。
parent和children
boradcast和dispatch
vue1.0中提供了这种方式,但vue2.0中没有,但很多开源软件都自己封装了这种方式,比如min ui、element ui和iview等。 比如如下代码,一般都作为一个mixins去使用, broadcast是向特定的父组件,触发事件,dispatch是向特定的子组件触发事件,本质上这种方式还是on和on和emit的封装,但在一些基础组件中却很实用
vuex处理组件之间的数据交互
如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候才有上面这一些方法可能不利于项目的维护,vuex的做法就是将这一些公共的数据抽离出来,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的
1.17 vue组件间传值, attrs和listeners 了解过吗?
参考答案:
listeners的作用:解决多层嵌套情况下,父组件A下面有子组件B,组件B下面有组件C,组件A传递数据给组件B的问题,这个方法是在Vue 2.4提出的。
listeners解决问题的过程:
C组件
Vue.component('C',{ template:` <div> <input type="text" v-model="$attrs.messageC" @input="passCData($attrs.messageC)"> </div> `, methods:{ passCData(val){ //触发父组件A中的事件 this.$emit('getCData',val) } } })
B组件
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) } } })
A组件
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组件的数据') }, //执行C子组件触发的事件 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声明的)
1.18 组建传值,事件总线是怎么用的
参考答案:
中央事件总线主要用来解决兄弟组件通信的问题。
实现方式:新建一个Vue事件bus对象,然后通过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){ //触发全局事件globalEvent 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(){ //绑定全局事件globalEvent bus.$on('globalEvent',(val)=>{ this.brothermessage=val; }) } }) //中央事件总线 var bus=new Vue(); var app=new Vue({ el:'#app', template:` <div> <brother1></brother1> <brother2></brother2> </div> ` })
1.19 vue生命周期中异步加载在mouted还是create里实现
参考答案:
最常用的是在 created 钩子函数中调用异步请求
解析:
一般来说,可以在,created,mounted中都可以发送数据请求,但是,大部分时候,会在created发送请求。
Created的使用场景:如果页面首次渲染的就来自后端数据。因为,此时data已经挂载到vue实例了。
在 created(如果希望首次选的数据来自于后端,就在此处发请求)(只发了异步请求,渲染是在后端响应之后才进行的)、beforeMount、mounted(在mounted中发请求会进行二次渲染) 这三个钩子函数中进行调用。
因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。但是最常用的是在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有两个优点:
第一点:能更快获取到服务端数据,减少页面 loading 时间;
第二点:放在 created 中有助于一致性,因为ssr 不支持 beforeMount 、mounted 钩子函数。
1.20 vue钩子函数(重点问了keep-alive)
参考答案:
Vue生命周期经历哪些阶段:
- 总体来说:初始化、运行中、销毁
- 详细来说:开始创建、初始化数据、编译模板、挂载Dom、渲染→更新→渲染、销毁等一系列过程
生命周期经历的阶段和钩子函数:
实例化vue(组件)对象:new Vue()
初始化事件和生命周期 init events 和 init cycle
beforeCreate函数:
在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
即此时vue(组件)对象被创建了,但是vue对象的属性还没有绑定,如data属性,computed属性还没有绑定,即没有值。
此时还没有数据和真实DOM。
即:属性还没有赋值,也没有动态创建template属性对应的HTML元素(二阶段的createUI函数还没有执行)
挂载数据(属性赋值)
包括 属性和computed的运算
Created函数:
vue对象的属性有值了,但是DOM还没有生成,$el属性还不存在。
此时有数据了,但是还没有真实的DOM
即:data,computed都执行了。属性已经赋值,但没有动态创建template属性对应的HTML元素,所以,此时如果更改数据不会触发updated函数
如果:数据的初始值就来自于后端,可以发送ajax,或者fetch请求获取数据,但是,此时不会触发updated函数
- 检查
6.1 检查是否有el属性
检查vue配置,即new Vue{}里面的el项是否存在,有就继续检查template项。没有则等到手动绑定调用 vm.el的绑定。
6.2 检查是否有template属性
检查配置中的template项,如果没有template进行填充被绑定区域,则被绑定区域的el对outerHTML(即 整个#app DOM对象,包括
beforeMount函数:
模板编译(template)、数据挂载(把数据显示在模板里)之前执行的钩子函数
此时 this.$el有值,但是数据还没有挂载到页面上。即此时页面中的{{}}里的变量还没有被数据替换
模板编译:用vue对象的数据(属性)替换模板中的内容
Mounted函数:
模板编译完成,数据挂载完毕
即:此时已经把数据挂载到了页面上,所以,页面上能够看到正确的数据了。
一般来说,我们在此处发送异步请求(ajax,fetch,axios等),获取服务器上的数据,显示在DOM里。
beforeUpdate函数:
组件更新之前执行的函数,只有数据更新后,才能调用(触发)beforeUpdate,注意:此数据一定是在模板上出现的数据,否则,不会,也没有必要触发组件更新(因为数据不出现在模板里,就没有必要再次渲染)
数据更新了,但是,vue(组件)对象对应的dom中的内部(innerHTML)没有变,所以叫作组件更新前
updated函数:
组件更新之后执行的函数
vue(组件)对象对应的dom中的内部(innerHTML)改变了,所以,叫作组件更新之后
activated函数:keep-alive组件激活时调用
activated函数:keep-alive组件停用时调用
beforeDestroy:vue(组件)对象销毁之前
destroyed:vue组件销毁后
keep-alive
<keep-alive></keep-alive>
包裹动态组件时,会缓存不活动的组件实例,主要用于保留组件状态或避免重新渲染。
解析: 比如有一个列表和一个详情,那么用户就会经常执行打开详情=>返回列表=>打开详情…这样的话列表和详情都是一个频率很高的页面,那么就可以对列表组件使用<keep-alive></keep-alive>
进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是重新渲染