Vue源碼解析01
首先來一張Vue工作流程圖,作爲整個Vue源碼解析的基礎
初始化
new Vue() 初始化創建Vue實例,初始化data、props、events等
掛載
$mount 掛載執行編譯,首次渲染、創建和追加過程
編譯
compile() 編譯,該階段分爲三個階段parse、optimize、generate
渲染
render function 渲染函數,渲染函數執行時會觸發getter函數進行依賴收集,將來數據變化時會出發setter方法進行數據更新,這就是數據響應化
虛擬DOM
Virtual DOM 虛擬DOM,Vue2.0開始支持虛擬DOM,通過Js對象描述DOM,更新數據時映射爲DOM操作
更新視圖
patch 更新試圖,數據修改時Watcher(監聽器)會執行更新,對比新舊DOM,最小代價進行修改,就是patch
Vue源碼解析入口查找
首先,如果要分析源碼的化首先將Vue的項目遷移到本地,本文用的是2.1.10版本。
項目地址
-
項目clone:git clone https://github.com/vuejs/vue.git
-
配置運行環境這裏不過多贅述
♣記得在package.json中加入 –sourcemap,方便在瀏覽器中進行調試
"scripts":{
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
}
直接運行 npm run dev創建測試文件vue.js
bundles /Users/songyanan/Desktop/前端學習/kkbNote/上課筆記/Vue191008/vue/src/platforms/web/entry-runtime-with-compiler.js → dist/vue.js...
created dist/vue.js in 4.5s
[2019-10-18 13:58:09] waiting for changes...
dist/vue.js是生成的測試文件,可以用來進行瀏覽器調試。
- package.json中的dev命令指定了配置文件scripts/config.js 和打包的目標TARGET:web-full-dev
在scripts/config.js文件中存在打包的配置信息,其中代碼太多,只截取關鍵部分:
// npm run dev命令中打包的目標文件描述,其中入口就是entry:resolve('web/entry-runtime-with-compiler.js')
// umd格式的,對應package.json中的dev:web-full-dev
// Runtime+compiler development build (Browser)
'web-full-dev': {
// 入口文件
entry: resolve('web/entry-runtime-with-compiler.js'),
// 打包生成的文件
dest: resolve('dist/vue.js'),
// 規定了輸出規範
format: 'umd',
// 環境變量 development 開發時
env: 'development',
alias: { he: './entity-decoder' },
banner
}
通過查看上面的resolove()方法中引入的aliases類,我們可以找到上面的入口文件的實際地址爲:
src/platforms/web/entry-runtime-with-compiler.js至此,找到入口文件
Vue的初始化過程
入口文件entry-runtime-with-compiler.js的主要作用
- 擴展了mount可以實現跨平臺的作用
import Vue from " ./runtime/index";
// 取出vue的$mount 重新覆蓋
const mount = Vue.prototype.$mount
// 擴展了$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
// 處理傳入vue的options選項:el和template
// el和template都是掛載的時候用到的兩種方式 ?
const options = this.$options
// resolve template/el and convert to render function
// 只有render選項不存在時 考慮el和template
// 從這裏可以看出render的優先級是要高於template的,而tempelate的優先級高於el
if (!options.render) {
let template = options.template
// 先判斷template是否存在
if (template) {
if (typeof template === 'string') {
// template本身也可是一個選擇器
if (template.charAt(0) === '#') {//首字母爲#號的話,看作是ID選擇器
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {// template 是dom元素
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// 如果不存在template,存在el,則將el所在dom賦值給template
template = getOuterHTML(el)
}
// 編譯過程
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 編譯,這個地方就是上面圖中的compile
// 編譯的過程是將template轉換爲render函數
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
// 不管是template還是el最終都會轉變爲render函數去渲染
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
./runtime/index 文件
該入口文件並沒有初始化Vue,它是通過引入的 ./runtime/index.js,所以我們來看一下該文件中實現了什麼功能
import Vue from 'core/index'
// 此處只粘貼核心功能相關的代碼
//install platform patch function
//1、 定義了patch方法,這是真正的打補丁函數
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
// 定義了$mount掛載方法,這就是上述圖片中的$mount掛載的部分
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 找到宿主元素
el = el && inBrowser ? query(el) : undefined
// $mount的核心內容是執行mountComponent方法
return mountComponent(this, el, hydrating)
}
該文件依然沒有初始化定義Vue,所以我們接着找他引入的 core/index
core/index
core/index.js文件中代碼很簡單,核心功能是初始化了全局的API
//只粘貼部分全局API
import Vue from './instance/index'
// 定義了全局的API,後面文章再分析
initGlobalAPI(Vue)
繼續向下查找Vue的構造方法:./instance/index
./instance/index 定義了構造函數
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'
// 定義了Vue的構造函數
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)// 該方法實現了上面的_init()
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
從上面的代碼可以看出,該文件實現了Vue的初始化。至此我們真正的找到了Vue項目的起點,new Vue()
- Vue的構造方法的核心是**this._init(options)**方法,所以想進行下一步的源碼研究,我們需要看看initMixin(Vue)中是怎樣實現初始化的
初始化函數的實現 ./init
我們現在整體看一下initMinxin中的_init的代碼實現,忽略掉各種警告信息和選項初始化合並,查看其比較核心的功能:
import { initState } from './state'
import { initRender } from './render'
import { initEvents } from './events'
import { initLifecycle, callHook } from './lifecycle'
import { initProvide, initInjections } from './inject'
// 初始化聲明週期
initLifecycle(vm)
// 初始化事件,實現處理父組件傳遞的監聽事件的監聽器
initEvents(vm)
// 初始化渲染器$slots scopedSlots、_c、$createElement
initRender(vm)
// 調用生命週期鉤子函數beforeCreate
callHook(vm, 'beforeCreate')
// 獲取注入的數據
initInjections(vm) // resolve injections before data/props
// 初始化狀態props、methods、data、computed、watch
initState(vm)
// 提供數據
initProvide(vm) // resolve provide after data/props
// 調用生命週期鉤子函數 created
callHook(vm, 'created')
下面我們首先簡單看一下上述幾個方法,後面章節會有詳細解析
- initLifecycle初始化生命週期的作用:
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
// 通知父組件將當前實例加入父組件之中,創建的時候通知父組件
parent.$children.push(vm)
}
// 初始化Vue實例中的一些屬性
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
- initEvents(vm) 方法的作用
// 這個方法的核心功能是初始化了Vue實例中的_events選項,並且將父組件的監聽方法加入到當前實例中
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)
}
}
- 關於initRender(vm) 方法,看名稱能看出該方法是初始化了Vue的一些渲染函數相關
export function initRender (vm: Component) {
// 此處初始化了當前組件實例的Vnode,也就是虛擬dom
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
// 此處初始化了實例的$slots,插槽相關
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
//createElement,給編譯器生成render函數使用
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.
// 這是render(h)的h函數,給用戶編寫的render函數去使用
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
// 數據響應化處理相關
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
- 關於initInjections(vm)方法,初始化數據注入相關
// 主要功能是對
export function initInjections (vm: Component) {
// 對組件選項中的inject屬性中的各個key進行遍歷,通過父組件鏈一直向上查找provide()中和inject對應的屬性
// 通俗點就是獲取父組件和祖先組件中提供的一些數據,然後注入到當前組件中
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// 進行數據響應化的操作
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
// 進行數據響應化的操作
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
- 關於initState(vm)方法,初始化組件各種狀態相關
export function initState (vm: Component) {
// 初始化了實例中的_watchers數組
vm._watchers = []
const opts = vm.$options
// 初始化props
if (opts.props) initProps(vm, opts.props)
// 初始化方法
if (opts.methods) initMethods(vm, opts.methods)
// data的處理,響應化處理
if (opts.data) {
// 初始化data
initData(vm)
} else {
// 數據響應化
observe(vm._data = {}, true /* asRootData */)
}
// 初始化computed
if (opts.computed) initComputed(vm, opts.computed)
//如果當前選項中國呢傳入了watch 且 watch不等於nativeWatch(細節處理,在Firefox瀏覽器下Object的原型上含有一個watch函數)
if (opts.watch && opts.watch !== nativeWatch) {
// 初始化watch
initWatch(vm, opts.watch)
}
}
- 關於initProvide(vm),初始化注入所需數據相關
// initProvide 是一個非常簡單的函數
//他的主要作用就是將當前選項中的provide數據放到當前實例的_provided屬性中
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
總結
直接上一張思維導圖,後面會慢慢補充完整