You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// 省略import语句functionVue(options){if(process.env.NODE_ENV!=='production'&&!(thisinstanceofVue)){warn('Vue is a constructor and should be called with the `new` keyword')}this._init(options)}initMixin(Vue)stateMixin(Vue)eventsMixin(Vue)lifecycleMixin(Vue)renderMixin(Vue)exportdefaultVue
下面我们分成两段来讲解这些代码分别干了什么。
functionVue(options){if(process.env.NODE_ENV!=='production'&&!(thisinstanceofVue)){warn('Vue is a constructor and should be called with the `new` keyword')}this._init(options)// 构造函数有用的只有这一行代码,是不是很简单,至于这一行代码具体做了什么,在第二阶段我们详细讲解。}
exportfunctioninitGlobalAPI(Vue: GlobalAPI){// configconstconfigDef={}configDef.get=()=>config// 省略// 这里添加了一个`Vue.config` 对象,至于在哪里会用到,后面会讲Object.defineProperty(Vue,'config',configDef)// exposed util methods.// NOTE: these are not considered part of the public API - avoid relying on// them unless you are aware of the risk.Vue.util={
warn,
extend,
mergeOptions,
defineReactive
}//一般我们用实例方法而不是这三个类方法Vue.set=setVue.delete=delVue.nextTick=nextTick// 注意这里,循环出来的结果其实是三个 `components`,`directives`, `filters`,这里先创建了空对象作为容器,后面如果有对应的插件就会放进来。Vue.options=Object.create(null)ASSET_TYPES.forEach(type=>{Vue.options[type+'s']=Object.create(null)})// this is used to identify the "base" constructor to extend all plain-object// components with in Weex's multi-instance scenarios.Vue.options._base=Vue// 内置组件只有一个,就是 `keepAlive`extend(Vue.options.components,builtInComponents)initUse(Vue)// 添加了 Vue.use 方法,可以注册插件initMixin(Vue)//添加了Vue.mixin 方法initExtend(Vue)// 添加了 Vue.extend 方法// 这一步是注册了 `Vue.component` ,`Vue.directive` 和 `Vue.filter` 三个方法,上面不是有 `Vue.options.components` 等空对象吗,这三个方法的作用就是把注册的组件放入对应的容器中。initAssetRegisters(Vue)}
Vue.prototype._init=function(options?: Object){constvm: Component=this// a uidvm._uid=uid++letstartTag,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 observedvm._isVue=true// merge optionsif(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 selfvm._self=vminitLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm,'beforeCreate')initInjections(vm)// resolve injections before data/propsinitState(vm)initProvide(vm)// resolve provide after data/propscallHook(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)}}
我们来一段一段看看上面的代码分别作了什么。
constvm: Component=this// vm 就是this的一个别名而已// a uidvm._uid=uid++// 唯一自增IDletstartTag,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)}
这段代码首先生成了一个全局唯一的id。然后如果是非生产环境并且开启了 performance,那么会调用 mark 进行performance标记,这段代码就是开发模式下收集性能数据的,因为和Vue本身的运行原理无关,我们先跳过。
// a flag to avoid this being observedvm._isVue=true// merge options// // TODOif(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{// mergeOptions 本身比较简单,就是做了一个合并操作vm.$options=mergeOptions(resolveConstructorOptions(vm.constructor),options||{},vm)}
这里分析的是当前(2018/07/25)最新版
V2.5.16
的源码,如果你想一遍看一遍参阅源码,请务必记得切换到此版本,不然可能存在微小的差异。大家都知道,我们的应用是一个由Vue组件构成的一棵树,其中每一个节点都是一个 Vue 组件。我们的每一个Vue组件是如何被创建出来的,创建的过程经历了哪些步骤呢?把这些都搞清楚,那么我们对Vue的整个原理将会有很深入的理解。
从入口函数开始,有比较复杂的引用关系,为了方便大家理解,我画了一张图可以直观地看出他们之间的关系:
创建Vue实例的两步
我们创建一个Vue实例,只需要两行代码:
而这两步分别经历了一个比较复杂的构建过程:
Vue
构造函数,以及他的一系列原型方法和类方法Vue
实例,初始化他的数据,事件,模板等下面我们分别解析这两个阶段,其中每个
阶段
又分为好多个步骤
第一阶段:创建Vue类
第一阶段是要创建一个Vue类,因为我们这里用的是原型而不是ES6中的class声明,所以拆成了三步来实现:
Vue
Vue.prototype
上创建一系列实例属性方法,比如this.$data
等Vue
上创建一些全局方法,比如Vue.use
可以注册插件我们导入 Vue 构造函数
import Vue from ‘vue’
的时候(new Vue(options)
之前),会生成一个Vue的构造函数,这个构造函数本身很简单,但是他上面会添加一系列的实例方法和一些全局方法,让我们跟着代码来依次看看如何一步步构造一个 Vue 类的,我们要明白每一步大致是做什么的,但是这里先不深究,因为我们会在接下来几章具体讲解每一步都做了什么,这里我们先有一个大致的概念即可。我们看代码先从入口开始,这是我们在浏览器环境最常用的一个入口,也就是我们
import Vue
的时候直接导入的,它很简单,直接返回了 从platforms/web/runtime/index/js
中得到的Vue
构造函数,具体代码如下:platforms/web/entry-runtime.js
可以看到,这里不是 Vue 构造函数的定义地方,而是返回了从下面一步得到的Vue构造函数,但是做了一些平台相关的操作,比如内置 directives 注册等。这里就会有人问了,为什么不直接定义一个构造函数,而是这样不停的传递呢?因为 vue 有不同的运行环境,而每一个环境又有带不带
compiler
等不同版本,所以环境的不同以及版本的不同都会导致Vue
类会有一些差异,那么这里会通过不同的步骤来处理这些差异,而所有的环境版本都要用到的核心代码是相同的,因此这些相同的代码就统一到core/
中了。完整代码和我加的注释如下:
platforms/web/runtime/index.js
上面的代码终于把平台和配置相关的逻辑都处理完了,我们可以进入到了
core
目录,这里是Vue组件的核心代码,我们首先进入 core/index文件,发现Vue
构造函数也不是在这里定义的。不过这里有一点值得注意的就是,这里调用了一个initGlobalAPI
函数,这个函数是添加一些全局属性方法到Vue
上,也就是类方法,而不是实例方法。具体他是做什么的我们后面再讲core/index.js
到
core/instance/index.js
这里才是真正的创建了Vue
构造函数的地方,虽然代码也很简单,就是创建了一个构造函数,然后通过mixin把一堆实例方法添加上去。core/instance/index.js 完整代码如下:
下面我们分成两段来讲解这些代码分别干了什么。
这里才是真正的Vue构造函数,注意其实很简单,忽略在开发模式下的警告外,只执行了一行代码
this._init(options)
。可想而知,Vue初始化必定有很多工作要做,比如数据的响应化、事件的绑定等,在第二阶段我们会详细讲解这个函数到底做了什么。这里我们暂且跳过它。上面这五个函数其实都是在
Vue.prototype
上添加了一些属性方法,让我们先找一个看看具体的代码,比如initMixin
就是添加_init
函数,没错正是我们构造函数中调用的那个this._init(options)
哦,它里面主要是调用其他的几个初始化方法,因为比较简单,我们直接看代码:core/instance/init.js
另外的几个同样都是在
Vue.prototype
上添加了一些方法,这里暂时先不一个个贴代码,总结一下如下:$data
,$props
,$watch
,$set
,$delete
几个属性和方法$on
,$off
,$once
,$emit
三个方法_update
,$forceUpdate
,$destroy
三个方法$nextTick
和_render
两个方法以及一大堆renderHelpers
还记得我们跳过的在core/index.js中 添加
globalAPI
的代码吗,前面的代码都是在Vue.prototype
上添加实例属性,让我们回到 core/index 文件,这一步需要在Vue
上添加一些全局属性方法。前面讲到过,是通过initGlobalAPI
来添加的,那么我们直接看看这个函数的样子:至此,我们就构建出了一个
Vue
类,这个类上的方法都已经添加完毕。这里再次强调一遍,这个阶段只是添加方法而不是执行他们,具体执行他们是要到第二阶段的。总结一下,我们创建的Vue类都包含了哪些内容:上述就是我们的
Vue
类的全部了,有一些特别细小的点暂时没有列出来,如果你在后面看代码的时候,发现有哪个函数不知道在哪定义的,可以参考这里。那么让我们进入第二个阶段:创建实例阶段第二阶段:创建 Vue 实例
我们通过
new Vue(options)
来创建一个实例,实例的创建,肯定是从构造函数开始的,然后会进行一系列的初始化操作,我们依次看一下创建过程都进行了什么初始化操作:core/instance/index.js, 构造函数本身只进行了一个操作,就是调用
this._init(options)
进行初始化,这个在前面也提到过,这里就不贴代码了。core/instance/init.js 中会进行真正的初始化操作,让我们详细看一下这个函数具体都做了些什么。
先看看它的完整代码:
我们来一段一段看看上面的代码分别作了什么。
这段代码首先生成了一个全局唯一的id。然后如果是非生产环境并且开启了
performance
,那么会调用mark
进行performance标记,这段代码就是开发模式下收集性能数据的,因为和Vue本身的运行原理无关,我们先跳过。上面这段代码,暂时先不用管
_isComponent
,暂时只需要知道我们自己开发的时候使用的组件,都不是_isComponent
,所以我们会进入到else
语句中。这里主要是进行了options
的合并,最终生成了一个$options
属性。下一章我们会详细讲解options
合并的时候都做了什么,这里我们只需要暂时知道,他是把构造函数上的options和我们创建组件时传入的配置options
进行了一个合并就可以了。正是由于合并了这个全局的options
所以我们在可以直接在组件中使用全局的directives
等这段代码可能看起来比较奇怪,这个
renderProxy
是干嘛的呢,其实就是定义了在render
函数渲染模板的时候,访问属性的时候的一个代理,可以看到生产环境下就是自己。开发环境下作了一个什么操作呢?暂时不用关心,反正知道渲染模板的时候上下文就是
vm
也就是this
就行了。如果有兴趣可以看看非生产环境,作了一些友好的报错提醒等。这里只需要记住,在生产环境下,模板渲染的上下文就是
vm
就行了。这一段代码承担了组件初始化的大部分工作。我直接把每一步的作用写在注释里面了。 把这几个函数都弄懂,那么我们也就差不多弄懂了Vue的整个工作原理,而我们接下来的几篇文章,其实都是从这几个函数中的某一个开始的。
开始mount,注意这里如果是我们的
options
中指定了el
才会在这里进行$mount
,而一般情况下,我们是不设置el
而是通过直接调用$mount("#app")
来触发的。比如一般我们都是这样的:以上就是Vue实例的初始化过程。因为在
create
阶段和$mount
阶段都很复杂,所以后面会分几个章节来分别详细讲解。下一篇,让我们从最神秘的数据响应化说起。下一篇:Vue2.x源码解析系列三:Options配置的处理
The text was updated successfully, but these errors were encountered: