Vue源码精解_02_new_Vue发生了什么(一)

new Vue 发生了什么

在我的上一篇文章中《Vue源码精解_01_从构建开始》已经解释过,当我们执行new Vue的时候实际上是实例化一个对象。

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

Vue实际上是用Function实现的类,调用内部的this_init方法,而这个方法在上一篇讲过这个是执行initMixin(Vue)对Vue的扩展,在Vue.prototype上实现了_init方法。这个方法在core/instance/init.js文件中

export function initMixin(Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this;
    // a uid
    vm._uid = uid++;

    let startTag, endTag;
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== "production" && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`;
      endTag = `vue-perf-end:${vm._uid}`;
      mark(startTag);
    }

    // a flag to avoid this being observed
    vm._isVue = true;
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options);
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );
    }
    /* istanbul ignore else */
    // 开发阶段初始化 proxy
    if (process.env.NODE_ENV !== "production") {
      initProxy(vm);
    } else {
      vm._renderProxy = vm;
    }
    // expose real self
    vm._self = vm;
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, "beforeCreate");
    initInjections(vm); // resolve injections before data/props
    initState(vm);
    initProvide(vm); // resolve provide after data/props
    callHook(vm, "created");

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== "production" && config.performance && mark) {
      vm._name = formatComponentName(vm, false);
      mark(endTag);
      measure(`vue ${vm._name} init`, startTag, endTag);
    }
    
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };
}

我们逐一来分析上述代码

const vm: Component = this;
    // a uid
vm._uid = uid++;

let startTag, endTag;
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && config.performance && mark) {
  startTag = `vue-perf-start:${vm._uid}`;
  endTag = `vue-perf-end:${vm._uid}`;
  mark(startTag);
}

首先缓存当前的上下文到vm变量中,方便之后调用。然后设置_uid属性,这个属性是唯一的。当触发init方法,新建Vue实例时(当组件渲染时也会触发)uid都会递增。

    let startTag, endTag;
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== "production" && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`;
      endTag = `vue-perf-end:${vm._uid}`;
      mark(startTag);
    }

上面这段代码主要是用来测试代码性能的,在这个时候相当于打了一个“标记点”来测试性能。但是只适用于开发模式和支持performance.mark API的浏览器

    // a flag to avoid this being observed
    vm._isVue = true;
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options);
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );
    }

通过代码的注释我们可以知道,这是对options做一个合并,但是在合并前执行了vm._isVue = true乍看起来好像不太明白,好像是说防止当前实例被 observed实例化。我们可以简单看下observer的代码,之后研究响应式会详细讲解

export function observe (value: any, asRootData: ?boolean): Observer | void {
  ...
  else if (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  ...
}

如果传入值的_isVuetrue时(即传入的值是Vue实例本身)不会新建observer实例(这里可以暂时理解Vue的实例不需要监听变化)。
再回到init源码部分

    if (options && options._isComponent) {
      initInternalComponent(vm, options);
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );
    }

mergeOptions

如果当前的Vue实例是组件的话,就执行initInternalComponent方法。(这个方法的主要作用就是为vm.$options添加一些属性,后面讲到组件的时候再详细介绍)否则当前的操作是实例化Vue对象,因为我们从入口开始是通过new Vue实例化的对象,所以调用mergeOptions方法,这个方法接受三个参数。把vm.constructor给到resolveConstructorOptions调用得到的结果作为mergeOptions的第一个参数,并把options作为第二个参数,这个options是在 new Vue(options)的时候传进来的,然后传递给this._init(options)最终传递到mergeOptions里面,做合并。第三个参数是vm。那么这个方法是如何合并的呢?我们先来研究resolveConstructorOptions中的内容

function resolveConstructorOptions (Ctor) {
  var options = Ctor.options;
  if (Ctor.super) {
    var superOptions = resolveConstructorOptions(Ctor.super);
    var cachedSuperOptions = Ctor.superOptions;
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions;
      // check if there are any late-modified/attached options (#4976)
      var modifiedOptions = resolveModifiedOptions(Ctor);
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions);
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
      if (options.name) {
        options.components[options.name] = Ctor;
      }
    }
  }
  return options
}

