每日一题1.0

63 阅读8分钟

VUE

说说对于MVVM的理解

1.英文缩写:

MVVM 是 Model-View-ViewModel 的缩写;

2.层级解释:

Model:数据层;泛指后端进行的业务逻辑处理和数据操控,主要围绕数据库系统展开。前端人员大多不需要管理数据层,只要后端保证对外接口足够简单调用即可,同时保证数据结构方便识别和调用,前端请求API,后端把数据按照规定结构返回来让我用就行。

View:视图层;用户界面层;前端主要使用HTML和CSS构件视图层

ViewModel:业务逻辑层;前端人员生成和维护的视图数据层,是MVVM的核心;注意:ViewModel层封装的数据模型包括视图的状态和行为两部分(视图样式和点击交互行为),而Model层的数据模型只包括视图数据,没有视图的动画交互;View层不是直接展示Mode层的数据,而是直接展示ViewModel层的数据和行为,间接的使用Model层数据,这样就完全实现了前后端分离。

3.三者联系:

数据层只写数据;视图层是图片显示;业务逻辑层负责将数据转化为可读的视图,同时将视图的变化反馈到数据的变化。

4.MVVM模式的意义:

MVVM模式促进了前后端的业务逻辑分离;提高前端开发效率;后端负责数据修改,前端负责调用数据进行视图更新。

MVVM模式最重要的是ViewModel层:相当于一个中转站,将数据和视图进行转化;一方面和视图层进行双向数据绑定,一方面调用数据层的接口进行数据交互

5.图示解释:

image.png

6.优点:

(1)双向绑定技术;单向绑定就是ViewModel层变化时,自动更新View层,不会反着来;双向绑定就是在单向绑定基础上监测View层变化来更新ViewModel层;

(2)提高了可维护性和可测试性;视图界面的直接测试是比较难的,而现在测试可以针对ViewModel来写。

(3)低耦合可重用:一个ViewModel可以重用在不同View上,可以把一些视图逻辑封装在ViewModel中,重用这段视图逻辑在不同View上。

7.缺点:

(1)BUG难调试;因为双向绑定的原因,当你看到界面异常时,这个错误可能出现在View层也可能是Model层;数据绑定使得这个bug被快速传递到别的位置,要定位这个bug的原位置,就很不容易了。

(2)大型的图形应用程序,视图状态较多,ViewModel的构件和维护成本比较高。

(3)大的模块中的MOdel数据很多,虽然使用方便了,保证数据的一致性,但是长期使用,不释放内存时就会花费更多的内存。

8.MVVM的使用范围:

MVVM不是万能的,目的是为了解决复杂的前端逻辑,尤其是解决需要大量DOM操作的逻辑;需要SEO的页面,不能使用MVVM展示数据(页面需要被搜索引擎搜索,而搜索引擎无法获取使用MVVM并通过API加载的数据);如果前端逻辑复杂,就适合使用MVVM展示数据(例如:工具类页面、复杂的表单页面、用户登录后才能操作的页面等)

9.常见的MVVM框架有:

Angular:Google出品,名气大,但是学习难度有些大;适合PC,代码结构会比较清晰;

Backbone.js:入门非常困难,因为自身API太多;

Ember:一个大而全的框架,想写个Hello world都很困难。

Avalon:属于轻量级的,并且对老的浏览器支持程度较高,最低支持到IE6,所以适合兼容老刘浏览器的项目;

Vue:主打轻量级,仅作为MV*中的视图部分使用,优点轻量级,易学易用,缺点是大项目的时候还要配合其他框架或者库来使用,比较麻烦

详细说下你对VUE生命周期的理解:

1.是什么:vue生命周期是指vue实例对象从创建之初到销毁的过程,vue所有功能的实现都是围绕其生命周期进行的,在生命周期的不同阶段调用对应的钩子函数实现组件数据管理和DOM渲染两大重要功能。

1.生命周期8阶段:创建前/后、载入前/后、更新前/后、销毁前/后

