loading

【vue】🔥vue2.x底层的问题,你能答多少

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

# 首发个人博客

种一棵树,最好的时机是十年前,其次是现在 (opens new window)

# vue 源码中值得学习的点

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

# vue 响应式系统

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

# 简易图解:

# 详细版本

可以参考下图片引用地址: 图解 Vue 响应式原理 (opens new window)

# 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()原理 (opens new window)

# 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 原理详解 (opens new window)

# 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 生命周期官方图解 (opens new window)

# 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;
}
1
2
3
4
5
6
7
8
9

# 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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 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]);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 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);
    },
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 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);
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 为什么访问 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;
    },
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13

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

最近更新时间: 2021/06/17 10:13:36
最近更新
01
2023/07/03 00:00:00
02
2023/04/22 00:00:00
03
2023/02/16 00:00:00