Skip to content

Vue 2.0

导航目录

Vue 挂载流程

Vue 挂载流程是组件从创建到渲染到页面的完整过程,主要包括以下步骤:

  1. 创建 Vue 实例并传入配置项,对配置项进行 mergeOptions 合并,初始化 options 配置项
  2. 初始化组件上的 propsdatacomputedwatch 等选项
  3. 如果有 el 属性,会调用 $mount 方法,通过 compile 函数 把模板转换成 render 函数
  4. 模板转换完成后执行 mountComponent 方法,实例化渲染 Watcher,将组件渲染、更新的控制权交给 Watcher
  5. 当 render 函数被调用时,如果响应式数据有取值操作,dep 订阅器依赖收集当前的 Watcher
  6. 最终调用 patch 函数将 Vnode 转换成真实的 DOM 并渲染在页面(同时每一个虚拟节点的 el 属性会对应一个真实的 DOM,后续更新时复用)
  7. 当响应式数据发生变化后,会触发 dep 依赖通知渲染 watcher 更新视图

vue2lifecycle

Vue 数据响应式处理

Vue 2.0 是通过 Object.defineProperty 进行属性代理,实现数据响应式的。主要流程如下:

  1. 如果 data 数据不是对象或者已经被代理过,不做任何处理,直接返回
  2. 如果是对象,会给当前的对象创建一个 dep 订阅器,让当前的对象也具有收集 Watcher 的能力,同时挂载一个 __ob__ 属性指向当前的 observer 实例(主要是用来解决数组问题)
  3. 递归遍历对象的每一个属性,给每一个属性新增 getter/setter 拦截,同时给每一个属性关联一个 dep 订阅器,用来收集、触发 Watcher
  4. 如果是数组,会对 "push", "pop", "unshift", "shift", "reverse", "sort", "splice" 等方法进行 AOP 拦截,同时会遍历数组,让数组中的对象进行代理
  5. 组件在渲染时,如果有取值操作会触发 get 拦截,会让 dep 订阅器去收集当前的渲染 watcher(如果值是一个对象,会让当前的对象也收集渲染的 watcher,解决数组改变更新视图的问题)
  6. 当响应式数据发生变化时,会触发 setter 拦截,让 dep 去通知收集的 watcher 更新视图(如果新的值是一个对象,也会进行代理)

数据的特殊处理

数组的特殊处理:当数组有 pushpopunshiftshiftreversesortsplice 操作时,会进行 AOP 拦截,具体处理步骤如下:

  1. 首先会通过 call 调用数组原有的函数,更新数组的值
  2. 判断是否有新增操作(push、unshift、splice),如果有会对新增的元素(如果是对象)进行代理
  3. 最终找到自身的 __ob__ 属性,触发 dep 去通知收集的 watcher 更新视图
js
const oldArrayPrototype = Array.prototype;
export const proto = Object.create(oldArrayPrototype);

["push", "pop", "unshfit", "shift", "reverse", "sort", "splice"].forEach(
  (method) => {
    proto[method] = function (...args) {
      let ob = this.__ob__;
      let r = oldArrayPrototype[method].call(this, ...args);
      let inserted;
      switch (method) {
        case "push":
        case "unshift": // 前后新增
          inserted = args;
        case "splice":
          // arr.splice(0,1,新增的内容)
          // 如果是splice,参数大于2个后门的都是新增的值
          inserted = args.slice(2);
        default:
          break;
      }
      // 数组发生变化,通知dep更新
      ob.dep.notify();
      // 如果有新增,需要新增的做代理
      if (inserted) ob.observeArray(inserted);
      return r;
    };
  }
);

模板中数组的 stringify 处理

在模板中访问数组时,会进行 stringify 处理,具体表现为:

  • 如果数组里有对象,会访问对象的每一个属性触发 getter 做依赖收集
  • 如果数组里有数组,由于数组本身没有 getter 拦截,需要递归让每一个数组收集 watcher
js
let arr = [1, [2, 3, [4, 5]], { name: "hl" }];
// 如果数组里有对象,会访问对象的每一个属性触发 get 做依赖收集
setTimeout(() => {
  app.arr[2].name = "hulei";
}, 6000);

