关于Vue在面试中常常被提到的几点(持续更新……)(一)

简介: 现在Vue几乎公司里都用,所以掌握Vue至关重要,这里我总结了几点,希望对大家有用

1、Vue项目中为什么要在列表组件中写key,作用是什么?


我们在业务组件中,会经常使用循环列表,当时用v-for命令时,会在后面写上:key,那么为什么建议写呢?


key的作用是更新组件时判断两个节点是否相同。相同则复用,不相同就删除旧的创建新的。正是因为带唯一key时每次更新都不能找到可复用的节点,不但要销毁和创建节点,在DOM中还要添加移除节点,对性能的影响更大。所以才说,当不带key时,性能可能会更好。 因为不带key时,节点会复用(复用是因为Vue使用了Diff算法),省去了销毁或创建节点的开销,同时只需要修改DOM文本内容而不是移除或添加节点。既然如此,为什么我们还要建议带key呢?因为这种不带key的模式只适合渲染简单的无状态的组件。对于大多数场景来说,列表都得必须有自己的状态。避免组件复用引起的错误。 带上key虽然会增加开销,但是对于用户来说基本感受不到差距,为了保证组件状态正确,避免组件复用,这就是为什么建议使用key。


2、Vue的双向绑定,Model如何改变View,View又是如何改变Model的?


我们先看一幅图,下面一幅图就是Vue双向绑定的原理图。

微信截图_20220425183203.png


第一步,使数据对象变得“可观测”


我们要知道数据在什么时候被读或写了。


let person = {
        'name': 'maomin',
        'age': 23
    }
    let val = 'maomin';
    Object.defineProperty(person, 'name', {
        get() {
            console.log('name属性被读取了')
            return val
        },
        set(newVal) {
            console.log('name属性被修改了')
            val = newVal
        }
    })
    // person.name
    // name属性被读取了
    // "maomin"
    // person.name='xqm'
    // name属性被修改了
    // "xqm"


通过Object.defineProperty()方法给person定义了一个name属性,并把这个属性的读和写分别使用get()set()进行拦截,每当该属性进行读或写操作的时候就会触发get()set()。这样数据对象已经是“可观测”的了。


核心是利用es5Object.defineProperty,这也是Vue.js为什么不能兼容IE8及以下浏览器的原因。


Object.defineProperty方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。


Object.defineProperty(
        obj, // 定义属性的对象
        prop, // 要定义或修改的属性的名称
        descriptor // 将要定义或修改属性的描述符【核心】
    )


写一个简单的双向绑定:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <input type="text" id="input"/>
    <div id="text"></div>
</body>
<script>
    let input = document.getElementById('input');
    let text = document.getElementById('text');
    let data = {value:''};
    Object.defineProperty(data,'value',{
        set:function(val){
            text.innerHTML = val;
            input.value = val;
        },
        get:function(){
            return input.value;
        }
    });
    input.onkeyup = function(e){
        data.value = e.target.value;
    }
</script>
</html>


第二步,使数据对象的所有属性变得“可观测”


上面,我们只能观测person.name的变化,那么接下来我们要让所有的属性都变得可检测。


let person = observable({
        'name': 'maomin',
        'age': 23
    })
    /**
     * 把一个对象的每一项都转化成可观测对象
     * @param { Object } obj 对象
     */
    function observable(obj) {
        if (!obj || typeof obj !== 'object') {
            return;
        }
        let keys = Object.keys(obj); //返回一个表示给定对象的所有可枚举属性的字符串数组
        keys.forEach((key) => {
            defineReactive(obj, key, obj[key])
        })
        return obj;
    }
    /**
     * 使一个对象转化成可观测对象
     * @param { Object } obj 对象
     * @param { String } key 对象的key
     * @param { Any } val 对象的某个key的值
     */
    function defineReactive(obj, key, val) {
        Object.defineProperty(obj, key, {
            get() {
                console.log(`${key}属性被读取了`);
                return val;
            },
            set(newVal) {
                console.log(`${key}属性被修改了`);
                val = newVal;
            }
        })
    }
    // person.age
    // age属性被读取了
    // 23
    // person.age=24
    // age属性被修改了
    // 24


我们通过Object.keys()将一个对象返回一个表示给定对象的所有可枚举属性的字符串数组,然后遍历它,使得所有对象可以被观测到。


第三步,依赖收集,制作一个订阅器


我们就可以在数据被读或写的时候通知那些依赖该数据的视图更新了,为了方便,我们需要先将所有依赖收集起来,一旦数据发生变化,就统一通知更新。 创建一个依赖收集容器,也就是消息订阅器Dep,用来容纳所有的“订阅者”。订阅器Dep主要负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数。


