整体流程
在之前的介绍中,我们知道Vue.js
内部会根据Web浏览器
、Weex
跨平台和SSR服务端渲染
不同的环境寻找不同的入口文件,但其核心代码是在src/core
目录下,我们这一篇文章的主要目的是为了搞清楚从入口文件到Vue
构造函数执行,这期间的整体流程。
在分析完从入口到构造函数的各个部分的流程后,我们可以得到一份大的流程图:
initGlobalAPI流程
我们会在src/core/index.js
文件中看到如下精简代码:
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
initGlobalAPI(Vue)
export default Vue
在以上代码中,我们发现它引入了Vue
随后调用了initGlobalAPI()
函数,此函数的作用是挂载一些全局API
方法。
我们首先能在src/core/global-api
文件夹下看到如下目录结构:
|-- global-api
| |-- index.js # 入口文件
| |-- assets.js # 挂载filter、component和directive
| |-- extend.js # 挂载extend方法
| |-- mixin.js # 挂载mixin方法
| |-- use.js # 挂载use方法
随后在index.js
入口文件中,我们能看到如下精简代码:
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { observe } from 'core/observer/index'
import { extend, nextTick } from '../util/index'
export function initGlobalAPI (Vue: GlobalAPI) {
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
Vue.observable = (obj) => {
observe(obj)
return obj
}
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
我们能从以上代码很清晰的看到在index.js
入口文件中,会在Vue
构造函数上挂载各种全局API
函数,其中set
、delete
、nextTick
和observable
直接赋值为一个函数,而其他几种API
则是调用了一个以init
开头的方法,我们以initAssetRegisters()
方法为例,它的精简代码如下:
// ['component','directive', 'filter']
import { ASSET_TYPES } from 'shared/constants'
export function initAssetRegisters (Vue: GlobalAPI) {
ASSET_TYPES.forEach(type => {
Vue[type] = function () {
// 省略了函数的参数和函数实现代码
}
})
}
其中ASSET_TYPES
是一个定义在src/shared/constants.js
中的一个数组,然后在initAssetRegisters()
方法中遍历这个数组,依次在Vue
构造函数上挂载Vue.component()
、Vue.directive()
和Vue.filter()
方法,另外三种init
开头的方法调用挂载对应的全局API
是一样的道理:
// initUse
export function initUse(Vue) {
Vue.use = function () {}
}
// initMixin
export function initMixin(Vue) {
Vue.mixin = function () {}
}
// initExtend
export function initExtend(Vue) {
Vue.extend = function () {}
}
最后,我们发现还差一个Vue.compile()
方法,它其实是在runtime+compile
版本才会有的一个全局方法,因此它在src/platforms/web/entry-runtime-with-compile.js
中被定义:
import Vue from './runtime/index'
import { compileToFunctions } from './compiler/index'
Vue.compile = compileToFunctions
export default Vue
因此我们根据initGlobalAPI()
方法的逻辑,可以得到如下流程图:
initMixin流程
在上一目录我们讲到了initGlobalAPI
的整体流程,这一,我们来介绍initMixin
的整体流程。首选,我们把目光回到src/core/index.js
文件中:
源码地址
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
initGlobalAPI(Vue)
export default Vue
我们发现,它从别的模块中引入了大Vue
,那么接下来我们的首要任务就是揭开Vue
构造函数的神秘面纱。
在看src/core/instance/index.js
代码之前,我们发现instance
目录结构如下:
|-- instance
| |-- render-helpers # render渲染相关的工具函数目录
| |-- events.js # 事件处理相关
| |-- init.js # _init等方法相关
| |-- inject.js # inject和provide相关
| |-- lifecycle.js # 生命周期相关
| |-- proxy.js # 代理相关
| |-- render.js # 渲染相关
| |-- state.js # 数据状态相关
| |-- index.js # 入口文件
可以看到,目录结构文件有很多,而且包含的面也非常杂,但我们现在只需要对我们最关心的几个部分做介绍:
events.js
:处理事件相关,例如:$on
,$off
,$emit
以及$once
等方法的实现。init.js
:此部分代码逻辑包含了Vue
从创建实例到实例挂载阶段的所有主要逻辑。lifecycle.js
:生命周期相关,例如:$destroy
、$activated
和$deactivated
。state.js
:数据状态相关,例如:data
、props
以及computed
等。render.js
:渲染相关,其中最值得关注的是Vue.prototype._render
渲染函数的定义。
在介绍了instance
目录结构的及其各自的作用以后,我们再来看入口文件,其实入口文件这里才是Vue
构造函数庐山真面目:
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
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)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
代码分析:
Vue
构造函数其实就是一个普通的函数,我们只能通过new
操作符进行访问,既new Vue()
的形式,Vue
函数内部也使用了instanceof
操作符来判断实例的父类是否为Vue
构造函数,不是的话则在开发环境下输出一个警告信息。- 除了声明
Vue
构造函数,这部分的代码也调用了几种mixin
方法,其中每种mixin
方法各司其职,处理不同的内容。
从以上代码中,我们能得到src/core/instance/index.js
文件非常直观的代码逻辑流程图:
接下来我们的首要任务是弄清楚_init()
函数的代码逻辑以及initMixin
的整体流程。我们从上面的代码发现,在构造函数内部会调用this._init()
方法,也就是说:
// 实例化时,会调用this._init()方法。
new Vue({
data: {
msg: 'Hello, Vue.js'
}
})
然后,我们在init.js
中来看initMixin()
方法是如何被定义的:
export function initMixin (Vue) {
Vue.prototype._init = function (options) {
// 省略代码
}
}
我们可以发现,initMixin()
方法的主要作用就是在Vue.prototype
上定义一个_init()
实例方法,接下来我们来看一下_init()
函数的具体实现逻辑:
Vue.prototype._init = function (options) {
const vm = this
// 1. 合并配置
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 2.render代理
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// 3.初始化生命周期、初始化事件中心、初始化inject,
// 初始化state、初始化provide、调用生命周期
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
// 4.挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
因为我们是要分析initMixin
整体流程,对于其中某些方法的具体实现逻辑会在后续进行详细的说明,因此我们可以从以上代码得到initMixin
的整体流程图。
stateMixin流程
stateMixin
主要是处理跟实例相关的属性和方法,它会在Vue.prototype
上定义实例会使用到的属性或者方法,这一节我们主要任务是弄清楚stateMixin
的主要流程。在src/core/instance/state.js
代码中,它精简后如下所示:
import { set, del } from '../observer/index'
export function stateMixin (Vue) {
// 定义$data, $props
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
// 定义$set, $delete, $watch
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function() {}
}
我们可以从上面代码中发现,stateMixin()
方法中在Vue.prototype
上定义的几个属性或者方法,全部都是和响应式相关的,我们来简要分析一下以上代码:
$data和$props
:根据以上代码,我们发现$data
和$props
分别是_data
和_props
的访问代理,从命名中我们可以推测,以下划线开头的变量,我们一般认为是私有变量,然后通过$data
和$props
来提供一个对外的访问接口,虽然可以通过属性的get()
方法去取,但对于这两个私有变量来说是并不能随意set
,对于data
来说不能替换根实例,而对于props
来说它是只读的。因此在原版源码中,还劫持了set()
方法,当设置$data
或者$props
时会报错:
if (process.env.NODE_ENV !== 'production') {
dataDef.set = function () {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
)
}
propsDef.set = function () {
warn(`$props is readonly.`, this)
}
}
$set
和$delete
:set
和delete
这两个方法被定义在跟instance
目录平级的observer
目录下,在stateMixin()
中,它们分别赋值给了$set
和$delete
方法,而在initGlobalAPI
中,也同样使用到了这两个方法,只不过一个是全局方法,一个是实例方法。$watch
:在stateMixin()
方法中,详细实现了$watch()
方法,此方法实现的核心是通过一个watcher
实例来监听。当取消监听时,同样是使用watcher
实例相关的方法,关于watcher
我们会在后续响应式章节详细介绍。
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function () {
watcher.teardownunwatchFn()
}
}
在以上代码分析完毕后,我们可以得到stateMixin
如下流程图:
eventsMixin流程
在使用Vue
做开发的时候,我们一定经常使用到$emit
、$on
、$off
和$once
等几个实例方法,eventsMixin
主要做的就是在Vue.prototype
上定义这四个实例方法:
export function eventsMixin (Vue) {
// 定义$on
Vue.prototype.$on = function (event, fn) {}
// 定义$once
Vue.prototype.$once = function (event, fn) {}
// 定义$off
Vue.prototype.$off = function (event, fn) {}
// 定义$emit
Vue.prototype.$emit = function (event) {}
}
通过以上代码,我们发现eventsMixin()
所做的事情就是使用发布-订阅模式来处理事件,接下来让我们先使用发布-订阅实现自己的事件中心,随后再来回顾源码。
$on的实现
$on
方法的实现比较简单,我们先来实现一个基础版本的:
function Vue () {
this._events = Object.create(null)
}
Vue.prototype.$on = function (event, fn) {
if (!this._events[event]) {
this._events[event] = []
}
this._events[event].push(fn)
return this
}
接下来对比一下Vue
源码中,关于$on
的实现:
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
代码分析:
- 我们发现在
Vue
源码中,$on
方法还接受一个数组event
,这其实是在Vue2.2.0
版本以后才有的,当传递一个event
数组时,会通过遍历数组的形式递归调用$on
方法。 - 我们还发现,所有
$on
的事件全部绑定在_events
私有属性上,这个属性其实是在我们上面已经提到过的initEvents()
方法中被定义的。
export function initEvents (vm) {
vm._events = Object.create(null)
}
$emit的实现
我们先来实现一个简单的$emit
方法:
Vue.prototype.$emit = function (event) {
const cbs = this._events[event]
if (cbs) {
const args = Array.prototype.slice.call(arguments, 1)
for (let i = 0; i < cbs.length; i++) {
const cb = cbs[i]
cb && cb.apply(this, args)
}
}
return this
}
接下来,我们使用$emit
和$on
来配合测试事件的监听和触发:
const app = new Vue()
app.$on('eat', (food) => {
console.log(`eating ${food}!`)
})
app.$emit('eat', 'orange')
// eating orange!
最后我们来看Vue
源码中关于$emit
的实现:
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
// ...省略处理边界代码
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
代码分析:
- 从整体上看,
$emit
实现方法非常简单,第一步从_events
对象中取出对应的cbs
,接着一个个遍历cbs
数组、调用并传参。 invokeWithErrorHandling
代码中会使用try/catch
把我们函数调用并执行的地方包裹起来,当函数调用出错时,会执行Vue
的handleError()
方法,这种做法不仅更加友好,而且对错误处理也非常有用。
$off的实现
$off
方法的实现,相对来说比较复杂一点,因为它需要根据不同的传参做不同的事情:
- 当没有提供任何参数时,移除全部事件监听。
- 当只提供
event
参数时,只移除此event
对应的监听器。 - 同时提供
event
参数和fn
回调,则只移除此event
对应的fn
这个监听器。
在了解了以上功能点后,我们来实现一个简单的$off
方法:
Vue.prototype.$off = function (event, fn) {
// 没有传递任何参数
if (!arguments.length) {
this._events = Object.create(null)
return this
}
// 传递了未监听的event
const cbs = this._events[event]
if (!cbs) {
return this
}
// 没有传递fn
if (!fn) {
this._events[event] = null
return this
}
// event和fn都传递了
let i = cbs.length
let cb
while (i--) {
cb = cbs[i]
if (cb === fn) {
cbs.splice(i, 1)
break
}
}
return this
}
接下来,我们撰写测试代码:
const app = new Vue()
function eatFood (food) {
console.log(`eating ${food}!`)
}
app.$on('eat', eatFood)
app.$emit('eat', 'orange')
app.$off('eat', eatFood)
// 不执行回调
app.$emit('eat', 'orange')
最后我们来看Vue
源码中关于$off
的实现:
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// all
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific event
const cbs = vm._events[event]
if (!cbs) {
return vm
}
if (!fn) {
vm._events[event] = null
return vm
}
// specific handler
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
$once的实现
关于$once
方法的实现比较简单,可以简单的理解为在回调之后立马调用$off
,因此我们来实现一个简单的$once
方法:
Vue.prototype.$once = function (event, fn) {
function onFn () {
this.$off(event, onFn)
fn.apply(this, arguments)
}
this.$on(event, onFn)
return this
}
接着我们对比一下Vue
源码中的$once
方法:
Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this
function on () {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
注意:在源码中$once
的实现是在回调函数中使用fn
绑定了原回调函数的引用,在上面已经提到过的$off
方法中也同样进行了cb.fn === fn
的判断。
在实现完以上几种方法后,我们可以得到eventsMixin
如下流程图:
lifecycleMixin流程
和以上其它几种方法一样,lifecycleMixin
主要是定义实例方法和生命周期,例如:$forceUpdate()
、$destroy
,另外它还定义一个_update
的私有方法,其中$forceUpdate()
方法会调用它,因此lifecycleMixin
精简代码如下:
export function lifecycleMixin (Vue) {
// 私有方法
Vue.prototype._update = function () {}
// 实例方法
Vue.prototype.$forceUpdate = function () {
if (this._watcher) {
this._watcher.update()
}
}
Vue.prototype.$destroy = function () {}
}
代码分析:
_update()
会在组件渲染的时候调用,其具体的实现我们会在组件章节详细介绍$forceUpdate()
为一个强制Vue
实例重新渲染的方法,它的内部调用了_update
,也就是强制组件重选编译挂载。$destroy()
为组件销毁方法,在其具体的实现中,会处理父子组件的关系,事件监听,触发生命周期等操作。
lifecycleMixin()
方法的代码不是很多,我们也能很容易的得到如下流程图:
renderMixin流程
相比于以上几种方法,renderMixin
是最简单的,它主要在Vue.prototype
上定义各种私有方法和一个非常重要的实例方法:$nextTick
,其精简代码如下:
export function renderMixin (Vue) {
// 挂载各种私有方法,例如this._c,this._v等
installRenderHelpers(Vue.prototype)
Vue.prototype._render = function () {}
// 实例方法
Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this)
}
}
代码分析:
installRenderHelpers
:它会在Vue.prototype
上挂载各种私有方法,例如this._n = toNumber
、this._s = toString
、this._v = createTextVNode
和this._e = createEmptyVNode
。_render()
:_render()
方法会把模板编译成VNode
,我们会在其后的编译章节详细介绍。nextTick
:就像我们之前介绍过的,nextTick
会在Vue
构造函数上挂载一个全局的nextTick()
方法,而此处为实例方法,本质上引用的是同一个nextTick
。
在以上代码分析完毕后,我们可以得到renderMixin
如下流程图:
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!