面试题集锦:Vue

310 阅读37分钟

vue

常用的修饰符有哪些

修饰符:

  • .lazy 改变后触发,光标离开input 输入框的时候值才会改变
  • .number 将输出字符串转为 number类型
  • .trim 自动过滤用户输入的首尾空格

事件修饰符∶

  • .stop 阻止点击事件冒泡,相当于原生js 中的event.stopPropagation0
  • .prevent 防 止 执 行 预 设 的 行为,相 当 于原 生 js 中event.preventDefault()
  • .capture 添加事件侦听器时使用事件捕获模式,就是谁有该事件修饰符,就先触发谁
  • .self 只会触发自己范围内的事件,不包括子元素
  • .once 只执行—次

键盘修饰符∶

  • .esc 返回键
  • .space 空格
  • .tab 制表键
  • .enter 回车键键
  • .left 向左建
  • .right 向右键
  • .up向上键
  • .down 向下键

系统修饰符∶

  • .ctrl
  • .alt
  • .shift
  • .meta

v-if 和 v-for 哪个优先级更高?如果两个同时出现,应该怎么优化得到更好的性能? 

源码中找答案 compiler/codegen/index.js

<p v-for="item in items" v-if="condition">

做个测试如下

<body> 
    <div id="demo"> 
        <h1>v-for和v-if谁的优先级高?应该如何正确使用避免性能问题?</h1>
        
        <!-- 两者同级 -->
        <p v-for="child in children" v-if="isFolder">{{child.title}}</p>

        <!-- 两者不同级 -->
        <template v-if="isFolder"> 
            <p v-for="child in children">{{child.title}}</p> 
        </template>
    </div> 

    <script src="../../dist/vue.js"></script> 
    <script> 
        // 创建实例 
        const app = new Vue({ 
            el: '#demo', 
            data() { 
                return { 
                    children: [ 
                        {title:'foo'},
                        {title:'bar'}, 
                    ] 
                } 
            }, 
            computed: { 
                isFolder() {
                    return this.children && this.children.length > 0    
                } 
            }, 
        }); 

        console.log(app.$options.render); 
    </script> 
</body> 

两者同级时,渲染函数如下: 

(function anonymous( 
) { 
with(this){return _c('div',{attrs:{"id":"demo"}},[_c('h1',[_v("v-for和v-if谁的优先 级高?应该如何正确使用避免性能问题?")]),_v(" "), 
_l((children),function(child){return (isFolder)?_c('p', \
[_v(_s(child.title))]):_e()})],2)} 
}) 

_l(列表渲染的函数) 包含了 isFolder 的条件判断 。即 v-for 先执行

两者不同级时,渲染函数如下 

(function anonymous( 
) { 
with(this){return _c('div',{attrs:{"id":"demo"}},[_c('h1',[_v("v-for和v-if谁的优先 级高?应该如何正确使用避免性能问题?")]),_v(" "), \
(isFolder)?_l((children),function(child){return _c('p', \
[_v(_s(child.title))])}):_e()],2)} 
}) 

先判断了条件再看是否执行 _l

结论: 

  1. 显然 v-for 优先于 v-if 被解析(把你是怎么知道的告诉面试官) 

  2. 如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性能 

  3. 要避免出现这种情况,则在外层嵌套 template,在这一层进行 v-if 判断,然后在内部进行 v-for 循环 

  4. 如果条件出现在循环内部,可通过计算属性提前过滤掉那些不需要显示的项

Vue 组件 data 为什么必须是个函数而 Vue 的根实例则没有此限制?

data 是一个对象,对象是引用类型,若改动了 第一个引用的 Foo 组件 内的 data,其他引用的 Foo 组件 的 data 也会被改变

data 变成一个函数,让他每次都创建一个新的对象。这样每个组件对应的 data 对象都是独立的,就不会有这种问题了。

源码中找答案:src\core\instance\state.js - initData() 

函数每次执行都会返回全新的 data 对象实例 

测试代码如下:

<body> 
    <div id="demo"> 
        <h1>vue组件data为什么必须是个函数? </h1> 
        <comp></comp> 
        <comp></comp> 
    </div> 

    <script src="../../dist/vue.js"></script> 
    <script> 
        Vue.component('comp', { 
            template:'<div @click="counter++">{{counter}}</div>',             data: {counter: 0} 
        }) 

        // 创建实例 
        const app = new Vue({ 
            el: '#demo', 
        }); 
    </script> 
</body> 

程序甚至无法通过vue检测:

image.png

结论:Vue 组件可能存在多个实例,如果使用对象形式定义 data,则会导致它们共用一个 data 对象,那么状态变更将会影响所有组件实例,这是不合理的;采用函数形式定义,在 initData 时会将其作为工厂函数,返回全新的 data 对象,有效规避多实例之间状态污染问题。而在 Vue 根实例创建过程中则不存在该限制,也是因为根实例只能有一个,不需要担心这种情况。

computedwatch 的区别

computed 是计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会重新进行计算。

  • 支持 getter 、 setter 。
  • 可以传入参数,返回一个函数进行计算即可。

watch 监听到值的变化就会回调,在回调中可以进行一些逻辑操作,可以进行异步操作。

  • 支持字符串形式的监听(对象中的属性),比如 '$route.params.type'
  • deep深度监听,发现对象内部值的变化
  • immediate立马调用

vue2 中为什么检测不到数组的变化,如何解决?

答案:由于 JavaScript 的限制,Vue 不能检测数组变动。vue2 中采取 Object.defineProperty 来实现数据响应式, Object.defineProperty 虽然可以监听到数组的变化,但是由于在性能和体验的性价比上考虑,vue2放弃了这个特性

当然在 vue2 中想实现数组的响应式可以通过全局 Vue.set 或者用实例方法 vm.$set 来修改。也可以通过数组的变异方法来实现( push 、 pop 、 shift 、 unshift 、 splice 、 sort 、 reverse )。

vue 生命周期

