vue源碼分析.md

New Vue發(fā)生了什么

Vue定義為一個(gè)函數(shù),new的過(guò)程相當(dāng)于實(shí)例化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) }

在init方法中主要干了這幾件事情,

  • 合并option
  • 初始化生命周期state等
  • 調(diào)用$mount方法
Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to avoid this being observed vm._isVue = true // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm 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') /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) } if (vm.$options.el) {//如果有 el 屬性,則調(diào)用 vm.$mount 方法掛載 vm,掛載的目標(biāo)就是把模板渲染成最終的 DOM, vm.$mount(vm.$options.el) } }

二、Vue實(shí)例掛載

在Vue中我們是通過(guò)$mount實(shí)例方法掛載VM(vm就是一個(gè)Vue對(duì)象的實(shí)例)。

接下來(lái)分析的是在compiler中$mount的實(shí)現(xiàn)

const mount = Vue.prototype.$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 } const options = this.$options // resolve template/el and convert to render function if (!options.render) { let template = options.template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { 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 = 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) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') } const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) 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) }

這段代碼首先緩存了原型上的 mount 方法,再重新定義該方法,我們先來(lái)分析這段代碼。首先,它對(duì) el 做了限制,Vue 不能掛載在 body、html 這樣的根節(jié)點(diǎn)上。接下來(lái)的是很關(guān)鍵的邏輯 —— 如果沒(méi)有定義 render 方法,則會(huì)把 el 或者 template 字符串轉(zhuǎn)換成 render 方法。這里我們要牢記,在 Vue 2.0 版本中,所有 Vue 的組件的渲染最終都需要 render 方法,無(wú)論我們是用單文件 .vue 方式開(kāi)發(fā)組件,還是寫了 el 或者 template 屬性,最終都會(huì)轉(zhuǎn)換成 render 方法,那么這個(gè)過(guò)程是 Vue 的一個(gè)“在線編譯”的過(guò)程,它是調(diào)用 compileToFunctions 方法實(shí)現(xiàn)的,編譯過(guò)程我們之后會(huì)介紹。最后,調(diào)用原先原型上的mount 方法掛載。

三、render

vm._render 最終是通過(guò)執(zhí)行 createElement 方法并返回的是 vnode,它是一個(gè)虛擬 Node。Vue 2.0 相比 Vue 1.0 最大的升級(jí)就是利用了 Virtual DOM。

四、Update

Vue 的 _update 是實(shí)例的一個(gè)私有方法,它被調(diào)用的時(shí)機(jī)有 2 個(gè),一個(gè)是首次渲染,一個(gè)是數(shù)據(jù)更新的時(shí)候;實(shí)際上也是將Vnode掛載到真實(shí)DOM上的過(guò)程。

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const prevActiveInstance = activeInstance activeInstance = vm vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } activeInstance = prevActiveInstance // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. }

_update的核心就是調(diào)用vm.patch方法

patch方法也就是將Vnode掛載到真實(shí)DOM上

[圖片上傳失敗...(image-aeec32-1597199836113)]

響應(yīng)式設(shè)計(jì)

一、數(shù)據(jù)劫持 Object.defineProperty

在Vue的初始化階段,_init方法試行的時(shí)候,會(huì)執(zhí)行很多init方法,initProps、initMethods、initData

export function initState (vm: Component) { 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) } }

initProps

props 的初始化主要過(guò)程,就是遍歷定義的 props 配置。遍歷的過(guò)程主要做兩件事情:一個(gè)是調(diào)用 defineReactive 方法把每個(gè) prop 對(duì)應(yīng)的值變成響應(yīng)式,可以通過(guò) vm._props.xxx 訪問(wèn)到定義 props 中對(duì)應(yīng)的屬性。對(duì)于 defineReactive 方法,我們稍后會(huì)介紹;另一個(gè)是通過(guò) proxy 把 vm._props.xxx 的訪問(wèn)代理到 vm.xxx 上,我們稍后也會(huì)介紹。

initData

data 的初始化主要過(guò)程也是做兩件事,一個(gè)是對(duì)定義 data 函數(shù)返回對(duì)象的遍歷,通過(guò) proxy 把每一個(gè)值 vm._data.xxx 都代理到 vm.xxx 上;另一個(gè)是調(diào)用 observe 方法觀測(cè)整個(gè) data 的變化,把 data 也變成響應(yīng)式,可以通過(guò) vm._data.xxx 訪問(wèn)到定義 data 返回函數(shù)中對(duì)應(yīng)的屬性,observe 我們稍后會(huì)介紹。

proxy(非ES6)