(1)beforeCreate(创建前):vue实例的挂载元素$el和数据对象 data都是undefined, 还未初始化(未赋值);场景:可以在这里加个loading事件,在加载实例时触发

(2)created (创建后) :vue实例已经创建;完成了 data数据初始化;el还未初始化()当这个函数执行的时候,我们已经可以拿到data下的数据以及methods下的方法了,所以在这里,我们可以开始调用方法进行数据请求了

(3)beforeMount (载入前): vue实例的el和data都初始化了, 相关的render函数首次被调用。首先我们会先生产一个虚拟dom(用于后续数据发生变化时,新老虚拟dom对比计算),进行保存,然后再开始将render渲染成为真实的dom。渲染成真实dom后,会将渲染出来的真实dom替换掉原来的vm.el,然后再将替换后的el,然后再将替换后的el append到我们的页面内。

(4)mounted (载入后) :在el 被新创建的 vm.$el替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互(mounted是平常使用最多的函数)场景:挂载元素,获取到DOM节点

**第一次加载页面时加载会触发的钩子:beforeCreate, created, beforeMount, mounted **

(5)beforeUpdate (更新前) :重新生成新的虚拟DOM(Vnode),然后和旧的比较计算,算出最小的更新范围,更新render函数中的最新数据,渲染render函数为真实dom。

(6)updated (更新后) :可以拿到更新后的DOM(mouted和updated的执行并不会等待所以子组件都被挂载完毕后再执行,所以如果你希望所有视图都更新完毕后再做些什么行为,那你最好在mouted/updated中加一个$nextTick(),然后把要做的事情放在里面)(在这一阶段DOM会和更改过的内容同步)场景:如果对数据统一处理,在这里写上相应函数

(7)beforeDestroy (销毁前): 此时实例未销毁,可以操作实例。场景:可以做一个确认停止事件的确认框

(8)destroyed (销毁后):所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用

  1. created和mounted的区别:

created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。(场景:渲染html前先请求将数据请求完毕,然后使用数据进行渲染)

mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。(场景:异步请求一般都谢在这里)

3.生命周期函数的作用:

以案例来说明:我们在created函数里写入数据请求函数,在渲染html页面前使用API获取后端数据(否则无法使用数据进行布局)这里created钩子函数的作用就是:让请求后台数据这个行为更加具有逻辑性,能让我们知道应该在什么阶段进行后台数据请求

4.图例理解整个流程: 详细解释生命周期整个流程

重点是:判断el对象的存在、检测是否有手动挂载vm.$mount(el)、检测template存在、转化render函数、虚拟dom替换真实dom

