Vue—关于响应式(四、深入学习Vue响应式源码)

前面三个小节我们根据下图分析了Vue整个响应式系统的闭环,这一节我们直接来看Vue源码。

1635411772.png

前面小节的链接在这里:

Vue—关于响应式(一、依赖收集原理分析)

Vue—关于响应式(二、异步更新队列原理分析)

Vue—关于响应式(三、Diff Patch原理分析)

一、Vue响应式系统分析

1.1. 简单回顾

回顾一下我们自己的响应式实现过程:

  1. 首先将data通过Object.defineProperty处理成可响应的数据(getter/setter)
  2. 【读】数据时会触发getter调用Dep.depend将依赖上下文进行收集
  3. 【写】数据时会触发setter调用Dep.notify通知依赖进行更新
  4. 在收到【通知依赖更新】后,开启一个异步更新队列,而演示的demo代码我们是直接在Dep.notify方法中将依赖上下文推入任务队列queue中,然后通过nextTick(promise)来执行异步更新。

这些是我们简化后的过程,Vue的响应式设计远比我们自己做的复杂的多,简化是为了帮助我们更好的理解响应式原理,接下来我们从Vue的源码中一步一步来看整个响应式系统的设计。

在此之前我们先来了解一下响应式系统的初始化过程。

1.2. Vue响应式系统流程梳理

从Vue实例化到响应式系统初始完成的过程是:

  1. 新创建一个实例后,Vue调用compile将el转换成vnode。

  2. 调用initState, 创建props, data等钩子以及其对象成员的Observer(添加getter和setter)。

  3. 执行mount挂载操作,在挂载时建立一个直接对应render的Watcher,并且编译模板生成render函数,执行vm._update来更新DOM。

  4. 每当有数据改变,都将通知对应的Watcher执行回调函数,更新视图。

    • 当修改这个数据时,会触发set。
    • set调用,触发Dep.notify()向对应的Watcher通知变化。
    • Watcher调用update方法重新渲染视图。
1635412002.png

在这个过程中:

  1. Observer类用来将data处理成响应式数据,并将依赖该data的上下文添加到Dep类中。
  2. Dep类会在data中每个对象包括子对象都闭包一个实例, 当所绑定的数据有变更时, 通过dep.notify()通知Watcher进行更新。
  3. Compile类是HTML指令解析器,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
  4. Watcher类是连接Observer和Compile的桥梁,Compile解析指令时会创建一个对应的Watcher并绑定update方法 , 添加到Dep类上。

有了以上的梳理之后,现在我们可以结合源码来进行验证了。(演示源码版本为vue 2.6.12)

二、Vue源码阅读

真正创建compile的函数是createCompiler,这个函数在源码中直接就传入了默认参数(baseOptions)进行了调用以生成vnode。

var createCompiler = createCompilerCreator(function baseCompile(
  template,
  options
) {
  var ast = parse(template.trim(), options);
  if (options.optimize !== false) {
    optimize(ast, options);
  }
  var code = generate(ast, options);
  return {
    ast: ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns,
  };
});

/*  */

var ref$1 = createCompiler(baseOptions);

compile并不是本节重点,对源码感兴趣请参考字节大佬的文章:https://www.jianshu.com/p/743166a8968c

接着找到以下代码位置:

function Vue(options) {
  if (!(this instanceof Vue)) {
    warn("Vue is a constructor and should be called with the `new` keyword");
  }
  this._init(options);
}

可以看到在初始化Vue实例时,只调用了_init方法,而_init方法是在initMixin函数中设置的,去掉其它的代码,我们找到了initState调用和vm.$mount方法调用(用于挂载模板)

function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    var vm = this;

    // 省略部分代码....
    initState(vm);

    // 省略部分代码....
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };
}

initState的源码如下,它的作用初始化props、methods、data、computed、watch,而对数据进行observer在initData中

function initState(vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.props) {
    initProps(vm, opts.props);
  }
  if (opts.methods) {
    initMethods(vm, opts.methods);
  }
  // 如果vm.$options.data存在则调用initData
  // 否则给vm添加_data,通过observe处理成响应式数据
  if (opts.data) {
    initData(vm);
  } else {
    observe((vm._data = {}), true /* asRootData */);
  }
  if (opts.computed) {
    initComputed(vm, opts.computed);
  }
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