这个方法要分成两种情况来说明。第一种是Ctor是基础Vue构造器的情况,另一种Ctor是通过Vue.extend方法扩展的情况

Ctor是基础Vue构造器

比如是通过new Vue创建的实例,Ctor其实就是基础的构造函数,直接返回options,我们在源码中调试输出

    <script src="./dist/vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          message: "hello world",
        },
      });
      console.dir(vm.constructor.options);
    </script>

在这里插入图片描述
那么这个options是在哪里定义的呢?它是什么时候跑到Vue上的?(vm.constructor --> Vue),我们应该如何去找这个options?从构建开始!入口文件的地址在web/entry-runtime-with-compiler.js,这个东西我在上一篇文章中已经讲过如何从构建开始找
这个文件里面有对Vue的第一层包装,但是这层包装里面没有options相关的内容,所以这个文件这里不展开讲,后面讲挂载的时候会详细说明。里面有这行文件引入

...
import Vue from "./runtime/index";
...

Vue构造函数的第二层包装就在这个文件里

...
import Vue from 'core/index' // 第三层包装
import platformDirectives from './directives/index'
import platformComponents from './components/index'
...
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
...

// platformDirectives相关
// 这里导出Vue全局指令model,show
import model from './model'
import show from './show'
export default {
  model,
  show
}

// platformComponents相关
// 这里导出Vue全局组件Transition,TransitionGroup
import Transition from './transition'
import TransitionGroup from './transition-group'
export default {
  Transition,
  TransitionGroup
}

上面的代码主要是给Vue.options.directives添加modelshow属性,给Vue.options.components添加TransitionTransitionGroup属性。那么还有filters_base属性,以及components中的KeepAlive又是怎么来的呢?
这就要看Vue的第三层包装里都做了些什么?找到core/index.js,同样我们只看Vue.options相关代码。

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
...
initGlobalAPI(Vue)
...

options就是这个时候跑到Vue上去的,initGlobalAPI在上一节带过,我们现在看看/global-api/index.js的代码。

...
import { initExtend } from "./extend";
import { ASSET_TYPES } from 'shared/constants'
...
export function initGlobalAPI (Vue: GlobalAPI) {
  ...
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })
  Vue.options._base = Vue
  extend(Vue.options.components, builtInComponents)
  ...
}

// shared/constants.js
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

// core/components/index
import KeepAlive from './keep-alive'
export default {
  KeepAlive
}

上面这层包装就把filters_basecomponents中的KeepAlive都实现了。通过这三层包装,Vue构造函数的options对象就生成了

Ctor.super

通过Vue.extends构造子类时。Vue.extends方法会为Ctor添加一个super属性,指向父类构造器。(提一下,Vue3中已经将Vue.extends方法废弃,原因官网vue3文档有解释)

Vue.extend = function (extendOptions: Object): Function {
  ...
  Sub['super'] = Super
  ...
}

所以当Ctor时基础构造器的时候,resolveConstructorOptions方法返回基础构造器的options。除了Ctor是基础构造器之外,还有一种是Ctor是通过Vue.extend构造的子类。这种情况比较复杂。Ctor上有了super属性,就会去执行if块内的代码。首先递归调用resolveConstructorOptions方法,返回"父类"上的options并赋值给superOptions变量,然后把"自身"的options赋值给cachedSuperOptions变量。然后比较这两个变量的值,当这两个变量值不等时,说明"父类"的options改变过了。例如执行了Vue.mixin方法,这时候就需要把"自身"的superOptions属性替换成最新的。然后检查是否自身的options是否发生变化。resolveModifiedOptions的功能就是这个

