這是我第一次正式閱讀大型框架源碼,剛開始的時候徹底不知道該如何入手。Vue源碼clone下來以後這麼多文件夾,Vue的這麼多方法和概念都在哪,徹底沒有頭緒。如今也只是很粗略的瞭解一下,我的認爲這篇只是能作到你們閱讀Vue的參考導航,能夠較快的找到須要看的文件或方法。不少細節依然沒有理解到位,可是能夠慢慢來,先分享一波~vue
- benchmarks 暫時不知道是什麼node
- dist 存放打包後的文件夾webpack
- examples 示例,這個地方能夠本身寫一些簡單例子,而後經過調試看整個代碼運行的過程來了解源碼是怎麼寫的web
- flow 靜態類型檢查,好比 (n:number)即n須要是number類型算法
- packages 查資料說是vue還能夠分別生成其餘的npm包npm
- scripts 打包相關的配置文件夾json
- src 咱們研究的主要文件夾,下面會詳細再說明api
- test 測試文件夾數組
- types 暫時不知道是什麼緩存
接下來重點說src這個文件夾,這裏面須要重點看core這個文件夾,這裏面纔是咱們真正須要研究的地方以下圖:
- components 組件,如今裏面只有KeepAlive一個
- global-api 全局api,能夠給Vue添加全局方法,好比裏面咱們常使用的Vue.use()
-instance 核心文件夾,裏面是實例相關的一些方法,例如初始化實例、實例事件綁定、渲染、狀態、生命週期等
- observe 雙向數據綁定相關文件(暫時不太清楚)
- util 工具方法,看到裏面有props、nextTick之類的方法(暫時不太清楚)
- vdom 虛擬dom
大致文件結構說了一下,可是不少還不是很清晰。對於我這樣的小白來講,個人建議是能夠從 npm run dev
開始一步步開始看,採起的方法是「倒序」
打開這個文件找到'dev'命令,以下
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
'rollup' 是Vue使用的打包工具,從上面能夠看出執行這個命令是到 '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'
在這個文件裏能夠看到
import Vue from './runtime/index'
該方法中有一個$mount須要注意,這個就是渲染的入口,接下來會說這個方法
`import Vue from 'core/index'`
import Vue from './instance/index'
終於進入到最重要的方法,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.prototype._init = function (options?: Object) {
這裏有一個options?:object
的校驗,剛開始即只是引入<script src="../../dist/vue.js"></script>
這個文件,當var vm = new Vue()
以後才進入_init方法內部。
vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm )
mergeOptions這個方法是合併option,第一個參數是往$options塞入下面的參數
第二個參數就是咱們本身設置的option,好比data、el;第三個參數以下:
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的初始化
if (vm.$options.el) { vm.$mount(vm.$options.el) }
調用$mount掛載根元素,這個方法就是以前提到的
第一點: stateMixin是對於Vue原型對象(Vue.prototype)加上$data、$props、$delete、$watch、$set屬性。而且經過Object.defineProperty對$data、$props屬性進行set和get
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的設置
第一點: eventsMixin是對於Vue原型對象(Vue.prototype)綁定一些事件方法,好比$on、$once、$off、$emit
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是一個優化標記(能夠暫時不理會),而後初始化父級事件。根據是否有父級監聽事件,若是有則更新父級事件
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 的鉤子
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(vm, 'beforeCreate')
callHook(vm, 'created')
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()
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節點。
在_init方法中調用了initProvide、initInjections兩個方法,這兩個方法在實際應用中不是不少,查看Vue API說provide 和 inject 主要爲高階插件/組件庫提供用例。並不推薦直接用於應用程序代碼中。因此這裏不作說明,有須要的能夠到這個文件查看相關方法
if (vm.$options.el) { vm.$mount(vm.$options.el) }
渲染入口,調用$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中建立的。
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方法
以上就是一個大致渲染的過程。
本文只是作了Vue構造函數總體的一個流程展現,哪些參數是在哪一個文件中掛載上去的以及vue渲染的一個簡單流程。但其實每一個環節均可以拓展出不少知識,好比響應式的數據綁定、虛擬DOM、diff算法、patch、生命週期等等,這些能夠在以後再一個個點進行了解。下圖是沒有參數的vue實例的參數