摘要:巴拉巴拉省略大法,去除無關代碼巴拉巴拉省略大法,去除無關代碼核心就這一句話。文章鏈接源碼分析系列源碼分析系列之環境搭建源碼分析系列之入口文件分析源碼分析系列之響應式數據一
前言
接著上一篇的初始化部分,我們細看initData中做了什么。
正文 initDatafunction initData (vm: Component) { let data = vm.$options.data // 獲得傳入的data.此處為{a:1, b:2} data = vm._data = typeof data === "function" ? getData(data, vm) : data || {} // 如果data不是純對象,則打印警告信息 if (!isPlainObject(data)) { data = {} process.env.NODE_ENV !== "production" && warn( "data functions should return an object: " + "https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function", vm ) } // 獲得data中所有的key,即a,b const keys = Object.keys(data) // 獲得組件中定義的props與methods const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { // data中的第i個k const key = keys[i] // 如果methods中定義過相同的key,報錯 if (process.env.NODE_ENV !== "production") { if (methods && hasOwn(methods, key)) { warn( `Method "${key}" has already been defined as a data property.`, vm ) } } // 如果props中定義過相同的key,報錯 if (props && hasOwn(props, key)) { process.env.NODE_ENV !== "production" && warn( `The data property "${key}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) // 如果key不是保留關鍵字,即不是_或者$開頭的key。 } else if (!isReserved(key)) { // 將屬性代理到實例的_data屬性上。 // 例如vm.a = 1實際上會被處理為vm._data.a = 1; // vm.a 將返回 vm._data.a; proxy(vm, `_data`, key) } } // 讓data變為響應式數據,即數據改變時候,UI也能跟著變。 observe(data, true /* asRootData */) }observe
export function observe (value: any, asRootData: ?boolean): Observer | void { // 如果設置的參數value不是一個對象或者是一個虛擬dom。則直接返回。 if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void // 如果這個value存在觀察者實例,則賦給返回值 if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( observerState.shouldConvert && // 需要轉化 !isServerRendering() && // 非服務端渲染 (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue // 是數組或者對象且可以擴展屬性,且不是vue組件實例 ) { // 根據給定的值創建一個觀察者實例 ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } // 返回觀察者實例 return ob }new Observer(value)
export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor (value: any) { // 要被觀察的值 this.value = value // new 一個用來收集依賴的對象 this.dep = new Dep() this.vmCount = 0 // 將該Observer實例,賦值給value的__ob__屬性。 def(value, "__ob__", this) if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment // 如果是數組的話,需要對數組的方法做特殊處理, // 并且依次的讓數組的每個對象元素變為可觀察的 augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { // 否則是對象的話,依次遍歷每個屬性,并設置其 // getter與setter,支持后續的響應式變化。 this.walk(value) } } /** * Walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */ walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) } } /** * Observe a list of Array items. */ observeArray (items: Arrayobserve 與 Observer簡單說明) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }
observe使一個對象變為響應式對象,在其值改變時候,可觸發ui改變。其做法是為每一個value(非原始類型的)設置一個__ob__,即Observer對象實例。如果value是一個對象,會遍歷它的每一個key,然后為其設置setter/getter,如果是數組會對數組元素依次的遞歸observe。
假設輸入為data = {a: 1, b: 2} 經過observe后,會變成 data = {a: 1, b:2, __ob__: ObserverInstance, a的 getter,setter,b的getter, b的setter}。 其中ObserverInstance.dep用來收集對這個對象變更感興趣的watcher, 其中ObserverInstance.value指向這個對象data;defineReactive
為了以最簡單的方式說明流程,這里我們只看對象的處理情況,到了這里,我們的邏輯進入walk函數內部,遍歷對象的每個屬性,即a,b;分別定義為響應式屬性。
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // 這里的Dep還是Watcher收集器,用來收集對該變量感興趣的watcher const dep = new Dep() // 獲取屬性描述符,不熟悉的同學請查閱es6的文檔 const property = Object.getOwnPropertyDescriptor(obj, key) // 如果屬性描述符顯示該對象不可配置, // 即無法設置getter,setter,也就無法處理為響應式屬性了,那直接返回。 // 一般我們定義的data里很少設置屬性描述符,默認property => undefined if (property && property.configurable === false) { return } // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set // 如果該obj的key,對應的val還是對象,則使其也變成可觀察的對象 // 這里是一個遞歸處理,shallow標識是否深度處理該值。 // 類似深拷貝,淺拷貝中是否深拷貝的邏輯。 let childOb = !shallow && observe(val) // 重點,設置getter,setter,這里我們定義data.a的getter,setter, Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { // 這里一般對象的屬性是沒有getter的,直接返回val const value = getter ? getter.call(obj) : val // 在特定的時機進行依賴收集。通過Dep.target // 這里在后期vue處理模板template的時候,會生成render函數, // render函數執行生成虛擬dom時候會去讀取vm實例的屬性,比如vm.a這時候會觸發這個getter, // 此時的Dep.target為一個watcher, // 內容為vm._update(vm._render)這個函數,用來更新視圖用 // 將該函數添加到defineReactive內部定義的dep中。 // 接下來我們看后面的set部分 if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { // 當我們觸發點擊事件時候,this.a += 1; // 此時newVal是value值加1,所以代碼會繼續走 const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== "production" && customSetter) { customSetter() } // 這里設置了newVal為val if (setter) { setter.call(obj, newVal) } else { val = newVal } // 如果新值是一個對象,會繼續被當做可觀察的對象。 childOb = !shallow && observe(newVal) // dep.notify就是挨個通知Dep中收集的watcher去搞事情。 // get函數內我提到,dep中被加入了一個watcher, // 其watcher實際作用就是觸發視圖更新,get時候,被收集了, // set時候就會觸發ui的更新。 dep.notify() } }) }initData的完結
至此initData完結,上文中defineReactive的getter和setter的設定,在前期到不會觸發,這里只是把規矩定下,真正用到的時候還需要跟模板結合。這個章節我們主要分析一下initData對data的處理。接下來我們看下模板的地方。
首先我們需要把代碼跳出來看這里,不要問我為什么知道看這里,因為我是看完一遍后有個印象,現在只不過在梳理流程。
this.init -> initState -> initData this.init -> initState -> this.init this.init -> vm.$mount(vm.options.el)
這里我們再貼一下init內部函數的代碼,讓大家對大概的方位有個了解
Vue.prototype._init = function (options?: Object) { // ***************************省略頂部代碼 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) { // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! vm.$mount(vm.$options.el) } }vm.$mount
接下來我們簡述一下vm.$mount內部的代碼
const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { // 根據id查找dom元素 el = el && query(el) // 巴拉巴拉省略魔法 // 如果沒有render函數則生成render函數 const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns return mount.call(this, el, hydrating) }compileToFunctions生成的render函數
compileToFunctions是怎么根據模板生成html的,這不本章節的重點,后面我會多帶帶去寫compileToFunctions的過程,本章節重點是render函數的結果,我以上一章節的html為例,這是render函數的結果。
(function anonymous() { with (this) { return _c("div", { attrs: { "id": "demo" } }, [_c("div", [_c("p", [_v("a:" + _s(a))]), _v(" "), _c("p", [_v("b: " + _s(b))]), _v(" "), _c("p", [_v("a+b: " + _s(total))]), _v(" "), _c("button", { on: { "click": addA } }, [_v("a+1")])])]) } })
其中_c就是vue封裝的createElement,用來生成虛擬dom。_s就是toString方法,等等。這里我們可以看到其中有些參數是變量,a,b,total。這與我們在js中定義的一致。接下來我們看下這個渲染函數執行的地方。
mount.call首先還是接上面的代碼。mount.call(this, el, hydrating)。
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }mountComponent
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el // 巴拉巴拉省略大法,去除無關代碼 callHook(vm, "beforeMount") // 巴拉巴拉省略大法,去除無關代碼 let updateComponent /* istanbul ignore if */ updateComponent = () => { vm._update(vm._render(), hydrating) } // 核心就這一句話。new 一個Watcher,上文多次提到的家伙。 vm._watcher = new Watcher(vm, updateComponent, noop) hydrating = false // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true callHook(vm, "mounted") } return vm }new Watcher()
上個代碼片段核心就這一句話。
vm._watcher = new Watcher(vm, updateComponent, noop)。
重點是這個Watcher第二個參數,updateComponent,很重要。
new Watcher我們著重看下構造函數內部的代碼即可,如下是精簡過后的代碼
export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; lazy: boolean; sync: boolean; dirty: boolean; active: boolean; deps: Array總結; newDeps: Array ; depIds: ISet; newDepIds: ISet; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: Object ) { this.vm = vm vm._watchers.push(this) // options 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 } // 此時expOrFn為上述的updateComponent this.getter = expOrFn // 由于此時lazy是falsly值,觸發get this.value = this.lazy ? undefined : this.get() } get () { // 該函數將Dep.target置為當前watcher。 pushTarget(this) let value const vm = this.vm try { // 調用getter實際是調用updateComponent // 由于updateComponent會調用options.render,所以會觸發 // vm._render函數,而vm._render函數中的核心則是 // vnode = render.call(vm, vm.$createElement) // 在 compileToFunctions生成的render函數 一節我們已經看到了一個rendre函數大概的面貌 // 此時render函數中有時候會取讀取vm.a的值。有時會取讀取vm.b的值。 // 當讀取vm.a或者b的時候,就會觸發對應屬性的getter // 然后會將當前的Watcher加入屬性對應的dep中。 // 聯系不起來的同學可以往回看,defineReactive中的dep收集的就是當前watcher了。 // 當用戶點擊頁面的a+1按鈕時,則會觸發this.a += 1。 // 則會觸發defineReactive(obj, a, {get,set})中的set, // set中會調用dep.notify。其實就是讓dep收集到的watcher挨個執行 // 下述中的run方法. // 而run方法又觸發了當前的這個get方法,執行到getter.call的時候,界面就更新了。 value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value } run () { if (this.active) { // 關鍵一句就是獲取值,實際上這里的獲取值就是 // get -> this.get -> updateComponent -> 虛擬節點中重新獲取 // 界面中需要的a,b的值。 const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value if (this.user) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } } }
此時一個大概的關于data是如何影響view的流程基本跑通了。以界面數據a為例,
核心思想就是defineReactive(data, a, {get,set})去設置屬性a的getter,setter。
getter將會在vue執行render函數生成虛擬dom的時候,將界面更新的watcher放入a的dep中。
當鼠標單擊界面上的a+1按鈕觸發this.a += 1時候,會觸發a的setter函數,此時會將getter時收集的依賴————更新界面的watcher————觸發。watcher執行自身的run方法,即更新界面。
至此data -> view這一層算是通了,至于input中的v-model實際上是input + onInput事件的語法糖,監聽input,值改變時候通過事件修改vm.a的值,進一步觸發————更新界面的watcher,使界面更新。
文章鏈接vue源碼分析系列
vue源碼分析系列之debug環境搭建
vue源碼分析系列之入口文件分析
vue源碼分析系列之響應式數據(一)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/101647.html
摘要:代碼初始化部分一個的時候做了什么當我們一個時,實際上執行了的構造函數,這個構造函數內部掛載了很多方法,可以在我的上一篇文章中看到。合并構造函數上掛載的與當前傳入的非生產環境,包裝實例本身,在后期渲染時候,做一些校驗提示輸出。 概述 在使用vue的時候,data,computed,watch是一些經常用到的概念,那么他們是怎么實現的呢,讓我們從一個小demo開始分析一下它的流程。 dem...
摘要:執行當時傳入的回調,并將新值與舊值一并傳入。文章鏈接源碼分析系列源碼分析系列之環境搭建源碼分析系列之入口文件分析源碼分析系列之響應式數據一源碼分析系列之響應式數據二源碼分析系列之響應式數據三 前言 上一節著重講述了initComputed中的代碼,以及數據是如何從computed中到視圖層的,以及data修改后如何作用于computed。這一節主要記錄initWatcher中的內容。 ...
摘要:目標是為了可以調試版本的,也就是下的源碼,所以主要是的開啟。結語至此就可以開心的研究源碼啦。文章鏈接源碼分析系列源碼分析系列之入口文件分析源碼分析系列之響應式數據一源碼分析系列之響應式數據二 概述 為了探究vue的本質,所以想debug一下源碼,但是怎么開始是個問題,于是有了這樣一篇記錄。目標是為了可以調試es6版本的,也就是src下的源碼,所以主要是sourceMap的開啟。原文來自...
摘要:中引入了中的中引入了中的中,定義了的構造函數中的原型上掛載了方法,用來做初始化原型上掛載的屬性描述符,返回原型上掛載的屬性描述符返回原型上掛載與方法,用來為對象新增刪除響應式屬性原型上掛載方法原型上掛載事件相關的方法。 入口尋找 入口platforms/web/entry-runtime-with-compiler中import了./runtime/index導出的vue。 ./r...
閱讀 1010·2021-11-22 13:52
閱讀 924·2019-08-30 15:44
閱讀 570·2019-08-30 15:43
閱讀 2424·2019-08-30 12:52
閱讀 3473·2019-08-29 16:16
閱讀 637·2019-08-29 13:05
閱讀 2943·2019-08-26 18:36
閱讀 1975·2019-08-26 13:46