vue面试题

Vue是完全的MVVM模型吗?

严格的MVVVM要求View不能和Model直接通信,而Vue提供了$refs这个属性,让Model可以直接操作View,违反了这一规定,所以是Vue没有完全遵循MVVM。


Vue的单项数据流

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。


Vue的父子组件生命周期钩子函数执行顺序

  • 加载渲染过程

  • 父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted

  • 子组件更新过程

  • 父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated

  • 父组件更新过程

  • 父beforeUpdate -> 父updated

  • 销毁过程

  • 父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed


MVC和MVVM区别

MVC

MVC是model view controller

MVC的思想:Controller负责将Model的数据用View显示出来,换句话说就是在Controller里面把Model的数据赋值给View。

MVC的特点:实现关注点分离,即应用程序中的数据模型与业务和展示逻辑解耦。就是将模型和视图之间实现代码分离,松散耦合,使之成为一个更容易开发、维护和测试的客户端应用程序。

一般用于中大型项目开发。

MVVM

MVVM是Model-View-ViewModel的简写,即模型-视图-视图模型。

它有两个方向:

  • 一是将模型转化成视图,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。
  • 二是将视图转化成模型,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。

这两个方向都实现的,就是数据的双向绑定。


diff算法


双向绑定


详解

什么是双向绑定

单项绑定就是通过Model去改变view。

当实现用户更新view,model的数据也自动被更新就实现了双向绑定。

例如当用户填写表单时候改变了view,Model的数据也被自动更新了,这就实现了双向绑定。


双向绑定原理

vue采用的是 数据劫持结合 发布-订阅模式

通过ES5的 Object.defineProperty()getset来进行数据的劫持。

1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。

2.实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。

3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。

需要自己动手实现代码

现在复述下实现过程

observe 作为一个收集者,将watch全收集起来,当发生改变的时候,会通过属性的set去notify。watch主要是对这个vue实例模板节点进行订阅后存进observe。存的包括这个节点、节点类型、节点value。每次初始化watch就会将将碰到的这个模板节点进行存放,调用这个属性的Obejct.defineProperty的get方法,然后进行DeP的存放。每次这个值就行更改就会调用set方法,然后再把所有watch进行执行。



vuex

vuex是为专门开发的状态管理模式。采用集中式存储管理所有组件的状态。


出现原因

vue一般是单项数据流,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
多个视图依赖于同一状态、来自不同视图的行为需要变更同一状态。

作用:多个组件共享数据或者是跨组件传递数据


使用

// store.js
1.引入Vuex插件;
Vue.use(Vuex);

2.将Vuex.Store这个类实例化,并传入一些配置
const store = new Vuex.Store({
    state:{
        count:0
    },
    mutations:{
        increment(state){
            state.count++;
        },
        del(state){
            state.count--;
        },
    },
    actions:{
        asyncAdd({commit}){
            setTimeout(() => {
                commit("increment");
            }, 2000);
        }
    }
})

3.将store的实例配置给Vue
// main.js
new Vue({
  store,
  render: h => h(App),
}).$mount('#app')


4.组件中使用时
// App.vue
add(){
    this.$store.commit('increment');
},
asyncAdd(){
    this.$store.dispatch('asyncAdd');
}

</script>

实现

let Vue

class Store{
    constructor(options){
       let { state, getters, actions, mutations } = options
    }
}

const install=function(_Vue){
    Vue=_Vue
    //使用Vue的混入方法
    Vue.mixin({
        beforeCreat(){
            //根实例有store属性
            if(this.$options && this.$options.store)
            {
                this.$store=this.$options.store
            }
            else{
            	//根实例没有store实例,往父节点找
                new Vue({store})
                this.$store=this.$parent && this.$parent.$store
            }
        }
    })
}

export default install

beforeCreate中,第一次根组件通过store属性挂载$store,后面子组件调用beforeCreate挂载的$store都会向上找到父级的$store,这样子通过层层向上寻找,让每个组件都挂上了一个$store属性,而这个属性的值就是我们的new Store({...})的实例。如下图

设置state响应数据

通过创建vue实例来实现双向绑定

class Store{
    constructor(options){
        let {state,getters,mutations,actions}=options
        this.getters = {}
   		this.mutations = {}
    	this.actions = {}
    }
    // vuex的核心就是借用vue的实例,因为vuex的数据更改回更新视图
    this._vm=new Vue({
        data:{
            state
        }
    })
}

 // 访问state对象时候,就直接返回响应式的数据
  get state() { // Object.defineProperty get 同理
    return this._vm.state
  }
}



组件中的通信

vue bus

通过注册一个Vue对象,其他组件通过这个中间商传递信息

import Vue from 'vue'
const bus = new Vue()
export default bus 

组件发送消息

import bus from '@/utils/bus'

bus.$emit('message', 'hello');


组件接收消息

import bus from '@/utils/bus'

bus.$on('message', (e) => {
    console.log(e)
})


Vuex

通过组件共享的store来实现全局共享

