Skip to content
微信公众号

mpvue 响应式源码分析

状态初始化

mpvue 的实例化主要执行的是 Vue.prototype._init 方法,该方法通过 initState 函数初始化各种状态,包括:props、data 和 computed,这些状态的值变化,会触发界面 UI 的更新,这是响应式的核心功能。_init 方法源码节选如下:

js
Vue.prototype._init = function (options) {
    var vm = this;
  
    vm._self = vm;
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, 'beforeCreate');
    initInjections(vm);
    initState(vm);
    initProvide(vm);
    callHook(vm, 'created');
  };

在 beforeCreate 生命周期后 mpvue 会调用 initState 实例化状态,本文将统一讲解 data 这个特性,props 和 computed 与 data 是非常类似的,所以理解了 data 再看 props 和 computed 都将非常容易。我们的 mpvue 测试程序非常简单,源码如下:

vue
<template>
  <div>{{message}}</div>
</template>

<script>
export default {
  data () {
    return {
      message: 'Hello miniprograme'
    }
  }
}
</script>

我们只定义了 data,里面包含一个属性 message。下面回到 initState 流程,initState 中与 data 初始化相关的源码节选如下:

js
function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.data) {
    initData(vm);
  } else {
    observe(vm._data = {}, true /* asRootData */);
  }
}

data 初始化

首先,mpvue 从 vm.$options 中获取 data 属性,vm.$options.data 指向我们自己编写的 data 属性,它是一个 function,当 data 存在时会指向 initData (vm),initData 的源码如下:

js
var data = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
  if (!isPlainObject(data)) {
    data = {};
    "production" !== 'production' && 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;
  while (i--) {
    var key = keys[i];
    if (props && hasOwn(props, key)) {
      "production" !== 'production' && 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, true /* asRootData */);
}

这部分代码较长,我们逐一分析,首先 mpvue 会调用 getData 函数获取 data 的结果,如果我们的 data 是 Object 型,则会直接返回:

js
data = vm._data = typeof data === 'function'
  ? getData(data, vm)
	: data || {};

之后 mpvue 会判断返回结果是否为 Object,如果不是,则抛出警告:

js
if (!isPlainObject(data)) {
  data = {};
  "production" !== 'production' && warn(
    'data functions should return an object:\n' +
    'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
    vm
  );
}

下一步,mpvue 会判断 data 中的数据是否与 props 重名,这是因为 data 中的数据和 props 一样,都会被作为 mpvue 实例的根属性,如:

js
this.message = 'xxx'

如果重名了,将会出现冲突,判断逻辑如下:

js
// proxy data on instance
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
  var key = keys[i];
  if (props && hasOwn(props, key)) {
    "production" !== 'production' && 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);
  }
}

可以看到,mpvue 遍历了 data 的 key,依次与 props 对比,如果不冲突,还会调用 isReserved (key) 函数,判断 data 的 key 是否包含保留关键字,具体源码如下:

js
function isReserved (str) {
  var c = (str + '').charCodeAt(0);
  return c === 0x24 || c === 0x5F
}

这里 0x24 表示 $,0x5F 表示 _,这段代码的含义是判断我们 data 中属性的第一个字符是否为 $ 或 _,因为这两个符号标识的变量为 mpvue 内置的变量,所以 mpvue 不允许我们使用这两个字符作为变量前缀,以免发生冲突。如果我们 data 的 key 与 props 不冲突,同时非 $ 和 _ 开头,那么 mpvue 将执行下面一个关键的步骤:

js
proxy(vm, "_data", key);

这步简单来说,就是在 mpvue 实例下添加 data 属性,并将 vm.key 的值指向 vm._data.key,这样说有点抽象。下面我们详细分析,先看下 proxy 的源码:

js
function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  };
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

很多同学都应该听说过 Vue 的响应式核心原理是 Object.defineProperty,那么 Object.defineProperty 到底在哪里使用的呢?答案之一就是在 proxy 方法里。proxy 定义了变量的 get 和 set 方法,这说明当我们取值时将调用 proxyGetter 方法,赋值时将调用 proxySetter 方法。那么取什么值时会调用这两个方法呢?答案是取 vm.key 的值的时候,原因是下面这行代码:

js
Object.defineProperty(target, key, sharedPropertyDefinition);

它表示为 target 添加 key 属性,并传入自定义的属性描述对象。target 是 vm,也就是 mpvue 实例,key 是 message,因为我们在 data 下定义了一个属性 message,而 sharedPropertyDefinition 就是 get 和 set 方法。这段代码执行完毕后,将在 vm 下生成一个 message 属性: 当我们获取 vm.message 的值的时候会调用 proxyGetter 方法,该方法执行逻辑如下:

js
sharedPropertyDefinition.get = function proxyGetter () {
  return this[sourceKey][key]
};