设计了一个订阅器Dep类:


class Dep {
        constructor(){
            this.subs = []
        },
        //增加订阅者
        addSub(sub){
            this.subs.push(sub);
        },
        //判断是否增加订阅者
        depend () {
            if (Dep.target) {
                this.addSub(Dep.target)
            }
        },
        //通知订阅者更新
        notify(){
            this.subs.forEach((sub) =>{
                sub.update()
            })
        }
    }
Dep.target = null;


创建完订阅器,然后还要修改一下defineReactive


function defineReactive (obj,key,val) {
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            get(){
                dep.depend(); //判断是否增加订阅者
                console.log(`${key}属性被读取了`);
                return val;
            },
            set(newVal){
                val = newVal;
                console.log(`${key}属性被修改了`);
                dep.notify() //数据变化通知所有订阅者
            }
        })
    }


我们将订阅器Dep添加订阅者的操作设计在get()里面,这是为了让订阅者初始化时进行触发,因此需要判断是否要添加订阅者。


第四步,订阅者Watcher


设计一个订阅者Watcher类:


class Watcher {
      // 初始化
        constructor(vm,exp,cb){
            this.vm = vm; // 一个Vue的实例对象
            this.exp = exp; // 是node节点的v-model或v-on:click等指令的属性值。如v-model="name",exp就是name;
            this.cb = cb; // 是Watcher绑定的更新函数;
            this.value = this.get();  // 将自己添加到订阅器的操作
        },
    // 更新
        update(){
            let value = this.vm.data[this.exp];
            let oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal);
            },
        get(){
            Dep.target = this;  // 缓存自己
            let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
            Dep.target = null;  // 释放自己
            return value;
        }
    }


订阅者Watcher在初始化的时候需要将自己添加进订阅器Dep中,如何添加呢?我们已经知道监听器Observer是在get()执行了添加订阅者Wather的操作的,所以我们只要在订阅者Watcher初始化的时候触发对应的get()去执行添加订阅者操作即可。那要如何触发监听器get(),再简单不过了,只要获取对应的属性值就可以触发了。


订阅者Watcher运行时,首先进入初始化,就会执行它的 this.get() 方法, 执行Dep.target = this;,实际上就是把Dep.target 赋值为当前的渲染 Watcher ,接着又执行了let value = this.vm.data[this.exp];。在这个过程中会对数据对象上的数据访问,其实就是为了触发数据对象的get()


每个对象值的get()都持有一个dep,在触发 get()的时候会调用 dep.depend()方法,也就会执行this.addSub(Dep.target),即把当前的 watcher订阅到这个数据持有的dep.subs中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。完成依赖收集后,还需要把 Dep.target恢复成上一个状态Dep.target = null; 因为当前vm的数据依赖收集已经完成,那么对应的渲染Dep.target 也需要改变。


update()是用来当数据发生变化时调用Watcher自身的更新函数进行更新的操作。先通过let value = this.vm.data[this.exp];获取到最新的数据,然后将其与之前get()获得的旧数据进行比较,如果不一样,则调用更新函数cb进行更新。


总结:


实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。


微信截图_20220425183222.png


实现一个Vue数据绑定:


index.html


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <h1 id="name"></h1>
    <input type="text">
    <input type="button" value="改变data内容" onclick="changeInput()">
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script>
    function myVue (data, el, exp) {
        this.data = data;
        observable(data);                      //将数据变的可观测
        el.innerHTML = this.data[exp];         // 初始化模板数据的值
        new Watcher(this, exp, function (value) {
            el.innerHTML = value;
        });
        return this;
    }
    var ele = document.querySelector('#name');
    var input = document.querySelector('input');
    var myVue = new myVue({
        name: 'hello world'
    }, ele, 'name');
    //改变输入框内容
    input.oninput = function (e) {
        myVue.data.name = e.target.value
    }
    //改变data内容
    function changeInput(){
        myVue.data.name = "改变后的data"
    }
</script>
</body>
</html>


observer.js(为了方便,这里将订阅器与监听器写在一块)


