Vue源码解析:Vue实例(一)
为什么要写这个
用了很久的JS,也用了很久的Vue.js,从最初的Angular到React到现在在用的Vue。学习路径基本上是:
- 别人告诉我,这个地方你要用ng-module,那么我就用ng-module,至于ng-modul的功能是什么,我不知道
- 带我的大佬不厌其烦了,教授了我查阅API的方法(找到官网,一般都有),自从开始阅读API以后,我会的方法越来越多,心情非常激动的使用一个又一个新功能
- 开始去思考每一个框架的实现细节原理
所以就有现在我想要去研究Vue的源码,研究的方法是跟着Vue官网的教程,一步步的找到教程中功能的实现代码分析实现的代码细节,并且会详细解释代码中涉及的JS(ES6)知识。即使是前端新人也可以轻松阅读
你能得到什么
你可以得到以下知识:
- Vue.js 源码知识
- ES5、ES6基础知识
面对对象
- 前端新人
- 不想花大量时间阅读源码但是想快速知道Vue.js实现细节的人
- 我自己
话不多说,下面就开始我的第一节笔记,对应官网教程中的Vue实例
Vue实例
Vue实例包含
-
创建一个Vue实例
var vm = new Vue({ // 选项 options }) 复制代码
-
数据与方法
// 该对象被加入到一个 Vue 实例中 var vm = new Vue({ data: data }) 复制代码
-
实例生命周期钩子
new Vue({ data: { a: 1 }, created: function () { // `this` 指向 vm 实例 console.log('a is: ' + this.a) } }) // => "a is: 1" 复制代码
-
生命周期图示
创建一个Vue实例
每个Vue应用都是通过Vue函数创建一个新的Vue实例开始:
var vm = new Vue({
// 选项
})
复制代码
我们从Github下载到Vue.js源码后解压打开,探索new Vue创建了一个什么东西
打开下载的vue-dev,找到vue-dev/src/core/index.js
// vue-dev/src/core/index.js
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
Vue.version = '__VERSION__'
export default Vue
复制代码
Vue是从这里定义的,在vue-dev/src/core/index.js的头部找到
import Vue from './instance/index'
复制代码
打开vue-dev/src/core/instance/index.js
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)
}
export default Vue
复制代码
Vue实例在这里得到了各种初始化(init),在这里申明了一个构造器(Constructor)Vue,在构造器里调用了_init方法,并向_init方法中传入了options
this._init(options)
复制代码
显然_init不是Function原型链中的方法,必定是在某处得到定义。紧接着后面看到一系列的Mixin函数调用
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
复制代码
显然是这一堆Mixin方法赋予了Vue实例一个_init方法(之后会有单独的一篇笔记讲述Mixin是怎样的一种设计思维,相关知识会从原型链讲起)
顾名思义,根据函数名字猜测_init是来自于initMixin方法,根据
import { initMixin } from './init'
复制代码
找到vue-dev/src/core/instance/init.js(由于实在是太长了全粘贴过来不方便阅读,故根据需要粘贴相应的节选,如果想要全览的小伙伴可以去下载源码来看完整的)
在vue-dev/src/core/instance/init.js中我们搜索_init,找到下面这个方法
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 */
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)
}
}
}
复制代码
其中
export function initMixin (Vue: Class<Component>) {}
复制代码
里的
Vue: Class<Component>
复制代码
来自于flow语法,一个不错的静态类型检测器
这个initMixin方法里只干了一件事,就是给Vue.prototype._init赋值,即在所有Vue实例的原型链中添加了_init方法。这个_init方法又做了些什么呢?
-
它给Vue实例添加了很多的属性,比如$options
-
它给vm初始化了代理
initProxy(vm) 复制代码
-
它给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') 复制代码
-
它甚至偷偷的唤起了钩子函数
callHook(vm, 'beforeCreate') callHook(vm, 'created') 复制代码
实例生命周期钩子 & 生命周期图示
所谓的唤起钩子函数callHook是做什么的呢?我们找到
import { initLifecycle, callHook } from './lifecycle'
复制代码
打开这个文件lifecycle.js
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
复制代码
可以看到,callHook函数的作用是,调用option里用户设定的生命周期函数。例如
new Vue({
data: {
a: 1
},
created: function () {
// `this` 指向 vm 实例
console.log('a is: ' + this.a)
}
})
// => "a is: 1"
复制代码
new Vue() 到 beforeCreate 到 created
它在'beforeCreate'和'created'之间干了什么呢?
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
复制代码
对应生命周期图示来看代码
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')
复制代码
在new Vue()之后,调用了_init(),在_init()内,调用了
initLifecycle(vm)
initEvents(vm)
initRender(vm)
复制代码
这点正好对应官网生命周期图示中new Vue()
与生命周期钩子'beforeCreate'之间的Init Events & Lifecycle,也就是说我们在option中设置的钩子函数,会在这个生命周期节点得到调用,是因为这个callHook(vm, 'beforeCreate')
(vue-dev/src/core/instance/init.js),而在这个时间节点之前完成Init Events & Lifecycle的正是
initLifecycle(vm)
initEvents(vm)
initRender(vm)
复制代码
除了官方提到的Events和Lifecycle的Init之外,还在这个生命周期节点完成了Render的Init
之后是Init injections & reactivity,对应的函数调用是
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
复制代码
这段函数调用之后_init()还没有结束,后面有
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
复制代码
对应生命周期示意图中的
依据options中是否包含el来决定是否mount(挂载)这个el
毫无疑问,$mount
函数必定是完成下一步的关键,在src文件夹中搜索$mount的定义,在/vue-dev/src/platforms/web/runtime/index.js中找到了
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
复制代码
Vue.$mount
函数内包含两个重要的函数
query()
mountComponent()
其中
// src/platforms/web/util/index.js
export function query (el: string | Element): Element {
if (typeof el === 'string') {
const selected = document.querySelector(el)
if (!selected) {
process.env.NODE_ENV !== 'production' && warn(
'Cannot find element: ' + el
)
return document.createElement('div')
}
return selected
} else {
return el
}
}
复制代码
可以看到,query()
是对document.querySelector()
的一个包装,作用是依据new Vue(options)
中options
内el
设定的元素选择器进行DOM内元素的选取,并设定了相应的容错、报错方法
created 到 beforeMount 到 mounted
// src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
复制代码
当options里不存在render函数的时候,会执行createEmptyVNode
,添加到vm.$options.render
,之后执行生命周期钩子函数callHook(vm, 'beforeMount')
,即对应的生命周期为
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
复制代码
如果render function
存在,则直接调用beforeMount生命周期钩子函数,如果不存在,则通过createEmptyVNode
Compile template into render function Or compile el's outerHTML as template。
下一步就是看createEmptyVNode
是如何做到compile something into render function的。
// src/core/vdom/vnode.js
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
复制代码
createEmptyVNode
通过new VNode()
返回了VNode实例
VNode是一个很长的class,这里只放VNode的constructor作参考
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
复制代码
事实上VNode是Vue虚拟的DOM节点,最后这个虚拟DOM节点被挂载到vm.$options.render
,到这里
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
复制代码
唤起生命周期钩子函数beforeMount
,正式进入beforeMount
to mounted
的阶段
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
复制代码
倒着来找callHook(vm, 'mounted')
的触发,在这之前,做了这么几件事
- 定义
updateComponent
,后被Watcher
使用 - 调用构造函数
Watcher
产生新实例 - 判断
vm.$vnode
是否为null
,如果是,则callHook(vm, 'mounted')
在src/core/observer/watcher.js
里可以找到Watcher的定义,这里展示它的constructor
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.computed = !!options.computed
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.computed = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.computed // for computed watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
复制代码
(累死我了,要休息一哈,第一次写,对于部分细节是否要深入把握不好。深入的话太深了一个知识点要讲好多好多好多,可能一天都说不完。讲太浅了又觉得啥干活都没有,不好把握各位谅解。要是有错误的望各位提出来,我也算是抛砖引玉了)