proxy 方法的實(shí)現(xiàn)很簡(jiǎn)單,通過(guò) Object.defineProperty 把 target[sourceKey][key] 的讀寫變成了對(duì) target[key] 的讀寫。所以對(duì)于 props 而言,對(duì) vm._props.xxx 的讀寫變成了 vm.xxx 的讀寫,而對(duì)于 vm._props.xxx 我們可以訪問(wèn)到定義在 props 中的屬性,所以我們就可以通過(guò) vm.xxx 訪問(wèn)到定義在 props 中的 xxx 屬性了。同理,對(duì)于 data 而言,對(duì) vm._data.xxxx 的讀寫變成了對(duì) vm.xxxx 的讀寫,而對(duì)于 vm._data.xxxx 我們可以訪問(wèn)到定義在 data 函數(shù)返回對(duì)象中的屬性,所以我們就可以通過(guò) vm.xxxx 訪問(wèn)到定義在 data 函數(shù)返回對(duì)象中的 xxxx 屬性了

const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop } export function proxy (target: Object, sourceKey: string, key: string) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] } sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val } Object.defineProperty(target, key, sharedPropertyDefinition) }

二 派發(fā)更新

監(jiān)控的屬性如果更改的話會(huì)調(diào)用getter方法,getter方法里面調(diào)用訂閱者集合通知訂閱者進(jìn)行更新。這里引入了一個(gè)隊(duì)列的概念,這也是 Vue 在做派發(fā)更新的時(shí)候的一個(gè)優(yōu)化的點(diǎn),它并不會(huì)每次數(shù)據(jù)改變都觸發(fā) watcher 的回調(diào),而是把這些 watcher 先添加到一個(gè)隊(duì)列里,然后在 nextTick 后執(zhí)行 flushSchedulerQueue。

可以知道數(shù)據(jù)的變化到 DOM 的重新渲染是一個(gè)異步過(guò)程,發(fā)生在下一個(gè) tick。這就是我們平時(shí)在開(kāi)發(fā)的過(guò)程中,比如從服務(wù)端接口去獲取數(shù)據(jù)的時(shí)候,數(shù)據(jù)做了修改,如果我們的某些方法去依賴了數(shù)據(jù)修改后的 DOM 變化,我們就必須在 nextTick 后執(zhí)行。

三 檢測(cè)變化的注意事項(xiàng)

對(duì)象

Object.defineProperty檢測(cè)不到對(duì)象屬性的新增,Vue則定義了一個(gè)全局的API Vue.set方法。

set 方法接收 3個(gè)參數(shù),target 可能是數(shù)組或者是普通對(duì)象,key 代表的是數(shù)組的下標(biāo)或者是對(duì)象的鍵值,val 代表添加的值。首先判斷如果 target 是數(shù)組且 key 是一個(gè)合法的下標(biāo),則之前通過(guò) splice 去添加進(jìn)數(shù)組然后返回,這里的 splice 其實(shí)已經(jīng)不僅僅是原生數(shù)組的 splice 了,稍后我會(huì)詳細(xì)介紹數(shù)組的邏輯。接著又判斷 key 已經(jīng)存在于 target 中,則直接賦值返回,因?yàn)檫@樣的變化是可以觀測(cè)到了。接著再獲取到 target.ob 并賦值給 ob,之前分析過(guò)它是在 Observer 的構(gòu)造函數(shù)執(zhí)行的時(shí)候初始化的,表示 Observer 的一個(gè)實(shí)例,如果它不存在,則說(shuō)明 target 不是一個(gè)響應(yīng)式的對(duì)象,則直接賦值并返回。最后通過(guò) defineReactive(ob.value, key, val) 把新添加的屬性變成響應(yīng)式對(duì)象,然后再通過(guò) ob.dep.notify() 手動(dòng)的觸發(fā)依賴通知。

數(shù)組

接著說(shuō)一下數(shù)組的情況,Vue 也是不能檢測(cè)到以下變動(dòng)的數(shù)組:

1.當(dāng)你利用索引直接設(shè)置一個(gè)項(xiàng)時(shí),例如:vm.items[indexOfItem] = newValue

2.當(dāng)你修改數(shù)組的長(zhǎng)度時(shí),例如:vm.items.length = newLength

對(duì)于第一種情況,可以使用:Vue.set(example1.items, indexOfItem, newValue);而對(duì)于第二種情況,可以使用 vm.items.splice(newLength)。

也就是通過(guò)數(shù)據(jù)劫持重新寫了數(shù)組的方法,定義了一個(gè)類數(shù)組,重寫了push、unshift、splice方法

四、計(jì)算屬性(computed)和偵聽(tīng)屬性(watch)

五、虛擬DOM

虛擬dom就是用一個(gè)原生的JS對(duì)象去描述一個(gè)DOM節(jié)點(diǎn),所以它比創(chuàng)建一個(gè)DOM代價(jià)要小很多

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容