ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# VUE面试题 vue中MVVM的理解 M:模型(Model):数据模型;负责数据存储。泛指后端进行的各种业务逻辑处理和数据操控,主要围绕数据库系统展开。 V就是:View 视图: 负责页面展示,也就是用户界面。主要由 HTML 和 CSS 来构建 VM就是:视图模型(View-Model): 负责业务逻辑处理(比如Ajax请求等),对数据进行加工后交给视图展示 通过vue类创建的对象叫Vue实例化对象,这个对象就是MVVM模式中的VM层,模型通过它可以将数据绑定到页面上,视图可以通过它将数据映射到模型上 优点 1.低耦合。视图(View)可以独立于Model变化和修改, 2.可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑 3.前后端分离,开发人员可以专注于业务逻辑(ViewModel)和数据的开发,设计人员可以专注于页面设计 为什么说VUE是一个渐进式的javascript框架, 渐进式是什么意思? VUE允许你将一个网页分割成可复用的组件,每个组件都包含属于自己的HTML、CSS、JAVASCRIPT以用来渲染网页中相应的地方。对于VUE的使用可大可小,它都会有相应的方式来整合到你的项目中。所以说它是一个渐进式的框架。VUE是响应式的(reactive)这是VUE最独特的特性,也就是说当我们的数据变更时,VUE会帮你更新所有网页中用到它的地方。 vue生命周期 beforeCreate(创建前) :组件实例被创建之初,组件的属性生效之前 //beforeCreate生命周期执行的时候,data和methods中的数据都还没有初始化。不能在这个阶段使用data中的数据和methods中的方法 created(创建后) :组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用 // data 和 methods都已经被初始化好了,如果要调用 methods 中的方法,或者操作 data 中的数据,最早可以在这个阶段中操作 beforeMount(挂载前) :在挂载开始之前被调用:相关的 render 函数首次被调用 //执行到这个钩子的时候,在内存中已经编译好了模板了,但是还没有挂载到页面中,此时,页面还是旧的 mounted(挂载后) :在el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子 //到mounted周期的时候,Vue实例已经初始化完成了。此时组件脱离了创建阶段,进入到了运行阶段。 如果我们想要通过插件操作页面上的DOM节点,最早可以在和这个阶段中进行 beforeUpdate(更新前) :组件数据更新之前调用,真实DOM还没被渲染 // 当执行这个钩子时,页面中的显示的数据还是旧的,data中的数据是更新后的,页面还没有和最新的数据保持同步 update(更新后) :组件数据更新之后 //页面显示的数据和data中的数据已经保持同步了,都是最新的 activated(激活前) :keep-alive专属,组件被激活时调用 //当组件被切回来时,再去缓存里找这个组件、触发 activated钩子函数。 deactivated(激活后) :keep-alive专属,组件被销毁时调用 //当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期 beforeDestory(销毁前) :组件销毁前调用 //Vue实例从运行阶段进入到了销毁阶段,这个时候上所有的 data 和 methods , 指令, 过滤器 ……都是处于可用状态。还没有真正被销毁 destoryed(销毁后) :组件销毁前调用 //这个时候上所有的 data 和 methods , 指令, 过滤器 ……都是处于不可用状态。组件已经被销毁了。 Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。 Vue子组件和父组件执行顺序 加载渲染过程:beforeCreate(父) —> created(父)—>beforeMount(父)—>beforeCreate(子)—>created(子)—>beforeMount(子)—>mounted(子)—>mounted(父) 更新过程:beforeUpdate(父) —> beforeUpdate(子) —> update(子) —> update(父) 销毁过程:beforeDestory(父) —> beforeDestory(子) —> destoryed(子) —> destoryed(父) v-el 作用是什么 提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标。可以是 CSS 选择器,也可以是一个 HTMLElement 实例。 1 Vue的el属性和$mount优先级? new Vue({ router, store, el: '#app', render: h => h(App) }).$mount('#div') /*当出现上面的情况就需要对el和$mount优先级进行判断,从下面的官方图片我们可以看出来, el的优先级是高于$mount的,因此以el挂载节点为准*/ Vue实现数据双向绑定的原理:Object.defineProperty() vue实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调。 vue的数据双向绑定 将MVVM作为数据绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听自己的model的数据变化,通过Compile来解析编译模板指令(vue中是用来解析 {undefined{}}),最终利用watcher搭起observer和Compile之间的通信桥梁,达到数据变化 —>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。 数据双向绑定示例: <body> <div id="app"> <input type="text" id="txt"> <p id="show"></p> </div> </body> <script type="text/javascript"> var obj = {} Object.defineProperty(obj, 'txt', { get: function () { return obj }, set: function (newValue) { document.getElementById('txt').value = newValue document.getElementById('show').innerHTML = newValue } }) document.addEventListener('keyup', function (e) { obj.txt = e.target.value }) </script> 假如data里面的数据不想做响应式,该怎么做 1、数据放在vue实例外(vue template中访问不到数据) 2、created, mounted钩子函数中定义(注意data中不要声明该变量名) 3、自定义Options 4、Object.freeze() 如何将获取data中某一个数据的初始状态? data() { return { num: 10 }, mounted() { this.num = 1000 }, methods: { countNum() { // 可以通过this.$options.data().keyname来获取初始值 // 计算出num增加了多少 console.log(1000 - this.$options.data().num) } } 动态指令设置及动态传参 <template> ... <child @[someEvent]="handleSomeEvent()" :[someProps]="1000" />... </template> <script> ... data(){ return{ ... someEvent: type ? "click" : "dbclick", someProps: type ? "num" : "price" } }, methods: { handleSomeEvent(){ // do some } } </script> //应用场景:用于页面中根据不同的返回值进行事件的触发和值的传参 Vue组件间的参数传递 1.父组件传给子组件:子组件通过props方法接受数据; 2.子组件传给父组件:$emit方法传递参数 3.非父子组件间的数据传递,兄弟组件传值借用eventBus,就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。发送数据使用 $emi t方法,使用 $on 接收 更多详细内容可以查看我的另一篇文章Vue父子组件间传值方法集 provide和inject使用(响应式) provide和inject是用来实现父组件向深层的子组件传值和接收的语法,具体如下: // 祖先组件 provide(){ return { // keyName: { name: this.name }, // value 是对象才能实现响应式,也就是引用类型 keyName: this.changeValue // 通过函数的方式也可以[注意,这里是把函数作为value,而不是this.changeValue()] // keyName: 'test' value 如果是基本类型,就无法实现响应式 } }, data(){ return { msg:'初始mesg' } }, methods: { changeValue(){ this.msg= '改变后的msg' } } // 后代组件 inject:['keyName'] create(){ console.log(this.keyName) // 改变后的msg } 详情参考我的这篇文章 provide / Inject浅析 Vue的路由实现:hash模式 和 history模式 hash模式: 在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取; 特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。 history模式: history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。 $nextTick原理及运用 1.nextTick是啥? Vue.nextTick( [callback, context] ):在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。 1 2.为什么需要它呢? Vue是异步执行dom更新的,一旦观察到数据变化,Vue就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个watcher被触发多次,只会被推送到队列一次。 这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOm操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。 假使你设置 vm.someData = 'new value',DOM 并不会马上更新,而是在异步队列被清除,也就是下一个事件循环 开始时执行更新时才会进行必要的DOM更新。如果此时你想要根据更新的 DOM 状态去做某些事情,就会出现问题。 为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。 这样回调函数在 DOM 更新完成后就会调用。 3我再什么地方用它呢? 1、在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中。 原因是在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳, 所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted钩子函数, 因为该钩子函数执行时所有的DOM挂载和渲染都已完成,此时在该钩子函数中进行任何DOM操作都不会有问题 。 2、在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候 (譬如v-if/v-show根据字段变化显隐),这个操作都应该放进Vue.nextTick()的回调函数中。 Compute和watch区别和应用场景 computed //计算属性中的属性不需要在data中定义,而且必须有return data(){ return{ firstname:"张", lastname:"三" } } computehd(){ fullname(){ return this.firstname+this.lastname } } /*计算属性具有缓存,计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改变时才会重新求值。 只要计算属性的依赖没有改变,那么调用它就会直接返回之前的缓存。 同时computed对于其中变量的依赖时多个 的时候,只要其中一个发生了变化都会触发这个函数*/ //应用场景:当一个变量的值受多个变量的值影响 watch //监听器watch中的值需要在data中定义,且函数有参数,newval和oldval data: { firstName: '张', lastName: '三', fullName: '张三r' }, watch: { firstName: function (oval,nval) { this.fullName = nval + ' ' + this.lastName }, lastName: function (oval,nval) { this.fullName = this.firstName + ' ' + nval }, immediate: true,// 代表在wacth里声明了firstName之后立即先去执行其函数方法 deep: true //深度监听 } //watch的依赖是单个的,它每次只可以对一个变量进行监控,并且区别于computed属性,监听器watch可以是异步的而computed则不行 //应用场景:当一个变量的值影响着多个变量的值 filters //过滤器分为全局过滤和局部过滤,当命名冲突时以局部过滤器权重高 //插值中 {{msg|filterMsg}} //bind中 <div v-bind:"id|filterId"></div> //运用场景: //一般来说我们用过滤器来格式化一些数据或者渲染的文本对于格式展现的要求 全局: Vue.filter('过滤器名',function(value){ //do some }) 局部: filters:{ 过滤器名称:function(value){ //do some } } vuex vuex是什么?怎么使用?哪种功能场景使用它? //是什么 vue框架中状态管理。在main.js引入store注入。新建一个目录store 。场景有:单页应用中,组件之间的状态, 音乐播放、登录状态、加入购物车等。 //属性: State、 Getter、Mutation 、Action、 Module //State state是数据源存放地,对应于一般Vue对象里面的data。state里面存放的数据是响应式的, Vue组件从store中读取数据,若是store中的数据发生改变,依赖这个数据的组件也会发生更新 需要通过mapState把全局 state 和 getters 映射到当前组件的 computed 计算属性中。 //Getter getters 可以对State进行计算操作,在多个组件间复用 //Mutation 、Action Action 类似于 mutation,不同在于Action 提交的是 mutation,而不是直接变更状态;Action 可以包含任意异步操作。 //Module Vuex允许我们将store分隔成模块(module),每个模块拥有自己的state,mutation,action,getter,甚至是嵌套子模块 //使用场景 一句话,不要为了使用vuex而去使用vuex,推荐组件间数据复用,记录登录及其它状态值数据,一些需要缓存的数据使用vuex都能达到很好的管理 v-show 与 v-if 的区别,两者的优先级 v-show指令是通过修改元素的display的CSS属性让其显示或者隐藏; v-if指令是直接销毁和重建DOM达到让元素显示和隐藏的效果; 使用v-show会更加节省性能上的开销;当只需要一次显示或隐藏时,使用v-if更加合理。 //优先级 v-for优先级比v-if高 //注意事项 不要把 v-if 和 v-for 同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断) 正确的做法应该是再v-for的外面新增一个模板标签template,在template上使用v-if也能结合filters或者是computed属性对数据进行加工,避免v-if判断,更好的渲染 vue路由传参 //通过 params 传参 this.$router.push({ name: '目标组件名', params: { id: id } }) //接收: this.$route.params //通过 query 传参 this.$router.push({ path: '目标组件路径', query: { id: id } }) //接收 this.$route.query //区别:query使用path来引入,params使用name来引入,接收方式是this.$route.query.name和this.$route.params.name,值得注意的是query传递的参数会显示在url后面以?id=?形式展示。 //动态路由传参 //直接调用$router.push 实现携带参数的跳转 this.$router.push({ path: `/particulars/${id}`, }) //通过this.$route.params.id接收,可以看到,和上面传参不一样的是我们直接把动态参数加在路径后面实现动态路由 vue路由的钩子函数 导航钩子种类 全局导航钩子、组件内钩子、单独路由独享组件 //路由的钩子函数总结有6个 全局的路由钩子函数:beforeEach、afterEach 单个的路由钩子函数:beforeEnter 组件内的路由钩子函数:beforeRouteEnter、beforeRouteLeave、beforeRouteUpdate 全局导航钩子 全局前置守卫:beforeEach const router = new VueRouter({ ... }) router.beforeEach((to, from, next) => { // ... }) to: Route: 即将要进入的目标 路由对象 from: Route: 当前导航正要离开的路由 next: Function: 一定要调用该方法不然会阻塞路由。执行效果依赖 next 方法的调用参数。 next()方法接收的参数: 全局后置钩子:afterEach router.afterEach((to, from) => { // do someting }); //后置钩子并没有 next 函数 路由独享的钩子 路由独享的导航钩子,它是在路由配置上直接进行定义的,参数的使用,和全局前置守卫是一样的 使用的钩子函数与全局路由守卫一致,为beforeEnter,不同的是,路由独享守卫是定义在路由记录中,全局路由守卫是定义在入口文件中,路由独享守卫只在路由进入时有效,全局路由守卫是所有路由跳转都会被拦截。 组件内的导航钩子 beforeRouteEnter:在渲染该组件的对应路由前调用 beforeRouteUpdate:在当前路由改变,但是该组件被复用时调用 beforeRouteLeave:导航离开该组件的对应路由时调用 //注意:beforeRouteEnter 不能获取组件实例 this,因为当守卫执行前,组件实例被没有被创建出来,剩下两个钩子则可以正常获取组件实例 this 共享组件将不会重新渲染问题 我们有时候开发中会把多个路由解析为同一个Vue组件。问题是,Vue默认情况下共享组件将不会重新渲染,如果你尝试在使用相同组件的路由之间进行切换,则不会发生任何变化,此时我们需要传递key来区分,达到刷新的目的 const routes = [ { path: "/a", component: MyComponent }, { path: "/b", component: MyComponent }, ]; <template> <router-view :key="$route.path"></router-view> </template> 插槽 插槽就是子组件中用slot标签定义的预留位置,有name属性叫具名插槽,不设置name属性的叫不具名插槽,使用插槽主要是为了在父组件中使用子组件标签的时候可以往子组件内写入html代码。 插槽使用: //父组件: <template> <div> <div>这是父组件</div> <son>slot内容</son> </div> </template> //子组件 <template> <div> <div>这是子组件</div> <input type="text" placeholder="请输入"> </div> </template> //一般情款下想在子组件内插入内容像上面直接在标签里书写时不显示的需要以slot为媒介 //改写后: //子组件: <template> <div> <div>这是子组件</div> <input type="text" placeholder="请输入"> <slot></slot> </div> </template> //此时我们没有给插槽设置name,所以这是一个不具名插槽 //具名插槽: //父组件: <template> <div> <div>这是父组件</div> <son> <template slot="myslot"> <div> 实践具名slot </div> </template> </son> </div> </template> //子组件 <template> <div> <div>这是子组件</div> <input type="text" placeholder="请输入"> <slot name="myslot"></slot> </div> </template> //此时设置name属性的插槽为具名插槽,与之相对应的用了slot的设置为相同属性名的内容则会被渲染在插槽中,此时如果有未设置slot插槽名的内容则会被渲染在不具名插槽中 插槽作用域: //父组件: <template> <div> <div>父组件</div> <son> <template slot="myslot" slot-scope="props"> <ul> <li v-for="item in props.data"> {{item}} </li> </ul> </template> </son> </div> </template> //子组件: <template> <div> <div>子组件</div> <input type="text" placeholder="请输入"> <slot name="myslot" :data='list'></slot> </div> </template> <script> export default { name:'Son', data(){ return{ list:[ {name:"张三",age:3}, {name:"李四",age:4}, {name:"王五",age:5} ] } } } </script> mixins mixins是一种分发Vue组件中可复用功能的一种灵活方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。 mixins是一个JavaScript对象,可以包含组件中的任意选项,比如Vue实例中生命周期的各个钩子函数,也可以是data、components、methods或directives等 运用: //mixin文件 export const myMixin={ data(){ return{ msg:1 } }, created(){ console.log('myMixin') }, methods:{ Fn(){ console.log('myMixin') } } } //引入 <template> <div>运用mixin的组件</div> </template> <script> import {myMixin} from'目标文件路径' export default{ mixins:[myMixin] } </script> 特点: 1、在组件A对混入的数据做出更改后组件B获取到的仍是混入初始设置的数据,组件间操作互不污染。 2、值为对象的如methods,components等,选项会被合并,组件会覆盖混入对象的方法。 比如混入对象里有个方法A,组件里也有方法A,这时候在组件里调用的话,执行的是组件里的A方法。 3、created,mounted等,就会被合并调用,混合对象里的钩子函数在组件里的钩子函数之前调用, 同一个钩子函数里,会先执行混入对象的东西,再执行本组件的。 4、在mixins里面包含异步请求函数的时候,通过直接调用异步函数获取返回数据 运用场景区别: vuex:用来做状态管理,可以看做全局变量,里面定义的变量在每个组件中均可以使用和修改, 在任一组件中修改此变量的值之后,其他组件中此变量的值也会随之修改。 mixins:可以定义共用的变量,在每个组件中使用,引入组件中之后,各个变量是相互独立的, 值的修改在组件中不会相互影响。 父子组件:父子组件相对来说比较独立,只是父组件将一部分使用子组件,而mixins更像是对于组件的拓展,并且 组件可以对于混入的数据和方法进行多样化操作。 vue自定义组件添加事件 使用修饰符.native 监听组件根元素的原生事件 <my-button @click.native="alert()" names="点击触发"></my-button> axios //axios特点: 在浏览器中创建XMLHttpRequest请求 在node.js中发送http请求 支持Promise API 拦截请求和响应 转换请求和响应数据 取消要求 自动转换JSON数据 客户端支持防止CSRF/XSRF(跨域请求伪造) //axios的请求方式: axios(config) axios.request(config) axios.get(url [,config]) axios.post(url [,data [,config]]) axios.put(url [,data [,config]]) axios.delete(url [,config]) axios.patch(url [,data [,config]]) axios.head(url [,config]) axios一般用法配置与请求 //引入axios import axios from 'axios' //定义axios请求接口的baseURL axios.default.baseURL = 'http://localhost:8080/api/products' //执行GET请求 axios.get('/user?ID=12345') //返回的是一个Promise .then(res=>console.log(res)) .catch(err=>console.log(err)); //可配置参数的方式 axios.get('/user',{ params:{ ID:12345 } }).then(res=>console.log(res)) .catch(err=>console.log(err)); //发送post请求 axios.post('/user',{ firstName: 'simon', lastName:'li' }).then(res=>console.log(res)) .catch(err=>console.log(err)); //发送post请求 axios({ method: 'post', //请求方式,默认是get请求 url:'/user/12345', //地址 data:{ //参数 firstName: 'simon', lastName: 'li' } }); 发送并发请求: //发送多个请求(并发请求),类似于promise.all,若一个请求出错,那就会停止请求 const get1 = axios.get('/user/12345'); const get2 = axios.get('/user/12345/permission'); axios.all([get1,get2]) .then(axios.spread((res1,res2)=>{ console.log(res1,res2); })) .catch(err=>console.log(err)) //函数返回的是一个数组axios.spread(callback)可用于将结果数组展开 请求配置: { //服务器的地址,是必须的选项 url: '/user', //请求的方式,若没有则默认是get method:'get', //如果url不是绝对地址,则会加上baseURL baseURL: 'http://localhost:3000/', //transformRequest允许请求的数据在发送至服务器之前进行处理,这个属性只适用于put、post、patch方式 //数组的最后一个函数必须返回一个字符串或者一个'ArrayBuffer'或'Stream'或'Buffer' 实例或'ArrayBuffer','Formdata', //若函数中用到了headers,则需要设置headers属性 transformRequest: [function(data,headers){ //根据需求对数据进行处理 return data; }], //transformResponse允许对返回的数据传入then/catch之前进行处理 transformResponse:[function(data){ //依需要对数据进行处理 return data; }], //headers是自定义的要被发送的信息头 headers: {'X-Requested-with':'XMLHttpRequest'}, //params是请求连接中的请求参数,必须是一个纯对象 params:{ ID:12345 }, //paramsSerializer用于序列化参数 paramsSerializer: function(params){ return Qs.stringify(params,{arrayFormat:'brackets'}); }, //data是请求时作为请求体的数据——request.body //只适用于put、post、patch请求方法 //浏览器:FormData,File,Blob;Node:stream data:{ firstName: 'simon', }, //timeout定义请求的时间,单位是毫秒,如果请求时间超过设定时间,请求将停止 timeout:1000, //withCredentials表明跨跨域请求书否需要证明。 withCredentials:false, //默认值 //adapter适配器,允许自定义处理请求 //返回一个promise adapter:function(config){ /*...*/ }, //auth表明HTTP基础的认证应该被使用,并提供证书 auth:{ username:'simon', password:'123456', }, //responseType表明服务器返回的数据类型,这些类型包括:json/blob/document/ arraybuffer/text/stream responseType: 'json', //proxy定义服务器的主机名和端口号 //auth属性表明HTTP基本认证应该跟proxy相连接,并提供证书 //这将设置一个'Proxy-Authorization'头(header),覆盖原来自定义的 proxy:{ host:127.0.0.1, port:8080, auth:{ username:'simon', password:'123456' } }, //取消请求 cancelToken: new CancelToken(cancel=>{}) } ajax 详情可以查看我之前写过的ajax封装文章,详细了说明封装过程ajax优劣分析及封装