这里的 sourceKey 是 _data,key 是 message,所以 vm.message 实际访问的值是:vm._data.message,如上图所示。至此在 vm 下添加 message 属性的目的非常明确了,就是为了简化我们的开发。我们知道在 React 框架中状态的获取必须通过 this.state.xxx 或 this.props.xxx,这样比较麻烦。Vue 在这一点上进了一步,它设置了一个前提条件:不允许 props 和 data 重名,从而可以直接将 props 和 data 中的属性挂载到 vm 实例上,并通过 es5 新增的 Object.defineProperty 定义了其取值和赋值的代码方法。同理,我们再看一下 vm.message 的赋值方法:

js
sharedPropertyDefinition.set = function proxySetter (val) {
  this[sourceKey][key] = val;
};

很明显也是将值赋给了 vm._data.message。当这些步骤都执行完毕了 mpvue 会为 vm._data 下的属性添加响应式属性:

js
observe(data, true);

虽然上述代码只有一行,但却是整个 initData 方法中最重要的,它是 mpvue 实现响应式的精髓之一。

响应式初始化

observe 方法的源码如下:

js
function observe (value, asRootData, key) {
  if (!isObject(value)) {
    return
  }
  var ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value, key);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob
}

上述代码最关键的步骤是:

js
ob = new Observer(value, key);

其中 value 就是 vm._data,key 为 undefined,Observer 表示一个响应对象,它的实例化源码如下:

js
var Observer = function Observer (value, key) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  if (key) {
    this.key = key;
  }
  def(value, '__ob__', this);
  if (Array.isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment;
    augment(value, arrayMethods, arrayKeys);
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};

Observer 创建了一个 Dep 对象,Dep 主要用途是完成依赖收集,当触发状态变更时,mpvue 会通知 Dep 中的所有依赖进行更新:

js
this.dep = new Dep();

这个我们会在后续分析中再次提到,之后通过 def 方法,为 vm._data 对象添加了一个 ob 属性,带有 ob 属性的对象,mpvue 会将其视为响应式对象:

js
def(value, '__ob__', this);

def 方法的源码如下:

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

调用完毕后 vm._data 的属性如下:

js
{message: "Hello miniprograme", __ob__: Observer}

可以看到多出了一个 ob 属性。之后程序继续往下执行,由于 vm._data 不是数组,所以会调用:

js
this.walk(value);

该方法的主要用途是依次为 vm._data 下的所有属性定义响应式方法,walk 是 mpvue 的 data 属性具备响应式能力的最关键的一步,它的源码如下:

js
Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    defineReactive$$1(obj, keys[i], obj[keys[i]]);
  }
};

这段源码比较简单,它遍历了 vm._data 的所有属性,并调用了 defineReactive$$1 方法, defineReactive$$1 方法的源码如下:

js
function defineReactive$$1 (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
  var dep = new Dep();

  var property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get;
  var setter = property && property.set;

  var childOb = !shallow && observe(val, undefined, key);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // ...
    },
    set: function reactiveSetter (newVal) {
      // ...
    }
  });
}

这里省略了 get 和 set 的实现细节,因为这些会在下一节中重点讲解。上述代码中 obj 代表 vm._data,key 代表 message,这里的 Object.defineProperty (obj, key, {}) 对 vm._data.message 进行了重新定义,对其 get 和 set 方法进行了代理。下一节我们将重点分析,当我们对 data 中属性进行 get 和 set 操作了,具体做了哪些操作,为什么能实现界面更新。由此可见我们通过 this.message 取值时,实际调用了 this._data.message 的 reactiveGetter 方法,这一过程远比我们想象中要复杂,因为经历了两次代理:

  • 第一次代理是 this.message 的 proxyGetter,它返回了 this._data.message 的值;
  • 第二次代理是获取 this._data.message 值的时候,实际调用了 this._data.message 的 reactiveGetter 方法。

defineReactive 方法是 mpvue 响应式源码中非常重要的一个方法,它对 data 中的属性的 get 和 set 方法进行了代理,并实际实现了响应式功能,本节我们将深入分析 defineReactive 方法:

js
function defineReactive$$1 (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
  var dep = new Dep();

  var property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return
  }
    
  var getter = property && property.get;
  var setter = property && property.set;

  var childOb = !shallow && observe(val, undefined, key);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      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;
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal, undefined, key);
      dep.notify();
    }
  });
}

首先看第一行代码:

js
var dep = new Dep();

它完成了 Dep 对象的实例化,Dep 函数源码如下:

js
var Dep = function Dep () {
  this.id = uid$1++;
  this.subs = [];
};

每个 Dep 对象维护了一个 id 和 子 Dep 对象集合 subs 数组。接下来 mpvue 获取了对象上该属性的 PropertyDescriptor:

js
var property = Object.getOwnPropertyDescriptor(obj, key);

这里使用了 Object.getOwnPropertyDescriptor 方法进行获取,这里的 property 是 Object.defineProperty 的第三个参数,用于描述对象的特性,如:是否可以修改,并支持代理 get 和 set 方法,关于 property 的更多信息大家可以到 MDN 上进行学习(地址)。接着 mpvue 判断该属性是否可以改变,通过 property.configurable 进行判断,如果不能改变,将直接退出 defineReactive 方法:

js
if (property && property.configurable === false) {
  return
}

