雖然已經完成了underscore和zepto的源碼學習,但是還是很有必要講講自己對源碼學習的理解:根據"刻意練習"理論, 人的學習過程分爲: “輸入-練習-反饋-修正"四個步驟,而我們的實際工作中,學習方式一般分爲兩種,一種是經驗式學習,很多時候由於項目時間緊,我們匆匆看下文檔瞭解50%的東西就上手工作了,具體是邊做邊查,這種方法的好處是所見即所得,很容易有成就感,但也會有一些問題: 一是使用的方法很可能不是"best practice”,而僅僅是happen to make it work. 二是很容易侷限與思維定勢中,只使用自己熟悉的方法去解決問題.第二種學習方法是"系統的學習",這種方法耗時長,見效慢,看起來吃力不討好,但是對長遠有幫助,看書,看源碼屬於這種方法,如果把學寫代碼比作練習書法之類的技能的話,學習的過程肯定是免不了臨摹自己練習得多了,心中有字,才能下筆如有神.對於源碼學習,個人的感受是,underscore加強了對js語言以及函數式編程的理解,zepto加深了我對瀏覽器api的理解,另外還有設計模式,算法的實際運用.
本系列是慕課網上的<vue源碼解析>課程的學習筆記,(400軟妹幣,20h+課程,另有配套電子書),之所以購買課程是有幾個原因:一是:Vue源碼代碼量太大,按照以往一個個函數看的方式容易導致迷失在細節中,忽略了整體的脈絡的把握.二是網上Vue的源碼解析系列不多,本課程號稱是第一個全方位解析Vue源碼的, 三是買課程當天剛發了工資,一咬牙就剁了手.總體而言,課程物有所值,廢話不多說,開始本系列的第一篇: 數據驅動
一. 引入vue的時候做了什麼
可以看到的是, vue在開發的時候將各個功能模塊進行了拆分,在使用的時候通過Mixin的方法將各個模塊相關的方法掛載在Vue.prototype上,好處是權責分明,條理清晰, 下面看看vue的目錄
二. new Vue時候做了什麼
new Vue的時候調用了私有的_init方法,通過圖片可以看到,init方法的邏輯也是十分清晰的, 但是裏面涉及了許多過程,而目前我們要關注的就是我們是如何將data中定義參數插入到template中的,先來看一個簡單的例子
//模板html
<div id="app">
{{ message }}
</div>
//js文件
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
第一種是沒有compliler的runtime-only版本,也就是用戶必須手寫render函數(什麼?你沒寫過render函數?那很正常,因爲寫template的方式更加符合我們的開發習慣),或者藉助webpack的vue-loader打包工具將template轉化爲render函數,我們使用vue-cli在開發SPA的時候使用的就是runtime-only版本,第二種是runtime only + compiler版本,該版本下vue內部的編譯器會經過Parse-optimise-codegen三道步驟生成render函數,上述的簡單代碼用手寫的render函數的話會是這樣:
//js文件
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
}
兩種方式最終都會調用_update函數生成真實的dom
vm._update(vm._render(), hydrating)
結合上圖再回到我們的$mount方法
//緩存原型鏈上的定義好的$mount
const mount = Vue.prototype.$mount
//,重新定義$mount, hydrate參數爲true表明是服務端
Vue.prototype.$mount = function (el, hydrating){
//拿到要進行掛載的錨點,一般是#app
el = el && query(el)
//錨點元素會被整個替換,因此要判斷錨點元素是否爲html或者body,爲這兩者時報錯
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
}
//判斷傳入的options是否有render函數
const options = this.$options
if (!options.render) {
//直接在Vue函數裏面定義template的情況
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
//通過id拿到html字符串
template = idToTemplate(template)
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
//模板是一個dom對象,也拿到html字符串
template = template.innerHTML
} else {
//無template,無render函數,報錯
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
//無template參數,無render參數,而是像上面的例子一樣在html中直接使用插值
template = getOuterHTML(el)
}
//如果模板存在的話,進入編譯的過程
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
//傳入特定的平臺相關的參數,利用閉包特性編譯生成平臺相關的compileToFunction函數,好處是在數據發生變化需要重新
//生成dom的時候不必再次創建相同的函數,提高了性能
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
//將生成的render和staticRenderFun掛載到options上
options.render = render
options.staticRenderFns = staticRenderFns
//和性能相關的錨點,可暫時不管
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
//調用了原型鏈上緩存的$mount
return mount.call(this, el, hydrating)}
爲什麼要緩存$mount然後又定義一遍呢?原因是vue最終只認render函數,在runtime-only版本是不需要走這段邏輯的,webpack打包工具已經生成了render函數,而compiler版本則需要將模板編譯成render函數
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean //參數是和服務端渲染相關,在瀏覽器環境下我們不需要傳第二個參數
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
下面來看看mountComponent方法,這個方法定義在 src/core/instance/lifecycle.js 文件中
export function mountComponent(
vm: Component,
el: ? Element,
hydrating ? : boolean
): Component {
vm.$el = el
//首先還是判斷template或者render函數是否存在
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
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
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)
//調用_render函數生成virtual dom
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
//調用_update方法根據virtual dom生成真實的dom
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
//渲染watcher,與響應式原理有關,後面會仔細講
new Watcher(vm, updateComponent, noop, {
before() {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* 是否爲renderWatcher*/ )wa
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
可以看到除了和性能相關的錨點之外,兩段邏輯都執行了vm._render方法生成vnode, 然後調用update方法生成真實的dom
const vnode = vm._render()
vm._update(vm._render(), hydrating)
接下來看看我們的vnode以及vdom的生成過程,首先要了解的是瀏覽器中生成dom是很昂貴的,如圖
而vnode實際上就是通過一個js對象去描述dom,在渲染的之前通過differ算法判斷vnode是否發生了變化進而確定是否要生成這個dom,vue的vdom借鑑了snabbdom的實現
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
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
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}