精华内容
最热下载
问答
  • 5星
    52.18MB huayula 2021-09-07 06:59:33
  • 5星
    19.23MB qq_29777207 2020-12-08 20:23:01
  • 5星
    91.31MB weixin_44510615 2021-07-09 17:18:52
  • 5星
    14.4MB qq_37740841 2021-08-07 21:45:39
  • 5星
    26.79MB m0_46360532 2021-06-27 10:14:07
  • 5星
    18.25MB ylcto 2020-12-23 13:59:32
  • 5星
    12.74MB weixin_44611398 2021-06-02 07:50:21
  • 5星
    8.67MB GISuuser 2021-09-17 19:32:38
  • 5星
    849KB qq_29918313 2021-07-05 11:13:55
  • 5星
    46.38MB weixin_40228600 2021-03-26 09:10:00
  • 1、computed 的实现原理 computed 本质是一个惰性求值的观察者。 computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。 其内部通过 this.dirty ...

    1、computed 的实现原理

    computed 本质是一个惰性求值的观察者。

    computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。

    其内部通过 this.dirty 属性标记计算属性是否需要重新求值。

    当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,

    computed watcher 通过 this.dep.subs.length 判断有没有订阅者,

    有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)

    没有的话,仅仅把 this.dirty = true。(当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)

    2、computed 和 watch 有什么区别及运用场景?

    区别:
    computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。

    watch 侦听器 : 更多的是「观察」的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

    运用场景:

    当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算。

    当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

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

    Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性(Vue 为什么不能检测数组变动 )。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组

    push();
    pop();
    shift();
    unshift();
    splice();
    sort();
    reverse();
    

    由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。

    Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。
    Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

    4、Vue 中的 key 到底有什么用?

    key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。)

    diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.

    更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。

    更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1),源码如下:

    function createKeyToOldIdx(children, beginIdx, endIdx) {
      let i, key;
      const map = {};
      for (i = beginIdx; i <= endIdx; ++i) {
        key = children[i].key;
        if (isDef(key)) map[key] = i;
      }
      return map;
    }
    

    5、vue 是如何对数组方法进行变异的 ?

    const arrayProto = Array.prototype;
    
    export const arrayMethods = Object.create(arrayProto);
    
    const methodsToPatch = [
      "push",
      "pop",
      "shift",
      "unshift",
      "splice",
      "sort",
      "reverse"
    ];
    
    /**
     * Intercept mutating methods and emit events
     */
    
    methodsToPatch.forEach(function(method) {
      // cache original method
      const original = arrayProto[method];
      def(arrayMethods, method, function mutator(...args) {
        const result = original.apply(this, args);
        const ob = this.__ob__;
        let inserted;
        switch (method) {
          case "push":
          case "unshift":
            inserted = args;
            break;
          case "splice":
            inserted = args.slice(2);
            break;
        }
        if (inserted) ob.observeArray(inserted);
        // notify change
        ob.dep.notify();
        return result;
      });
    });
     
    /**
     * Observe a list of Array items.
     */
    
    Observer.prototype.observeArray = function observeArray(items) {
      for (var i = 0, l = items.length; i < l; i++) {
        observe(items[i]);
      }
    };
    

    简单来说,Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知 render watcher,执行 update

    6、Vue 组件 data 为什么必须是函数 ?

    new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢?
    因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。

    所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。

    7、vm.$set()实现原理是什么?

    受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。

    由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

    对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。

    那么 Vue 内部是如何解决对象新增属性不能响应的问题的呢?

    export function set(target: Array | Object, key: any, val: any): any {
      // target 为数组
      if (Array.isArray(target) && isValidArrayIndex(key)) {
       // 修改数组的长度, 避免索引>数组长度导致splice()执行有误
        target.length = Math.max(target.length, key);
        // 利用数组的splice变异方法触发响应式
        target.splice(key, 1, val);
        return val;
      }
    
      // target为对象, key在target或者target.prototype上 且必须不能在 Object.prototype 上,直接赋值
      if (key in target && !(key in Object.prototype)) {
        target[key] = val;
        return val;
      }
    
      // 以上都不成立, 即开始给target创建一个全新的属性
      // 获取Observer实例
      const ob = (target: any).__ob__;
      // target 本身就不是响应式数据, 直接赋值
      if (!ob) {
        target[key] = val;
        return val;
      }
    
      // 进行响应式处理
      defineReactive(ob.value, key, val);
      ob.dep.notify();
      return val;
    }
    

    1、如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式
    2、如果目标是对象,判断属性存在,即为响应式,直接赋值
    3、如果 target 本身就不是响应式,直接赋值
    4、如果属性不是响应式,则调用 defineReactive 方法进行响应式处理

    8、说说 Vue 的渲染过程

    在这里插入图片描述

    调用 compile 函数,生成 render 函数字符串 ,编译过程如下:

    • parse 函数解析 template,生成 ast(抽象语法树)
    • optimize 函数优化静态节点 (标记不需要每次都更新的内容,diff 算法会直接跳过静态节点,从而减少比较的过程,优化了 patch 的性能)
    • generate 函数生成 render 函数字符串

    调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
    调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素

    9、谈谈 Vue 事件机制,手写$ on,$ off, $ emit, $ once

    Vue 事件机制 本质上就是 一个 发布-订阅 模式的实现。

    class Vue {
      constructor() {
        //  事件通道调度中心
        this._events = Object.create(null);
      }
    
      $on(event, fn) {
        if (Array.isArray(event)) {
          event.map(item => {
            this.$on(item, fn);
          });
        } else {
          (this._events[event] || (this._events[event] = [])).push(fn);
        }
        return this;
      }
    
      $once(event, fn) {
        function on() {
          this.$off(event, on);
          fn.apply(this, arguments);
        }
    
        on.fn = fn;
        this.$on(event, on);
        return this;
      }
    
      $off(event, fn) {
        if (!arguments.length) {
          this._events = Object.create(null);
          return this;
        }
    
        if (Array.isArray(event)) {
          event.map(item => {
            this.$off(item, fn);
          });
          return this;
        }
    
        const cbs = this._events[event];
        if (!cbs) {
          return this;
        }
    
        if (!fn) {
          this._events[event] = null;
          return this;
        }
    
        let cb;
        let i = cbs.length;
        while (i--) {
          cb = cbs[i];
          if (cb === fn || cb.fn === fn) {
            cbs.splice(i, 1);
            break;
          }
        }
        return this;
      }
    
      $emit(event) {
        let cbs = this._events[event];
        if (cbs) {
          const args = [].slice.call(arguments, 1);
          cbs.map(item => {
            args ? item.apply(this, args) : item.call(this);
          });
        }
        return this;
      }
    }
    
    展开全文
    g1437353759 2020-09-29 22:28:38
  • Vue中先遍历data选项中所有的属性(发布者)用Object.defineProperty劫持这些属性将其转为getter/setter。读取数据时候会触发getter。修改数据时会触发setter。 然后给每个属性对应new Dep(),Dep是专门收集依赖、...

    说说你对SPA单页面的理解,它的优缺点分别是什么?

    是一种只需要将单个页面加载到服务器之中的web应用程序。当浏览器向服务器发出第一个请求时,服务器会返回一个index.html文件,它所需的js,css等会在显示时统一加载,部分页面按需加载。url地址变化时不会向服务器在请求页面,通过路由才实现页面切换。

    优点:

    • 良好的交互体验,用户不需要重新刷新页面,获取数据也是通过Ajax异步获取,页面显示流畅;
    • 良好的前后端工作分离模式。

    缺点:

    • SEO难度较高,由于所有的内容都在一个页面中动态替换显示,所以在SEO上其有着天然的弱势。
    • 首屏加载过慢(初次加载耗时多)

    SPA单页面的实现方式有哪些? 

    • 在hash模式中,在window上监听hashchange事件(地址栏中hash变化触发)驱动界面变化;
    • 在history模式中,在window上监听popstate事件(浏览器的前进或后退按钮的点击触发)驱动界面变化,监听a链接点击事件用history.pushState、history.replaceState方法驱动界面变化;
    • 直接在界面用显示隐藏事件驱动界面变化。

    说说你对MVC、MVP、MVVM模式的理解 

    在以往开发程序过程中,界面布局代码、界面交互逻辑代码、业务逻辑代码三者代码都是混在一起的。随着业务需求越来越大,代码越来越复杂,不仅导致开发困难,更是导致维护代码更困难,特别是维护别人的代码。

    所以就出现MVC模式来解决这个问题,其中M代表Model,专门来处理、存储数据。V代表View,专门来处理页面的展示。C代表Controller专门处理业务逻辑。

    用户操作View,View发送指令到Control,完成业务逻辑处理后,要求Model处理相应的数据,将处理好的数据发送到View,要求View把这些数据展示给用户。

    当然用户也可以直接下发指令到Control,完成对应业务逻辑处理后,要求Model处理相应的数据,将处理好的数据发送到View,要求View把这些数据展示给用户。

    也可以通过View直接要求Moder处理数据,将处理好的数据发送到View,要求View把这些数据展示给用户。

    然而在MVC模式中,Model、Control、View三者相互依赖,修改起来要兼顾其他两者,还是非常困难。

    所以又出现了MVP模式来解决这个问题,在MVP模式中P代表Presenter替代原来的Control。

    当用户操作View,View发送指令到Presenter,完成业务逻辑处理后,要求Model处理相应的数据,将处理好的数据返回到Presenter中,Presenter将数据发送到View中,要求View把这些数据展示给用户。

    MVP模式中,Presenter将View和Model完全隔离开,Presenter和View相互依赖,Presenter和Model相互依赖,View和Model不再相互依赖,使代码耦合降低。

    因为Presenter和View相互依赖,这样Presenter就没办法单独做单元测试,非得等到View做好以后才行。所以对View分割一部分叫做View接口,Presenter只依赖View接口,这样Presenter不用依赖View就可以测试了,并且也增加了复用性,只要View实现了View接口部分,Presenter就可以大发神威。

    然而在MVP模式中,因为让Presenter发送数据到View,让View展示,仍然需要大量的、烦人的代码,这实在是一件不舒服的事情。 那么可不可以让View在Model变化时自动更新。

    所以出现了MVVM模式来实现这个设想,其中VM代表ViewModel负责视图显示逻辑和监听视图变化,M代表Model变成处理业务逻辑和数据。

    当用户操作View时,ViewModel监听到View的变化,会通知Model中对应的方法进行业务逻辑和数据处理,处理完毕后,ViewModel会监听到自动让View做出相应的更新。ViewModel可以对应多个View,具有很强的复用性。

    在Vue项目中。new Vue()就是一个ViewModel,View就是template模板。Model就是Vue的选项如data、methods等。在开发过程我们只关注View怎么展示,Model怎么处理业务逻辑和数据。不要去管处理业务逻辑和数据后怎么让View更新,View上有操作,怎么让Model处理这个操作,这些通通交给ViewModel来实现,大大降低了开发成本。

    说说你对Object.defineProperty的理解 

    • Object.defineProperty(obj,prop,descriptor)方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
      • obj:要在其上定义属性的对象。
      • prop:要定义或修改的属性的名称。
      • descriptor:将被定义或修改的属性描述符。
    • descriptor属性描述符主要有两种形式:数据描述符和存取描述符。描述符必须是这两种形式之一;不能同时是两者。
      • 数据描述符和存取描述符共同拥有
        • configurable:特性表示对象的属性是否可以被删除,以及除value和writable特性外的其他特性是否可以被修改。默认为false。
        • enumerable:当该属性的enumerable为true时,该属性才可以在for...in循环和Object.keys()中被枚举。默认为false。
      • 数据描述符
        • value:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为undefined。
        • writable:当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为false。
      • 存取描述符
        • get:一个给属性提供 getter的方法,如果没有getter则为undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。默认为undefined。
        • set:一个给属性提供 setter的方法,如果没有setter则为undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为undefined。
    • 定义descriptor时,最好先把这些属性都定义清楚,防止被继承和继承时出错
    function Archiver() {
        var temperature = null;
        var archive = [];
        Object.defineProperty(this, 'temperature', {
            get: function() {
              console.log('get!');
              return temperature;
            },
            set: function(value) {
              temperature = value;
              archive.push({ val: temperature });
            }
        });
        this.getArchive = function() { return archive; };
    }
    var arc = new Archiver();
    arc.temperature; // 'get!'
    arc.temperature = 11;
    arc.temperature = 13;
    arc.getArchive(); // [{ val: 11 }, { val: 13 }]
    

     说说你对Proxy的理解

    官方定义:proxy对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

    通俗来说是在对目标对象的操作之前提供了拦截,对外界的操作进行过滤和修改某些操作的默认行为,可以不直接操作对象本身,而是通过操作对象的代理对象来间接来操作对象。

    let proxy = new Proxy(target, handler)

    • target 是用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理);
    • handler 一个对象,其属性是当执行一个操作时定义代理的行为的函数,也就是自定义的行为。

    handle可以为{},但是不能为null,否则会报错

    Proxy 目前提供了 13 种可代理操作,比较常用的

    • handler.get(target,property,receiver)获取值拦截
    • handler.set(target,property,value,receiver)设置值拦截
    • handler.has(target,prop)in 操作符拦截
    let obj = {
    	a : 1,
    	b : 2
    }
    let test = new Proxy(obj,{
        get : function (target,property) {
            return property in target ? target[property] : 0
        },
        set : function (target,property,value) {
            target[property] = 6;
        },
        has: function (target,prop){
            if(prop == 'b'){
                target[prop] = 6;
            }
            return prop in target;
        },
    })
    
    console.log(test.a);        // 1
    console.log(test.c);        // 0
    
    test.a = 3;
    console.log(test.a)         // 6
    
    if('b' in test){
        console.log(test)       // Proxy {a: 6, b: 6}
    }
    

     Object.defineProperty和Proxy的区别

    • Object.defineProperty
      • 不能监听到数组length属性的变化;
      • 不能监听对象的添加;
      • 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。
    • Proxy
      • 可以监听数组length属性的变化;
      • 可以监听对象的添加;
      • 可代理整个对象,不需要对对象进行遍历,极大提高性能;
      • 多达13种的拦截远超Object.defineProperty只有get和set两种拦截。

    你认为Vue的核心是什么? 

    Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统。 

    以上是官方原话,从中可以得知Vue的核心是模板语法和数据渲染。 

    说说你对单向数据流和双向数据流的理解 

    单向数据流是指数据只能从父级向子级传递数据,子级不能改变父级向子级传递的数据。

    双向数据流是指数据从父级向子级传递数据,子级可以通过一些手段改变父级向子级传递的数据。

    比如用v-model.sync来实现双向数据流。

    什么是双向绑定?原理是什么? 

    双向绑定是指数据模型(Module)和视图(View)之间的双向绑定。

    其原理是采用数据劫持结合发布者-订阅者模式的方式来实现。

    Vue中先遍历data选项中所有的属性(发布者)用Object.defineProperty劫持这些属性将其转为getter/setter。读取数据时候会触发getter。修改数据时会触发setter。

    然后给每个属性对应new Dep(),Dep是专门收集依赖、删除依赖、向依赖发送消息的。先让每个依赖设置在Dep.target上,在Dep中创建一个依赖数组,先判断Dep.target是否已经在依赖中存在,不存在的话添加到依赖数组中完成依赖收集,随后将Dep.target置为上一个依赖。

    组件在挂载过程中都会new一个Watcher实例。这个实例就是依赖(订阅者)。Watcher第二参数式一个函数,此函数作用是更新且渲染节点。在首次渲染过程,会自动调用Dep方法来收集依赖,收集完成后组件中每个数据都绑定上该依赖。当数据变化时就会在seeter中通知对应的依赖进行更新。在更新过程中要先读取数据,就会触发Wacther的第二个函数参数。一触发就再次再次自动调用Dep方法收集依赖,同时在此函数中运行patch(diff运算)来更新对应的DOM节点,完成了双向绑定。

    什么是虚拟DOM? 

    虚拟DOM是将状态映射成视图的众多解决方案中的一种,其是通过状态生成一个虚拟节点树,然后使用虚拟节点树进行渲染生成真实DOM,在渲染之前,会使用新生成的虚拟节点树和上一次虚拟节点树进行对比,只渲染不同的部分。 

    Vue中如何实现一个虚拟DOM?说说你的思路 

    首先要构建一个VNode的类,DOM元素上的所有属性在VNode类实例化出来的对象上都存在对应的属性。例如tag表示一个元素节点的名称,text表示一个文本节点的文本,chlidren表示子节点等。将VNode类实例化出来的对象进行分类,例如注释节点、文本节点、元素节点、组件节点、函数式节点、克隆节点。

    然后通过编译将模板转成渲染函数render,执行渲染函数render,在其中创建不同类型的VNode类,最后整合就可以得到一个虚拟DOM(vnode)。

    最后通过patch将vnode和oldVnode进行比较后,生成真实DOM。

    Vue实例挂载的过程是什么? 

    在初始化的最后,如果检测到选项有el属性,则调用vm.$mount方法挂载vm,挂载的目标就是把模板渲染成最终的DOM

    • 第一步:确保vm.$options有render函数。

    因为在不同构建版本上的挂载过程都不一样,所以要对Vue原型上的$mount方法进行函数劫持。

    首先创建一个变量mount将Vue原型上的$mount方法保存到这个变量上。然后Vue原型上的$mount方法被一个新的方法覆盖。在这个新方法中调用mount这个原始方法。

    通过el属性进行获取DOM元素。如果el是字符串,则使用document.querySelector获取DOM元素并赋值给el。如果获取不到,则创建一个空的div元素并赋值给el。如果el不是字符串,默认el是DOM元素,不进行处理。

    判断el是不是html元素或body元素,如果是则给出警告退出程序。

    因为挂载后续过程中需要render函数生成vnode,故要判断$options选项中是否有render函数这个属性,如果有直接调用原始的$mount方法。

    如果没有,则判断template是否存在。若不存在则将el的outerHTML赋值给template。若存在,如果template是字符串且以#开头,通过选择符获取DOM元素获取innerHTML赋值给template,如果template已经是DOM元素类型直接获取innerHTML赋值给template。

    然后将template编译成代码字符串并将代码字符串转成render函数,并赋值到vm.$options的render属性上。

    最后调用原始的$mount方法。

    • 第二步: 在原始的$mount方法,先触发beforeMount钩子函数,然后创建一个Watcher实例,在第二参数传入一个函数vm._update

    该函数是首次渲染和更新渲染作用,参数为render函数(vnode),如果vm._vnode不存在则进行首次渲染。

    同时vnode中被劫持的数据自动收集依赖。当vnode中被劫持的数据变化时候触发对应的依赖,从而触发vm._update进行更新渲染。

    最后触发mounted钩子函数。

    Vue为什么要求组件模板只能有一个根元素? 

    当前的virtualDOM差异和diff算法在很大程度上依赖于每个子组件总是只有一个根元素。 

    axios是什么?怎样使用它?怎么解决跨域的问题? 

    axios 是一个基于 promise 的 HTTP 库,先封装在使用。

    使用proxyTable配置解决跨域问题。

    比如你要调用http://172.16.13.205:9011/getList这个接口

    先在axios.create()配置baseURL增加标志

    const service = axios.create({
      baseURL: '/api',
    });
    

     service.get(getList, {params:data});

    然后在config/index.js文件中配置 

    dev:{
        proxyTable: {
            '/api': {
                target: 'http://172.16.13.205:9011', // 设置你调用的接口域名和端口号
                secure: false,
                changeOrigin: true,// 跨域
                pathRewrite: {
                    '^/api': '' // 去掉标志
                }
            }
        },
    }
    

     配置后要重新npm run dev

    F12中看到请求是http://localhost:8080/api/getList,实际上请求是http://172.16.13.205:9011/getList

    你有使用过JSX吗?说说你对JSX的理解? 

    JSX就是Javascript和XML结合的一种格式。React发明了JSX,利用HTML语法来创建虚拟DOM。当遇到<,JSX就当HTML解析,遇到{就当JavaScript解析。 

    如果想扩展某个现有的Vue组件时,怎么做呢? 

    • 用mixins混入
    • 用extends,比mixins先触发
    • 用高阶组件HOC封装

    Vue渲染大量数据时应该怎么优化?说下你的思路! 

    懒加载和分页 

    vue-loader是什么?它有什么作用? 

    vue-loader是一个webpack的loader,是一个模块转换器,用于把模块原内容按照需求转换成新内容。

    它允许你以一种名为单文件组件 (SFCs)的格式撰写 Vue 组件。可以解析和转换 .vue 文件,提取出其中的逻辑代码 script、样式代码 style、以及 HTML 模版 template,再分别把它们交给对应的loader去处理。 

    展开全文
    weixin_48181168 2021-11-21 17:04:41
  • 本文分享 12 道 vue 高频原理面试题,覆盖了 vue 核心实现原理,其实一个框架的实现原理一篇文章是不可能说完的,希望通过这 12 道问题,让读者对自己的 Vue 掌握程度有一定的认识(B 数),从而弥补自己的不足,更好的掌握 ...

    前言

    本文分享 12 道 vue 高频原理面试题,覆盖了 vue 核心实现原理,其实一个框架的实现原理一篇文章是不可能说完的,希望通过这 12 道问题,让读者对自己的 Vue 掌握程度有一定的认识(B 数),从而弥补自己的不足,更好的掌握 Vue ❤️

    1. Vue 响应式原理

    vue-reactive

    vue-reactive

    核心实现类:

    Observer : 它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新

    Dep : 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个 Dep 实例(里面 subs 是 Watcher 实例数组),当数据有变更时,会通过 dep.notify()通知各个 watcher。

    Watcher : 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种

    Watcher 和 Dep 的关系

    watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。

    依赖收集

    1. initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集

    2. initState 时,对侦听属性初始化时,触发 user watcher 依赖收集

    3. render()的过程,触发 render watcher 依赖收集

    4. re-render 时,vm.render()再次执行,会移除所有 subs 中的 watcer 的订阅,重新赋值。

    派发更新

    1. 组件中对响应的数据进行了修改,触发 setter 的逻辑

    2. 调用 dep.notify()

    3. 遍历所有的 subs(Watcher 实例),调用每一个 watcher 的 update 方法。

    原理

    当创建 Vue 实例时,vue 会遍历 data 选项的属性,利用 Object.defineProperty 为属性添加 getter 和 setter 对数据的读取进行劫持(getter 用来依赖收集,setter 用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。

    每个组件实例会有相应的 watcher 实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有 computed watcher,user watcher 实例),之后依赖项被改动时,setter 方法会通知依赖与此 data 的 watcher 实例重新计算(派发更新),从而使它关联的组件重新渲染。

    一句话总结:

    vue.js 采用数据劫持结合发布-订阅模式,通过 Object.defineproperty 来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发响应的监听回调

    2. computed 的实现原理

    computed 本质是一个惰性求值的观察者。

    computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。

    其内部通过 this.dirty 属性标记计算属性是否需要重新求值。

    当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,

    computed watcher 通过 this.dep.subs.length 判断有没有订阅者,

    有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)

    没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)

    3. computed 和 watch 有什么区别及运用场景?

    区别

    computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。

    watch 侦听器 : 更多的是「观察」的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

    运用场景

    运用场景:

    当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算。

    当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

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

    Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性(Vue 为什么不能检测数组变动 )。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组

    push();
    pop();
    shift();
    unshift();
    splice();
    sort();
    reverse();
    复制代码

    由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。

    Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。

    Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

    5. Vue 中的 key 到底有什么用?

    key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。)

    diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.

    更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。

    更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1),源码如下:

    function createKeyToOldIdx(children, beginIdx, endIdx) {
      let i, key;
      const map = {};
      for (i = beginIdx; i <= endIdx; ++i) {
        key = children[i].key;
        if (isDef(key)) map[key] = i;
      }
      return map;
    }
    复制代码

    6. 谈一谈 nextTick 的原理

    JS 运行机制

    JS 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:

    1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
    2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
    3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
    4. 主线程不断重复上面的第三步。

    event-loop

    event-loop

    主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。

    for (macroTask of macroTaskQueue) {
      // 1. Handle current MACRO-TASK
      handleMacroTask();
    
      // 2. Handle all MICRO-TASK
      for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
      }
    }
    复制代码

    在浏览器环境中 :

    常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate

    常见的 micro task 有 MutationObsever 和 Promise.then

    异步更新队列

    可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

    如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。

    然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

    Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

    在 vue2.5 的源码中,macrotask 降级的方案依次是:setImmediate、MessageChannel、setTimeout

    vue 的 nextTick 方法的实现原理:

    1. vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行

    2. microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕

    3. 考虑兼容问题,vue 做了 microtask 向 macrotask 的降级方案

    7. vue 是如何对数组方法进行变异的 ?

    我们先来看看源码

    const arrayProto = Array.prototype;
    export const arrayMethods = Object.create(arrayProto);
    const methodsToPatch = [
      "push",
      "pop",
      "shift",
      "unshift",
      "splice",
      "sort",
      "reverse"
    ];
    
    /**
     * Intercept mutating methods and emit events
     */
    methodsToPatch.forEach(function(method) {
      // cache original method
      const original = arrayProto[method];
      def(arrayMethods, method, function mutator(...args) {
        const result = original.apply(this, args);
        const ob = this.__ob__;
        let inserted;
        switch (method) {
          case "push":
          case "unshift":
            inserted = args;
            break;
          case "splice":
            inserted = args.slice(2);
            break;
        }
        if (inserted) ob.observeArray(inserted);
        // notify change
        ob.dep.notify();
        return result;
      });
    });
    
    /**
     * Observe a list of Array items.
     */
    Observer.prototype.observeArray = function observeArray(items) {
      for (var i = 0, l = items.length; i < l; i++) {
        observe(items[i]);
      }
    };
    复制代码

    简单来说,Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知 render watcher,执行 update

    8. Vue 组件 data 为什么必须是函数 ?

    new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢?

    因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。

    所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。

    9. 谈谈 Vue 事件机制,手写$on,$off,$emit,$once

    Vue 事件机制 本质上就是 一个 发布-订阅 模式的实现。

    class Vue {
      constructor() {
        //  事件通道调度中心
        this._events = Object.create(null);
      }
      $on(event, fn) {
        if (Array.isArray(event)) {
          event.map(item => {
            this.$on(item, fn);
          });
        } else {
          (this._events[event] || (this._events[event] = [])).push(fn);
        }
        return this;
      }
      $once(event, fn) {
        function on() {
          this.$off(event, on);
          fn.apply(this, arguments);
        }
        on.fn = fn;
        this.$on(event, on);
        return this;
      }
      $off(event, fn) {
        if (!arguments.length) {
          this._events = Object.create(null);
          return this;
        }
        if (Array.isArray(event)) {
          event.map(item => {
            this.$off(item, fn);
          });
          return this;
        }
        const cbs = this._events[event];
        if (!cbs) {
          return this;
        }
        if (!fn) {
          this._events[event] = null;
          return this;
        }
        let cb;
        let i = cbs.length;
        while (i--) {
          cb = cbs[i];
          if (cb === fn || cb.fn === fn) {
            cbs.splice(i, 1);
            break;
          }
        }
        return this;
      }
      $emit(event) {
        let cbs = this._events[event];
        if (cbs) {
          const args = [].slice.call(arguments, 1);
          cbs.map(item => {
            args ? item.apply(this, args) : item.call(this);
          });
        }
        return this;
      }
    }
    复制代码

    10. 说说 Vue 的渲染过程

    render

    render

    1. 调用 compile 函数,生成 render 函数字符串 ,编译过程如下:
    • parse 函数解析 template,生成 ast(抽象语法树)

    • optimize 函数优化静态节点 (标记不需要每次都更新的内容,diff 算法会直接跳过静态节点,从而减少比较的过程,优化了 patch 的性能)

    • generate 函数生成 render 函数字符串

    1. 调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象

    2. 调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素

    11. 聊聊 keep-alive 的实现原理和缓存策略

    export default {
      name: "keep-alive",
      abstract: true, // 抽象组件属性 ,它在组件实例建立父子关系的时候会被忽略,发生在 initLifecycle 的过程中
      props: {
        include: patternTypes, // 被缓存组件
        exclude: patternTypes, // 不被缓存组件
        max: [String, Number] // 指定缓存大小
      },
    
      created() {
        this.cache = Object.create(null); // 缓存
        this.keys = []; // 缓存的VNode的键
      },
    
      destroyed() {
        for (const key in this.cache) {
          // 删除所有缓存
          pruneCacheEntry(this.cache, key, this.keys);
        }
      },
    
      mounted() {
        // 监听缓存/不缓存组件
        this.$watch("include", val => {
          pruneCache(this, name => matches(val, name));
        });
        this.$watch("exclude", val => {
          pruneCache(this, name => !matches(val, name));
        });
      },
    
      render() {
        // 获取第一个子元素的 vnode
        const slot = this.$slots.default;
        const vnode: VNode = getFirstComponentChild(slot);
        const componentOptions: ?VNodeComponentOptions =
          vnode && vnode.componentOptions;
        if (componentOptions) {
          // name不在inlcude中或者在exlude中 直接返回vnode
          // check pattern
          const name: ?string = getComponentName(componentOptions);
          const { include, exclude } = this;
          if (
            // not included
            (include && (!name || !matches(include, name))) ||
            // excluded
            (exclude && name && matches(exclude, name))
          ) {
            return vnode;
          }
    
          const { cache, keys } = this;
          // 获取键,优先获取组件的name字段,否则是组件的tag
          const key: ?string =
            vnode.key == null
              ? // same constructor may get registered as different local components
                // so cid alone is not enough (#3269)
                componentOptions.Ctor.cid +
                (componentOptions.tag ? `::${componentOptions.tag}` : "")
              : vnode.key;
          // 命中缓存,直接从缓存拿vnode 的组件实例,并且重新调整了 key 的顺序放在了最后一个
          if (cache[key]) {
            vnode.componentInstance = cache[key].componentInstance;
            // make current key freshest
            remove(keys, key);
            keys.push(key);
          }
          // 不命中缓存,把 vnode 设置进缓存
          else {
            cache[key] = vnode;
            keys.push(key);
            // prune oldest entry
            // 如果配置了 max 并且缓存的长度超过了 this.max,还要从缓存中删除第一个
            if (this.max && keys.length > parseInt(this.max)) {
              pruneCacheEntry(cache, keys[0], keys, this._vnode);
            }
          }
          // keepAlive标记位
          vnode.data.keepAlive = true;
        }
        return vnode || (slot && slot[0]);
      }
    };
    复制代码

    原理

    1. 获取 keep-alive 包裹着的第一个子组件对象及其组件名

    2. 根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例

    3. 根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)

    4. 在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 的设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key)

    5. 最后组件实例的 keepAlive 属性设置为 true,这个在渲染和执行被包裹组件的钩子函数会用到,这里不细说

    LRU 缓存淘汰算法

    LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

    LRU

    LRU

    keep-alive 的实现正是用到了 LRU 策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]

    12. vm.$set()实现原理是什么?

    受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。

    由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

    对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。

    那么 Vue 内部是如何解决对象新增属性不能响应的问题的呢?

    export function set(target: Array<any> | Object, key: any, val: any): any {
      // target 为数组
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        // 修改数组的长度, 避免索引>数组长度导致splice()执行有误
        target.length = Math.max(target.length, key);
        // 利用数组的splice变异方法触发响应式
        target.splice(key, 1, val);
        return val;
      }
      // target为对象, key在target或者target.prototype上 且必须不能在 Object.prototype 上,直接赋值
      if (key in target && !(key in Object.prototype)) {
        target[key] = val;
        return val;
      }
      // 以上都不成立, 即开始给target创建一个全新的属性
      // 获取Observer实例
      const ob = (target: any).__ob__;
      // target 本身就不是响应式数据, 直接赋值
      if (!ob) {
        target[key] = val;
        return val;
      }
      // 进行响应式处理
      defineReactive(ob.value, key, val);
      ob.dep.notify();
      return val;
    }
    复制代码
    1. 如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式

    2. 如果目标是对象,判断属性存在,即为响应式,直接赋值

    3. 如果 target 本身就不是响应式,直接赋值

    4. 如果属性不是响应式,则调用 defineReactive 方法进行响应式处理

    顺便推荐下我的小说网站

    虫鱼小说网

    知秋小说网 

    这两个小说网站收录了当前最火热的网络小说,免费提供高质量的小说最新章节,是广大网络小说爱好者必备的小说阅读网。免费在线无弹窗阅读,没有中间商赚差价,欢迎收藏哟

    展开全文
    allen_he_123 2021-08-07 09:41:28
  • 前言最近拿到一套高级前端的Vue面试题,从头往下看了一遍,居然大部分都是一知半解的,遂准备一道一道的认真学习总结,立志做一位Vue高级开发者!看一下你是...

    前言

    最近拿到一套高级前端的Vue面试题,从头往下看了一遍,居然大部分都是一知半解的,遂准备一道一道的认真学习总结,立志做一位Vue高级开发者!

    看一下你是否也对上面的34个问题一知半解,如果和我差不多的话就来一起学习吧!

    如何理解MVVM原理?

    提到MVVM,很多前端开发者都会想到Vue的双向绑定,然而它们并不能划等号,MVVM是一种软件架构模式,而Vue只是一种在前端层面上的实现,其实不单在Vue里,在很多Web 框架应用里都有相关的实现。MVVM模式到底是什么呢?要说到MVVM这种模式,则必须要提及另一种大多数开发者都能耳熟能详的模式,就是MVC模式。

    什么是MVC?

    在前几年,前后端完全分离开之前,很多很火的后端框架都会说自己是支持MVC模式,像JAVA的SpringMVC、PHP的smarty、Nodejs的express和Koa,那么MVC的模式到底是什么样的?先看看下面这张经典的MVC模型图,Model(模型)、View(视图)、 Controller(控制器)相互依赖关系的三部分组成模型。认识一下这三部分具体是指什么。

    Model

    这里的Model在MVC中实际是数据模型的概念,可以把它当成从数据库里查出来后的一条数据,或者是将查询出来的元数据经过裁剪或者处理后的一个特定数据模型结构。

    View

    View是视图,是将数据内容呈现给用户肉眼的界面层,View层最终会将数据模型下的信息,渲染显示成人类能易于识别感知的部分。

    Controller

    Controller是数据模型与View之间的桥梁层,实际界面层的各种变化都要经过它来控制,而且像用户从界面提交的数据也会经过Controller的组装检查生成数据模型,然后改变数据库里的数据内容。

    MVC的使用

    像接触过MVC框架的同学就知道,如果想把数据从数据库里渲染到页面上,先要查询完数据库后,将拿到的元数据进行一些处理,一般会删掉无用的字段,或者进行多个数据模型间的数据聚合,然后再给到页面模板引擎(ejs,Thymeleaf等)进行数据组装,最后组合完成进行渲染后生成HTML格式文件供浏览器展示使用。

    像前面提到的各大支持MVC模式的Web开发框架,在前后端彻底分离之后就很少再提了。因为前端独立开发发布,实际相对原来的MVC模式是少了View这一层,这也让新的概念Restful出现在我们的视野里,很多新的框架又开始支持提供这种前端控制轻量级模式下的适配方案。

    但是前后端分离的出现后,MVC就此没有了吗?当然不是。实际对于MVC模式没有特别明确的概念,在前后端分离之后可以尝试从不同的角度去看。可以理解整个系统在原先的MVC基础上View层进行细化,把整个前端项目当成一个View层,也可以从前端视角去看,Restful接口返回的Json数据当成一个数据模型,作为MVC的Model层,而前端Javascript自身对数据的处理是Contrller层,真正的页面渲染结果是View层。

    下面以前端视角下的MVC模式中举个例子,接口返回的数据Model模型与View页面之间由Controller连接,来完成系统中的数据展示。

    <!--view-->
    <html>
      ...
      <div>
        <span id="name"></span>
        <div id="data"></div>
      </div>
      ...
    </html>
    <script>
      // 生成model数据模型
      function getDataApi() {
        // 模拟接口返回
        return {
          name: 'mvc',
          data: 'mvc 数据信息'
        }
      }
    
      // controller控制逻辑
      function pageController() {
        const result = getDataApi();
        document.getElementById('name').innerText = `姓名:${result.name}`;
        document.getElementById('data').innerText = result.data;
      }
    </script>
    

    什么是MVVM?

    随着前端对于控制逻辑的越来越轻量,MVVM模式作为MVC模式的一种补充出现了,万变不离其宗,最终的目的都是将Model里的数据展示在View视图上,而MVVM相比于MVC则将前端开发者所要控制的逻辑做到更加符合轻量级的要求。

    ViewModel

    在Model和View之间多了叫做View-Model的一层,将模型与视图做了一层绑定关系,在理想情况下,数据模型返回什么试图就应该展示什么,看看下面这个例子。

    <!--view页面-->
    <html>
      ...
      <div>
        <span vm-bind-key="name"></span>
        <div vm-bind-key="data"></div>
      </div>
      ...
    </html>
    <script>
      // 生成model数据模型
      function getDataApi() {
        // 模拟接口返回
        return {
          name: 'mvc',
          data: 'mvc 数据信息'
        }
      }
    
      // ViewModel控制逻辑
      function pageViewModel() {
        const result = getDataApi();
        return result;
      }
    </script>
    

    上面作为理想情况下例子,在ViewModel引入之后,视图完全由接口返回数据驱动,由开发者所控制的逻辑非常轻量。不过这里要说明的是,在MVVM模式下,Controller控制逻辑并非就没了,像操作页面DOM响应的逻辑被SDK(如Vue的内部封装实现)统一实现了,像不操作接口返回的数据是因为服务端在数据返回给前端前已经操作好了。

    例子里pageViewModel函数的实现是非常关键的一步,如何将数据模型与页面视图绑定起来呢?在目前的前端领域里有三类实现,Angularjs的主动轮询检查新旧值变化更新视图、Vue利用ES5的Object.defineProperty的getter/setter方法绑定、backbone的发布订阅模式,从主动和被动的方式去实现了ViewModel的关系绑定,接下来主要看看Vue中的MVVM的实现。

    Vue2.0中的MVVM实现

    Vue2.0的MVVM实现中,对View-Model的实现本质利用的ES5的Object.defineProperty方法,当Object.defineProperty方法在给数据Model对象定义属性的时候先挂载一些方法,在这些方法里实现与界面的值绑定响应关系,当应用的属性被读取或者写入的时候便会触发这些方法,从而达到数据模型里的值发生变化时同步响应到页面上。

    Vue的响应式原理

    // html
    <body>
      <div>
        <span>{{name}}</span>
        <span>{{data}}</span>
      </div>
    <body>
    
    //js
    <script src="vue.js"></script>
    <script>
      // 生成model数据模型
      function getDataApi() {
        // 模拟接口返回
        return {
          name: 'mvc',
          data: 'mvc 数据信息'
        }
      }
      new Vue({
        el: 'body',
        data() {
          return {
            name:'',
            data: '',
          }
        },
        mounted() {
          const result = getDataApi();
          this.name = result.name;
          this.data = result.data;
        }
    })
    </script>
    

    当new Vue在实例化的时候,首先将data方法里返回的对象属性都挂载上setter方法,而setter方法里将页面上的属性进行绑定,当页面加载时,浏览器提供的DOMContentloaded事件触发后,调用mounted挂载函数,开始获取接口数据,获取完成后给data里属性赋值,赋值的时候触发前面挂载好的setter方法,从而引起页面的联动,达到响应式效果。

    简易实现Object.defineProperty下的绑定原理

    // html
    <body>
      <span id="name"></span>
    <body>
    <script>
     var data = {
        name: ''
     };
      // Data Bindings
      Object.defineProperty(data, 'name', {
        get: function(){},
        set: function(newValue){ // 页面响应处理
          document.getElementById('name').innerText = newValue
          data.name = value
        },
        enumerable: true,
        configurable: true
      });
      // 页面DOM listener
      document.getElementById('name').onchange = function(e) {
        data.name = e.target.value;
      }
    </script>
    

    实现Vue3.0版本的MVVM

    这里采用Vue3.0最新的实现方式,用Proxy和Reflect来替代Object.definePropertypry的方式。至于Vue3.0为何不再采用2.0中Object.defineProperty的原因,我会在后续详写,先来介绍一下ES6里的Proxy与Reflect。

    Proxy

    Proxy是ES6里的新构造函数,它的作用就是代理,简单理解为有一个对象,不想完全对外暴露出去,想做一层在原对象操作前的拦截、检查、代理,这时候你就要考虑Proxy了。

    const myObj = {
      _id: '我是myObj的ID',
      name: 'mvvm',
      age: 25
    }
    
    const myProxy = new Proxy(myObj, {
      get(target, propKey) {
        if (propKey === 'age') {
          console.log('年龄很私密,禁止访问');
          return '*';
        }
        return target[propKey];
      },
      set(target, propKey, value, receiver) {
        if (propKey === '_id') {
          console.log('id无权修改');
          return;
        }
        target[propKey] = value + (receiver.time || '');
      },
      // setPrototypeOf(target, proto) {},
      // apply(target, object, args) {},
      // construct(target, args) {},
      // defineProperty(target, propKey, propDesc) {},
      // deleteProperty(target, propKey) {},
      // has(target, propKey) {},
      // ownKeys(target) {},
      // isExtensible(target) {},
      // preventExtensions(target) {},
      // getOwnPropertyDescriptor(target, propKey) {},
      // getPrototypeOf(target) {},
    });
    
    myProxy._id = 34;
    // id无权修改
    console.log(`age is: ${myProxy.age}`);
    //年龄很私密,禁止访问
    // age is: *
    
    myProxy.name = 'my name is Proxy';
    console.log(myProxy);
    // { _id: '我是myObj的ID', name: 'my name is Proxy', age: 25}
    
    
    const newObj = {
      time: ` [${new Date()}]`,
    };
    // 原对象原型链赋值
    Object.setPrototypeOf(myProxy, newObj);
    myProxy.name = 'my name is newObj';
    console.log(myProxy.name);
    //my name is newObj [Thu Mar 19 2020 18:33:22 GMT+0800 (GMT+08:00)]
    

    Reflect

    Reflect是ES6里的新的对象,非构造函数,不能用new操作符。可以把它跟Math类比,Math是处理JS中数学问题的方法函数集合,Reflect是JS中对象操作方法函数集合,它暴露出来的方法与Object构造函数所带的静态方法大部分重合,实际功能也类似,Reflect的出现一部分原因是想让开发者不直接使用Object这一类语言层面上的方法,还有一部分原因也是为了完善一些功能。Reflect提供的方法还有一个特点,完全与Proxy构造函数里Hander参数对象中的钩子属性一一对应。

    看下面一个改变对象原型的例子。

    const myObj = {
      _id: '我是myObj的ID',
      name: 'mvvm',
      age: 25
    }
    
    const myProxy = new Proxy(myObj, {
      get(target, propKey) {
        return target[propKey];
      },
      set(target, propKey, value, receiver) {
        target[propKey] = value + (receiver.time || '');
      },
      setPrototypeOf(target, proto) {
        if (proto.status === 'enable') {
          Reflect.setPrototypeOf(target, proto);
          return true;
        }
        return false;
      },
    });
    
    
    const newObj = {
      time: ` [${new Date()}]`,
      status: 'sable'
    };
    // 原对象原型链赋值
    const result1 = Reflect.setPrototypeOf(myProxy, {
      time: ` [${new Date()}]`,
      status: 'disable'
    });
    myProxy.name = 'first set name'
    console.log(result1)  //false
    console.log(myProxy.name);  //first set name
    
    // 原对象原型链赋值
    const result2 = Reflect.setPrototypeOf(myProxy, {
      time: ` [${new Date()}]`,
      status: 'enable'
    });
    
    myProxy.name = 'second set name'
    console.log(result1)  //true
    console.log(myProxy.name); //second set name [Thu Mar 19 2020 19:43:59 GMT+0800 (GMT+08:00)]
    
    
    /*当执行到这里时直接报错了*/
    // 原对象原型链赋值
    Object.setPrototypeOf(myProxy, {
      time: ` [${new Date()}]`,
      status: 'disable'
    });
    myProxy.name = 'third set name'
    console.log(myProxy.name);
    
    

    解释一下上面的这段代码,通过Reflec.setPrototypeOf方法修改原对象原型时,必须经过Proxy里hander的挂载的setPrototypeOf挂载函数,在挂载函数里进行条件proto.status是否是enable筛选后,再决定是否真正修改原对象myObj的原型,最后返回true或者false来告知外部原型是否修改成功。

    这里还有一个关键点,就是在代码执行到原有的Object.setPrototypeOf方法时,程序则直接抛错,这其实也是Reflect出现的一个原因,即使现在ES5里的Object有同样的功能,但是Reflect实现的更友好,更适合开发者开发应用程序。

    实现MVVM

    接下来使用上面的Proxy和Reflect来实现MVVM,这里将data和Proxy输出到全局Window下,方便我们模拟数据双向联动的效果。

    <!DOCTYPE html>
    <html>
      <div>
        name: <input id="name" />
        age: <input id="age" />
      </div>
    </html>
    <script>
    // 与页面绑定
    const data = {
      name: '',
      age: 0
    }
    
    // 暴露到外部,便于查看效果
    window.data = data;
    window.myProxy = new Proxy(data, {
      set(target, propKey, value) {
        // 改变数据Model时修改页面
        if (propKey === 'name') {
          document.getElementById('name').value = value;
        } else if (propKey === 'age') {
          document.getElementById('age').value = value;
        }
        Reflect.set(...arguments);
      },
    });
    
    // 页面变化改变Model内数据
    document.getElementById('name').onchange = function(e) {
      Reflect.set(data, 'name', e.target.value);
    }
    document.getElementById('age').onchange = function(e) {
      Reflect.set(data, 'age', e.target.value);
    }
    </script>
    

    先打印了data,然后模拟有异步数据过来,手动修改data里的数据window.myProxy.age=25,这时候页面上的age联动变化为25,再次打印了查看data。接下来在页面上手动输入name,输入完成后触发输入框的onchange事件后,再次查看data,此时model里的数据已经变化为最新的与页面保持一致的值。

    总结

    上面整篇内容介绍了MVC和MVVM两种模式的差异性,还介绍了在Vue在2.0和3.0中MVVM的实现,最后利用Vue3.0中提供的原理思路来实现了一次View和Model的双向绑定。那么Vue2.0升级到3.0为什么要从将双向绑定的实现从Object.definePropertypry变成ES6的Proxy来实现呢?下一篇再讲。

    如上内容均为自己总结,难免会有错误或者认识偏差,如有问题,希望大家留言指正,以免误人,若有什么问题请留言,会尽力回答之。如果对你有帮助不要忘了分享给你的朋友或者点击右下方的“在看”哦!也可以关注作者,查看历史文章并且关注最新动态,助你早日成为一名全栈工程师!

    展开全文
    qq_27053493 2020-03-20 08:22:09
  • weixin_39907591 2020-12-24 19:13:03
  • weixin_42898315 2021-02-17 21:39:29
  • weixin_44475093 2021-03-15 00:47:21
  • webyouxuan 2020-08-19 08:31:00
  • weixin_45735355 2021-10-09 13:53:34
  • weixin_43299180 2021-05-25 10:35:19
  • weixin_40599109 2020-12-09 20:08:56
  • qq_27053493 2020-04-06 08:56:00
  • qq_37481512 2019-07-01 18:28:50
  • qq_37174991 2021-04-13 11:58:01
  • weixin_31936393 2020-12-24 19:15:03
  • zhangpei1314 2021-07-14 13:05:27
  • wjindu666 2020-07-17 09:39:59
  • weixin_44499465 2020-07-15 21:16:27
  • is_monkey1 2021-03-14 19:13:57
  • a754334599 2021-05-26 20:35:13
  • doinb_6 2018-08-02 11:05:56
  • weixin_40589472 2021-04-09 10:24:32
  • HwH66998 2020-04-25 22:00:14
  • xiyue001 2019-06-20 15:58:58
  • weixin_52092151 2021-07-08 22:41:15
  • ch834301 2019-08-15 08:45:00
  • weixin_59728765 2021-10-18 19:28:24
  • weixin_50959040 2020-09-23 15:02:44
  • qq_34273059 2021-05-25 17:48:35

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,459
精华内容 1,383
热门标签
关键字:

vue原理高级面试题

vue 订阅
友情链接: LASSESRC.rar