initData源码如下:前面的代码可以略过不看(为什么组件data中需要传函数并return一个对象,你可以在这里找到答案),最后是调用了observe函数

function initData(vm) {
  var data = vm.$options.data;
  // 对data进行函数验证
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
  // 如果data函数返回的不是对象则提示应该返回对象
  if (!isPlainObject(data)) {
    data = {};
    warn(
      "data functions should return an object:\n" +
        "https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function",
      vm
    );
  }
  // proxy data on instance
  var keys = Object.keys(data);
  var props = vm.$options.props;
  var methods = vm.$options.methods;
  var i = keys.length;
  // 判断props和methods中是否有跟data中同名的属性
  // 进行相应的警告处理
  while (i--) {
    var key = keys[i];
    {
      if (methods && hasOwn(methods, key)) {
        warn(
          'Method "' + key + '" has already been defined as a data property.',
          vm
        );
      }
    }
    if (props && hasOwn(props, key)) {
      warn(
        'The data property "' +
          key +
          '" is already declared as a prop. ' +
          "Use prop default value instead.",
        vm
      );
    } else if (!isReserved(key)) {
      proxy(vm, "_data", key);
    }
  }
  // observe data
  // 调用observe函数对data进行响应式处理
  observe(data, true /* asRootData */);
}

这个observe函数是什么呢?去掉一些影响阅读的部分,可以看到它是Observer类的实例

function observe(value, asRootData) {
  // 省略部分代码...
  var ob;
  ob = new Observer(value);
  return ob;
}

2.1. Observer

Observer类源码如下,前面一大堆参数验证巴拉巴拉后,最后调用了this.walk方法

var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  // 给value添加__ob__属性,值就是本Observer对象,value.__ob__ = this;
  // Vue.$data 中每个对象都 __ob__ 属性,包括 Vue.$data对象本身
  def(value, '__ob__', this);
  // 判断是否为数组,不是的话调用walk()添加getter和setter
  // 如果是数组,调用observeArray()遍历数组,为数组内每个对象添加getter和setter
  if (Array.isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment;
    augment(value, arrayMethods, arrayKeys);
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};

walk方法源码如下:主要作用是遍历所有属性并调用defineReactive$$1函数处理数据

Observer.prototype.walk = function walk(obj) {
  var keys = Object.keys(obj);
  // 遍历每个属性并调用defineReactive$$1将数据转化成getter和setter
  for (var i = 0; i < keys.length; i++) {
    defineReactive$$1(obj, keys[i]);
  }
};

defineReactive$$1源码如下:在第一小节的时候我们就提过这个函数的作用,主要是通过Object.defineProperty来将普通的数据处理成响应式数据

function defineReactive$$1(obj, key, val, customSetter, shallow) {
  // 闭包dep实例
  var dep = new Dep();

  // 省略部分代码...

  var childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      var value = getter ? getter.call(obj) : val;
      // Dep.target 全局变量指向的就是当前正在解析指令的Complie生成的 Watcher
      // 会执行到 dep.addSub(Dep.target), 将 Watcher 添加到 Dep 对象的 Watcher 列表中
      if (Dep.target) {
        // 添加依赖
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value;
    },
    set: function reactiveSetter(newVal) {
      var value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }
      /* eslint-enable no-self-compare */
      if (customSetter) {
        customSetter();
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) {
        return;
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      // 如果数据被重新赋值了, 调用 Dep 的 notify 方法, 通知所有的 Watcher
      dep.notify();
    },
  });
}

关于defineReactive$$1的源码解析我们就不再详细展开了,本节主要是了解整个Vue响应式源码设计,有兴趣可以自己再去搜索引擎一下。

接着我们来看Dep类源码。

2.2. Dep

Dep类的作用就是把一个数据用到的地方收集起来,在这个数据发生改变的时候,统一去通知各个地方做对应的操作,是典型的发布订阅模式体现。