if (superOptions !== cachedSuperOptions) {
  // super option changed,
  // need to resolve new options.
  Ctor.superOptions = superOptions
  // check if there are any late-modified/attached options (#4976)
  const modifiedOptions = resolveModifiedOptions(Ctor)
  ....
}

举个例子说明一下:

var Profile = Vue.extend({
   template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>'
})
Vue.mixin({ data: function () {
  return {
    firstName: 'Walter',
    lastName: 'White',
    alias: 'Heisenberg'
  }
 }})
 new Profile().$mount('#example')

由于Vue.mixin改变了"父类"的options。源码中superOptionscachedSuperOptions就不相等了,就更新自身的superOptions属性。接下来执行resolveModifiedOptions

function resolveModifiedOptions (Ctor) {
  var modified; // 定义modified变量
  var latest = Ctor.options; // 自身的options
  var sealed = Ctor.sealedOptions; // 构造"自身"时传入的options
  for (var key in latest) {
    if (latest[key] !== sealed[key]) {
      if (!modified) { modified = {}; }
      modified[key] = latest[key];
    }
  }
  return modified
}

遍历当前构造器上的options属性,如果在"自身"封装的options里没有,则证明是新添加的。执行if内的语句。最终返回modified变量(即自身新添加的options)。

  if (modifiedOptions) {
    extend(Ctor.extendOptions, modifiedOptions)
  }
  options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
  if (options.name) {
    options.components[options.name] = Ctor
  }

如果”自身“有新添加的options,则把新添加的options属性添加到Ctor.extendOptions属性上。调用mergeOptions方法合并"父类"构造器上的options和”自身“上的extendOptions

resolveConstructorOptions,它的主要功能是解析当前实例构造函数上的options,在解析完其构造函数上的options之后,需要把构造函数上的options和实例化时传入的options进行合并操作并生成一个新的optionsmergeOptions具体的内容我打算放到组件化里面去分析,这里暂时知道又这个合并配置的操作。合并后vm.$options大致的内容如下:

vm.$options = {
  components: { },
  created: [
    function created() {
      console.log('parent created')
    }
  ],
  directives: { },
  filters: { },
  _base: function Vue(options) {
    // ...
  },
  el: "#app",
  render: function (h) {
    //...
  }
}

initProxy

接着合并配置之后,就会在开发阶段初始化代理

// 开发阶段初始化 proxy
if (process.env.NODE_ENV !== "production") {
  initProxy(vm);
} else {
  vm._renderProxy = vm;
}

如果不是开发环境,则vue实例的_renderProxy属性指向vue实例本身
进到initProxy方法内

...
const hasProxy = typeof Proxy !== "undefined" && isNative(Proxy);
...
initProxy = function initProxy(vm) {
	if (hasProxy) {
	  // determine which proxy handler to use
	  const options = vm.$options;
	  const handlers =
	    options.render && options.render._withStripped
	      ? getHandler
	      : hasHandler;
	  // handlers 这个场景下 指向 hasHandler
	  vm._renderProxy = new Proxy(vm, handlers);
	} else {
	  vm._renderProxy = vm;
	}
};
export { initProxy };

首先判断当前环境是否支持 Proxy API,如果options上存在render属性,且render属性上存在_withStripped属性,则proxytraps(traps其实也就是自定义方法)采用getHandler方法,否则采用hasHandler方法

接下来看看getHandlerhasHandler方法

const getHandler = {
  get (target, key) {
    if (typeof key === 'string' && !(key in target)) {
      warnNonPresent(target, key)
    }
    return target[key]
  }
}

getHandler方法主要是针对读取代理对象的某个属性时进行的操作。当访问的属性不是string类型或者属性值在被代理的对象上不存在,则抛出错误提示,否则就返回该属性值。
该方法可以在开发者错误的调用vm属性时,提供提示作用。

const hasHandler = {
  has (target, key) {
  	// 首先使用in操作符判断该属性是否在vm实例上存在。
    const has = key in target
    // 判断属性名称是否可用
    const isAllowed = allowedGlobals(key) || key.charAt(0) === '_'
    if (!has && !isAllowed) {
      warnNonPresent(target, key)
    }
    return has || !isAllowed
  }
}

