vue2.x高阶问题,你能答多少

32,243 阅读2分钟

有句老话说,在父母那里我们永远是孩子,同样在各位大佬这里,我永远是菜鸡🐣🐣🐣。不管怎样,学习的激情永远不可磨灭。答案如有错误,感谢指教🌚

首发个人博客

种一棵树,最好的时机是十年前,其次是现在

vue源码中值得学习的点

  1. 柯里化: 一个函数原本有多个参数, 只传入一个参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构
  2. 偏函数: 一个函数原本有多个参数, 只传入一部分参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构
  3. 高阶函数: 一个函数参数是一个函数, 该函数对参数这个函数进行加工, 得到一个函数, 这个加工用的函数就是高阶函数
  4. ...

vue 响应式系统

简述: vue 初始化时会用Object.defineProperty()给data中每一个属性添加gettersetter,同时创建depwatcher进行依赖收集派发更新,最后通过diff算法对比新老vnode差异,通过patch即时更新DOM

简易图解:

详细版本

可以参考下图片引用地址: 图解 Vue 响应式原理

Vue的数据为什么频繁变化但只会更新一次

  1. 检测到数据变化
  2. 开启一个队列
  3. 在同一事件循环中缓冲所有数据改变
  4. 如果同一个 watcher (watcherId相同)被多次触发,只会被推入到队列中一次

不优化,每一个数据变化都会执行: setter->Dep->Watcher->update->run

优化后:执行顺序update -> queueWatcher -> 维护观察者队列(重复id的Watcher处理) -> waiting标志位处理 -> 处理$nextTick(在为微任务或者宏任务中异步更新DOM)

vue使用Object.defineProperty() 的缺陷

数组的length属性被初始化configurable false,所以想要通过get/set方法来监听length属性是不可行的。

vue中通过重写了七个能改变原数组的方法来进行数据监听

对象还是使用Object.defineProperty()添加get和set来监听

参考

Vue.nextTick()原理

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

源码实现:Promise > MutationObserver > setImmediate > setTimeout

参考文章:浅析Vue.nextTick()原理

computed 的实现原理

computed 本质是一个惰性求值的观察者computed watcher。其内部通过 this.dirty 属性标记计算属性是否需要重新求值。

  • 当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,computed watcher 通过 this.dep.subs.length 判断有没有订阅者,
  • 有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)
  • 没有的话,仅仅把 this.dirty = true (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)

watch 的理解

watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听

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

vue diff 算法

  • 只对比父节点相同的新旧子节点(比较的是Vnode),时间复杂度只有O(n)
  • 在 diff 比较的过程中,循环从两边向中间收拢

新旧节点对比过程

1、先到 不需要移动的相同节点,借助key值找到可复用的节点是,消耗最小

2、再找相同但是需要移动的节点,消耗第二小

3、最后找不到,才会去新建删除节点,保底处理

注意:新旧节点对比过程,不会对这两棵Vnode树进行修改,而是以比较的结果直接对 真实DOM 进行修改

Vue的patch是即时的,并不是打包所有修改最后一起操作DOM(React则是将更新放入队列后集中处理)

参考文章:Vue 虚拟dom diff原理详解

vue 渲染过程

  1. 调用 compile 函数,生成 render 函数字符串 ,编译过程如下:
  • parse 使用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST。模板 -> AST (最消耗性能)
  • optimize 遍历AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点,优化runtime的性能
  • generate 将最终的AST转化为render函数字符串
  1. 调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
  2. 调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素

结合源码,谈一谈vue生命周期

vue 生命周期官方图解

Vue 中的 key 到底有什么用?

key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速

更准确 : 因为带 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;
}

注意:在没有key的情况下,会更快。感谢评论区老哥fengyangyang的提醒: 引用官网的话:key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

vue-router 路由模式有几种

默认值: "hash" (浏览器环境) | "abstract" (Node.js 环境)

可选值: "hash" | "history" | "abstract"

配置路由模式:

  • hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器。
  • history: 依赖 HTML5 History API 和服务器配置。
  • abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

说一说keep-alive实现原理

定义

keep-alive组件接受三个属性参数:includeexcludemax

  • include 指定需要缓存的组件name集合,参数格式支持String, RegExp, Array。当为字符串的时候,多个组件名称以逗号隔开。
  • exclude 指定不需要缓存的组件name集合,参数格式和include一样。
  • max 指定最多可缓存组件的数量,超过数量删除第一个。参数格式支持String、Number。

原理

keep-alive实例会缓存对应组件的VNode,如果命中缓存,直接从缓存对象返回对应VNode

LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。(墨菲定律:越担心的事情越会发生)

对对象属性访问的解析方法

eg:访问 a.b.c.d

函数柯里化 + 闭包 + 递归

    function createGetValueByPath( path ) {
      let paths = path.split( '.' ); // [ xxx, yyy, zzz ]
      
      return function getValueByPath( obj ) {
        let res = obj;
        let prop;
        while( prop = paths.shift() ) {
          res = res[ prop ];
        }
        return res;
      }
    }
    
    let getValueByPath = createGetValueByPath( 'a.b.c.d' );
    
    var o = {
      a: {
        b: {
          c: {
            d: {
              e: '正确了'
            }
          }
        }
      }
    };
    var res = getValueByPath( o );
    console.log( res );

vue中针对7个数组方法的重写

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

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse"
];

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;
  });
});

Observer.prototype.observeArray = function observeArray(items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

vue处理响应式 defineReactive 实现

    // 简化后的版本 
    function defineReactive( target, key, value, enumerable ) {
      // 折中处理后, this 就是 Vue 实例
      let that = this;

      // 函数内部就是一个局部作用域, 这个 value 就只在函数内使用的变量 ( 闭包 )
      if ( typeof value === 'object' && value != null && !Array.isArray( value ) ) {
        // 是非数组的引用类型
        reactify( value ); // 递归
      }

      Object.defineProperty( target, key, {
        configurable: true,
        enumerable: !!enumerable,

        get () {
          console.log( `读取 ${key} 属性` ); // 额外
          return value;
        },
        set ( newVal ) {
          console.log( `设置 ${key} 属性为: ${newVal}` ); // 额外

          value = reactify( newVal );

        }
      } );
    }

vue响应式 reactify 实现

// 将对象 o 响应式化
    function reactify( o, vm ) {
      let keys = Object.keys( o );

      for ( let i = 0; i < keys.length; i++ ) {
        let key = keys[ i ]; // 属性名
        let value = o[ key ];
        if ( Array.isArray( value ) ) {
          // 数组
          value.__proto__ = array_methods; // 数组就响应式了
          for ( let j = 0; j < value.length; j++ ) {
            reactify( value[ j ], vm ); // 递归
          }
        } else {
          // 对象或值类型
          defineReactive.call( vm, o, key, value, true );
        }
      }
    }

为什么访问data属性不需要带data

vue中访问属性代理this.data.xxx 转换 this.xxx的实现

    /** 将 某一个对象的属性 访问 映射到 对象的某一个属性成员上 */
    function proxy( target, prop, key ) {
      Object.defineProperty( target, key, {
        enumerable: true,
        configurable: true,
        get () {
          return target[ prop ][ key ];
        },
        set ( newVal ) {
          target[ prop ][ key ] = newVal;
        }
      } );
    }

参考文章(除文章已指出的)