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()
}
}
複製程式碼
(累死我了,要休息一哈,第一次寫,對於部分細節是否要深入把握不好。深入的話太深了一個知識點要講好多好多好多,可能一天都說不完。講太淺了又覺得啥幹活都沒有,不好把握各位諒解。要是有錯誤的望各位提出來,我也算是拋磚引玉了)