var Dep = function Dep() {
  // 每个Dep都有唯一的ID
  this.id = uid++;
  // subs用于存放依赖
  this.subs = [];
};
// 向subs数组添加依赖
Dep.prototype.addSub = function addSub(sub) {
  this.subs.push(sub);
};
// 移除依赖
Dep.prototype.removeSub = function removeSub(sub) {
  remove(this.subs, sub);
};
// 设置某个Watcher的依赖
// 这里添加了Dep.target是否存在的判断,目的是判断是不是Watcher的构造函数调用
// 也就是说判断他是Watcher的this.get调用的,而不是普通调用
Dep.prototype.depend = function depend() {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};
// 通知依赖更新
Dep.prototype.notify = function notify() {
  // stabilize the subscriber list first
  var subs = this.subs.slice();
  if (!config.async) {
    // subs aren't sorted in scheduler if not running async
    // we need to sort them now to make sure they fire in correct
    // order
    subs.sort(function (a, b) {
      return a.id - b.id;
    });
  }
  // 通知所有绑定 Watcher。调用watcher的update()
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null;

2.3. Watcher

前面我们一直在说Watcher,那么Watcher是在什么时候调用的呢?

根据前面的分析和源码,我们看到了在initMixin()调用时分别调用了initState()和vm.mount(),mount方法的作用就是挂载模板,源码如下:

Vue.prototype.$mount = function (
  el,
  hydrating
) {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating)
};

可以看到最后$mount方法中是调用了mountComponent,mountComponent源码如下,去掉影响阅读的代码,可以看到Watcher类在mountComponent函数中进行了实例化。

function mountComponent(vm, el, hydrating) {
  // 省略部分代码...

  // 获取更新组件
  var updateComponent;
  /* istanbul ignore if */
  if (config.performance && mark) {
    updateComponent = function () {
      var name = vm._name;
      var id = vm._uid;
      var startTag = "vue-perf-start:" + id;
      var endTag = "vue-perf-end:" + id;

      mark(startTag);
      var vnode = vm._render();
      mark(endTag);
      measure("vue " + name + " render", startTag, endTag);

      mark(startTag);
      vm._update(vnode, hydrating);
      mark(endTag);
      measure("vue " + name + " patch", startTag, endTag);
    };
  } else {
    updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before: function before() {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, "beforeUpdate");
        }
      },
    },
    true /* isRenderWatcher */
  );

  return vm;
}

首先会new一个watcher实例对象(主要是将模板与数据建立联系),在watcher对象创建后,会运行传入的方法 vm._update(vm._render(), hydrating) 。其中的vm._render()主要作用就是运行前面compiler生成的render方法,并返回一个vNode对象。vm.update() 则会对比新的 vdom 和当前 vdom,并把差异的部分渲染到真正的 DOM 树上。

Watcher类源码比较多,简化后的模型如下:

class Watcher {
  constructor(vm, expOrFn, cb, options) {
    //传进来的对象 例如Vue
    this.vm = vm;
    // 在Vue中cb是更新视图的核心,调用diff并更新视图的过程
    this.cb = cb;
    this.id = ++uid$2; // 生成一个唯一id
    this.sync = !!options.sync; // 默认一般为false
    // 收集Deps,用于移除监听
    this.newDeps = [];
    this.getter = expOrFn;
    // 设置Dep.target的值,依赖收集时的watcher对象
    this.value = this.get();
  }

  get() {
    // 设置Dep.target值,用以依赖收集
    pushTarget(this);
    const vm = this.vm;
    let value = this.getter.call(vm, vm);
    return value;
  }

  //添加依赖
  addDep(dep) {
    // 这里简单处理,在Vue中做了重复筛选,即依赖只收集一次,不重复收集依赖
    this.newDeps.push(dep);
    dep.addSub(this);
  }

  // 更新
  update() {
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      //如果是同步那就立刻执行回调
      this.run();
    } else {
      // 否则把这次更新缓存起来
      // 但是就像上面说的,异步更新往往是同一事件循环中多次修改同一个值,
      // 那么一个wather就会被缓存多次。因为需要一个id来判断一下,
      queueWatcher(this);
    }
  }

  // 更新视图
  run() {
    // 这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图
    console.log(`这里会去执行Vue的diff相关方法,进而更新数据`);
  }
}

