Vue源碼學習1——Vue構造函數#
這是我第一次正式閱讀大型框架源碼,剛開始的時候完全不知道該如何入手。Vue源碼clone下來之后這么多文件夾,Vue的這么多方法和概念都在哪,完全沒有頭緒。現在也只是很粗略的了解一下,個人認為這篇只是能做到大家閱讀Vue的參考導航,可以較快的找到需要看的文件或方法。很多細節依然沒有理解到位,但是可以慢慢來,先分享一波~
源碼文件目錄結構
- benchmarks 暫時不知道是什么
- dist 存放打包后的文件夾
- examples 示例,這個地方可以自己寫一些簡單例子,然后通過調試看整個代碼運行的過程來了解源碼是怎么寫的
- flow 靜態類型檢查,比如 (n:number)即n需要是number類型
- packages 查資料說是vue還可以分別生成其他的npm包
- scripts 打包相關的配置文件夾
- src 我們研究的主要文件夾,下面會詳細再說明
- test 測試文件夾
- types 暫時不知道是什么
/src
接下來重點說src這個文件夾,這里面需要重點看core這個文件夾,這里面才是我們真正需要研究的地方如下圖:
- components 組件,現在里面只有KeepAlive一個
- global-api 全局api,可以給Vue添加全局方法,比如里面我們常使用的Vue.use()
-instance 核心文件夾,里面是實例相關的一些方法,例如初始化實例、實例事件綁定、渲染、狀態、生命周期等
- observe 雙向數據綁定相關文件(暫時不太清楚)
- util 工具方法,看到里面有props、nextTick之類的方法(暫時不太清楚)
- vdom 虛擬dom
大體文件結構說了一下,但是很多還不是很清晰。對於我這樣的小白來說,我的建議是可以從 npm run dev
開始一步步開始看,采取的方法是“倒序”
package.json
打開這個文件找到'dev'命令,如下
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
'rollup' 是Vue使用的打包工具,從上面可以看出執行這個命令是到 'scripts/config.js' 那就打開這個文件
scripts/config.js
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
genConfig 方法是設置一些配置,和webpack里的設置差不多,然后找到 web-full-dev
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
}
可以看到入口文件是'web/entry-runtime-with-compiler.js'
src/platforms/web/entry-runtime-with-compiler.js
在這個文件里可以看到
import Vue from './runtime/index'
該方法中有一個$mount需要注意,這個就是渲染的入口,接下來會說這個方法
src/platforms/web/runtime/index.js
`import Vue from 'core/index'`
src/core/index.js
import Vue from './instance/index'
src/core/instance/index.js
終於進入到最重要的方法,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
- initMixin :對於各種vue實例各種屬性進行初始化
- stateMixin :Vue原型上綁定state相關的方法和屬性,data、props等
- eventsMixin :Vue原型上綁定事件相關方法
- lifecycleMixin :Vue原型上綁定生命周期相關方法,比如_update、$forceUpdate、$destroy
- renderMixin : Vue原型上綁定和渲染相關的方法
src/core/instance/init.js
- 第一點:initMixin 是Vue的一些初始化實例的方法,在還沒有構造一個對象前是不會進入到這個方法內部,當通過new出一個對象后才會進入,原因如下:
Vue.prototype._init = function (options?: Object) {
這里有一個options?:object
的校驗,剛開始即只是引入<script src="../../dist/vue.js"></script>
這個文件,當 var vm = new Vue()
之后才進入_init方法內部。
- 第二點:_init方法中對於$options設置:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
mergeOptions這個方法是合並option,第一個參數是往$options塞入下面的參數
第二個參數就是我們自己設置的option,比如data、el;第三個參數如下:
- 第三點:_init方法中其他初始化方法
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')
接下來將會一個個初始化方法說明,初次之外_init方法還有一些變量的初始化,比如_uid、_isVue、_name、_renderProxy的初始化
- 第四點:最后在_init方法中需要注意
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
調用$mount掛載根元素,這個方法就是之前提到的
src/core/instance/state.js
-
第一點: stateMixin是對於Vue原型對象(Vue.prototype)加上$data、$props、$delete、$watch、$set屬性。並且通過Object.defineProperty對$data、$props屬性進行set和get
-
第二點:initState方法是在init.js中調用,即實例化之后才調用的,是個實例對象添加屬性。
export function initState(vm: Component) {
// 首先在vm上初始化一個_watchers數組,緩存這個vm上的所有watcher
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
對於實例對象進行相關屬性的初始化,另外data、props因為需要雙向綁定,在initData、initProps中都有一個proxy方法對這兩個屬性進行set和get的設置
src/core/instance/events.js
-
第一點: eventsMixin是對於Vue原型對象(Vue.prototype)綁定一些事件方法,比如$on、$once、$off、$emit
-
第二點: initEvents是對於實例對象初始化事件
export function initEvents(vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events 初始化父級相關事件
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
創建_events一個空對象之后用來存放事件,_hasHookEvent是一個優化標記(可以暫時不理會),然后初始化父級事件。根據是否有父級監聽事件,如果有則更新父級事件
src/core/instance/lifecycle.js
- 第一點: lifecycleMixin是對Vue原型對象(Vue.prototype)綁定_update、$forceUpdate、$destroy三個生命周期方法。_update方法中通過調用__patch__方法更新虛擬dom;
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
$forceUpdate強制重新渲染實例本身和插入插槽內容的子組件;$destroy銷毀一個實例,清理它與其它實例的連接,解綁它的全部指令及事件監聽器,觸發 beforeDestroy 和 destroyed 的鈎子
- 第二點: initLifecycle是在_init方法中調用,是實例生命周期的初始化,其中會包括很多變量
export function initLifecycle(vm: Component) {
const options = vm.$options
// locate first non-abstract parent 創建第一個非抽象父組件,抽象組件:它自身不會渲染一個 DOM 元素,也不會出現在父組件鏈中,例如<keep-alive>
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 // watcher對象
vm._inactive = null // 和keep-alive中組件狀態有關系
vm._directInactive = false // 和keep-alive中組件狀態有關系
vm._isMounted = false //當前實例是否被掛載
vm._isDestroyed = false // 當前實例是否被銷毀
vm._isBeingDestroyed = false // 當前實例是否正在被銷毀或者沒銷毀完全
}
- 第三點: callHook是在_init方法中調用,這個方法是直接調用鈎子,調用形式如下
callHook(vm, 'beforeCreate')
callHook(vm, 'created')
src/core/instance/render.js
- 第一點: renderMixin方法主要是給Vue原型對象綁定$nextTick、_render兩個方法,其中_render方法代碼如下:
Vue.prototype._render = function (): VNode {
……
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
if (process.env.NODE_ENV !== 'production') {
if (vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
}
……
return vnode
}
在這個方法中主要是try……catch這里創建了vnode。 vnode = render.call(vm._renderProxy, vm.$createElement)
創建一個vnode並且返回,如果失敗則返回一個空的vnode vnode = createEmptyVNode()
- 第二點: initRender是在_init方法中調用,進行實例渲染屬性的綁定並且對一些屬性的監聽
export function initRender(vm: Component) {
……
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
……
}
這里着重關注一下createElement方法,傳入vnode以及dom的屬性創建真正dom節點。
src/core/instance/inject.js
在_init方法中調用了initProvide、initInjections兩個方法,這兩個方法在實際應用中不是很多,查看Vue API說provide 和 inject 主要為高階插件/組件庫提供用例。並不推薦直接用於應用程序代碼中。所以這里不做說明,有需要的可以到這個文件查看相關方法
vue的渲染過程
- 第一步: _init方法中
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
渲染入口,調用$mount方法開始
- 第二步:entry-runtime-with-compiler.js中的$mount方法,代碼如下
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
……
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') { // 圖一
template = idToTemplate(template)
……
}
} else if (template.nodeType) { // 圖一
template = template.innerHTML
} else { // 圖二
……
return this
}
} else if (el) { // 圖三
template = getOuterHTML(el)
}
if (template) {
……
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
……
}
}
return mount.call(this, el, hydrating)
}
可以從上面大致看出結構,template是可以從el傳入,也可以是options中的template以及render方法三種方式傳入,對應Vue官網如下:
其中可以看到,通過el或者template的方式都需要調用compileToFunctions將字符串轉換成方法,而render是不需要,這里可以看出render的性能應該會好一些,但是el和template我們使用較易理解。但是不管是哪一種最后都是生成render方法,然后再綁定到實例對象上。另外方法中的mount是從runtime/index.js中創建的。
- 第三步: 接下來就進入runtime/index.js看到mount方法調用mountComponent,然后找到這個方法是在lifecycle.js
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
……
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
……
const vnode = vm._render()
……
vm._update(vnode, hydrating)
……
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
……
callHook(vm, 'beforeUpdate')
……
}, true /* isRenderWatcher */)
……
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
然后調用前一步調用的_render方法是在render.js中的_render方法中try……catch地方調用了第二步中生成的render方法。通過_render方法生成vnode,傳入_update方法
- 第四步:最后在前面也提到在_update方法中有一個patch對比更新真實dom,這里是涉及到diff算法進行對比新舊VNode對象進行更新,暫時不太了解,下圖是我網上找到可以比較形象解釋diff、patch的作用
以上就是一個大體渲染的過程。
總結
本文只是做了Vue構造函數整體的一個流程展示,哪些參數是在哪個文件中掛載上去的以及vue渲染的一個簡單流程。但其實每個環節都可以拓展出很多知識,比如響應式的數據綁定、虛擬DOM、diff算法、patch、生命周期等等,這些可以在之后再一個個點進行了解。下圖是沒有參數的vue實例的參數