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