在第二小节的时候提过收到通知后Vue会开启一个异步更新队列,至于原因也说的很清楚了,所谓的同步更新是指当观察的主体改变时立刻触发更新,而实际开发中这种需求并不多,同一事件循环中可能需要改变好几次state状态,但视图view只需要根据最后一次计算结果同步渲染就行(react中的setState就是典型)。如果一直做同步更新无疑是个很大的性能损耗。

这就要求watcher在接收到更新通知时不能立刻执行callback,而是将本次更新缓存起来,等到事件循环的下一次Tick时才执行。

2.3.1. 产生Watcher的几种情况

在Vue中,一共有四种情况会产生Watcher:

  1. Vue实例对象上的watcher,观测根数据,发生变化时重新渲染组件
    • updateComponent = () => { vm._update(vm._render(), hydrating)}
    • vm._watcher = new Watcher(vm, updateComponent, noop)
  2. 用户在vue对象内用watch属性创建的watcher
  3. 用户在vue对象内创建的计算属性,本质上也是watcher
  4. 用户使用vm.$watch创建的watcher

2.3.2. Watcher的调度

这里的一个要注意的地方是,考虑到极限情况,如果正在更新队列中wather时,又塞入进来该怎么处理?因此,必须有一个Schedule来进行Watcher的调度。

在Vue中,这个负责调度的函数是flushSchedulerQueue,源码如下:

/**
 * 清空两个队列并运行watcher
 */
function flushSchedulerQueue() {
  currentFlushTimestamp = getNow();
  flushing = true; // 加入一个flushing来表示队列的更新状态
  var watcher, id;

  // 在刷新之前对队列进行排序。
  // 这将确保:
  // 1. 组件从父组件更新到子组件。(因为父组件总是在子组件之前创建)
  // 2. 组件的user watchers在它的render watchers之前运行(因为user watchers在render watchers之前创建)
  // 3. 如果在父组件的监视程序运行期间组件被销毁,则可以跳过它的监视程序。
  queue.sort(function (a, b) {
    return a.id - b.id;
  });

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      watcher.before();
    }
    id = watcher.id;
    has[id] = null;
    watcher.run();
    // in dev build, check and stop circular updates.
    if (has[id] != null) {
      circular[id] = (circular[id] || 0) + 1;
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          "You may have an infinite update loop " +
            (watcher.user
              ? 'in watcher with expression "' + watcher.expression + '"'
              : "in a component render function."),
          watcher.vm
        );
        break;
      }
    }
  }
}

Schedule 调度的作用(管理Watcher):

  1. 去重,每个Watcher有一个唯一的id,如果id已经在队列里了就没必要重复执行,如果id不在队列里,要看队列是否正在执行中。如果不在执行中,则在下一个时间片执行队列,因此队列永远是异步执行的。

  2. 排序,按解析渲染的先后顺序执行,即Watcher小的先执行。Watcher里面的id是自增的,先创建的id比后创建的id小。所以会有如下规律:

    • 组件是允许嵌套的,而且解析必然是先解析了父组件再到子组件。所以父组件的id比子组件小。

    • 用户创建的Watcher会比render时候创建的先解析。所以用户创建的Watcher的id比render时候创建的小。

  3. 删除Watcher,如果一个组件的Watcher在队列中,而他的父组件被删除了,这个时候也要删掉这个Watcher。

  4. 队列执行过程中,存一个对象circular,里面有每个watcher的执行次数,如果哪个watcher执行超过MAX_UPDATE_COUNT定义的次数就认为是死循环,不再执行,默认是100次。

64fb2adbfc3a66a279b74ead172613a0.jpeg
data() {
  return {
    a: 1,
  };
},
computed: {
  b: function () {
    this.a + 1;
  },
},
methods: {
  editA: function () {
    this.a = 2;
    this.a = 3;
    this.a = 1;
  },
},

在editA方法中,对a属性进行了三次更新,最后一次与初始值相同,理想情况是a没变,b也不重新计算。这就要求b的watcher执行update时要拿到a最新的值来计算,这里a最新的值是1,如果队列中a的watcher已经更新过,那么就应该把后面的a的watcher放到当前的watcher后面并立即更新,这样可以保证后面的watcher可以拿到a最新的值。

同理,如果a的watcher还没有更新,那么新的a的watcher放在之前的a的watcher的下一位,也是为了保证后面的watcher可以拿到a最新的值。