new Vue() -> 初始化事件 & 生命周期 -> beforeCreate -> 初始化依赖注入 & 响应性 -> created -> 判断是否有 “el” 选项 (有则继续判断是否有 “template” 选项,无则调用 app.$mount(el))-> 判断是否有 “template” 选项 (有则编译模板至渲染函数,无则编译 el 的 innerHTML 至模板)-> beforeMount -> 创建 app.$el 并添加至 el -> mounted -> 当数据发生变化,虚拟 DOM 重新渲染和更新 之前是 beforeUpdate,之后是 updated -> 当调用了 app.$destroy -> beforeDestroy -> 拆卸 观察器(watch)、子组件、事件监听器 -> destroyed

  • beforeCreate 创建前
    • 初始化事件 & 生命周期之后触发
    • 这个时期 this、data、methods、watcher 中的事件都不能获取到
  • created 创建
    • 初始化依赖注入 & 响应性之后触发
    • 可以操作 vue 实例中的数据和各种方法,但不能对 dom 节点进行操作(非要的话,加 $nextTick()
  • beforeMount 挂载前
    • 相关的 render 函数首次被调用
    • $el 处理
    • 子组件创建挂载
  • mounted 挂载
    • el 赋值
    • 所有子组件挂载完毕
    • 一些需要 dom 的操作,此刻才能执行
  • beforeUpdate 更新前
    • 数据发生改变,但此时视图尚未更新
  • updated 视图更新
    • 视图更新之后
  • beforeDestroy 销毁前
    • 实例上的事件、指令等还可以使用,组件还未真正被销毁
    • 调用 app.$destroy
  • destroyed 销毁
    • 数据、指令、 watch、子组件、事件监听等完全销毁

vue 生命周期

<div id="app">
    {{msg}}

    <!-- v-if 销毁与重建 -->
    <Foo v-if="isShow"></Foo>
</div>
// hook 钩子
const Foo = {
    template: `
        <div>Foo
            {{count}}
            <button @click="count++">click</button>
        </div>
    `,
    data() {
        return {
            count: 1,
        };
    },
    // 创建
    beforeCreate() {
        console.log("before - create");
        console.log(this.$el); // undefined
        console.log(this.$data); // undefined
    },
    created() {
        console.log("created");
        console.log(this.$el); // undefined
        console.log(this.$data); // ok
    },

    // 挂载
    beforeMount() {
        console.log("before- mount");

        // $el 开始处理。此时 $el 还没处理完,不能在这里获取
        console.log(this.$el); // 虚拟 DOM
        console.log(this.$data); // undefined
    },
    mounted() {
        console.log("mounted");
        
        // $el 处理完成,可以获取
        console.log(this.$el); // 真实 DOM
        
        // children 若有子组件,则此时所有子组件才完成挂载
    },

    // 更新
    beforeUpdate() {
        // 点击按钮,数据发生改变,但此时视图尚未更新,获取到的是更新前的数据
        console.log(this.$el);
        console.log("before - update");
    },

    updated() {
        // 视图更新,获取到的是更新后的数据
        console.log("updated");
        console.log(this.$el);
    },

    // 销毁
    beforeDestroy() {
        console.log("before - destroy");
    },
    destroyed() {
        // 清除 watch、清空子组件、处理事件监听之后
        console.log("destroyed");
    },
};

const app = new Vue({
    el: "#app",
    components: {
        Foo,
    },
    data: {
        msg: "hello ",
        isShow: true,
    },
});

生命周期钩子的 this 上下文指向调用它的 Vue 实例

不要在选项 property 或回调上使用箭头函数,比如 created: () => console.log(this.a)vm.$watch('a', newValue => this.myMethod())

因为箭头函数并没有 thisthis 会作为变量一直向上级词法作用域查找,直至找到为止,经常导致 Uncaught TypeError: Cannot read property of undefinedUncaught TypeError: this.myMethod is not a function 之类的错误。

多个组件调用顺序(面试点 )

调用顺序:

  • before - create
  • created
  • before- mount
    • bar - before - create
    • bar -created
    • bar -before- mount
    • bar -mounted
  • mounted

注意:父组件挂载之前要先完成其子组件的创建与挂载

vue 中组件之间的通信方式?

组件可以有以下几种关系:

image.png

A-B、B-C、B-D 都是父子关系,C-D 是兄弟关系,A-C、A-D 是隔代关系

组件通信方式共有以下8种:

  • props
  • $emit/$on
  • $parent/$children
  • $attrs/$listeners
  • provide/inject
  • ref
  • $root
  • evevtbus
  • vuex

根据组件之间关系讨论组件通信最为清晰:

  • 父子组件通信

    • props:父组件通过 props 向下传递数据给子组件(父 => 子)
    // 父组件
    <template>
        <div id="app">
            <Child name="una"></Child>
        </div>
    </template>
    
    <script>
    import Child from "./components/Child" //子组件
    export default {
        components: {
            Child
        }
    }
    </script>
    
    // 子组件
    <template>
        <div>{{name}}</div>
    </template>
    
    <script>
    export default {
        props:{
            name:{
                type: String,
                required: true // 必填
            }
        }
    }
    </script>
    
    • $emit/$on:子组件通过 events 给父组件发送消息,实际上就是子组件把自己的数据发送到父组件(子 => 父)。
    // 父组件
    <template>
        <div id="app">
            <Child name="una" @handle="handle"></Child>
        </div>
    </template>
    
    <script>
    import Child from "./components/Child" //子组件
    export default {
        components: {
            Child
        },
        methods: {
            handle(val) {
                console.log('handle', val)
            }
        }
    }
    </script>
    
    // 子组件
    <template>
        <div @click="handle">{{name}}</div>
    </template>
    
    <script>
    export default {
        props:{
            name:{
                type: String,
                required: true // 必填
            }
        },
        methods: {
            handle() {
                this.$emit('handle', '123')
            }
        }
    }
    </script>
    
    • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。得到组件实例后可以直接调用组件的方法或访问数据。
    // 父组件
    <template>
        <div id="app">
            <Child ref="ChildRef" name="una" ></Child>
            <button @click="getChildRef">获取 ChildRef</button>
        </div>
    </template>
    
    <script>
    import Child from "./components/Child" //子组件
    export default {
        components: {
            Child
        },
        methods: {
            getChildRef() {
                console.log(this.$refs.getChildRef.age) // 23
            }
        }
    }
    </script>
    
    // 子组件
    <template>
        <div>{{name}}</div>
    </template>
    
    <script>
    export default {
        // props: ['name'], // 这样直接获取也许,但是不能做校验
        props: {
            name:{
                type: String,
                required: true // 必填
            }
        },
        data () {
            return {
                age: 23
            }
        },
    }
    </script>
    
    • $parent/$children$parent:子 => 父实例 ,$children:父 -> 子实例,是个数组,通过索引获取。

    • $attrs/$listeners(vue 2.4)

      • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 interitAttrs 选项一起使用。

      • $listeners :包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

    // 父组件
    <template>
        <div id="app">
            <Child name="una" height="180cm" age="23" @handle="handle"></Child>
        </div>
    </template>
    
    <script>
    import Child from "./components/Child" //子组件
    export default {
        components: {
            Child
        },
        methods: {
            handle(val) {
                console.log('handle', val)
            }
        }
    }
    </script>
    
    // 子组件
    <template>
        <div @click="handle">{{name}}--{{$attrs}}</div>
        <!-- 属性值继续往下传递 -->
        <!-- <child-2 v-bind="$attrs"></child-2> -->
    </template>
    
    <script>
    export default {
        props:{
            name:{
                type: String,
                required: true // 必填
            }
        },
        created() {
            console.log(this.$attrs, this.$listeners) // 父组件中绑定的 非 Props 属性、非原生事件
        },
        methods: {
            handle() {
                this.$emit('handle', '123')
            }
        }
    }
    </script>
    
  • 兄弟组件通信

    • $parent

    • $root

    • eventbus:通过一个空的 vue 实例作为事件总线,用来触发事件和监听事件,从而实现任何组件间的通信。

      • bus.$emit
      • bus.$on
      • bus.$off
    • vuex

  • 跨层组件通信

    • eventbus

    • vuex

    • provide/inject:依赖注入。祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系

    // 父组件
    <template>
        <div id="app">
            <Child name="una" height="180cm" age="23"></Child>
        </div>
    </template>
    
    <script>
    import Child from "./components/Child" //子组件
    export default {
        provide: {
            message: '父组件的 provide' // /这种绑定是不可响应的
        },
        components: {
            Child
        },
    }
    </script>
    
    // 子组件
    <template>
        <div @click="handle">{{name}}--{{message}}</div>
    </template>
    
    <script>
    export default {
        inject: ['message'],
        props:{
            name:{
                type: String,
                required: true // 必填
            }
        },
    }
    </script>
    
    • provide 与 inject 怎么实现数据响应式

      • provide 祖先组件的实例,然后在子孙组件中注入依赖,这样就可以在子孙组件中直接修改祖先组件的实例的属性,不过这种方法有个缺点就是这个实例上挂载很多没有必要的东西比如 props, methods
      // 父组件
      provide() {
          return {
              theme: this // 提供祖先组件的实例。注:对象形式获取不到 this
          };
      },
      methods: {
          changeColor(color) {
              if (color) {
                  this.color = color;
              } else {
                  this.color = this.color === "blue" ? "red" : "blue";
              }
          }
      }
      
      // 子组件
      inject: {
          theme: {
              default: () => ({}) // 函数式组件取值不一样
          }
      }
      
      • 使用 Vue.observable(2.6)优化响应式 provide
      // 使用 Vue.observable 
      provide() {
          this.theme = Vue.observable({
              color: "blue"
          });
          return {
              theme: this.theme
          };
      },
      methods: {
          changeColor(color) {
              if (color) {
                  this.theme.color = color;
              } else {
                  this.theme.color = this.theme.color === "blue" ? "red" : "blue";
              }
          }
      }
      

组件接收数据 props 父 -> 子

prop:能够在组件实例中访问这个值,就像访问 data 中的值一样。

prop 命名:HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名

  • 数组

    props: ["title"]
    
    • 不能设置更多属性
  • 对象

    props: {
        type: {
            default: "info", // 默认值
            validator(val) { // 验证
                return ['success', 'warning', 'info', 'error'].indexOf(value) !== -1
            },
            type: String, // 类型检查
            required: true // 必填项
        },
        // 对象或数组的默认值必须从一个工厂函数获取
        userInfo: {
            type: Object,
            // 对象或数组默认值必须从一个工厂函数获取
            // default: () => {}
            default: () => {
                return { message: 'hello' }
            }
        }
    },
    
    • 可设置参数类型默认值,还能设置 validator 进行数据验证

    • prop在组件实例创建之前进行验证,所以实例的 property (如 datacomputed 等) 在 defaultvalidator 函数中是不可用的

    • prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告。

    • 类型检查的 type 可以是以下类型:

      • StringNumberBooleanArrayObjectDateFunctionSymbol
      • 自定义构造函数,通过 instanceof 来进行检查确认
      function Person (firstName, lastName) {
          this.firstName = firstName
          this.lastName = lastName
      }
      
      Vue.component('blog-post', {
          props: {
              author: Person // 判断 author 的值是否是通过 new Person 创建的
          }
      })
      

一个 prop 被注册之后,可以把数据作为一个自定义属性传递进组件:

<Foo title="a"></Foo>

<Foo :title="msg"></Foo>

使用:

<div id="app">
    {{msg}}
    <Foo title="a"></Foo>
</div>
const Foo = {
    /* 组件传入参数*/

    // 数组,不能设置更多
    // title -> 响应式对象
    // props: ["title"],

    // 对象
    props: {
        title: {
            default: "456", // 默认值
            validator(val) { // 验证
                console.log(val);
                // 组件可以接受的参数
                return val === "a" || val === "b";
            },
            type: String, // 类型检查
        },
    },

    computed: {
        titlePlus() {
            return this.title + "plus";
        },
    },
    template: `
        <div>Foo
            {{title}}
            {{titlePlus}}
            <button @click="getProps">get props</button>
        </div>
    `,
    methods: {
        getProps() {
            // console.log(this.$props); // {title: 'a'}
            // 直接挂载在实例上
            // console.log(this.title) // 'a'

            // 单向数据流,组件传入参数不可修改
            // props 是不可以被修改的
            this.$props.title = "bbbbbbbbbb"; // 报错:props 不能被修改
        },
    },
};

const app = new Vue({
    el: "#app",
    components: {
        Foo,
    },
    data: {
        msg: "hello ",
    },
});

单向数据流

防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解

若子组件能修改父组件传来的数据,相当于父组件的数据就被修改了。若有多个子组件,以及子组件的子组件,当某一个子组件修改了来自父组件的数据,父组件也被修改了,但是你不知道具体是哪个子组件修改的父组件。为了项目的可维护性考虑,于是 vue 采用了单向数据流,从源头解决此种问题。

Prop 的 属性

一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 prop 定义的 attribute。

组件可以接受任意的 attribute,而这些 attribute 会被添加到这个组件的根元素上。

  • 对于绝大多数 attribute 来说,从外部提供给组件的值会替换掉组件内部设置好的值。所以如果传入 type="text" 就会替换掉 type="date" 并把它破坏。
  • class 和 style 会被合并,从而得到最终的值。

禁用 Attribute 继承

不希望组件的根元素继承 attribute,可以在组件的选项中设置 inheritAttrs: false不会影响 style 和 class 的绑定)。