// 如果数组里有数组,但是数组本身没有 get 操作,需要递归让每一个数组收集 watcher
setTimeout(() => {
  app.arr[1].push(6);
}, 9000);

setTimeout(() => {
  app.arr[1][2].push(6);
}, 12000);
js
// 每一个对象的属性都会关联一个dep实例
const dep = new Dep();
Object.defineProperty(target, key, {
  get: () => {
    if (Dep.target) {
      dep.depend();
      if (childOb) {
        // 如果对象中有数组,让数组本身也依赖收集
        // 主要是解决数组更新问题,数组不会被拦截
        childOb.dep.depend();

        // 如果数组里有数组,但是数组本身没有 get 操作,需要递归让每一个数组收集 watcher
        if (Array.isArray(value)) {
          this.dependArray(value);
        }
      }
    }
    return value;
  },
});

Vue watch 监听

watch 是基于 Watcher 来实现的,用于监听数据变化并触发回调执行。主要流程如下:

  1. watch 在初始化时,会实例化一个 Watcher,将回调执行权交给它,watcher 实例本身会存储监听的值和更新的watch 回调
  2. Watcher 会立即调用 get 函数,对监听的属性进行访问,此时触发 getter 拦截,让当前的属性触发 dep 订阅器依赖收集当前的 Watcher
  3. 当监听的属性发生变化后,触发 dep 去通知收集的 Watcher 执行 watch 回调
js
Vue.prototype.$watch = function (key, handler) {
  new Watcher(this, key, handler, { user: true });
};

Vue computed 计算属性

computed 也是基于 Watcher 来实现的,具有缓存特性。主要流程如下:

  1. computed 在初始化时不会立马执行,而是把所有的计算属性存储在组件实例上
  2. 当计算属性被访问时,如果被缓存过直接返回缓存的值
  3. 如果没有缓存,调用计算属性函数 evaluate,进行取值操作(触发 getter 拦截,让 dep 会收集当前的计算属性 watcher),并且标记为已经缓存
  4. 如果计算属性里的响应式数据发生改变,会触发 getter 拦截,让 dep 去通知计算属性 watcher 取消缓存标记
js
function initComputed(vm) {
  let computed = vm.$options.computed;
  vm._computedWatchers = {};
  for (const key in computed) {
    vm._computedWatchers[key] = new Watcher(vm, computed[key], () => {}, {
      lazy: true,
    });

    Object.defineProperty(vm, key, {
      get() {
        let watcher = vm._computedWatchers[key];
        // 如果没有缓存
        if (watcher && watcher.dirty) {
          // 实际上是调用了计算属性watcher.get,如果此时有响应式数据取值会触发get操作,让对应的dep订阅器收集计算属性watcher
          watcher.evaluate();
          // 调用结束后计算属性watcher出栈
        }
        // 此时响应式数据只收集了计算属性watcher,如果数据发生变化我们是希望页面视图能够刷新

        if (Dep.target) {
          // 因此我们也需要响应式数据收集渲染的watcher
          watcher.depend();
        }
        return watcher.value;
      },
    });
  }
}

watch vs computed

computed 特点

  • 可以监听多个值
  • 只有在获取数据的时候,才会触发依赖收集(计算属性 watcher)
  • 支持缓存,只有当依赖的数据发生变化时才会重新计算
  • 名称不能和 data 里的对象重复

watch 特点

  • 名称只能和 data 里的对象一致
  • 只能监听一个值
  • 初始化会触发依赖收集(watch watcher)
  • 不支持缓存,数据变化时会立即触发回调

Vue diff(五种策略)

Vue 2.0 的 diff 算法只会对比同级节点,不考虑跨层级情况,如果节点类型key不一样直接删除并创建新的节点。Vue 2.0 采用的是双指针策略,具体包括以下五种对比策略:

  1. 头头对比:如果 tag、key 相同复用老节点,新老起始索引递增,对比 props 和子元素
  2. 尾尾对比:如果 tag、key 相同复用老节点,新老起始索引递减
  3. 老头新尾对比:如果 tag、key 相同复用老节点(把老的移动 DOM 插入到尾部),老的起始索引递增、新的结束索引递减
  4. 老尾新头对比:如果 tag、key 相同复用老节点(把老的移动 DOM 插入到头部),老的结束索引递减、新的起始索引递增
  5. 乱序对比:用新的起始的 Vnode key 去老的 map 映射中查找,如果没有能够复用的,把新的 Vnode 转换成真实的 DOM 插入在老的起始 DOM 之前;如果能够复用老的 DOM,移动老的 DOM 插入在老的起始 DOM 之前,新的起始索引递增

