在開始之前,閱讀原始碼你需要有紮實的基本功,還要有耐心,能把握全域性,不要扣細節!
一、先看看目錄
├── build --------------------------------- 構建相關的檔案
├── dist ---------------------------------- 構建後檔案的輸出目錄
├── examples ------------------------------ 存放使用Vue開發的的例子
├── flow ---------------------------------- 型別宣告,使用開源專案 [Flow](https://flowtype.org/)
├── package.json -------------------------- 專案依賴
├── test ---------------------------------- 包含所有測試檔案
├── src ----------------------------------- 這個是我們最應該關注的目錄,包含了原始碼
│ ├──platforms --------------------------- 包含平臺相關的程式碼
│ │ ├──web ----------------------------- 包含了不同構建的包的入口檔案
│ │ | ├──entry-runtime.js ---------------- 執行時構建的入口,輸出 dist/vue.common.js 檔案,不包含模板(template)到render函式的編譯器,所以不支援 `template` 選項,我們使用vue預設匯出的就是這個執行時的版本。大家使用的時候要注意
│ │ | ├── entry-runtime-with-compiler.js -- 獨立構建版本的入口,輸出 dist/vue.js,它包含模板(template)到render函式的編譯器
│ ├── compiler -------------------------- 編譯器程式碼的存放目錄,將 template 編譯為 render 函式
│ │ ├── parser ------------------------ 存放將模板字串轉換成元素抽象語法樹的程式碼
│ │ ├── codegen ----------------------- 存放從抽象語法樹(AST)生成render函式的程式碼
│ │ ├── optimizer.js ------------------ 分析靜態樹,優化vdom渲染
│ ├── core ------------------------------ 存放通用的,平臺無關的程式碼
│ │ ├── observer ---------------------- 反應系統,包含資料觀測的核心程式碼
│ │ ├── vdom -------------------------- 包含虛擬DOM建立(creation)和打補丁(patching)的程式碼
│ │ ├── instance ---------------------- 包含Vue建構函式設計相關的程式碼
│ │ ├── global-api -------------------- 包含給Vue建構函式掛載全域性方法(靜態方法)或屬性的程式碼
│ │ ├── components -------------------- 包含抽象出來的通用元件
│ ├── server ---------------------------- 包含服務端渲染(server-side rendering)的相關程式碼
│ ├── sfc ------------------------------- 包含單檔案元件(.vue檔案)的解析邏輯,用於vue-template-compiler包
│ ├── shared ---------------------------- 包含整個程式碼庫通用的程式碼複製程式碼
二、Vue 的建構函式是什麼樣的
使用 new
操作符來呼叫 Vue
,Vue
是一個建構函式,瞭解了目錄結構,下面來看下下入口檔案
開啟package.json
當我們執行npm run dev,看看幹了啥,rollup也是類似webpack的打包工具,根據
TARGET=web-full-dev
開啟入口檔案找到了web/entry-runtime-with-compiler.js
依照以上查詢路徑,我們找到了Vue建構函式
定義了建構函式,引入依賴,呼叫初始化函式,最後匯出Vue
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue複製程式碼
開啟這五個檔案,找到相應的方法,你會發現,這些方法的作用,就是在 Vue 的原型 prototype 上掛載方法或屬性
1. 先進入 initMixin(Vue),在prototype上掛載
Vue.prototype._init = function (options) {}
複製程式碼
2. 進入 stateMixin(Vue),在prototype上掛載
Vue.prototype.$data
Vue.prototype.$props
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function(){}
複製程式碼
3. 進入eventsMixin(Vue),在prototype上掛載
Vue.prototype.$on
Vue.prototype.$once
Vue.prototype.$off
Vue.prototype.$emit
複製程式碼
4.進入lifecycleMixin(Vue),在prototype上掛載
Vue.prototype._update
Vue.prototype.$forceUpdate
Vue.prototype.$destroy 複製程式碼
5. 最後進入renderMixin(Vue),在prototype上掛載
Vue.prototype.$nextTick
Vue.prototype._render
Vue.prototype._o = markOnce
Vue.prototype._n = toNumber
Vue.prototype._s = toString
Vue.prototype._l = renderList
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = renderStatic
Vue.prototype._f = resolveFilter
Vue.prototype._k = checkKeyCodes
Vue.prototype._b = bindObjectProps
Vue.prototype._v = createTextVNode
Vue.prototype._e = createEmptyVNode
Vue.prototype._u = resolveScopedSlots
Vue.prototype._g = bindObjectListeners
複製程式碼
根據上面的查詢路徑下一步到src/core/index.js
引入依賴,在Vue上掛載靜態方法和屬性
import { initGlobalAPI } from './global-api/index' import { isServerRendering } from 'core/util/env' initGlobalAPI(Vue) Object.defineProperty(Vue.prototype, '$isServer', { get: isServerRendering }) Object.defineProperty(Vue.prototype, '$ssrContext', { get () { /* istanbul ignore next */ return this.$vnode && this.$vnode.ssrContext } }) Vue.version = '__VERSION__' export default Vue複製程式碼
進入initGlobalAPI(Vue),在Vue上掛載靜態屬性和方法
Vue.config Vue.util = util
Vue.set = set
Vue.delete = del
Vue.nextTick = util.nextTick
Vue.options = {
components: { KeepAlive },
directives: {},
filters: {},
_base: Vue
}
Vue.use
Vue.mixin
Vue.cid = 0
Vue.extend
Vue.component = function(){}
Vue.directive = function(){}
Vue.filter = function(){}
複製程式碼
接著掛載
Vue.prototype.$isServer
Vue.version = '__VERSION__'
複製程式碼
根據上面的查詢路徑下一步到runtime/index.js,安裝平臺特有的工具
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
// 安裝平臺特定的 指令 和 元件
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
}
Vue.prototype.__patch__
Vue.prototype.$mount複製程式碼
根據上面的查詢路徑最後一步到web/entry-runtime-with-compiler.js
- 快取來自
web-runtime.js
檔案的$mount
函式,const mount = Vue.prototype.$mount - 在 Vue 上掛載
compile
然後覆蓋覆蓋了Vue.prototype.$mount
- Vue.compile = compileToFunctions 就是將模板
template
編譯為render函式。
到這整個Vue建構函式就還原了
三、通過一個例子說明整個流程
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vue.js grid component example</title>
</head>
<body>
<div id="app">
<ol>
<li v-for="todo in todos">
{{ todo.text }}
</li>
</ol>
<Child></Child>
</div>
</body>
</html>
複製程式碼
grid.js
let Child = {
data: function() {
return {child: '你好哈'}
},
template: '<div>{{child}}</div>'
}
new Vue({
el: '#app',
data: {
todos: [
{text: '學習 JavaScript'},
{text: '學習 Vue'},
{text: '整個牛專案'}]
},
components: {'Child': Child}
})複製程式碼
想要知道Vue都幹了什麼,首先檢視建構函式function Vue (options) { this._init(options) }
- new Vue({//傳入上面的內容 }) ,首先進入Vue.prototype._init,建構函式第一個掛載的方法
Vue.prototype._init = function (options) {
const vm= this
vm._uid = uid++
let startTag, endTag
vm._isVue = true
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
vm._renderProxy = vm
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
複製程式碼
_init()
方法在一開始的時候,在 this
物件上定義了兩個屬性:_uid
和 _isVue
,然後判斷有沒有定義 options._isComponent
這裡會走 else
分支,也就是這段程式碼:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)mergeOptions使用策略模式合併傳入的options和Vue.options複製程式碼
mergeOptions使用策略模式合併傳入的options和Vue.options合併後的程式碼結構, 可以看到通過合併策略components,directives,filters繼承了全域性的, 這就是為什麼全域性註冊的可以在任何地方使用,因為每個例項都繼承了全域性的, 所以都能找到
initLifecycle
、initEvents
、initRender、initState
,且在 initState
前後分別回撥了生命週期鉤子 beforeCreate
和 created,
看到這裡,也就明白了為什麼 created 的時候不能操作DOM了。因為這個時候還沒有渲染真正的DOM元素到文件中。created
僅僅代表資料狀態的初始化完成。重點看下initState()initState (vm) {
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)
}
}複製程式碼
通過initData看Vue的資料響應系統,由於只是傳了data,所以執行initData(vm)
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
proxy(vm, `_data`, key)
}
observe(data, true /* asRootData */)
}
複製程式碼
proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}複製程式碼
取出data中的key,進行迴圈,通過proxy代理,可以直接通過 this.屬性 訪問data中的值,在例項物件上對資料進行代理,這樣我們就能通過this.todos 來訪問 data.todos 了 接下啦,正式的資料響應系統observe(data, true /* asRootData */),將資料通過Object.defineProperty進行get,set處理,使得資料響應
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value)
} else {
this.walk(value)
}
}
walk(obj: Object) {
const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
複製程式碼
在 Observer
類中,我們使用 walk
方法對資料data的屬性迴圈呼叫 defineReactive
方法,defineReactive
方法很簡單,僅僅是將資料data的屬性轉為訪問器屬性,並對資料進行遞迴觀測,否則只能觀測資料data的直屬子屬性。這樣我們的第一步工作就完成了,當我們修改或者獲取data屬性值的時候,通過 get
和 set
即能獲取到通知。
function defineReactive (
obj,
key,
val,
customSetter,
shallow
) {
const dep = new Dep()
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
複製程式碼
- 其中 let childOb = !shallow && observe(val),進行遞迴呼叫,將所有data資料包含子集進行get set化,進行響應
- 其中在Observe類中,如果屬性是陣列,會進行改造
if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { this.walk(value) }export const arrayMethods = Object.create(arrayProto) ;[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() return result }) }) 複製程式碼
initData(vm)執行完成,相應系統就完成了,這時候執行callHook(vm, 'created') 觸發created,繼續回到_init(),執行到 vm.$mount(vm.$options.el)
Vue.prototype._init = function (options) {
const vm= this
vm._uid = uid++
let startTag, endTag
vm._isVue = true
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
vm._renderProxy = vm
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}複製程式碼
進入$mount會先獲取掛載el節點,然後先判斷有沒有傳入render方法,沒有在去找有沒有傳入template,
Vue.prototype.$mount = function (
el,
hydrating
){
el = el && query(el)
const options = this.$options
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 {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
複製程式碼
有了模板經過compileToFunctions,將模板編譯為ast語法樹,經過靜態優化, 最後處理成render函式,本例中的render函式,使用了with(this), 將this作用域提前, {{ todo.text }} 所以我們可以直接在模板中使用屬性,不加this! 當然加上this也是可以的 解除了我的很多迷惑,為什麼模板中可以不加this(react中的render是要使用this的), 其中的v-for轉換成_l,根據之前的Vue.prototype._l = renderList
function() {
with (this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('ol', _l((this.todos), function(todo) {
return _c('li', [_v("\n " + _s(todo.text) + "\n ")])
})), _v(" "), _c('child')], 1)
}
}複製程式碼
- 生成了render函式後接著進入mountComponent,
- 首先呼叫了beforeMount函式,
- 接著執行vm._watcher = new Watcher(vm, updateComponent, noop)
- 最後callHook(vm, 'mounted'),執行mounted,所以在mounted執行之前就已經掛載到dom上
所以重點 vm._watcher = new Watcher(vm, updateComponent, noop)
function mountComponent (
vm,
el,
hydrating
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
vm._watcher = new Watcher(vm, updateComponent, noop)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
複製程式碼
檢視Watcher程式碼
class Watcher {
constructor (
vm,
expOrFn,
cb,
options
) {
this.vm = vm
vm._watchers.push(this)
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy 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)
}
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm)
return value
}
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
evaluate () {
this.value = this.get()
this.dirty = false
}
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
複製程式碼
執行建構函式由於 this.lazy=false; this.value = this.lazy ? undefined : this.get();
執行get方法 其中pushTarget(this),給Dep.target新增靜態屬性this(當前new Watcher()例項 )
function pushTarget (_target) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
get () {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm)
return value
}
複製程式碼
接著執行 this.getter.call(vm, vm)複製程式碼
this.getter就是
updateComponent = () => {
vm._update(vm._render(), hydrating)
} 複製程式碼
- 開始先呼叫vm._render()
Vue.prototype._render = function (){
const vm = this
const {
render,
staticRenderFns,
_parentVnode
} = vm.$options
let vnode = render.call(vm._renderProxy, vm.$createElement)
return vnode
}
複製程式碼
開始執行之前編譯好的render函式了,在執行render函式時,通過獲取todos屬性等,觸發相應
function() {
with (this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('ol', _l((this.todos), function(todo) {
return _c('li', [_v("\n " + _s(todo.text) + "\n ")])
})), _v(" "), _c('child')], 1)
}
}複製程式碼
的get方法,這個時候Dep.target已經存在靜態屬性,Watcher例項了
所以相應的dep例項就會收集對應的Watcher例項了
複製程式碼
執行完之後返回vnode,
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
其中vm._render()執行render函式返回vnode作為引數
接下來執行vm._update
這是首次渲染,所以執行
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false,
vm.$options._parentElm,
vm.$options._refElm
)複製程式碼
Vue.prototype._update = function (vnode, hydrating) {
const vm: Component = this
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
if (!prevVnode) {
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false,
vm.$options._parentElm,
vm.$options._refElm
)
vm.$options._parentElm = vm.$options._refElm = null
} else {
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
複製程式碼
vm.__patch__( vm.$el, vnode, hydrating, false, vm.$options._parentElm, vm.$options._refElm ) 根據vnode中樹,建立對應元素,插入到父節點中,通過對vnode遞迴迴圈建立所有子節點 插入到父節點中 其中如果遇到子元素是元件,例如本例中Child,會建立對應VueComponent的例項,執行 和new Vue()一樣的流程
如果還沒有 prevVnode
說明是首次渲染,直接建立真實DOM。如果已經有了 prevVnode
說明不是首次渲染,那麼就採用 patch
演算法進行必要的DOM操作。這就是Vue更新DOM的邏輯。只不過我們沒有將 virtual DOM 內部的實現。
當改變屬性值時,會觸發對應的屬性的set方法,由於之前執行render的時候觸發了get,收集了對應的Watcher,所以改變值時觸發set,通知之前收集的Watcher例項執行,重新計算render方法進行patch操作
最後盜取一張圖:
寫了半天,實在寫不下去了,以後有好的語言,再來整理吧!