Vue源码解析

本文仅对Vue主体做一个流程化的简单解析,其他详细功能的代码分析过程是相同的。阅读源码之前,首先要确保对Vue.js的使用较为熟悉。先知道怎么用,再研究怎么实现。

 

首先去github下载官方源码:

https://github.com/vuejs/vue

当前版本为2.5.17。

解压缩。为了方便查看,导入到webstrom中:

Vue源码解析_第1张图片

源码必然在src文件夹。

查看src文件夹:

Vue源码解析_第2张图片

从文件夹名看,compiler应该是编译相关;core应该是内核相关;platforms应该是平台相关;server应该是服务端相关;sfc无法直观猜测;shared是共享的意思,从名称也无法直接猜测出作用。要查看vue的主体实现,那这几个文件夹,必然应该选core。

Vue源码解析_第3张图片

打开core,列出了几个文件夹,以及两个js文件:config.js和index.js。

config.js很明显是配置文件。index.js,这必然就是入口了。于是打开index.js:

Vue源码解析_第4张图片

可以看到做了几个事情:

1.    import了一些对象进来。

2.    调用了initGlobalAPI()。

3.    调用了Object.defineProperty()。

4.    为Vue对象设置version变量。

5.    将Vue对象默认导出。也就是说,虽然Vue是导入的,但经过一系列操作后,又将Vue进行了导出。相当于是Vue对象在该文件中进行了加工。

imoprt进来的对象,必然是后面用到的。所以可以先不管,等用到的时候再回来查。先来看执行的第一个函数:

initGlobalAPI(Vue)

面对这行代码,需要搞清楚两个问题:

①   initGlobalAPI()是在哪定义的,具体实现是什么。

②   参数Vue明显是个对象,且跟Vue库同名,所以Vue必然是最重要的主对象。这个Vue是在哪定义的,具体实现是什么。

这两个问题,明显②比较重要。所以先看Vue的实现。

查看最上方的import,有这么一行:

import Vue from './instance/index'

也就是说,Vue是在与index.js同目录下的instance文件夹下的index.js中定义的。

Vue源码解析_第5张图片

打开该index.js:

Vue源码解析_第6张图片

可以看到在这个index.js中做了几个事情:

1.    import了一些对象进来。

2.    定义了一个名叫Vuefunction,且传入了一个options参数

3.    调用了许多函数,且都将Vue作为参数传入。

4.    将Vue默认导出。

同样不管import,先来分析Vue这个function的定义。

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

这个function的几个要点:

①   名称是Vue,js常用的扩展方式是实现一个function,然后再new。这个函数就是这样的一个构造函数。回想我们在用Vue的时候,也都是采用

var vm = new Vue({
 
// 选项
})

这样的形式。

②   函数内首先是一个if。判断条件暂且不看,if若成立,会有一个warn:

warn('Vue is a constructor and should be called with the `new` keyword')

大意是,该Vue是一个构造函数,必须使用new来调用。

于是就可以猜测if的判断条件,必然是:若不使用new来调用了Vue,则提示warn。

③   函数最后是一个函数调用:

this._init(options)

this就是new出的Vue对象了。_init()必然是Vue的一个成员函数,从函数名看,其作用是初始化。options是我们传入的参数。结合Vue的具体使用,可知传入的options就是那一堆data,props,methods等。

那么,现在的一个疑问就是,Vue对象是在什么时候定义了这个_init()成员函数?

继续往下看代码,此时执行了5个函数:

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

根据import,可以找到这5个函数的js文件。先来看第一个:

import { initMixin } from './init'

打开同级目录下的init.js,可以看到:

Vue源码解析_第7张图片

首先imoprt了一堆对象进来。

然后定义了一个uid,并赋值为0。

然后导出了一个function,名为initMixin(),接受一个class类型参数,参数名为Vue。

进入initMixin(),第一行代码就是:

Vue.prototype._init = function (options?: Object) {

这里直接修改了传入参数Vue的prototype,为其设置了一个名为_init的函数,该函数接收一个Object类型参数,参数名为options。

于是上面的疑问得以解答:Vue对象的_init()成员函数是在initMixin()中创建并挂载到Vue对象上的,并且对Vue对象而言是个prototype全局函数。

然后继续来看_init()都做了些什么工作:

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 */
  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)
  }
}

首先保存this对象到vm变量中:

const vm: Component = this

用const定义,所以对于vm中的变量不会被修改,当然对象变量和数组变量的内容除外。

然后保存uid,设置_isVue变量等,都没有涉及到感兴趣的操作,忽略。

接着是一段代码:

// merge options
if (options && options._isComponent) {
 
// optimize internal component instantiation
  // since dynamic options merging ispretty slow, and none of the
  // internal component options needsspecial treatment.
 
initInternalComponent
(vm, options)
}
else {
 
vm.$options = mergeOptions(
    resolveConstructorOptions(
vm.constructor),
    options || {},
   
vm
 
)
}

最上方的注释意思是合并options,所以下面的if..else主要作用的合并传入的options。

先看if条件。当options存在并且options._isComponent为true时,就会调用initInternalComponent(vm, options)

_isComponent从变量名看,其含义应该是一个标记,用来记录当前options是否属于组件。而initInternalComponent()从函数名看,其作用应当是初始化内部组件。

所以推断,当options属于一个组件时,就会进入if,对内部组件进行初始化。

而对于else,将传入的options与vm本身的属性进行了合并,并重新赋值给vm.$options。

所以经过这一步,Vue实例会将传入的用户自定义options合并到自身属性中。

接着是:

/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
 
initProxy(vm)
}
else {
 
vm._renderProxy = vm
}

proxy是代理的意思。所以这一步是初始化代理。

然后是一堆函数调用:

// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(
vm)
initRender(
vm)
callHook(
vm, 'beforeCreate')
initInjections(
vm) // resolveinjections before data/props
initState
(vm)
initProvide(
vm) // resolve provide afterdata/props
callHook
(vm, 'created')

注释expose real self的意思是暴露自身。因此猜测这一堆函数应该是为Vue实例添加各种接口,事件,以及可调用的属性。

从import找到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
}

可以看出主要是做了几件事情:

①   从当前Vue实例vm开始向上查找,找到最近的一级abstract为false的parent。将vm push给它的$children。

②   将vm的parent修改为①中找到的parent。

③   修改vm的根$root。若vm的parent存在,则与parent的$root统一;否则$root就是vm自身。

④   修改vm的各种变量。

于是,经过initLifecycle(),vm的parent,以及parent的children,都得到了更新。同时为vm新增了各种变量。

实际上,lifecycle是生命周期的意思,所以initLifecycle从字面即可清晰理解意图:为vm的生命周期做准备,初始化各种变量。

同理,
initEvents(
vm)
initRender(
vm)
callHook(
vm, 'beforeCreate')
initInjections(
vm) // resolveinjections before data/props
initState
(vm)
initProvide(
vm) // resolve provide afterdata/props
callHook
(vm, 'created')

这些,从名称就可以看出,是为vm实例装配事件、渲染器、状态,data等一系列属性或函数。

注意在这个过程中,插入了两个callHook():

callHook(vm, 'beforeCreate')

callHook(vm, 'created')

也就是说,执行完initLifecycle(),initEvents (),initRender()之后,会callHook(vm, 'beforeCreate'),即调用'beforeCreate'钩子函数。

然后继续执行initInjections(),initState(),initProvide(),接着callHook(vm'ceated'),即调用'created'钩子函数。

在这里体现出了Vue生命周期中的前两步。

最后:

if (vm.$options.el) {
 
vm.$mount(vm.$options.el)
}

当el存在时,将其挂载到vm实例上。

这样,initMixin()就执行完成了。综上可知,该函数为vm添加了各种变量与函数,就像函数名一样:init是初始化的意思,mixin是混入的意思,initMixin()的主要作用就是将vm所需的各种初始化变量与函数混入到vm对象中。

然后返回到instance/index.js中来。

同理:

stateMixin(Vue) :混入props,methods,data,computed,watch。
eventsMixin(Vue)
:混入_events,并更新组件的listeners。
lifecycleMixin(Vue)
:混入_vnode,$el,$parent相关。
renderMixin(Vue)
:混入渲染相关的内容,如$slots等。

经过这些混入操作,Vue由一个什么属性都没有的空对象,变为拥有一堆变量与函数的丰满对象。

现在回到core/index.js。

上面已经解决了最初的两个问题中的第②个,即Vue对象是在哪里定义的,具体实现是什么。

然后来看问题①:initGlobalAPI()是在哪定义的,具体实现是什么。

同理,根据imoprt,打开core/global-api/index.js,查看initGlobalAPI()的具体实现,会发现该函数对Vue对象进行了以下几个操作:

①   为Vue实例设置了一个空的config属性。

②   为Vue的util设置了几个成员,包含 :warn,extend,mergeOptions,defineReactive。

③   为Vue设置了set,delete,nextTick,以及一个空的options。其中options按类型填充默认空对象。

④   将自身赋给options._base。

⑤   将options.components与builtInComponents合并。

⑥   初始化usemixinextendassetRegisters功能。

于是,经过initGlobalAPI(),Vue实例增加了更多的变量和功能,主要是全局化相关的内容。

现在回到core/index.js。

经过三个属性的设置,以及version修改,最后将功能非常完善的Vue实例对象默认返回。

这个Vue本质上依然是一个构造函数。于是,我们在前端导入Vue.js后,调用:

var vm = new Vue({
 
// 选项
})

得到的vm对象就是一个包含了所有功能的Vue实例。

 

 

你可能感兴趣的:(Vue)