this.$store.state.xxx



propsemit

<div>
	<child :message="message" @changemessage="handlechild"></child>
</div>

<script>
	expore default{
    	data(){
            message:'fanfan',
        },
        
        methods:{
            handlechild(){
                ...
            }
        }
    }
</script>

孩子

<div>
    {{message}}
</div>

<script>
	expore default{
    props:{
        message:String
    },
    	data(){
            message:'fanfan',
        },
        
        methods:{
            somemethods(){
                this.$emit('changemessage',this.messgae)
            }
        }
    }
</script>

或者使用更加简洁的写法

<child :message.sync="message">

this.$emit('update:message',this.message)



attrsattrs**和**listeners

经常用于多层嵌套

attrs:包含的是父域中未被props识别的特性属性。通过v-bind="$attrs"传入下一个子组件

listeners: 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件存储的父实例传入的方法(存放的是父组件中绑定的非原生事件)



provide和inject

向子孙后代注入一个依赖。

祖先通过provide提供变量,然后在子孙组件通过inject来注入变量。

// 父级组件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子组件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}


//`inject`也可以有默认项
const Child = {
  inject: {
    foo: { default: 'foo' }
  }
}

但需要注意的是 provide和inject的绑定并不是响应式的。如果传入的是可监听的对象,那么其对象还是可响应的。

var privider={
    provide:{
        name:'fan'
    }
}

var child={
    inject:['name'],
    created(){
        console.log(this.name)
    }
}

如果这个父组件的name改变了,子组件的name还是不会变的。

当然我们可以直接提供祖先实例,这样对子孙组件对祖先的修改也会直接响应。

var provider={
    provide(){
        return{
            myfather:this //注入祖先实例
        }
    }
}


var child={
    inject:{
        myfather:{
            default:()=>({})
        }
    },
    
    
}

这样带来的不便就是将多余的方法等不需要的东西也挂载了。


当然也可以用Vue.observable这个API来实现,它是让一个对象可响应,用于组件状态的共享。

var provider={
    data(){
        return {
            myobj:{}
        }
    },
    provide(){
        this.myobj=Vue.Observable({
            name:'fan',
            age:'20'
        })
        
        return this.myobj
    }
}


var child={
    inject:{
        myobj:{
            default:()=>({})
        }
    },
}


parentparent、children和$ref

<My-Comp ref="myref"></My-Comp>

//直接获得子组件的实例
this.$ref.myref.xxx

不建议频繁使用$parent、$children



vue-router

更新视图而不更新页面,是路由实现的核心功能

HashHistoryHTML5HistoryAbstratHistory

主要是靠以下两种方法实现

  • HashHistory

    hash 虽然出现在 url 中,但不会被包括在 http 请求中,它是用来指导浏览器动作的,对服务器端完全无用,因此,改变 hash 不会重新加载页面

    给hash添加监听事件

     window.addEventListener("hashchange",function(){},false)
    

    HashHistory.push()HashHistory.replace()

    第一个是将新路由推入浏览器访问历史的栈顶

    第二个只替换当前路由,而不记录


  • HashHistory

    通过back(),forward(),go()等方法,我们可以读取浏览器历史记录栈的信息,进行各种跳转操作。

    HTML5开始,History interface提供了2个新的方法:pushState(),replaceState()使得我们可以对浏览器历史记录栈进行修改:这2个方法有个共同的特点:当调用他们修改浏览器历史栈后,虽然当前url改变了,但浏览器不会立即发送请求该url,这就为单页应用前端路由,更新视图但不重新请求页面提供了基础

    HTML5History中添加对修改浏览器地址栏URL的监听**popstate**是直接在构造函数中执行的:


<body>
    <div id="app">
    <a href="#foo">foo</a>
    <a href="#bar">bar</a>

    </div>

</body>


<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>

const foo={template:`<div> foo</div>`}
const bar={template:`<div> bar</div>`}
const mydefault={template:`<div>mydefault</div>`}

const routetable={
    'foo':foo,
    'bar':bar,
    '':mydefault,
}

const app=new Vue({
    el:'#app',
    data:{
         url:window.location.hash.slice(1)
    },
  
    render(h) {
        return h('div',[
            h(routetable[this.url]),
            h('a',{attrs:{href:'#foo'}},'foo'),
            ' | ',
            h('a',{attrs:{href:'#bar'}},'bar')
           ])
    },
})

window.addEventListener('hashchange',()=>{
    app.url=window.location.hash.slice(1)
})


</script>

源码解析


父子组件生命周期顺序

->父beforeCreate -> 父created -> 父beforeMount

**->子beforeCreate -> 子created -> 子beforeMount -> 子mounted

-> 父mounted

子组件更新过程

->父beforeUpdate

-> 子beforeUpdate -> 子updated

-> 父updated

父组件更新过程

父beforeUpdate -> 父updated

销毁过程

-> 父 beforeDestory

-> 子 beforeDestory -> 子 destoryed

-> 父 destoryed