当一方起始索引大于结束索引时,说明已经有一方对比完成:

  • 如果老的还有剩余的,删除老的真实 DOM
  • 如果新的还有剩余的,创建真实的 DOM 并插入在对应的位置

1、头头对比

diff

2、尾尾对比

diff

3、老头新尾对比、老尾新头对比

diff

4、乱序对比

diff

虚拟 DOM

虚拟 DOM 是用 JS 对象 来模拟 DOM 结构的一种技术,具有以下优势:

  • 跨平台:可以在不同平台上使用相同的代码
  • 减少真实 DOM 操作:通过 diff 算法减少不必要的 DOM 操作,提升性能
  • 差异化更新:当数据发生变化时,通过对比虚拟 DOM 的差异,只更新必要的部分
js
function vnode(vm, tag, props, children, text, key) {
  return {
    vm,
    tag,
    props,
    children,
    text,
    key,
  };
}

Vue.mixin

Vue.mixin 是一种代码复用机制,允许将相同的逻辑抽取到一个对象中,然后混入到多个组件中。

优点

  • 可以把相同的逻辑抽取,进行代码复用

缺点

  • 在组件中,数据来源不明确,数据会被覆盖

合并策略

  • 生命周期:生命周期在合并的时候,会按照执行的先后顺序排序形成一个队列
  • 非生命周期:如果属性相同,后者会覆盖掉前者

mixin 案例

js
// 第一次有生命周期,在合并的时候。Vue.options = {};
Vue.mixin({
  beforeCreate() {
    console.log("beforeCreate-hulei1");
  },
  name: "hl",
});
// 后续在合并的时候可能没有生命周期,直接返回上一个次
Vue.mixin({
  age: 18,
});

Vue.mixin({
  beforeCreate() {
    console.log("beforeCreate-hulei2");
  },
  name: "hulei",
});

const app = new Vue({
  el: "#app",
  beforeCreate() {
    console.log("beforeCreate-hulei3");
  },
  data() {
    return {
      name: "hulei",
    };
  },
});

mixin 全局合并

js
export function initGlobalAPI(Vue) {
  Vue.options = {};

  Vue.mixin = function (options) {
    // 全局合并
    this.options = mergeOptions(this.options, options);
  };
}

mixin 组件 options 合并

js
export function initMixin(Vue) {
  // initoptionsAPI初始化
  Vue.prototype._init = function (options) {
    const vm = this;
    // 合并mixin的options
    vm.$options = mergeOptions(Vue.options, options);
    // 触发生命周期钩子
    callHook(vm, "beforeCreate");
  };
}

mergeOptions 实现

js
const lifecycles = {};
["beforeCreate", "created", "beforeMount", "mounted"].forEach((method) => {
  // 策略模式
  lifecycles[method] = function (preLifecycle, postLifecycle) {
    if (!preLifecycle && postLifecycle) {
      // 第一次有生命周期合并的时候,上一个肯定是空对象,也需要形成一个队列
      return [postLifecycle];
    }
    // 第二次到N次,如果有生命周期按照前后顺序进行排序,如果没有直接把上一次的返回
    if (postLifecycle) {
      return preLifecycle.concat(postLifecycle);
    }
    return preLifecycle;
  };
});

export function mergeOptions(preOptions, postOptions) {
  const opts = {};

  // 循环处理上一个
  for (const key in preOptions) {
    mergeField(key);
  }

  // 循环处理上一个
  for (const key in postOptions) {
    // 相同的属性上一个循环已经处理
    if (!preOptions.hasOwnProperty(key)) {
      mergeField(key);
    }
  }

  function mergeField(key) {
    if (lifecycles[key]) {
      // 处理生命周期形成队列
      opts[key] = lifecycles[key](preOptions[key], postOptions[key]);
    } else {
      // 优先使用后者
      opts[key] = postOptions[key] || preOptions[key];
    }
  }
  return opts;
}