这个非常容易理解,configurable 为 false 时,无法修改属性值,响应式也就没有意义了。之后,mpvue 从 property 中取出 get 和 set 方法,并且调用 Object.defineProperty 重新定义 vm.message 的 property:

js
var getter = property && property.get;
var setter = property && property.set;
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    var value = getter ? getter.call(obj) : val;
    // ...
    return value
  },
  set: function reactiveSetter (newVal) {
    var value = getter ? getter.call(obj) : val;
    // ...
  }
})

新的 property 定义了 enumerable 和 configurable 为 true,表示该属性是可以被枚举和修改的。同时定义了新的 get 和 set 方法:reactiveGetter 和 reactiveSetter,在这两个方法中会优先判断是否已经定义了 getter 和 setter 方法,这里的 getter 和 setter 对应上面提到的 property.get 和 property.set。至此,defineReactive 的主干逻辑已经梳理清楚,下面我们就来分析 get 和 set 的实现细节。

响应式 reactiveGetter

当我们在 mpvue 中获取 vm.message 的值时就会调用 reactiveGetter 方法,reactiveGetter 方法完整源码如下:

js
function reactiveGetter () {
  var value = getter ? getter.call(obj) : val;
  if (Dep.target) {
    dep.depend();
    if (childOb) {
      childOb.dep.depend();
    }
    if (Array.isArray(value)) {
      dependArray(value);
    }
  }
  return value
}

第一行代码判断 property 是否已经定义 get 方法,如果定义了,则会调用该 get 方法进行取值,因为有可能属性的取值逻辑被我们自定义过,如果不存在,则直接返回 val,这里的 val 即我们在 data 中定义的 function 执行后的结果:

js
var value = getter ? getter.call(obj) : val;

之后 mpvue 会判断 Dep.target 对象是否存在,如果存在则会调用 dep.depend,这里的 dep 我们在上小节中分析过,它是一个 Dep 实例,dep.depend 方法会调用 Dep.target.addDep 方法,并将当前的 dep 实例作为参数传入:

js
Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};

addDep 方法会维护一个 newDepIds 变量,newDepIds 是一个 Set 型的集合,它会维护所有响应式的 dep 实例的 id:

js
Watcher.prototype.addDep = function addDep (dep) {
  var id = dep.id;
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id);
    this.newDeps.push(dep);
    if (!this.depIds.has(id)) {
      dep.addSub(this);
    }
  }
};

此步完成后,mpvue 会判断 message 属性是否存在子属性,如果存在子属性,则会调用子属性的 depend 方法,这里由于 vm.message 不存在 childOb,所以该方法会跳过:

js
if (childOb) {
  childOb.dep.depend();
}

接着 mpvue 会判断 vm.message 的值是否为数组:

js
if (Array.isArray(value)) {
  dependArray(value);
}

这里 vm.message 的值是 String,所以此步也会跳过。所以 reactiveGetter 方法的核心就是调用了 Dep.target.addDep 方法,将当前属性 vm.message 加入到 Watcher 维护 newDepIds 集合中,通过 Watcher 完成了实际的渲染操作。

响应式 reactiveSetter

我们再分析 reactiveSetter 方法,当我们在 mpvue 中对 vm.message 的值进行修改时会调用 reactiveSetter 方法,reactiveSetter 方法源码如下:

js
function reactiveSetter (newVal) {
  var value = getter ? getter.call(obj) : val;
  if (newVal === value || (newVal !== newVal && value !== value)) {
    return
  }
  if (setter) {
    setter.call(obj, newVal);
  } else {
    val = newVal;
  }
  childOb = !shallow && observe(newVal, undefined, key);
  dep.notify();
}

前四行从 getter 中获取 val 值,并判断新值和原值是否发生变化,如果未发生变化,直接直接返回,不做任何操作:

js
var value = getter ? getter.call(obj) : val;
if (newVal === value || (newVal !== newVal && value !== value)) {
  return
}

然后 mpvue 会判断 property.set 方法是否存在,如果存在,则调用 property.set 方法进行赋值,否则直接将 newVal 赋值给 vm.message:

js
if (setter) {
  setter.call(obj, newVal);
} else {
  val = newVal;
}

接下来 mpvue 获取了 childOb,这里值为 undefined。下一步 mpvue 会调用一个非常关键的方法:

js
dep.notify();

这里用到了观察模式,通过 notify 方法,依次调用 subs 中对象的 update 方法,notify 方法源码如下:

js
Dep.prototype.notify = function notify () {
  var subs = this.subs.slice();
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};

这里的 this.subs 只包含一个元素,即 Watcher 对象:

js
this.subs
[Watcher]

每个 mpvue 页面都包含一个 Watcher,Watcher 完成了响应式的实际更新,具体更新逻辑我们将在下节中进行分析,目前大家只要知道 mpvue 通过 dep.notify 方法触发界面更新,而该方法又调用了 Watcher 的 update 进行实际更新的操作即可。这个更新动作是异步的,所以不会阻塞程序的继续执行。

本站总访问量次,本站总访客数人次
Released under the MIT License.