摘要:應(yīng)用啟動(dòng)一般是通過(guò),所以,先從該構(gòu)造函數(shù)著手。構(gòu)造函數(shù)文件該文件只是構(gòu)造函數(shù),原型對(duì)象的聲明分散在當(dāng)前目錄的多個(gè)文件中構(gòu)造函數(shù)接收參數(shù),然后調(diào)用。
源碼版本:v2.1.10
分析目標(biāo)通過(guò)閱讀源碼,對(duì) Vue2 的基礎(chǔ)運(yùn)行機(jī)制有所了解,主要是:
Vue2 中數(shù)據(jù)綁定的實(shí)現(xiàn)方式
Vue2 中對(duì) Virtual DOM 機(jī)制的使用方式
源碼初見(jiàn)項(xiàng)目構(gòu)建配置文件為 build/config.js,定位 vue.js 對(duì)應(yīng)的入口文件為 src/entries/web-runtime-with-compiler.js,基于 rollup 進(jìn)行模塊打包。
代碼中使用 flow 進(jìn)行接口類型標(biāo)記和檢查,在打包過(guò)程中移除這些標(biāo)記。為了閱讀代碼方便,在 VS Code 中安裝了插件 Flow Language Support,然后關(guān)閉工作區(qū) JS 代碼檢查,這樣界面就清爽很多了。
Vue 應(yīng)用啟動(dòng)一般是通過(guò) new Vue({...}),所以,先從該構(gòu)造函數(shù)著手。
注:本文只關(guān)注 Vue 在瀏覽器端的應(yīng)用,不涉及服務(wù)器端代碼。
Vue 構(gòu)造函數(shù)文件:src/core/instance/index.js
該文件只是構(gòu)造函數(shù),Vue 原型對(duì)象的聲明分散在當(dāng)前目錄的多個(gè)文件中:
init.js:._init()
state.js:.$data .$set() .$delete() .$watch()
render.js:._render() ...
events.js:.$on() .$once() .$off() .$emit()
lifecycle.js:._mount() ._update() .$forceUpdate() .$destroy()
構(gòu)造函數(shù)接收參數(shù) options ,然后調(diào)用 this._init(options)。
._init() 中進(jìn)行初始化,其中會(huì)依次調(diào)用 lifecycle、events、render、state 模塊中的初始化函數(shù)。
Vue2 中應(yīng)該是為了代碼更易管理,Vue 類的定義分散到了上面的多個(gè)文件中。
其中,對(duì)于 Vue.prototype 對(duì)象的定義,通過(guò) mixin 的方式在入口文件 core/index.js 中依次調(diào)用。對(duì)于實(shí)例對(duì)象(代碼中通常稱為 vm)則通過(guò) init 函數(shù)在 vm._init() 中依次調(diào)用。
Vue 公共接口文件:src/core/index.js
這里調(diào)用了 initGlobalAPI() 來(lái)初始化 Vue 的公共接口,包括:
Vue.util
Vue.set
Vue.delete
Vue.nextTick
Vue.options
Vue.use
Vue.mixin
Vue.extend
asset相關(guān)接口:配置在 src/core/config.js 中
Vue 啟動(dòng)過(guò)程調(diào)用 new Vue({...}) 后,在內(nèi)部的 ._init() 的最后,是調(diào)用 .$mount() 方法來(lái)“啟動(dòng)”。
在 web-runtime-with-compiler.js 和 web-runtime.js 中,定義了 Vue.prototype.$mount()。不過(guò)兩個(gè)文件中的 $mount() 最終調(diào)用的是 ._mount() 內(nèi)部方法,定義在文件 src/core/instance/lifecycle.js 中。
Vue.prototype._mount(el, hydrating)
簡(jiǎn)化邏輯后的偽代碼:
vm = this vm._watcher = new Watcher(vm, updateComponent)
接下來(lái)看 Watcher。
Watcher文件:src/core/observer/watcher.js
先看構(gòu)造函數(shù)的簡(jiǎn)化邏輯:
// 參數(shù):vm, expOrFn, cb, options this.vm = vm vm._watchers.push(this) // 解析 options,略.... // 屬性初始化,略.... this.getter = expOrFn // if `function` this.value = this.lazy ? undefined : this.get()
由于缺省的 lazy 屬性值為 false,接著看 .get() 的邏輯:
pushTarget(this) // ! value = this.getter.call(this.vm, this.vm) popTarget() this.cleanupDeps() return value
先看這里對(duì) getter 的調(diào)用,返回到 ._mount() 中,可以看到,是調(diào)用了 vm._update(vm._render(), hydrating),涉及兩個(gè)方法:
vm._render():返回虛擬節(jié)點(diǎn)(VNode)
vm._update()
來(lái)看 _update() 的邏輯,這里應(yīng)該是進(jìn)行 Virtual DOM 的更新:
// 參數(shù):vnode, hydrating vm = this prevEl = vm.$el prevVnode = vm._vnode prevActiveInstance = activeInstance activeInstance = vm vm._vnode = vnode if (!prevVnode) { // 初次加載 vm.$el = vm.__patch__(vm.$el, vnode, ...) } else { // 更新 vm.$el = vm.__patch__(prevVnode, vnode) } activeInstance = prevActiveInstance // 后續(xù)屬性配置,略....
參考 Virtual DOM 的一般邏輯,這里是差不多的處理過(guò)程,不再贅述。
綜上,這里的 watcher 主要作用應(yīng)該是在數(shù)據(jù)發(fā)生變更時(shí),觸發(fā)重新渲染和更新視圖的處理:vm._update(vm._render())。
接下來(lái),我們看下 watcher 是如何發(fā)揮作用的,參考 Vue 1.0 的經(jīng)驗(yàn),下面應(yīng)該是關(guān)于依賴收集、數(shù)據(jù)綁定方面的細(xì)節(jié)了,而這一部分,和 Vue 1.0 差別不大。
數(shù)據(jù)綁定watcher.get() 中調(diào)用的 pushTarget() 和 popTarget() 來(lái)自文件:src/core/observer/dep.js。
pushTarget() 和 popTarget() 兩個(gè)方法,用于處理 Dep.target,顯然 Dep.target 在 wather.getter 的調(diào)用過(guò)程中會(huì)用到,調(diào)用時(shí)會(huì)涉及到依賴收集,從而建立起數(shù)據(jù)綁定的關(guān)系。
在 Dep 類的 .dep() 方法中用到了 Dep.target,調(diào)用方式為:
Dep.target.addDep(this)
可以想見(jiàn),在使用數(shù)據(jù)進(jìn)行渲染的過(guò)程中,會(huì)對(duì)數(shù)據(jù)屬性進(jìn)行“讀”操作,從而觸發(fā) dep.depend(),進(jìn)而收集到這個(gè)依賴關(guān)系。下面來(lái)找一下這樣的調(diào)用的位置。
在 state.js 中找到一處,makeComputedGetter() 函數(shù)中通過(guò) watcher.depend() 間接調(diào)用了 dep.depend()。不過(guò) computedGetter 應(yīng)該不是最主要的地方,根據(jù) Vue 1.0 的經(jīng)驗(yàn),還是要找對(duì)數(shù)據(jù)進(jìn)行“數(shù)據(jù)劫持”的地方,應(yīng)該是defineReactive()。
defineReactive() 定義在文件 src/core/observer/index.js。
// 參數(shù):obj, key, val, customSetter? dep = new Dep() childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function () { // 略,調(diào)用了 dep.depend() }, set: function () { // 略,調(diào)用 dep.notify() } })
結(jié)合 Vue 1.0 經(jīng)驗(yàn),這里應(yīng)該就是數(shù)據(jù)劫持的關(guān)鍵了。數(shù)據(jù)原有的屬性被重新定義,屬性的 get() 被調(diào)用時(shí),會(huì)通過(guò) dep.depend() 收集依賴關(guān)系,記錄到 vm 中;而在 set() 被調(diào)用時(shí),則會(huì)判斷屬性值是否發(fā)生變更,如果發(fā)生變更,則通過(guò) dep.notify() 來(lái)通知 vm,從而觸發(fā) vm 的更新操作,實(shí)現(xiàn) UI 與數(shù)據(jù)的同步,這也就是數(shù)據(jù)綁定后的效果了。
回過(guò)頭來(lái)看 state.js,是在 initProps() 中調(diào)用了 defineReactive()。而 initProps() 在 initState() 中調(diào)用,后者則是在 Vue.prototype._init() 中被調(diào)用。
不過(guò)最常用的其實(shí)是在 initData() 中,對(duì)初始傳入的 data 進(jìn)行劫持,不過(guò)里面的過(guò)程稍微繞一些,是將這里的 data 賦值到 vm._data 并且代理到了 vm 上,進(jìn)一步的處理還涉及 observe() 和 Observer 類。這里不展開(kāi)了。
綜上,數(shù)據(jù)綁定的實(shí)現(xiàn)過(guò)程為:
初始化:new Vue() -> vm._init()
數(shù)據(jù)劫持:initState(vm) -> initProps(), initData() -> dep.depend()
依賴收集:vm.$mount() -> vm._mount() -> new Watcher() -> vm._render()
渲染首先來(lái)看 initRender(),這里在 vm 上初始化了兩個(gè)與創(chuàng)建虛擬元素相關(guān)的方法:
vm._c()
vm.$createElement()
其內(nèi)部實(shí)現(xiàn)都是調(diào)用 createElement(),來(lái)自文件:src/core/vdom/create-element.js。
而在 renderMixin() 中初始化了 Vue.prototype._render() 方法,其中創(chuàng)建 vnode 的邏輯為:
render = vm.$options.render try { vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { // ... }
這里傳入 render() 是一個(gè)會(huì)返回 vnode 的函數(shù)。
接下來(lái)看 vm._update() 的邏輯,這部分在前面有介紹,初次渲染時(shí)是通過(guò)調(diào)用 vm.__patch__() 來(lái)實(shí)現(xiàn)。那么 vm.__patch__() 是在哪里實(shí)現(xiàn)的呢?在 _update() 代碼中有句注釋,提到:
// Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used.
在文件 web-runtime.js 中,找到了:
Vue.prototype.__patch__ = inBrowser ? patch : noop
顯然示在瀏覽器環(huán)境下使用 patch(),來(lái)自:src/platforms/web/runtime/patch.js,其實(shí)現(xiàn)是通過(guò) createPatchFunction(),來(lái)自文件 src/core/vdom/patch。
OK,以上線索都指向了 vdom 相關(guān)的模塊,也就是說(shuō),顯然是 vdom 也就是 Virtual DOM 參與了渲染和更新。
不過(guò)還有個(gè)問(wèn)題沒(méi)有解決,那就是原始的字符串模塊,是如何轉(zhuǎn)成用于 Virtual DOM 創(chuàng)建的函數(shù)調(diào)用的呢?這里會(huì)有一個(gè)解析的過(guò)程。
回到入口文件 web-runtime-with-compiler.js,在 Vue.prototype.$mount() 中,有一個(gè)關(guān)鍵的調(diào)用:compileToFunctions(template, ...),template 變量值為傳入的參數(shù)解析得到的模板內(nèi)容。
模板解析文件:src/platforms/web/compiler/index.js
函數(shù) compileToFunctions() 的基本邏輯:
// 參數(shù):template, options?, vm? res = {} compiled = compile(template, options) res.render = makeFunction(compiled.render) // 拷貝數(shù)組元素: // res.staticRenderFns <= compiled.staticRenderFns return res
這里對(duì)模板進(jìn)行了編譯(compile()),最終返回了根據(jù)編譯結(jié)果得到的 render()、staticRenderFns。再看 web-runtime-with-compiler.js 中 Vue.prototype.$mount() 的邏輯,則是將這里得到的結(jié)果寫入了 vm.$options 中,也就是說(shuō),后面 vm._render() 中會(huì)使用這里的 render()。
再來(lái)看 compile() 函數(shù),這里是實(shí)現(xiàn)模板解析的核心,來(lái)做文件 src/compiler/index.js,基本邏輯為:
// 參數(shù):template, options ast = parse(template.trim(), options) optimize(ast, options) code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns }
邏輯很清晰,首先從模板進(jìn)行解析得到抽象語(yǔ)法樹(shù)(ast),進(jìn)行優(yōu)化,最后生成結(jié)果代碼。整個(gè)過(guò)程中肯定會(huì)涉及到 Vue 的語(yǔ)法,包括指令、組件嵌套等等,不僅僅是得到構(gòu)建 Virtual DOM 的代碼。
需要注意的是,編譯得到 render 其實(shí)是代碼文本,通過(guò) new Function(code) 的方式轉(zhuǎn)為函數(shù)。
總結(jié)Vue2 相比 Vue1 一個(gè)主要的區(qū)別在于引入了 Virtual DOM,但其 MVVM 的特性還在,也就是說(shuō)仍有一套數(shù)據(jù)綁定的機(jī)制。
此外,Virtual DOM 的存在,使得原有的視圖模板需要轉(zhuǎn)變?yōu)楹瘮?shù)調(diào)用的模式,從而在每次有更新時(shí)可以重新調(diào)用得到新的 vnode,從而應(yīng)用 Virtual DOM 的更新機(jī)制。為此,Vue2 實(shí)現(xiàn)了編譯器(compiler),這也意味著 Vue2 的模板可以是純文本,而不必是 DOM 元素。
Vue2 基本運(yùn)行機(jī)制總結(jié)為:
文本模板,編譯得到生成 vnode 的函數(shù)(render),該過(guò)程中會(huì)識(shí)別并記錄 Vue 的指令和其他語(yǔ)法
new Vue() 得到 vm 對(duì)象,其中傳入的數(shù)據(jù)會(huì)進(jìn)行數(shù)據(jù)劫持處理,從而可以收集依賴,實(shí)現(xiàn)數(shù)據(jù)綁定
渲染過(guò)程是將所有數(shù)據(jù)交由渲染函數(shù)(render)進(jìn)行調(diào)用得到 vnode,應(yīng)該 Virtual DOM 的機(jī)制實(shí)現(xiàn)初始渲染和更新
寫在最后對(duì) Vue2 的源碼分析,是基于我之前對(duì) Vue1 的分析和對(duì) Virtual DOM 的了解,見(jiàn)【鏈接】中之前的文章。
水平有限,錯(cuò)漏難免,歡迎指正。
感謝閱讀!
鏈接Vue 雙向數(shù)據(jù)綁定原理分析 - luobotang
一起理解 Virtual DOM - luobotang
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/81768.html
摘要:分享前啰嗦我之前介紹過(guò)如何實(shí)現(xiàn)和。我們采用用最精簡(jiǎn)的代碼,還原響應(yīng)式架構(gòu)實(shí)現(xiàn)以前寫的那篇源碼分析之如何實(shí)現(xiàn)和可以作為本次分享的參考。到現(xiàn)在為止,我們?cè)倏茨菑垐D是不是就清楚很多了總結(jié)我非常喜歡,以上代碼為了好展示,都采用最簡(jiǎn)單的方式呈現(xiàn)。 分享前啰嗦 我之前介紹過(guò)vue1.0如何實(shí)現(xiàn)observer和watcher。本想繼續(xù)寫下去,可是vue2.0橫空出世..所以 直接看vue2.0吧...
摘要:流程圖盜用一下官網(wǎng)關(guān)于生命周期的圖,對(duì)照之前的內(nèi)容梳理一下對(duì)照上面的分析基本上可以找到各個(gè)鉤子函數(shù)的位置,下面那個(gè)銷毀的我就沒(méi)用做分析了。。。 vue整體框架和主要流程分析 之前對(duì)看過(guò)比較多關(guān)于vue源碼的文章,但是對(duì)于整體框架和流程還是有些模糊,最后用chrome debug對(duì)vue的源碼進(jìn)行查看整理出這篇文章。。。。 本文對(duì)vue的整體框架和整體流程進(jìn)行簡(jiǎn)要的分析,不對(duì)某些具體的細(xì)...
摘要:至此算是找到了源碼位置。至此進(jìn)入過(guò)渡的部分完畢。在動(dòng)畫結(jié)束后,調(diào)用了由組件生命周期傳入的方法,把這個(gè)元素的副本移出了文檔流。這篇并沒(méi)有去分析相關(guān)的內(nèi)容,推薦一篇講非常不錯(cuò)的文章,對(duì)構(gòu)造函數(shù)如何來(lái)的感興趣的同學(xué)可以看這里 Vue transition源碼分析 本來(lái)打算自己造一個(gè)transition的輪子,所以決定先看看源碼,理清思路。Vue的transition組件提供了一系列鉤子函數(shù),...
閱讀 3635·2021-10-11 10:59
閱讀 1657·2021-09-29 09:35
閱讀 2326·2021-09-26 09:46
閱讀 3835·2021-09-10 10:50
閱讀 1005·2019-08-29 12:17
閱讀 880·2019-08-26 13:40
閱讀 2516·2019-08-26 11:44
閱讀 2165·2019-08-26 11:22