Vue.component('my-component', {
    inheritAttrs: false,
    // ...
})

这尤其适合配合实例的 $attrs 使用,包含了传递给一个组件的 attribute 名和 attribute 值,例如:

<base-input
    label="Username:"
    v-model="username"
    required
    placeholder="Enter your username"
></base-input>
<!-- 此时的 $attrs 包括属性:required、placeholder -->
<!-- $attrs: { required: true, placeholder: 'Enter your username' } -->

有了 inheritAttrs: false$attrs,你就可以手动决定这些 attribute 会被赋予哪个元素

Vue.component('base-input', {
    inheritAttrs: false,
    props: ['label', 'value'],
    template: `
        <label>
          {{ label }}
          <input
            v-bind="$attrs"
            :value="value"
            @input="$emit('input', $event.target.value)"
          >
        </label>
    `
})

mixin 和 mixins 区别

mixin 用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。

Vue.mixin({
    beforeCreate (){
        // ..逻辑
        // 这种方式会影响到每个组件的 beforeCreate 钩子函数
    }
})

虽然文档不建议我们在应用中直接使用 mixin,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的 ajax 或者一些工具函数等等。

mixins 应该是我们最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins 混入代码,比如上拉下拉加载数据这种逻辑等等。

// mixin.js
export default {
    mounted () {
        console.log('mixin 里的 mounted')
    }
}

// 组件
import mixin from './mixin.js'
export default {
    mixins: [mixin]
}
// 'mixin 里的 mounted'

mixin 的设计初衷:js extends 无法实现多重继承,通过混入来实现多重继承

class Yase {
    constructor() {
        this.name = '亚瑟'
    }
}

class Skills {
    release() {
        console.log('释放技能')
    }
    hurt() {
        console.log('造成伤害')
    }
    run() {
        console.log('跑')
    }
}

// 混入模式
function mixin(receivingClass, givingClass) {
    // 是否有要混入的方法
    if(typeof arguments[2] !== 'undefined'){
        for(let i = 2; i < arguments.length; i++){
            receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]]
        }
    }
}

mixin(Yase, Skills, 'release', 'run') // 若还有其他类,一个个混
let newYase = new Yase()
newYase.release() // '释放技能'

v - model 该如何实现

答案: v-model 本质上是 v-on 和 v-bind 的语法糖。 v-model 在内部为不同元素抛出不同的事件,如:text 和 textarea 元素使用 value 属性和 input 事件;checkbox 和 radio 使用 checked 属性和 change 事件;select 字段将 value 作为 prop 并将 change 作为事件。

  • v-model 作用在普通表单上
<input v-model="myvalue" />
// 等同于
<input put 
    v-bind:value="myvalue"
    v-on:inpute="myvalue = $event.target.value"
/>
  • v-model 作用在组件

父组件 v-model 语法糖本质上可以修改为<child :value="message" @input="function (e) {message = e }"></ child>在组件的实现中,我们是可以通过 v-model 属性来配置子组件接收的 prop 名称,以及派发的事件名称。例如:

父组件

// mycom 组件。emit 的事件名可通过 model.event 修改
<input :value="myvalue" @input="$emit('input', $event.target.value)"></input>

<mycom v-model="myvalue"></mycom>
// 等同于
<mycom :value="myvalue" @input="(e) => { myvalue = e }"></mycom>

