Skip to content

vue2.0

Vue 挂载流程

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

vue2lifecycle

Vue 数据响应式处理

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

数据的特殊处理

  • 如果数组有pushpop, unshfit, shift, reverse, sort, splice操作,会进行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),并且标记为已经缓存
  • 6、如果计算属性里的响应式数据发生改变,会触发 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 可以监听多个值

  • computed 只有在获取数据的时候,才会触发依赖收集(计算属性 watcher)

  • computed 支持缓存

  • computed 名称不能和 data 里的对象重复

  • watch 名称只能和 data 里的对象一致

  • watch 只能监听一个值

  • watch 初始化会触发依赖收集(watch watcher)

  • watch 不支持缓存

Vue diff(五种策略)

  • 只会对比同级节点,不考虑跨层级情况,如果节点类型key不一样直接删除,创建新的节点

  • vue2 采用的是双指针的策略

  • 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 之前,新的起始索引递增

  • 6、当前有一方起始索引大于结束的索引时,说明已经有一方对比完成

  • 7、如果老的还有剩余的,删除老的真实 dom

  • 7、如果新的还有剩余的,创建真实的 dom,插入在对应的位置

1、头头对比

diff

2、尾尾对比

diff

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

diff

4、乱序对比

diff

虚拟 DOM

  • 用 js 对象来模拟 DOM 结构
  • 可以实现跨平台
  • 减少真实 dom 的操作
  • 改生变化后可以差异化更新
js
function vnode(vm, tag, props, children, text, key) {
  return {
    vm,
    tag,
    props,
    children,
    text,
    key,
  };
}

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