以上便是Vue响应式系统源码的功能主体,核心部分是Observer、Dep、Watcher,当然还有很多细节部分,相信看了这么多你也能自己上高速了。

2.4. 小结

vue响应式原理是,我们通过递归遍历,把vue实例中data里面定义的数据,通过Observer类调用defineReactive(Object.defineProperty)重新定义。每个数据内新建一个Dep实例,闭包中包含了这个 Dep 类的实例,用来收集 Watcher 对象。在对象被「读」的时候,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。之后如果当该对象被「写」的时候,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。

三、补充

由于Object.defineProperty这个API不能检测数组和对象的变化,例如:

  • 检测不到对象属性的添加或删除
  • 修改数组下标项的值如arr[3] = 1,或者arr.length = 0不会触发响应式更新

3.1. 对象检测

对于对象,如果你需要让某个属性是响应式的,必须在data中显示声明它,如:

var vm = new Vue({
  data: {
    a: 1,
  },
});

// `vm.a` 是响应式的

vm.b = 2;
// `vm.b` 是非响应式的

当然,需求是千变万化的,有时候我们就是需要打破这个规则,比如你请求了后端接口,接口中的数据你是不确定的,但是这些数据又必须是响应式的,因此Vue也给我们提供了set方法用于向嵌套对象添加响应式属性,如:

// 全局
Vue.set(vm.someObject, 'b', 2)

// 组件内
this.$set(this.someObject,'b',2)

3.2. 数组检测

对于数组,直接修改下标项的值或者操作length都是非响应式的,如:

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

同样Vue也提供了解决办法,修改数组下标项的值可以使用如下方式:

// 全局
Vue.set(vm.items, indexOfItem, newValue)

// 组件内
vm.$set(vm.items, indexOfItem, newValue)

而操作length属性不响应可以使用如下方式解决:

vm.items.splice(newLength)

3.3. 数组方法重写

Vue对数组原型链上的方法做了重写,使用以下方法操作数组都可以触发响应式。

1635413872.png

3.4. 装饰者模式学习

到这里不得不提一种设计模式——装饰者模式。

什么是装饰者模式?

百度百科解释如下:

装饰模式指的是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

简单理解就是不用改变原对象,通过创建一个包装层来扩展原对象的功能。

3.4.1. 装饰者模式的使用场景

当你发现一个方法它的原功能不适用、要扩展,但你又不能直接去修改原方法的时候,装饰者模式就可以派上用场了。

3.4.2. 装饰者模式的基本结构

  1. 封装新方法
  2. 调用老方法
  3. 在新方法中加入扩展功能

3.4.3. 装饰者模式示例

需求1:有一个他人写好的a模块,内部有一个方法b,不能修改他人模块的情况下,扩展b方法。

var a = {
  b() {},
};

这个需求非常简单,按照装饰者模式的基本结构三步走,我们新建一个自己的方法,在其内部调用b方法,然后加入要扩展的功能,这样就可以在不修改原对象的情况下扩展b方法的功能了,代码示例:

function myb() {
  a.b();
  // 加入扩展操作
}

需求2:假设你进入一家新公司,接手了前同事的代码,他在dom上绑定了很多事件,比如删除按钮绑定了点击事件,点击就进行删除操作,你接手之后产品跟你说觉得之前这种点击就删除没有提示的方式不太友好,需要你在点击确定或者删除的同时给出一个提示,这时候你会怎么做?

给两个例子(自己对号入座【手动滑稽】)

  • 不去找他之前的代码写在哪了,直接重写整个绑定事件
  • 找到老代码,然后改一下

这两种方式都是错的,如果采用第一种方案,势必要把他之前的删除功能代码再写一遍,非常麻烦,如果采用第二种方案,去找老代码这个找的过程也很麻烦,所以我们用装饰者模式来做这个事情,考虑到要做这个事情的按钮可能有很多,我们可以采用工厂模式的思维,直接封装一个装饰工厂,使用时告诉我你要装饰哪个dom,要扩展什么操作就可以了,代码示例:

const decoractor = function(dom, fn) {
  if(typeof dom.onclick === 'function') {
    const _old = dom.onclick;
    // 装饰者模式三步走
    // 1.封装新方法
    dom.onclick = function() {
      // 2.调用老方法
      _old.apply(this, arguments);
      // 3.在新方法中加入扩展功能
      fn.apply(this, arguments);
    }
  }
}

在使用的时候比如说要装饰删除按钮,然后要扩展提示功能:

decoractor(deleteDom, function() {
  alert('删除成功');
})

这样既不用去找老代码也不用去重新写整个事件绑定,只需要调用装饰工厂就好了,扩展起来的速度就快多了。

现在我们需要扩展数组的功能,但是又不能修改数组的原型方法,正好是装饰者模式的应用场景,Vue源码是不是这么做的呢?

3.4.4. Vue源码中的装饰者模式应用

源码如下:

// 获取内置对象Array的prototype
var arrayProto = Array.prototype;
// 继承Array.prototype
var arrayMethods = Object.create(arrayProto);

// 将需要扩展的方法做成配置数组
var methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

methodsToPatch.forEach(function (method) {
  // 缓存老的方法
  var original = arrayProto[method];
  // 1.封装新方法
  def(arrayMethods, method, function mutator() {
    var args = [],
      len = arguments.length;
    while (len--) args[len] = arguments[len];

    // 2.调用老方法
    var result = original.apply(this, args);
    var ob = this.__ob__;
    var inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
        break;
    }
    if (inserted) {
      ob.observeArray(inserted);
    }
    // notify change
    // 3.在新方法中加入扩展功能,通过notify方法通知依赖更新
    ob.dep.notify();
    return result;
  });
});

// 数组原型上有很多属性,这一步是去除多余属性
var arrayKeys = Object.getOwnPropertyNames(arrayMethods);

以上源码的流程是:

  1. 首先把要装饰的方法名放进数组,到时候直接循环数组生成方法就可以了,不用一个个的改动,例如将push、pop、shift等进行扩展。
  2. 在循环开始前先获取一下数组的原型链,因为到时候要装饰的方法全都在原型链上,但是不能直接修改原型链上的方法,所以先拷贝一份。
  3. 循环数组,拿到我们要装饰的方法名,然后把我们要装饰的方法重写于拷贝对象上,装饰者模式三步走,① 重写新方法 ②调用老方法 ③ 扩展新功能;这里的新功能就是调用dep.notify()来触发vue的响应式。
  4. 最后就是将重写的arrayMethods对象给到vue的data上面所有数组原型链上,这样data里面的数组原型链上的push等方法就有了触发响应式的功能,它也不会影响到原生的数组和方法。

如果你好奇第4步是在哪里完成的,可以在Observer类中找到它,前面我们说过,在Vue实例化时,首先通过Observer类将data处理成响应式数据,而数组方法的重写也是在Observer类中完成的。

var Observer = function Observer(value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, "__ob__", this);
  // 判断传入的数据是不是数组
  if (Array.isArray(value)) {
    if (hasProto) {
      protoAugment(value, arrayMethods);
    } else {
      // 将重写的方法放到data中的数组原型链上
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};

copyAugment函数源码如下:

function copyAugment(target, src, keys) {
  for (var i = 0, l = keys.length; i < l; i++) {
    var key = keys[i]; // 取到重写方法名,如push、splice等
    // 调用def方法对target重新处理
    def(target, key, src[key]);
  }
}

def函数源码如下:只是使用了Object.defineProperty修改对象的现有属性

function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true,
  });
}

3.5. 小结

  • 整个Vue响应式系统设计细节非常多,学习源码不仅能帮助我们理解原理,提高工作效率,更重要的是学习优秀的框架是怎么一步步搭建出来的,帮助我们构建良好的代码布局能力,技巧可以复制,思想很难。
  • 设计模式真的对代码优化的帮助非常大,内功深不深厚就看设计模式的运用了,一起努力学习。

加油,打工人!

四、参考

Vue官网—深入响应式原理

VUE源码解读之响应式系统及Watcher的调度实现

源码学习VUE之Watcher

vue模板渲染--compile

Vue源码解读之Dep,Observer和Watcher

vue源码解析(14)-- watcher的实现

本文由博客一文多发平台 OpenWrite 发布!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容