// 监听器
     // 把一个对象的每一项都转化成可观测对象
     // @param { Object } obj 对象
    function observable (obj) {
        if (!obj || typeof obj !== 'object') {
            return;
        }
        let keys = Object.keys(obj);
        keys.forEach((key) =>{
            defineReactive(obj,key,obj[key])
        })
        return obj;
    }
     // 使一个对象转化成可观测对象
     // @param { Object } obj 对象
     // @param { String } key 对象的key
     // @param { Any } val 对象的某个key的值
    function defineReactive (obj,key,val) {
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            get(){
                dep.depend();
                console.log(`${key}属性被读取了`);
                return val;
            },
            set(newVal){
                val = newVal;
                console.log(`${key}属性被修改了`);
                dep.notify()                    //数据变化通知所有订阅者
            }
        })
    }
  // 订阅器Dep 
    class Dep {
        constructor(){
            this.subs = []
        }
        //增加订阅者
        addSub(sub){
            this.subs.push(sub);
        }
        //判断是否增加订阅者
        depend () {
            if (Dep.target) {
                this.addSub(Dep.target)
            }
        }
        //通知订阅者更新
        notify(){
            this.subs.forEach((sub) =>{
                sub.update()
            })
        }
    }
    Dep.target = null;


watcher.js


class Watcher {
        constructor(vm,exp,cb){
            this.vm = vm;
            this.exp = exp;
            this.cb = cb;
            this.value = this.get();  // 将自己添加到订阅器的操作
        }
        get(){
            Dep.target = this;  // 缓存自己
            let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
            Dep.target = null;  // 释放自己
            return value;
        }
        update(){
            let value = this.vm.data[this.exp];
            let oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal);
            }
    }
}



相关文章
|
2月前
|
JavaScript 前端开发 应用服务中间件
【Vue面试题三十】、vue项目本地开发完成后部署到服务器后报404是什么原因呢?
这篇文章分析了Vue项目在服务器部署后出现404错误的原因,主要是由于history路由模式下服务器缺少对单页应用的支持,并提供了通过修改nginx配置使用`try_files`指令重定向所有请求到`index.html`的解决方案。
【Vue面试题三十】、vue项目本地开发完成后部署到服务器后报404是什么原因呢?
|
2月前
|
JavaScript 前端开发 数据处理
【Vue面试题二十八】、vue要做权限管理该怎么做?如果控制到按钮级别的权限怎么做?
这篇文章讨论了Vue中实现权限管理的策略,包括接口权限、路由权限、菜单权限和按钮权限的控制方法,并提供了不同的实现方案及代码示例,以确保用户只能访问被授权的资源。
【Vue面试题二十八】、vue要做权限管理该怎么做?如果控制到按钮级别的权限怎么做?
|
26天前
|
缓存 JavaScript 前端开发
vue面试题
vue面试题
|
9天前
|
JavaScript
vue面试
vue面试
17 1
|
2月前
|
JavaScript 安全 前端开发
【Vue面试题二十九】、Vue项目中你是如何解决跨域的呢?
这篇文章介绍了Vue项目中解决跨域问题的方法,包括使用CORS设置HTTP头、通过Proxy代理服务器进行请求转发,以及在vue.config.js中配置代理对象的策略。
【Vue面试题二十九】、Vue项目中你是如何解决跨域的呢?
|
2月前
|
JavaScript 前端开发 编译器
【Vue面试题三十二】、vue3有了解过吗?能说说跟vue2的区别吗?
这篇文章介绍了Vue 3相对于Vue 2的改进和新增特性,包括性能提升、体积减小、更易维护、更好的TypeScript支持、新的Composition API、新增的Teleport和createRenderer功能,以及Vue 3中的非兼容性变更和API的移除或重命名。
【Vue面试题三十二】、vue3有了解过吗?能说说跟vue2的区别吗?
|
2月前
|
JavaScript 前端开发 API
【Vue面试题三十一】、你是怎么处理vue项目中的错误的?
这篇文章讨论了Vue项目中错误的处理方式,包括后端接口错误和代码逻辑错误的处理策略。文章详细介绍了如何使用axios的拦截器处理后端接口错误,以及Vue提供的全局错误处理函数`errorHandler`和生命周期钩子`errorCaptured`来处理代码中的逻辑错误。此外,还分析了Vue错误处理的源码,解释了`handleError`、`globalHandleError`、`invokeWithErrorHandling`和`logError`函数的作用和处理流程。
【Vue面试题三十一】、你是怎么处理vue项目中的错误的?
|
2月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
2月前
|
XML 存储 JSON
【IO面试题 六】、 除了Java自带的序列化之外,你还了解哪些序列化工具?
除了Java自带的序列化,常见的序列化工具还包括JSON(如jackson、gson、fastjson)、Protobuf、Thrift和Avro,各具特点,适用于不同的应用场景和性能需求。
|
2月前
|
Java
【Java基础面试三十七】、说一说Java的异常机制
这篇文章介绍了Java异常机制的三个主要方面:异常处理(使用try、catch、finally语句)、抛出异常(使用throw和throws关键字)、以及异常跟踪栈(异常传播和程序终止时的栈信息输出)。