new Vue ({
    el:"#app", 
    components :{},
    data: {
        myvalue: '数据'
    }
})

组件上的 v-model

一个组件上的 v-model 默认会利用名为 valueprop 和名为 input 的事件。

为了 v-model 在组件上正常工作,必须:

  • 将其 value 属性绑定到一个名叫 valueprop
  • 在其 input 事件被触发时,将新的值通过自定义的 input 事件抛出
const Foo = {
    props: ["value"], // 默认属性名
    template: `
        <div v-show="visible">
            Foo
            <button @click="handleClose">X</button>
        </div>
    `,
    methods: {
        handleClose() {
            // 去修改 props 的值
            // v-model -> input
            this.$emit("input", false); // 默认事件名 input
        },
    },
};

但是像单选框、复选框等类型的输入控件可能会将 value 用于不同的目的。可以通过组件的 model 修改 v-model 对应的属性名与事件名,用来避免这样的冲突:

const Foo = {
    // props: ["value"], // 默认属性名
    props: ["visible"], // 修改后的属性名 visible
    // 通过 model 修改 v-model 的 参数名与事件名
    model: {
        prop: "visible", // 修改属性名
        event: "close", // 修改事件名
    },

    template: `
        <div v-show="visible">
            Foo
            <button @click="handleClose">X</button>
        </div>
    `,
    methods: {
        handleClose() {
            // 去修改 props 的值
            // v-model -> input
            // this.$emit("input", false); // 默认事件名 input
            this.$emit("close", false); // 修改后的事件名 close
        },
    },
};

多个v-model .sync

update:myPropName 的模式触发事件(伪“双向绑定”)。

// text-document 组件
this.$emit('update:title', newTitle)
<!-- 父组件 -->
<text-document
    :title="doc.title"
    @update:title="doc.title = $event"
></text-document>

<!-- 缩写 `.sync` 修饰符 -->
<text-document :title.sync="doc.title"></text-document>

v-on可以绑定多个方法吗

如果绑定多个事件,可以用键值对的形式 事件类型∶事件名

如果绑定是多个相同事件,直接用逗号分隔就行

<p v-on="{click:dbClick,mousemove:MouseClick}">v-on绑定多个方法</p>

<p @click="one(),two()">一个事件绑定多个函数</p>

keep-alive 组件有什么作用?

概念:keep-alive是vue 的内置组件,当它动态包裹组件时,会缓存不活动的组件实例,它自身不会渲染成一个DOM元素也不会出现在父组件中。

作用:在组件切换过程中将状态保留在内存中,防止重复渲染 DOM,减少加载时间以及性能消耗,提高用户体验

生命周期函数:activated 在 keep-alive 组件激活时调用,deactivated 在 keep-alive 组件停用时调用

如果你需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组件包裹需要保存的组件。

  • include 属性:被缓存
  • exclude 属性:不会被缓存
<keep-alive include="bookLists,bookLists">
    <router-view></router-view>
</keep-alive>

<keep-alive exclude="indexLists">
    <router-view></router-view>
</keep-alive>

也可以在设置路由时 设置 meta: { keepAlive:true /* 是否需要被缓存 */ }

<keep-alive>
    <!--这里是会被缓存的组件-->
    <router-view v-if="$route.meat.keepAlive"></router-view>
</keep-alive>

<!--这里是不会被缓存的组件-->
<router-view v-if="!$route.meta.keepAlive"></router-view>

对于 keep-alive 组件来说,它拥有两个独有的生命周期钧子函数,分别为 activated 和 deactivated 。keep-alive 包裹的组件在切换时不会进行销毁,而是 缓存到内存中 并执行 deactivated (销毁)钩子函数,命中缓存渲染后会执行 actived (从缓存中获取)钧子函数

自定义指令

钩子函数

vue2:

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update所在组件的 VNode 更新时调用但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
  • componentUpdated指令所在组件的 VNode 及其子 VNode 全部更新后调用
  • unbind:只调用一次,指令与元素解绑时调用

vue3:

  • created:在绑定元素的 attribute 或事件监听器被应用之前调用。在指令需要附加须要在普通的 v-on 事件监听器前调用的事件监听器时,这很有用。
  • beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用。
  • mounted:在绑定元素的父组件被挂载后调用。
  • beforeUpdate:在更新包含组件的 VNode 之前调用。
  • updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用。
  • beforeUnmount:在卸载绑定元素的父组件之前调用
  • unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次。

钩子函数参数

  • el:指令所绑定的元素,可以用来直接操作 DOM

  • binding:一个对象,包含以下 property:

    • name指令名,不包括 v- 前缀。
    • value指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
  • vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。

  • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。

vue-router

Vue 路由的实现

前端路由就是更新视图但不请求页面,利用锚点完成切换,页面不会刷新。

定义路由组件

定义路由,使用component进行路由映射组件,用 name导航到对应路由

创建router 实例,传入 routes 配置

创建和挂载根实例

用 router-link 设置路由跳转

Vue 路由模式 hash 和 history,简单讲一下

Hash模式地址栏中有#,history 没有

history 模式下刷新,会出现 404情况,需要后台配置

使用 JavaScript 来对loaction.hash进行赋值,改变 URL 的 hash 值

可以使用 hashchange 事件来监听 hash 值的变化

HTML5 提供了 History API来实现 URL 的变化。其中最主要的 API 有以下两个∶ history.pushState()和 history.repalceState()。这两个API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。

Vue 路由传参的两种方式,params和 query方式与区别

动态路由也可以叫路由传参,就是根据不同的选择在同一个组件渲染不同的内容

用法上∶query 用path引入,params 用 name引入,接收参数都是类似的,分别是 route.query.nameroute.params.name

url展示上∶params类似于post,query 类似于 get,也就是安全问题,params传值相对更安全点,query通过 url 传参,刷新页面还在,params 刷新页面不在了

route 与 router 区别

router 是VueRouter 的一个对象,通过 Vue.use(VueRouter)和VueRouter 构造函数得到一个router 的实例对象,这个对象中是一个全局的对象,他包含了所有的路由包含了许多关键的对象和属性。

route是一个跳转的路由对象,每一个路由都会有一个 route 对象, 是一个局部的对象,可以获取对应的 name,path,params,query 等

钩子函数

钩子函数参数:

  • to: 即将要进入的路由

  • from: 当前导航正要离开的路由

  • next: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。

    • next() : 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
    • next(false) : 中断当前的导航URL 地址会重置到 from 路由对应的地址
    • next('/') 或者 next({ path: '/' }) : 跳转到一个不同的地址
    • next(error) : (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调

钩子函数:

  • 全局前置守卫 router.beforeEach :当一个导航触发时,全局前置守卫按照创建顺序调用
const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  // ...
})
  • 全局解析守卫 router.beforeResolve:和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。

  • 全局后置钩子 router.afterEach不会接受 next 函数也不会改变导航本身

router.afterEach((to, from) => {
  // ...
})
  • 路由独享守卫 beforeEnter
const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})
  • 组件内的守卫

    • beforeRouteEnter 路由改变,组件实例创建前。不能获取组件实例 this
    • beforeRouteUpdate 路由改变,该组件被复用
    • beforeRouteLeave 路由改变,离开该组件。通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。
const Foo = {
  template: `...`,
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
    
    // 在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数
    next(vm => {
        // 通过 `vm` 访问组件实例
    })
  },
  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave(to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
    
    
    const answer = window.confirm('你真的想离开吗?您有未保存的更改!')
    if (answer) {
      next() // 确定离开
    } else {
      next(false) // 不离开
    }
  }
}

