Vue2源码学习笔记之初始化终局

再次回到 initState 函数中,其中 data 、 watch 、 computed 和 props 选项的初始化都已经学完了,下面还剩下 methods 选项的初始化,看下面的代码。 通过调用 initMethods 函数初始化 m

目录
  1. initMethods
  2. provide
  3. inject
    1. initInjections
    2. resolveInject
  4. 参考资料

再次回到initState函数中,其中datawatchcomputedprops选项的初始化都已经学完了,下面还剩下methods选项的初始化,看下面的代码。

if (opts.methods) initMethods(vm, opts.methods);

通过调用initMethods函数初始化methods选项,还需要传入实例对象vm和合并选项后的methods属性。

initMethods#

下面看下initMethods函数的代码。

const props = vm.$options.props;
for (const key in methods) {
  //...
}

声明常量props存储合并选项后的props选项,这个常量在后面会用到。

然后接下来就是一个for循环语句,遍历methods选项,下面看下for循环语句里面的内容。

if (process.env.NODE_ENV !== "production") {
  // ! 方法必须是函数,否则报错
  if (typeof methods[key] !== "function") {
    warn(
      `Method "${key}" has type "${typeof methods[
        key
      ]}" in the component definition. ` +
        `Did you reference the function correctly?`,
      vm
    );
  }
  // ...
}

在非生产环境中,如果methods选项里面的value值不是函数类型会报错。

继续看下面的代码。

// ! 方法名不能和 props 里面的属性名相同,否则报错,这样会产生冲突,覆盖掉 props 的属性
if (props && hasOwn(props, key)) {
  warn(`Method "${key}" has already been defined as a prop.`, vm);
}
 
// ! 方法和实例对象属性同名 且 是 Vue 的保留字时(以_或者$开头的,如 _data $parent 等)会报错
// ! 会产生冲突,覆盖掉实例属性
if (key in vm && isReserved(key)) {
  warn(
    `Method "${key}" conflicts with an existing Vue instance method.` +
      `Avoid defining component methods that start with _ or $.`
  );
}

在非生产环境中,methods选项中的方法名不能和props选项中的key相同,否则会报错,因为这样会产生冲突,方法会覆盖掉props中的数据。

另外,methods选项中的方法和实例对象vm中的属性同名,并且方法名和 Vue 的保留属性重名时,会报错。

继续看剩下的代码。

// ! 代理方法到实例,并且指向实例
vm[key] = typeof methods[key] !== "function" ? noop : bind(methods[key], vm);

methods中的方法代理到实例对象vm中。这里并没有用object.definedProperty方法代理属性,而是直接调用了bind的函数,把方法的this指向实例对象。这个bind函数是对原生的Function.prototype.bind方法的做了兼容性处理的函数。

到这里,methods选项的初始化就讲解完成,initState函数的初始化全部讲解完成。

provide#

再次回到Vue.prototype._init函数中,查看下面的代码。

initLifecycle(vm); // ! 初始化生命周期相关配置 存储本身实例到父节点 新增属性 $parent $root $children $refs 等
initEvents(vm); // ! 初始化事件相关配置 更新 listeners
initRender(vm); // ! 初始化渲染, 创建 VNode 另新增属性 $attrs 和 $listeners
callHook(vm, "beforeCreate"); // ! 执行 beforeCreate 钩子函数
initInjections(vm); // ! 初始化 Injections resolve injections before data/props
initState(vm); // ! 初始化状态,响应式相关 按顺序 props => methods => data => computed  => watch
initProvide(vm); // ! 初始化 Provide resolve provide after data/props
callHook(vm, "created"); // ! 执行 created 钩子函数

上面初始化的函数中,只剩下initInjections函数和initProvide函数没有讲解,注意在上面的代码中,initInjections函数要比initProvide函数更早执行。

这是为什么呢?等我们讲解到initInjections函数时就知道了。

下面先看下provide选项的初始化,查看initProvide函数的代码,在core/instance/inject.js文件中

export function initProvide(vm: Component) {
  const provide = vm.$options.provide;
  if (provide) {
    vm._provided = typeof provide === "function" ? provide.call(vm) : provide;
  }
}

这个函数非常简单,只有几行代码。声明常量provide存储合并选项后provide选项,先判断provide是否存在,如果存在,定义实例对象的vm._provided属性存储provide,在存储前判断provide是不是函数类型,如果是的话,还需要执行函数获取值。

小结initProvide函数的作用就是创建实例的_provided属性,然后存储合并后的provide选项。

inject#

initInjections#

下面看inject的选项的初始化,查看initInjections函数的代码,在core/instance/inject.js文件中

export function initInjections(vm: Component) {
  const result = resolveInject(vm.$options.inject, vm); // ! 通过 inject 获取父组件 provide 的值
  // ...
}

声明常量result存储resolveInject函数的返回值。resolveInject函数调用时需要传入合并后的inject选项和实例对象vm

这个函数的作用应该是讲解inject选项。用过inject选项的同学应该都知道,inject选项中注入的属性是它所在实例组件的父组件或者更上一级的组件的provide选项提供的值。这个函数的作用是获取它所在组件的父组件中的_provide属性的值,实例组件的_provide属性是存储初始化后的provide选项。

resolveInject#

查看resolveInject函数的代码。

export function resolveInject(inject: any, vm: Component): ?Object {
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    const result = Object.create(null)
    const keys = hasSymbol ? Reflect.ownKeys(inject) : Object.keys(inject)
    // ...
 
    return result
  }

首先判断通过参数传入的inject是否存在,如果存在,才能继续下面的逻辑。

声明变量result,它的初始值是一个空对象,它也是这个函数的返回值,它的作用是存储从父组件的_provide属性中获取的值。

声明变量keys存储injectkeys,先判断当前宿主环境是否支持Symbol,如果支持的话,使用Reflect.ownKeys方法获取keys的值。如果不支持,使用Object.keys方法获取keys的值。

再看下中间省略的代码,这里是一个for循环语句。

for (let i = 0; i < keys.length; i++) {
  const key = keys[i];
  // #6574 in case the inject object is observed...
  if (key === "__ob__") continue;
  // ...
}

先看前面的代码,声明常量key缓存循环中的key值。

首先判断key是不是__ob__属性,如果是的话,跳过这次循环,这行代码是为了解决 Vue 的6574 issue的,我猜测主要原因是因为__ob__属性是通过Object.defineProperty方法定义的,而这个属性是不能通过Object.keys方法获取到的,所以为了保持在不支持Symbol环境下时keys的一致性,才会跳过这个循环。

继续看下面的代码。

const provideKey = inject[key].from; // ! 从 from 中获取对于 provide 的 key

声明常量provideKey存储inject[key]from属性,这个from属性就是父组件的_provide属性对应的key,从规范化inject选项的代码中我们知道,inject中的from属性存储它的来源属性的key值,另外,inject选项中的key值和这个from值不一定相同。

示例代码

provide: {
  name: "Hale";
}
// 规范化前
inject: {
  n: "name";
}
 
// 规范化后
inject: {
  n: {
    from: "name";
  }
}
 
// 获取值
n: "Hale";

继续看下面的代码。

let source = vm;
while (source) {
  // ! 查找并获取父组件的 provide 的值
  if (source._provided && hasOwn(source._provided, provideKey)) {
    result[key] = source._provided[provideKey]; // ! 注意:因为组件是先初始化 inject, 所以不会找到本身的 provide
    break;
  }
  source = source.$parent; // ! 循环往上找
}

声明变量source缓存实例对象vm。然后就是一个while循环语句,它每次循环执行的代码是source = source.$parent,就是通过$parent属性一直在组件树中往上去寻找source

循环的结束条件是找到组件source中有_provide属性且属性里面有和provideKey相同名字的key,这说明找到了父组件提供的key值,这个值就是source._provided[provideKey],然后把值存储到变量result中。

这里有个问题,就是通过这样的方法去查找值,会不会找到实例本身定义的provide选项中的值呢?

答案是不会的。因为初始化实例对象时,初始化inject选项比初始化provide的时间更早,所以在那个时候,实例组件还没有创建_provide属性,只能去父组件中去寻找。

继续看下面的代码。

// ! 如果循环之后还是找不到值
if (!source) {
  // ! 如果定义了默认值则使用默认值,函数类型的默认字需要求值
  if ("default" in inject[key]) {
    const provideDefault = inject[key].default;
    result[key] =
      typeof provideDefault === "function"
        ? provideDefault.call(vm)
        : provideDefault;
 
    // ! 找不到又没有默认值,非生产环境下报错
  } else if (process.env.NODE_ENV !== "production") {
    warn(`Injection "${key}" not found`, vm);
  }
}

判断source是否存在,如果不存在,说明循环结束后还找到满足条件的父组件。这时候先判断inject选项的属性里面是否设置了default属性,也就是默认值。如果设置了默认值,声明常量provideDefault存储这个默认值,即inject[key].default。然后再判断这个默认值是不是一个函数,如果是函数,就执行函数求值,并把求值的结果赋值到result对象中,它的key还是以前的keyvalue就是函数的求值结果。如果不是函数类型,直接赋值。

如果找不到满足条件的父组件,又没有设置默认值,在非生产环境中会报错,会提示找不到injectkey的值。

resolveInject函数讲解完毕,再次回到initInjections函数中,查看剩下的代码。

// ! 获取到 provide 的值后
if (result) {
  toggleObserving(false); // ! 不监听 inject 选项的属性不是响应式的
  // ...
  toggleObserving(true); // ! 恢复监听
}

首先判断是否获取到provide选项的值,获取到之后才能进行下面的逻辑。

执行toggleObserving(false)代码把shouldObserve的值设置为false,在前面我们知道把shouldObserve设置为false时,就不会去创建监听器属性。也就是说不会把inject中的数据变成响应式数据,因为inject选项的数据不需要是响应式数据。

这里中间省略了一些代码。

在最后又执行toggleObserving(true)代码把shouldObserve的值重新设置为true,这样就不会影响到其他选项的数据变成响应式数据。

下面看下中间的代码。

Object.keys(result).forEach((key) => {
  /* istanbul ignore else */
  if (process.env.NODE_ENV !== "production") {
    // ! 实例代理 key,另外不能修改 inject 的数据,否则报错
    defineReactive(vm, key, result[key], () => {
      warn(
        `Avoid mutating an injected value directly since the changes will be ` +
          `overwritten whenever the provided component re-renders. ` +
          `injection being mutated: "${key}"`,
        vm
      );
    });
  } else {
    defineReactive(vm, key, result[key]); // ! 实例代理 key
  }
});

通过Object.keys获取resultkeys的值,然后使用forEach遍历keys,再调用defineReactive函数使得实例对象vm代理inject上的key值。

注意,这里通过defineReactive定义的数据并不会变成响应式数据。因为前面已经把shouldObserve变量设置为false了。但是如果父组件的provide选项提供的值是一个响应式数据,那么注入的值也是响应式数据。

另外,不能修改inject选项中的值,因为它的值来源于父组件,有点类似于props属性。如果修改了inject的值在非生产环境中会报错。

到这里,Vue 的初始化就暂时告一个段落了

参考资料#