hasHandler方法的应用场景在于查看vm实例是否拥有某个属性。比如调用for in循环遍历vm实例属性时,会触发hasHandler方法,方法中首先使用in操作符判断该属性是否在vm实例上存在。然后判断属性名称是否可用。
allowedGlobals的定义如下:

 const allowedGlobals = makeMap(
    'Infinity,undefined,NaN,isFinite,isNaN,' +
    'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
    'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
    'require' // for Webpack/Browserify
  )

结合makeMap函数一起来看

export function makeMap (
  str: string,
  expectsLowerCase?: boolean
): (key: string) => true | void {
  const map = Object.create(null)
  const list: Array<string> = str.split(',')
  for (let i = 0; i < list.length; i++) {
    map[list[i]] = true
  }
  return expectsLowerCase
    ? val => map[val.toLowerCase()]
    : val => map[val]
}

分析makeMap,其作用是通过传入的string参数来生成映射表。如传入下列参数

'Infinity,undefined,NaN,isFinite,isNaN,'
....

通过makeMap方法可以生成下面这样的一个映射表

{
  Infinity: true,
  undefined: true
  ......
}

allowedGlobals最终存储的是一个代表特殊属性名称的映射表。
所以结合hasisAllowed属性,我们知道当读取对象属性时,如果属性名在vm上不存在,且不在特殊属性名称映射表中,或没有以_符号开头。则抛出异常。
最后回到initProxy代码中:

  if (hasProxy) {
      ...
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
  }

如果Proxy属性存在,则把包装后的vm属性赋值给_renderProxy属性值。否则把vm是实例本身赋值给_renderProxy属性

核心初始化

接着init源码分析:下面的几个初始化函数非常重要

// expose real self
 vm._self = vm;
 initLifecycle(vm);
 initEvents(vm);
 initRender(vm);
 callHook(vm, "beforeCreate");
 initInjections(vm); // resolve injections before data/props
 initState(vm);
 initProvide(vm); // resolve provide after data/props
 callHook(vm, "created");

initLifeCycle

export function initLifecycle(vm: Component) {
  const options = vm.$options;

  // locate first non-abstract parent
  let parent = options.parent;
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent;
    }
    parent.$children.push(vm);
  }

  vm.$parent = parent;
  vm.$root = parent ? parent.$root : vm;

  vm.$children = [];
  vm.$refs = {};

  vm._watcher = null;
  vm._inactive = null;
  vm._directInactive = false;
  vm._isMounted = false;
  vm._isDestroyed = false;
  vm._isBeingDestroyed = false;
}

首先将mergeOptions后的vm.$options赋值给options变量

// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
  while (parent.$options.abstract && parent.$parent) {
    parent = parent.$parent
  }
  parent.$children.push(vm)
}

作者首先对这段代码提供了一行注释

locate first non-abstract parent
定位一个非抽象的父组件

在这里插入图片描述
抽象组件自身不会渲染一个DOM元素,也不会出现在组件的父组件链中

let parent = options.parent
if (parent && !options.abstract) {
    ... 
}

当前vm实例有父实例parent,则赋值给parent变量。如果父实例存在,且该实例不是抽象组件。则执行下面代码

while (parent.$options.abstract && parent.$parent) {
  parent = parent.$parent
}
parent.$children.push(vm)

如果父实例parent是抽象组件,则继续找parent上的parent。直到找到非抽象组件为止。之后把当前vm实例push到定位的第一个非抽象parent$children属性上。这样我们就说完了怎么找vmparent属性。

之后我们回到initLifecycle继续往下看

  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}
  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false

这些代码都是为vm一些属性赋值。这些属性的作用如下表。
在这里插入图片描述

initEvents

initRender

callHook

initInjections

initState

initProvide

callHook

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值