完整的导航解析流程

  1. 导航被触发
  2. 在失活的组件里调用 beforeRouteLeave 守卫。(离开组件
  3. 调用全局的 beforeEach 守卫。(进入路由前
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 。(重用组件,如果有的话
  5. 在路由配置里调用 beforeEnter。(路由独享
  6. 解析异步路由组件
  7. 在被激活的组件里调用 beforeRouteEnter。(组件实例创建前
  8. 调用全局的 beforeResolve 守卫 。(组件内守卫及异步路由组件解析后,导航确认前
  9. 导航被确认
  10. 触发 DOM 更新
  11. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

vue-router 中如何保护指定路由的安全?

此题是考查项目实践能力,项目中基本都有路由守卫的需求,保护指定路由考查的就是这个知识点。

答题整体思路:

  1. 阐述 vue-router 中路由保护策略
  2. 描述具体实现方式
  3. 简单说一下它们是怎么生效的

回答范例:

  1. vue-router 中保护路由安全通常使用导航守卫来做,通过设置路由导航钩子函数的方式添加守卫函数,在里面判断用户的登录状态和权限,从而达到保护指定路由的目的

  2. 具体实现有几个层级:全局前置守卫 beforeEach 、路由独享守卫 beforeEnter 或 组件内守卫 beforeRouteEnter(只有它能获得 vue 实例)。以全局守卫为例来说,可以使 用 router.beforeEach (( to , from , next ) => {}) 方式设置守卫,每次路由导航时,都会执行该守卫,从而检查当前用户是否可以继续导航,通过给 next 函数传递多种参数达到不同的目的,比如如果禁止用户继续导航可以传递 next(false),正常放行可以不传递参数,传递 path 字符串可以重定向到一个新的地址等等。

  3. 这些钩子函数之所以能够生效,也与 vue-router 工作方式有关,像 beforeEach 只是注册一个 hook(钩子),这些钩子函数之所以能够生效,也和 vue-router 工作方式有关,像 beforeEach 只是注册一个 hook,当路由发生变化,router 准备导航之前会批量执行这些 hooks,并且把目标路由 to,当前路由 from ,以及后续处理函数 next 传递给我们设置的 hook 。

可能的追问:

  1. 能不能说说全局守卫、路由独享守卫和组件内守卫区别?
  • 作用范围
  • 组件实例的获取
beforeRouteEnter(to, from, next) {
    next(vm => {

    })
}
  • 名称/数量/顺序

    • 导航被触发。
    • 在失活的组件里调用离开守卫。
    • 调用全局的 beforeEach 守卫。
    • 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
    • 在路由配置里调用 beforeEnter 。
    • 解析异步路由组件。
    • 在被激活的组件里调用 beforeRouteEnter 。
    • 调用全局的 befoeResolve 守卫(2.5+)。
    • 导航被确认。
    • 调用全局的 afterEach 钩子。
    • 触发 DOM 更新。
    • 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。
  1. 你项目中的路由守卫是怎么做的?
  2. 前后端路由一样吗?
  3. 前端路由是用什么方式实现的?
  4. 你前面提到的 next 方法是怎么实现的?

vuex

简单说一说 vuex 使用及其理解?

此题考查基本能力,能说出用法只能60分。更重要的是对 vuex 设计理念和实现原理的解读。

image.png

回答策略:

  • 首先给 vuex 下一个定义
  • vuex解决了哪些问题,解读理念
  • 什么时候我们需要 vuex
  • 你的具体用法
  • 简述原理,提升层级

官网定义:

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种 可预测(Mutation) 的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension (opens new window),提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。

回答范例:

  1. vuex 是 vue 专用的状态管理库。它以全局方式集中管理应用的状态,并且可以保证状态变更的可预测性

  2. vuex 主要解决的问题是多组件之间状态共享的问题,利用各种组件通信方式,我们虽然能够做到状态共享,但是往往需要在多个组件之间保持状态的一致性,这种模式很容易出现问题,也会使程序逻辑变得复杂。 vuex 通过把组件的共享状态抽取出来,以全局单例模式管理,这样任何组件都能用一致的方式获取和修改状态,响应式的数据也能够保证简洁的单向数据流动,我们的代码将变得更结构化且易维护。

  3. vuex 并非必须的,它帮我们管理共享状态,但却带来更多的概念和框架。如果我们不打算开发大型单页应用或者我们的应用并没有大量全局的状态需要维护,完全没有使用 vuex 的必要。一个简单的 store 模式就足够了。反之, Vuex 将会成为自然而然的选择。引用 Redux 的作者 Dan Abramov 的话说就是:Flux 架构就像眼镜:您自会知道什么时候需要它。

  4. 我在使用 vuex 过程中有如下理解:首先是对核心概念的理解和运用,将全局状态放入 state 对象中,它本身是一棵状态树,组件中使用 store 实例的 state 访问这些状态;然后有配套的 mutation 方法修改这些状态,并且只能用 mutation 修改状态,在组件中调用 commit 方法提交 mutation;如果应用中有异步操作或者复杂逻辑组合,我们需要编写 action,执行结束如果有状态修改仍然需要提交 mutation,组件中调用这些 action 使用 dispatch 方法派发。最后是模块化,通过 modules 选项组织拆分出去的各个子模块,在访问状态时注意添加子模块的名称如果子模块有设置 namespace,那么在提交 mutation 和派发 action 时还需要额外的命名空间前缀

  5. vuex 在实现单项数据流时需要做到数据的响应式,通过源码的学习发现是借用了 vue 的数据响应化特性实现的,它会利用 Vue 将 state 作为 data 对其进行响应化处理,从而使得这些状态发生变化时,能够导致组件重新渲染。

vue 原理

你知道 nextTick 吗?它是干什么的?实现原理是什么?

这道题考查大家对 vue 异步更新队列的理解,有一定深度,如果能够很好回答此题,对面试效果有极大帮助。

答题思路:

  1. nextTick 是啥?下一个定义
  2. 为什么需要它呢?用异步更新队列实现原理解释
  3. 我再什么地方用它呢?抓抓头,想想你在平时开发中使用它的地方
  4. 下面介绍一下如何使用 nextTick
  5. 最后能说出源码实现就会显得你格外优秀

nextTick 官方文档的定义:

Vue.nextTick([callback, context])

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM

// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () { 
    // DOM 更新了
})

回答范例:

  1. nextTick 是 Vue 提供的一个全局 API,由于 vue 的异步更新策略导致我们对数据的修改不会立刻体现在 dom 变化上,此时如果想要立即获取更新后的 dom 状态,就需要使用这个方法。

  2. Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。 nextTick 方法会在队列中加入一个回调函数,确保该函数在前面的 dom 操作完成后才调用。

  3. 所以当我们想在修改数据后立即看到 dom 执行结果就需要用到 nextTick 方法。

  4. 比如我在干什么的时候就会使用 nextTick ,传一个回调函数进去,在里面执行 dom 操作即可。

  5. 我也有简单了解 nextTick 实现,它会在 callbacks 里面加入我们传入的函数,然后用 timerFunc 异步方式调用它们,首选的异步方式会是 Promise 。这让我明白了为什么可以在 nextTick 中看到 dom 操作结果。

谈一谈你对 vue 响应式原理的理解?

烂大街的问题,但却不是每个人都能回答到位。因为如果你只是看看别人写的网文,通常没什么底气,也经不住面试官推敲,但像我们这样即看过源码还造过轮子的,回答这个问题就会比较有底气。

答题思路:

  1. 啥是响应式?
  2. 为什么 vue 需要响应式?
  3. 它能给我们带来什么好处?
  4. vue 的响应式是怎么实现的?有哪些优缺点?
  5. vue3 中的响应式的新变化?

回答范例:

  1. 所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制。

  2. mvvm 框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理。

  3. 以 vue 为例说明,通过数据响应式加上虚拟 DOM 和 patch 算法,可以使我们只需要操作数据,完全不用接触繁琐的 dom 操作,从而大大提升开发效率,降低开发难度。

  4. vue2 中的数据响应式会根据数据类型来做不同处理,如果是对象则采用 Object .defineProperty() 的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是数组则通过覆盖该数组原型的方法,扩展它的7个变更方法,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失新增或删除属性时需要用户使用 Vue.set / delete 这样特殊的 api 才能生效;对于es6中新产生的 Map 、 Set 这些数据结构不支持等问题。

  5. 为了解决这些问题,vue3 重新编写了这一部分的实现:利用ES6的 Proxy 机制代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊 api,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的 reactivity 包,使得我们可以更灵活的使用它,我们甚至不需要引入 Vue 都可以体验。

vue 中 key 的作用和工作原理

源码中找答案:src\core\vdom\patch.js - updateChildren()  

测试代码如下 

<body> 
    <div id="demo"> 
        <p v-for="item in items" :key="item">{{item}}</p>     
    </div> 
    <script src="../../dist/vue.js"></script> 
    <script> 
        // 创建实例 
        const app = new Vue({ 
            el: '#demo', 
            data: { items: ['a', 'b', 'c', 'd', 'e'] }, 
            mounted () { 
                setTimeout(() => { 
                    this.items.splice(2, 0, 'f') 
                }, 2000); 
            }, 
        }); 
    </script> 
</body> 

上面案例重现的是以下过程:

image.png

不使用 key,由于没有 key 不知道要更新谁,于是见谁更新谁,前两个没变不更新,到第三个时需要更新,最后再生成一个新的 dom (但这个dom 是之前就渲染过的 F)。更新了5次(遍历节点),实际执行了3次更新操作和1次创建插入操作

image.png

如果使用 key ,前两个没变不更新,到第三个时需要更新,由于首尾假猜?策略,第三个更新时结尾的E 相同,于是第三个更新的是 E。以此类推。虽然更新了5次,但是实际没有执行更新操作,而是只执行了1次创建插入操作

// 首次循环patch A
A B C D E 
A B F C D E 

// 第2次循环patch B
B C D E 
B F C D E 

// 第3次循环patch E
C D E 
F C D E 

// 第4次循环patch D
C D 
F C D 

// 第5次循环patch C
C  
F C 

// oldCh 全部处理结束,newCh 中剩下的 F,创建 F 并插入到 C 前面 

结论:

  1. key 的作用主要是为了高效的更新虚拟 DOM,其原理是 vue 在 patch 过程中通过 key 可以精准判断两个节点是否是同一个(通过一系列内部算法,比如猜测首尾结构的相似性),从而避免频繁 更新 不同元素,使得整个 patch 过程更加高效,减少 DOM 操作量,提高性能。 

  2. 另外,若不设置 key 还可能在列表更新时引发一些隐蔽的 bug 

  3. vue 中在使用相同标签名元素的过渡切换时,也会使用到 key 属性,其目的也是为了让 vue 可以区分它们,否则 vue 只会替换其内部属性而不会触发过渡效果。

你怎么理解 vue 中的 diff 算法?

虚拟DOM也就是常说的虚拟节点,它是通过js的object对象模拟 DOM 中的节点,然后再通过特定的渲染方法将其渲染成真实的 DOM节点。频繁的操作 DOM,或大量造成页面的重绘和回流。

虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff算法避免了没有必要的 dom 操作,从而提高性能

Diff 算法∶把树形结构按照层级分解,只比较同级元素,给列表结构的每个单元添加唯一的 key 值,方便比较。

凡是涉及虚拟 dom 的,多半要使用 diff 算法

image.png

源码分析 1:必要性,lifecycle.js - mountComponent()

一个组件执行一次 $mount() 就会创建一个 watcher ,但组件中可能存在很多个 data 中的 key 使用。当页面中多个 key 发生变化时则执行一次 diff 算法,进行新旧2个 dom 的比较,就可以知道变化的地方为了精确的知道在更新时谁发生了变化,我们必须使用 diff 算法

源码分析 2:执行方式,patch.js - patchVnode()

patchVnode 是 diff 发生的地方,整体策略:深度优先(先比较子节点),同层比较

源码分析 3:高效性,patch.js - updateChildren()

测试代码

<body>
    <div id="demo">
        <h1>虚拟 DOM</h1>
        <p>{{foo}}</p>
    </div>

    <script>
    // 创建实例
    const app = new Vue({
        el: '#demo',
        data: { foo: 'foo' },
        mounted() {
            setTimeout(() => {
                this.foo = 'fooooo'
            }, 1000);
        }
    });
    </script>
</body>

总结

  1. diff 算法是虚拟 DOM 技术的必然产物:通过新旧虚拟 DOM 作对比(即 diff),将变化的地方更新在真实 DOM 上;另外,也需要 diff 高效的执行对比过程,从而降低时间复杂度为 O(n)

  2. vue 2.x 中为了降低 Watcher 粒度,每个组件只有一个 Watcher 与之对应,只有引入 diff 才能精确找到发生变化的地方

  3. vue 中 diff 执行的时刻是组件实例执行其更新函数时,它会比对上一次渲染结果 oldVnode 和新的渲染结果 newVnode,此过程称为 patch。

  4. diff 过程整体遵循深度优先、同层比较的策略;两个节点之间比较会根据它们是否拥有子节点或者文本节点做不同操作;比较两组子节点是算法的重点,首先假设头尾节点可能相同做 4 次比对尝试,如果没有找到相同节点才按照通用方式遍历查找,查找结束再按情况处理剩下的节点;借助 key 通常可以非常精确找到相同节点,因此整个 patch 过程非常高效。

谈一谈对 vue 组件化的理解?

回答总体思路:

组件化定义、优点、使用场景和注意事项等方面展开陈述,同时要强调 vue 中组件化的一些特点。

源码分析 1:组件定义

// 全局组件定义
Vue.component('comp', {
    template: '<div>this is a component</div>' 
})

组件定义,src\core\global-api\assets.js

<!-- 单文件组件:组件配置对象 -->
<template>
    <div>this is a component</div>
</template>

vue-loader 会编译 template 为 render 函数,最终导出的依然是组件配置对象

源码分析 2:组件化优点

lifecycle.js - mountComponent()

组件、Watcher、渲染函数和更新函数之间的关系

源码分析 3:组件化实现

构造函数,src\core\global-api\extend.js

实例化及挂载,src\core\vdom\patch.js - createElm()

总结

  1. 组件是独立和可复用的代码组织单元。组件系统是 Vue 核心特性之一,它使开发者使用小型、独立和通常可复用的组件构建大型应用;

  2. 组件化开发能大幅提高应用开发效率、测试性、复用性等;

  3. 组件使用按分类有:页面组件、业务组件、通用组件;

  4. vue 的组件是基于配置的,我们通常编写的组件是组件配置而非组件,框架后续会生成其构造函数,它们基于 VueComponent,扩展于 Vue

  5. vue 中常见组件化技术有:属性 prop,自定义事件,插槽等,它们主要用于组件通信、扩展等;

  6. 合理的划分组件,有助于提升应用性能;

  7. 组件应该是高内聚、低耦合的;

  8. 遵循单向数据流的原则。

谈一谈对 vue 设计原则的理解?

在 vue 的官网上写着大大的定义和特点:

  • 渐进式 JavaScript 框架
  • 易用、灵活和高效

所以阐述此题的整体思路按照这个展开即可。

渐进式 JavaScript 框架:

与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。

image.png

  • 易用性

vue 提供数据响应式、声明式模板语法和基于配置的组件系统等核心特性。这些使我们只需要关注应用的核心业务即可,只要会写 js、html 和 css 就能轻松编写 vue 应用。

  • 灵活性

渐进式框架的最大优点就是灵活性,如果应用足够小,我们可能仅需要 vue 核心特性即可完成功能;随着应用规模不断扩大,我们才可能逐渐引入路由、状态管理、vue-cli 等库和工具,不管是应用体积还是学习难度都是一个逐渐增加的平和曲线。

  • 高效性

超快的虚拟 DOM 和 diff 算法使我们的应用拥有最佳的性能表现

追求高效的过程还在继续,vue3 中引入 Proxy 对数据响应式改进以及编译器中对于静态内容编译的改进都会让 vue 更加高效。

vue 性能优化

1、Vue 首屏加载慢的原因,怎么解决,白屏时间怎么检测,怎么解决白屏问题

首屏加载慢的原因:第一次加载页面有很多组件数据需要渲染

解决方法∶

  • 路由懒加载 component: () => import("路由地址")
  • ui 框架按需加载
  • gzip压缩白屏时间检测

解决白屏问题

  • 使用v-text渲染数据
  • 使用{{}}语法渲染数据,但是同时使用v-cloak指令(防止页面加载时出现 vuejs 的变量名。用来保持在元素上直到关联实例结束时候进行编译),v-cloak 要放在什么位置呢,v-cloak并不需要添加到每个标签,只要在 el挂载的标签上添加就可以
<div v-cloak>{{ message }}</div>

[v-cloak] {
    display:none !important;
}

你了解哪些 Vue 性能优化方法?

image.png

路由懒加载

const router = new VueRouter({
    routes: [
        { 
            path: '/foo',
            component: () => import('./Foo.vue')
        }
    ]
})

keep-alive 缓存页面

<template>
    <div id="app">
        <keep-alive>
            <router-view/>
        </keep-alive>
    </div>
</template>

使用 v-show 复用 DOM

<template>
    <div class="cell">
        <!--这种情况用 v-show 复用 DOM,比 v-if 效果好-->
        <div v-show="value" class="on">
            <Heavy :n="10000"/>
        </div>
        <section v-show="!value" class="off">
            <Heavy :n="10000"/>
        </section>
    </div>
</template>

v-for 遍历避免同时使用 v-if

<template>
    <ul>
        <li v-for="user in activeUsers" :key="user.id">{{ user.name }}</li>
    </ul>
</template>

<script>
export default {
    computed: {
        activeUsers: function () {
            return this.users.filter(function (user) {
                return user.isActive
            })
        }
    }
}
</script>

长列表性能优化

  • 如果列表是纯粹的数据展示,不会有任何改变,就不需要做响应化
export default {
    data: () => ({
        users:[]
    }),
    async created(){
        const users = await axios.get("/api/users");
        this.users = Object.freeze(users); // 冻结
    }
};
  • 如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容

1、html 标签

// :size='30' 表示行高30px :remain='6'表示页面只每次渲染6条
<virtual-list :size="40" :remain="8">
    <div v-for="item of items" :key="item.id">{{ item }}</div>
</virtual-list>

2、子组件

<template>
    <div>{{ index }} - {{ source.name }}</div> 
</template> 

<script> 
export default { 
    name: "item-component", 
    props: { 
        index: {
            type: Number
        }, 
        source: {
            type: Object, 
            default() { 
                return {}; 
            }, 
        }, 
    }, 
}; 
</script>
<virtual-list 
    style="height: 360px; overflow-y: auto;" 
    :data-key="'id'" 
    :data-sources="items" 
    :data-component="itemComponent" 
/>

import virtualList from "vue-virtual-scroll-list"; 
import Item from "./Item"; 

export default { 
    components: { 
        "virtual-list": virtualList 
    }, 
    props: { msg: String, }, 
    data() { 
        return { 
            itemComponent: Item, 
            items: []
        }
    }, 
    created() { 
        this.dataSource(); 
    },
    methods: { 
        dataSource() { 
            for (let i = 1; i <= 900000; i++) { 
                this.items.push({ id: i, name: i + "模拟字段", }); 
            }
        }
    }
}

参考 vue-virtual-scroller、vue-virtual-scroll-list

事件的销毁

Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件

created() {
    this.timer = setInterval(this.refresh,2000) 
},

beforeDestroy() {
    clearInterval(this.timer)
}

图片懒加载

对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载。

<img v-lazy="/static/img/1.png">

参考项目:vue-lazyload

第三方插件按需引入

像 element-ui 这样的第三方组件库可以按需引入避免体积太大。

import Vue from 'vue';
import { Button, Select } from 'element-ui';
Vue.use(Button)
Vue.use(Select)

无状态的组件标记为函数式组件(即仅基于父组件传递过来的数据进行显示,没有其余操作)

特点:

  • 无状态
  • 无法实例化
  • 内部没有任何生命周期处理函数
  • 轻量,渲染性能高,适合只依赖于外部数据传递而变化的组件(展示组件,无逻辑和状态修改)
  • template标签里标明functional
  • 只接受props值
  • 不需要script标签

优点:

  • 渲染开销低,因为函数式组件只是函数
<template functional>
    <div class="cell">
        <div v-if="props.value" class="on"></div>
        <section v-else class="off"></section>
    </div>
</template>

<script>
export default {
    props: ['value']
}
</script>

子组件分割

<template>
    <div>
        <ChildComp/>
    </div>
</template>

<script>
export default {
    components: {
        ChildComp: {
            methods: {
                heavy () { /* 耗时任务 */ }
            },
            render (h) {
                return h('div', this.heavy())
            }
        }
    }
}
</script>

变量本地化,不要频繁引用 this.xxx

<template>
    <div :style="{ opacity: start / 300 }">{{ result }}</div>
</template>

<script>
import { heavy } from '@/utils'
export default {
    props: ['start'],
    computed: {
        base () { return 42 },
        result () {
            const base = this.base // 不要频繁引用 this.base
            let result = this.start
            for (let i = 0; i < 1000; i++) {
                result+= heavy(base)
            }
            return result
        }
    }
}
</script>

vue3.0

为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

Object.defineProperty 无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;

Object.defineProperty 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy 可以劫持整个对象,并返回一个新的对象。

Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

你对 Vue3.0 的新特性有没有了解?

根据尤大的 PPT 总结,Vue3.0 改进主要在以下几点:

  • 更快

    • 虚拟 DOM 重写(更精细)
    • 优化 slots 的生成(减少一些不必要的重新渲染)
    • 静态树提升(能不重新渲染就不重新渲染)
    • 静态属性提升(同上)
    • 基于 Proxy 的响应式系统
  • 更小:通过摇树优化核心库体积

  • 更容易维护:TypeScript + 模块化

  • 更加友好

    • 跨平台:编译器核心和运行时核心与平台无关,使得 Vue 更容易与任何平台(Web、Android、iOS)一起使用
  • 更容易使用

    • 改进的 TypeScript 支持,编辑器能提供强有力的类型检查和错误及警告
    • 更好的调试支持
    • 独立的响应化模块
    • Composition API

虚拟 DOM 重写

期待更多的编译时提示来减少运行时开销,使用更有效的代码来创建虚拟节点。

组件快速路径+单个调用+子节点类型检测

  • 跳过不必要的条件分支
  • JS引擎更容易优化

image.png

优化 slots 生成

vue3 中可以单独重新渲染父级和子级

  • 确保实例正确的跟踪依赖关系
  • 避免不必要的父子组件重新渲染

image.png

静态树提升(Static Tree Hoisting)

使用静态树提升,这意味着 Vue 3 的编译器将能够检测到什么是静态的,然后将其提升,从而降低了渲染成本(内存换时间)。

  • 跳过修补整棵树,从而降低渲染成本
  • 即使多次出现也能正常工作

image.png

静态属性提升

使用静态属性提升,Vue 3 打补丁时将跳过这些属性不会改变的节点

image.png

基于 Proxy 的数据响应式

Vue 2 的响应式系统使用 Object.defineProperty 的 getter 和 setter。Vue 3 将使用 ES2015 Proxy 作为其观察机制,这将会带来如下变化:

  • 组件实例初始化的速度提高 100%
  • 使用 Proxy 节省以前一半的内存开销,加快速度,但是存在低浏览器版本的不兼容
  • 为了继续支持 IE11,Vue 3 将发布一个支持旧观察者机制和新 Proxy 版本的构建

image.png

高可维护性

Vue 3 将带来更可维护的源代码。它不仅会使用 TypeScript,而且许多包被解耦,更加模块化。

image.png

扩展

单页面应用和多页面应用区别及优缺点

  • 单页面应用( SPA )
    • 优点
      • 用户体验好,快,内容的改变不需要重新加载整个页面,基于这一点 spa 对服务器压力较小
      • 前后端分离
      • 页面转场体验好
    • 缺点:
      • 不利于 seo
      • 导航不可用,需要自己实现导航
      • 初次加载耗时长
      • 页面复杂度提高
  • 多页面应用( MPA )
    • 优点:
      • 多页面应用对于 seo 更加友好
      • 更容易扩展
      • 更易的数据分析
    • 缺点:
      • 程序开发成本高
      • 增加服务端压力,多页面会不停的加载
      • 用户体验相对较差

谈谈你对 MVC、MVP 和 MVVM 的理解?

答题思路:此题涉及知识点很多,很难说清、说透,因为 mvc、mvp 这些我们前端程序员自己甚至都没用过。但是恰恰反映了前端这些年从无到有,从有到优的变迁过程,因此沿此思路回答将十分清楚。

Web1.0 时代

在 web1.0 时代,并没有前端的概念。开发一个 web 应用多数采用 ASP.NET/Java/PHP 编写,项目通常由多个 aspx/jsp/php 文件构成,每个文件中同时包含了 HTML、CSS、JavaScript、C#/Java/PHP 代码,系统整体架构可能是这样子的:

image.png

这种架构的好处是简单快捷,但是,缺点也非常明显:JSP 代码难以维护

为了让开发更加便捷,代码更易维护,前后端职责更清晰。便衍生出 MVC 开发模式和框架,前端展示以模板的形式出现。典型的框架就是 Spring、Structs、Hibernate。整体框架如图所示:

image.png

使用这种分层架构,职责清晰,代码易维护。但这里的 MVC 仅限于后端,前后端形成了一定的分离,前端只完成了后端开发中的 view 层

但是,同样的这种模式存在着一些问题:

  1. 前端页面开发效率不高
  2. 前后端职责不清

web 2.0 时代

自从 Gmail 的出现,ajax 技术开始风靡全球。有了 ajax 之后,前后端的职责就更加清晰了。因为前端可以通过 Ajax 与后端进行数据交互,因此,整体的架构图也变化成了下面这幅图:

image.png

通过 ajax 与后台服务器进行数据交换,前端开发人员,只需要开发页面这部分内容,数据可由后台进行提供。而且 ajax 可以使得页面实现部分刷新,减少了服务端负载和流量消耗,用户体验也更佳。这时,才开始有专职的前端工程师。同时前端的类库也慢慢的开始发展,最著名的就是 jQuery 了。

当然,此架构也存在问题:缺乏可行的开发模式承载更复杂的业务需求,页面内容都杂糅在一起,一旦应用规模增大,就会导致难以维护了。因此,前端的 MVC 也随之而来。

前后端分离后的架构演变——MVC、MVP 和 MVVM

MVC

前端的 MVC 与后端类似,具备着 View、Controller 和 Model。

  • Model:负责保存应用数据,与后端数据进行同步
  • Controller:负责业务逻辑,根据用户行为对 Model 数据进行修改
  • View:负责视图展示,将 model 中的数据可视化出来。

三者形成了一个如图所示的模型:

image.png

这样的模型,在理论上是可行的。但往往在实际开发中,并不会这样操作。因为开发过程并不灵活。例如,一个小小的事件操作,都必须经过这样的一个流程,那么开发就不再便捷了。在实际场景中,我们往往会看到另一种模式,如图:

image.png

这种模式在开发中更加的灵活,backbone.js 框架就是这种的模式。但是,这种灵活可能导致严重的问题:

  1. 数据流混乱。如下图:

image.png

  1. View 比较庞大,而 Controller 比较单薄:由于很多的开发者都会在 view 中写一些逻辑代码,逐渐的就导致 view 中的内容越来越庞大,而 controller 变得越来越单薄。

既然有缺陷,就会有变革。前端的变化中,似乎少了 MVP 的这种模式,是因为 AngularJS 早早地将 MVVM 框架模式带入了前端。MVP 模式虽然前端开发并不常见,但是在安卓等原生开发中,开发者还是会考虑到它。

MVP

MVP 与 MVC 很接近,P 指的是 Presenter,presenter 可以理解为一个中间人,它负责着 View 和 Model 之间的数据流动,防止 View 和 Model 之间直接交流。我们可以看一下图示:

image.png

我们可以通过看到,presenter 负责和 Model 进行双向交互,还和 View 进行双向交互。这种交互方式,相对于 MVC 来说少了一些灵活,VIew 变成了被动视图,并且本身变得很小。虽然它分离了 View 和 Model。但是应用逐渐变大之后,导致 presenter 的体积增大,难以维护。要解决这个问题,或许可 以从 MVVM 的思想中找到答案。

MVVM

首先,何为 MVVM 呢?MVVM 可以分解成(Model-View-VIewModel)。ViewModel 可以理解为在 presenter 基础上的进阶版。如图所示:

image.png

  • ViewModel 通过实现一套数据响应式机制自动响应 Model 中数据变化;
  • 同时 Viewmodel 会实现一套更新策略自动将数据变化转换为视图更新
  • 通过事件监听响应 View 中用户交互修改 Model 中数据

这样在 ViewModel 中就减少了大量 DOM 操作代码。

MVVM 在保持 View 和 Model 松耦合的同时,还减少了维护它们关系的代码,使用户专注于业务逻辑,兼顾开发效率和可维护性。

总结

  • 这三者都是框架模式,它们设计的目标都是为了解决 Model 和 View 的耦合问题。

  • MVC 模式出现较早主要应用在后端,如 Spring MVC、ASP.NET MVC 等,在前端领域的早期也有应用,如 Backbone.js。它的优点是分层清晰,缺点是数据流混乱,灵活性带来的维护性问题

  • MVP 模式在是 MVC 的进化形式,Presenter 作为中间层负责 MV 通信,解决了两者耦合问题,但 P 层 过于臃肿会导致维护问题

  • MVVM 模式在前端领域有广泛应用,它不仅解决 MV 耦合问题,还同时解决了维护两者映射关系的大量繁杂代码和 DOM 操作代码,在提高开发效率、可读性同时还保持了优越的性能表现