(1)此时设置el(#app),并没有设置template模板

<body>
    <div id="app">
        <p>{{message}}</p>
        <button @click="changeMsg">改变</button>
    </div>
</body>
<script>
    var vm = new Vue({
        el: '#app',  //挂载
        data: {
            message: 'hello world'
        },
        methods: {
            changeMsg () {
                this.message = 'goodbye world'
            }
        },
        beforeCreate() {
            console.log('------初始化前------')  //正常输出
            console.log(this.message)  //输出为undefined  ;因为此时我们拿不到data数据,数据还未赋值
            console.log(this.$el)  //输出为undefined
        },
        created () {
            console.log('------初始化完成------')  //正常输出
            console.log(this.message)  //输出为:hello world;data数据已经初始化
            console.log(this.$el)  //输出为:undefined:$el未初始化
        },
        beforeMount () {
            console.log('------挂载前---------')  //正常输出
            console.log(this.message) //输出:hello world
            console.log(this.$el)  //为什么输出的是源代码? 
            因为流程是先判断el为(#app)后会判断是否有template模板,没有则会将其源代码替换
            vm.$el(vm.$el的作用:获取Vue实例关联的DOM元素:也就是获取初始设置的渲染信息);有
            template模板的话就会将el(#app)编译为template模板,再转化为render函数,最后渲染为
            真实dom,用真实dom替换初始的vm.$el。
        },
        mounted () {
            console.log('------挂载完成---------')
            console.log(this.message)
            console.log(this.$el)  //这时候输出的是渲染完毕后的$el,因为$el已经是被替换为了源代码,所以输出依然是源代码
        },
        beforeUpdate () {
            console.log('------更新前---------')
            console.log(this.message)
            console.log(this.$el)
        },
        updated() {
            console.log('------更新后---------')
            console.log(this.message)
            console.log(this.$el)
        }
    })
</script>

image.png

(2)加上template模板

var vm = new Vue({
        el: '#app',
        data: {
            message: 'hello world'
        },
        template: '<div>我是模板内的{{message}}</div>',   //template模板
        methods: {
            changeMsg () {
                this.message = 'goodbye world'
            }
        },
        beforeCreate() {
            console.log('------初始化前------')
            console.log(this.message)
            console.log(this.$el)
        },
        created () {
            console.log('------初始化完成------')
            console.log(this.message)
            console.log(this.$el)
        },
        beforeMount () {
            console.log('------挂载前---------')
            console.log(this.message)
            console.log(this.$el)  //此时输出的仍是源代码,因为仅仅是进行的判断,并没有真正的进行替换参数,所以还是原来的源代码。
        },
        mounted () {
            console.log('------挂载完成---------')
            console.log(this.message)
            console.log(this.$el)  //此时执行的是真正的替换,将源代码替换为template模板中的内容
        },
        beforeUpdate () {
            console.log('------更新前---------')
            console.log(this.message)
            console.log(this.$el)
        },
        updated() {
            console.log('------更新后---------')
            console.log(this.message)
            console.log(this.$el)
        }
    })

image.png

5.生命周期流程图示:

注意:钩子函数是包含上面的,不是从下面的开始执行

微信图片_20210428112244.jpg

Vue的双向数据绑定原理是什么

详细教程

1.实现原理:Vue 则采用的是数据劫持与发布订阅相结合的方式实现双向绑定,数据劫持主要通过 Object.defineProperty 来实现。

2.需要的三个步骤

Observer 监听器:遍历地为每个属性加上set和get函数,实现所有数据监听使用Object.defineProperty来监听:当内容被浏览时触发get函数;当内容被更改时触发set函数

Watcher 订阅者:是Observe和Complie之间通信的桥梁;Observe监听到数据更改后,Compile解析数据,Watcher缓存自身并强制调用update函数进行数据修改

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

image.png

思路总结:
1.总体思路:数据变化更新视图、视图变化更新数据
2.Model-到-View(难点)
关键是如何知道model层数据改变了?
    通过set的触发与否来知道数据是不是被修改了
为什么使用发布订阅者模式?
    因为一个model数据对应的可能是多个视图层的数据展示,所以需要一对多的改变视图层,就要使用发布订阅者模式(相当于明星,很多粉丝是订阅者=关注/受众者,明星只有一个就是model数据;也就是说改变1个model的值可以改变多个view中的值)

3.View-到-Model
很简单,只需要事件监听input输入框里的值,然后获取后改变model层的data数据即可

4.思路:首先我们为每个vue属性用Object.defineProperty()实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep;然后在编译的时候在该属性的数组dep中添加订阅者,v-model会添加一个订阅者,{{}}也会,v-bind也会,只要用到该属性的指令理论上都会,接着为input会添加监听事件,修改值就会为该属性赋值,触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。

源码:
<!DOCTYPE html>
 <head>
    <meta charset="UTF-8">
    <title>双向绑定</title>
 </head>
 <body>

    <!-- 实现vue -->
    <div id="app">
        <input type="text" v-model="text">
        {{ text }}
    </div>

  <script type="text/javascript"> 
1.遍历监听数据
    function defineReactive(obj, key, val){
        var dep = new Dep();
        Object.defineProperty(obj, key, {
            get: function(){
                if(Dep.target){
                    dep.addSub(Dep.target); // 添加订阅者
                }
                return val
            },
            set: function(newVal){
                if(newVal === val){
                    return 
                }
                val = newVal;
                console.log('新值:' + val);

                // 一旦更新立马通知
                dep.notify();
            }
        })
    }
 /*观察者函数*/
  函数封装:遍历所有数据设置set和get
    function observe(obj,vm){
        for(let key of Object.keys(obj)){
            defineReactive(vm, key, obj[key]);
        }
    }


    function nodeToFragment(node,vm){
        var fragment = document.createDocumentFragment();
        var child;
        while(child = node.firstChild){
            compile(child, vm);
            fragment.appendChild(child);
        }
        return fragment
    }

2.Compile:数据解析:数据修改后通知订阅者Watcher
    /*编译函数*/
    function compile(node, vm){
        var reg = /\{\{(.*)\}\}/; // 来匹配 {{ xxx }} 中的xxx
        // 如果是元素节点
        if(node.nodeType === 1){
            var attr = node.attributes;
            // 解析元素节点的所有属性
            for(let i=0;i<attr.length;i++){
                if(attr[i].nodeName == 'v-model'){//遍历属性节点找到v-model的属性
                    var name = attr[i].nodeValue; // 获取v-model绑定的属性名,看看是与哪一个数据相关
                    node.addEventListener('input', function(e){
                        vm[name] = e.target.value;  // 给相应的data属性赋值,进而触发该属性的set方法
                    });
                    node.value = vm[name]; // 将data的值赋给该node
                    node.removeAttribute('v-model');
                    
                }
            };
        }
        // 如果是文本节点
        if(node.nodeType === 3){
            if(reg.test(node.nodeValue)){
                var name = RegExp.$1; // 获取到匹配的字符串
                name = name.trim(); 
                // node.nodeValue = vm[name];  // 将data的值赋给该node

                new Watcher(vm, node, name); // 不直接通过赋值的操作,而是通过绑定一个订阅者////创建新的watcher,会触发函数向对应属性的dep数组中添加订阅者
            }
        }
    }
3.Watcher订阅者:收到解析数据时调用update函数(get函数)进行更新
    /*Watcher构造函数*/
    function Watcher(vm, node, name){
        Dep.target = this; // Dep.target 是一个全局变量
        this.vm = vm;
        this.node= node;
        this.name = name;
        this.update(); //强行调用get函数
        Dep.target = null; //释放变量
    }
    /*update更新函数封装*/
    Watcher.prototype = {
        update(){
            this.get();
            this.node.nodeValue = this.value; // 注意,这是更改节点内容的关键
        },
        get(){
            this.value = this.vm[this.name]; // 触发相应的get
        }
    }

    /*dep构造函数:收集订阅者,为每个属性添加订阅者*/
    function Dep(){
        this.subs = [];//数组形式储存
    }
    Dep.prototype = {
    //添加订阅者
        addSub(sub){
            this.subs.push(sub);
        },
        notify(){
            this.subs.forEach(function(sub){
                sub.update();
            })
        }
    }

    /*Vue构造函数*/
    function Vue(options){
        this.data = options.data;
        var data = this.data;

        observe(data, this);

        var id = options.el;
        var dom = nodeToFragment(document.getElementById(id), this);
        // 处理完所有dom节点后,重新将内容添加回去
        document.getElementById(id).appendChild(dom);
    }

    var vm = new Vue({
        el: 'app',
        data: {
            text: 'hello world'
        }
    });

   </script>
  </body>
</html>

this的指向问题?

VueX里面的状态码你能下嘛?

三剑客

平常开发是如何清除浮动的

两侧固定,中间自适应布局的方法

平常是如何解决浏览器兼容问题的?

经常用的操作数据和字符串的方法有什么?

作用域和作用域链?

原型和原型链?

ES6的新特性?

手写promise?介绍一下promise

url输入到地址栏后,整个流程?

写完项目后你有做过哪些优化?