vue2.x高阶问题,你能答多少
有句老话说,在父母那里我们永远是孩子,同样在各位大佬这里,我永远是菜鸡🐣🐣🐣。不管怎样,学习的激情永远不可磨灭。答案如有错误,感谢指教🌚
首发个人博客
种一棵树,最好的时机是十年前,其次是现在

vue源码中值得学习的点
- 柯里化: 一个函数原本有多个参数, 只传入- 一个参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构
- 偏函数: 一个函数原本有多个参数, 只传入- 一部分参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构
- 高阶函数: 一个函数- 参数是一个函数, 该函数对参数这个函数进行加工, 得到一个函数, 这个加工用的函数就是高阶函数
- ... 
vue 响应式系统
简述:vue 初始化时会用Object.defineProperty()给data中每一个属性添加getter和setter,同时创建dep和watcher进行依赖收集与派发更新,最后通过diff算法对比新老vnode差异,通过patch即时更新DOM
简易图解:

详细版本
可以参考下图片引用地址: 图解 Vue 响应式原理
Vue的数据为什么频繁变化但只会更新一次
- 检测到数据变化 
- 开启一个队列 
- 在同一事件循环中缓冲所有数据改变 
- 如果同一个 - 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来监听
参考
- 为什么defineProperty不能检测到数组长度的变化 
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 渲染过程

- 调用 - compile函数,生成 render 函数字符串 ,编译过程如下:
- parse 使用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST。 - 模板 -> AST (最消耗性能)
- optimize 遍历AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点, - 优化runtime的性能
- generate 将最终的AST转化为render函数字符串 
- 调用 - new Watcher函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
- 调用 - 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组件接受三个属性参数:include、exclude、max
- 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;
        }
      } );
    } 
原文地址
- https://juejin.cn/post/6921911974611664903] 
