今天聽了一個大牛講課 除了膜拜 還是膜拜?
深入理解Vue的底層原理 通過一個手寫的過程可以深入的理解一下vue的底層設計原理旱眯,首先vue工作機制是怎么樣的 其次的vue的響應機制是怎么樣的贺纲,在vue的響應中依賴收集與追蹤是怎么實現(xiàn)的呢,最后是怎么編譯compile呢补憾?我們帶著這些問題去深入探究一下
vue工作機制
初始化,在new vue()之后,首先在內(nèi)部執(zhí)行了一個初始化方法涌乳,它做的就是一些最基礎的東西的初始化站欺,比如說初始化生命周期姨夹,我們知道有很多生命周期的鉤子,還有一些props矾策,還有我們一些數(shù)據(jù)data的響應化等磷账,其中最重要的是通過object.defineProperty設置getter和setter函數(shù),用來實現(xiàn)響應式以及依賴收集贾虽。?
在初始化之后調(diào)用$mount來執(zhí)行掛載函數(shù)逃糟,我們知道Vue的初始化就是通過$mount來實現(xiàn)的,$mount其實就是要指定一個掛載節(jié)點蓬豁,可能會是一個目標節(jié)點绰咽,也有可能會是一個dom節(jié)點,最終就是告訴我們vue將把那些寫好的模板通過編譯以后達到更新以后這個最新的東西 我到底要顯示在什么地方地粪,就是$mount最終指定的那個目標取募,然后$mount會啟動這個編譯器compile,這個編譯器最重要的事情就是對我們的Template里的東西進行一遍掃描蟆技,做parse optimize generate這三件事玩敏,compile在這個階段會生成一些渲染函數(shù)或者也可以叫更新函數(shù)斗忌,會生成一顆樹,我們叫虛擬節(jié)點樹旺聚,將來在做數(shù)據(jù)更新的時候飞蹂,其實我們改變的數(shù)據(jù)并不是真正的dom操作,而是這個虛擬dom上的數(shù)值翻屈,當我們準備更新之前我們會做一個diff算法的比較陈哑,通過最新的值和之前的老值進行比較,從而計算出我們應該做的最小的dom更新伸眶,然后我們才開始執(zhí)行到這個patch步驟來打補丁做界面更新惊窖,這樣兒做的目的是用js里面的計算時間來換dom操作時間,我們知道瀏覽器的瓶頸在對頁面操作這一塊兒比較耗時間厘贼,Vue的核心在于減少頁面渲染的次數(shù)和數(shù)量界酒,compile除了編譯渲染函數(shù)之外,還會做一個依賴收集的工作嘴秸,通過這個依賴收集我們可以知道當頁面數(shù)據(jù)發(fā)生變化的時候我應該去更新頁面中的那一個dom節(jié)點毁欣,這也就是將來這個數(shù)據(jù)發(fā)生變化的時候,我們可以通過這個watcher觀察者來知道數(shù)據(jù)發(fā)生變化岳掐,這時候調(diào)用更新渲染函數(shù)來打補丁凭疮。
上述提到的編譯器在掃描dom的時候做的三件事兒parse optimize generate
1 parse是使用正則解析template中的vue指令變量等,形成語法樹AST
2 optimize 標記一些靜態(tài)節(jié)點串述,用作后面的性能優(yōu)化执解,在diff的時候直接略過
3 generate 把第一部生成的AST轉(zhuǎn)化為渲染函數(shù)
我們今天要實現(xiàn)一個自己的mvvm框架,實現(xiàn)一個observer數(shù)據(jù)劫持監(jiān)聽纲酗,當數(shù)據(jù)發(fā)生變化的時候衰腌,通知watcher變化,讓他去調(diào)視圖更新,從而去做界面的更新觅赊,框架開始也會做一些編譯的過程右蕊,會初始化視圖,在初始化視圖的同時還做了另外一件事情吮螺,就是初始了我們的觀察者watcher
更新視圖
數(shù)據(jù)修改解發(fā)setter饶囚,然后監(jiān)聽器會通知進行修改,通過對比兩個dom樹规脸,得到改變的地方坯约,就是patch然后需要把這些差異修改即可
下面來一波實戰(zhàn)
vue響應式的原理:defineProperty
首先我定義一個對象obj 我期望obj.name='xx' 這個操作可以直接顯示在標簽內(nèi),這種操作是不是就是所謂的數(shù)據(jù)驅(qū)動莫鸭,數(shù)據(jù)的響應式闹丐,我們平常在寫的Vue的時候是不是就是這樣的,當一個屬性發(fā)生變化的時候被因,界面中的值就會動態(tài)的發(fā)生變化卿拴。我們使用object.defineProperty來添加屬性
通過defineProperty我們就可以知道vue數(shù)據(jù)響應式原理衫仑,來給我們的Data添加屬性 當這個屬性發(fā)生改變的時候,我們就可以指定的規(guī)則來作更新堕花。
那么我們在面試的時候怎么回答 vue的原理是怎樣的呢?
從原理上來講vue是利用了Object的defineProperty的屬性文狱,它把我們數(shù)據(jù)data中放的每一個屬性,都定義成一個屬性缘挽,賦予了getter和setter,這樣兒的話讓我們有機會去監(jiān)聽這些屬性的變化瞄崇,當這些屬性發(fā)生變化的時候,我們可以通知那些需要更新的地方去更新壕曼。
我們先簡單模擬一個vue數(shù)據(jù)更新的類來實現(xiàn)數(shù)據(jù)響應:
我們在defineProperty中的set中監(jiān)聽到了數(shù)據(jù)更新苏研。通過運行index.html我們可以看到test 和 bar的更新都打印出來了。
當發(fā)現(xiàn)在數(shù)據(jù)發(fā)生的變化的時候腮郊,我們需要做界面的更新摹蘑,上述中我們只是打印出的變化,因此引出來數(shù)據(jù)的依賴收集的概念
比如在界面中我們引用了name1 name2 name1 這時候我們created name1和name3的時候轧飞,會發(fā)生什么事情呢衅鹿?
name1我會發(fā)現(xiàn)在頁面中有兩個依賴,這樣兒在name1變化的時候过咬,我通知兩個部分變化就可以了大渤,這時候name3發(fā)生變化,其實不會做任何通知援奢,因為跟本沒有任何依賴兼犯,這就是為什么在程序開始之前一定會對模板進行一次遍歷,然后我們會從中找出一些和我數(shù)據(jù)有依賴的部分集漾,收集保存下來,在數(shù)據(jù)需要更新的時候調(diào)用砸脊。
依賴收集需要引入兩個概念具篇,一個是依賴對象depnice 一個是監(jiān)聽對象watcher,這兩個對象首先遵從一個發(fā)布訂閱模式,dep是訂閱者凌埂,它非常關心我們的數(shù)據(jù)發(fā)生變化驱显,觀察者其實是我們例子中的setter函數(shù),當數(shù)據(jù)發(fā)生變化的時候瞳抓,dep發(fā)出通知埃疫,去調(diào)用所有的watcher。
我們定義了一個dep類的對象孩哑,用來收集watcher對象栓霜,讀數(shù)據(jù)的時候,會觸發(fā)getter函數(shù)把當前的watcher對象(存放在Dep.target中)收集到Dep類中横蜒,寫數(shù)據(jù)的時候胳蛮,則會觸發(fā)stter方法销凑,通知Dep類調(diào)用notyfy來觸發(fā)所有的watcher對象的update方法更新對應視圖。
這里一定要注意仅炊,每一個依賴針對于一個單個的屬性斗幼,每個依賴當中還有可能會有多個watcher,key出現(xiàn)幾次就會有幾個watcher抚垄。
編譯compile
核心邏輯獲取dom,遍歷dom,獲取{{}}格式的變量蜕窿,以及每個dom的屬性,截獲k- @等開頭設置響應式
在compile.js中呆馁,我們首先需要將內(nèi)容轉(zhuǎn)換為代碼片斷桐经,以減少對dom的操作,然后進行編譯智哀,將編譯結果再追加到宿主對象el上次询。
在上述步驟中 轉(zhuǎn)換代碼片斷的方法node2Fragment,我們先創(chuàng)建一個代碼片斷瓷叫,通過查找宿主對象el上的firstChild屯吊,逐次的添加到新創(chuàng)建的代碼片斷中,最后返回這個代碼片斷的集合摹菠。
在編譯的方法compile中盒卸,遍歷代碼片斷,判斷是元素對象還是插值對象次氨,對應做的相應的操作蔽介,這個時候由于遍歷對象中也可能會包含子節(jié)點,所以我們要通過判斷el.childNodes節(jié)點是否存在來做遞歸判斷煮寡。
這個時候當我們在kvue.js中去new Compile(options.el, this)一個compile實例的時候虹蓄,就可以完成字符串轉(zhuǎn)換了,但是在我們的生命周期鉤子函數(shù)created中 如果有一個setTimeout來this.name的時候幸撕,就不會發(fā)生任何變化了薇组,這是因為我們還沒有做任何依賴收集的工作,當屬性更新的時候setter函數(shù)沒有被觸發(fā)坐儿,所以我們需要一個更新函數(shù)update 添加依賴收集律胀。
這時我們就需要改一下compileText方法,添加一個update方法的通用方法貌矿,執(zhí)行第一次修改炭菌,然后添加依賴。
首先watcher的構造函數(shù)需要接收三個參數(shù)vm,key,cb逛漫,這時候我們回到kvue.js 我們需要在Watcher函數(shù)中黑低,需要對三個參數(shù)做引用,接下來我們添加依賴屬性的地方尽楔,可以再讀一次添加的屬性投储,由次來添加依賴第练,觸發(fā)setter,然后再置空玛荞。避免重復添加娇掏,下一次再創(chuàng)建的時候還是這個過程。這時候update函數(shù)里勋眯,就可以直接執(zhí)行cb
到此這個流程基本上就串起來了婴梧,這時候我們會發(fā)現(xiàn)在watcher中,觸發(fā)屬性的時候使用的是vm客蹋,而不是$data塞蹭,所以要變成大家熟悉的那種寫法,因此需要寫一個代理讶坯。
接下來的代理工作番电,我們要回到observe中,我們在定義defineReactive的時候辆琅,我們還可以再定義一個代理proxyData漱办,代理data中的屬性到vue的實例上,這時候我們就可以用vue.xx 來直接使用了婉烟。
我們把新傳進來的值賦值給$data, this.$data[key]的重新賦值又會觸發(fā)上面我們定義在dfeinReactive中的data的setter方法娩井,然后又開始通知,這樣兒就串起來了似袁。
當別人問你vue的編譯過程是怎么樣的時候洞辣,你怎樣回答?
我們先說什么是編譯昙衅,為什么要編譯扬霜,首先因為vue寫的html模板,是瀏覽器識別不了的而涉,我們通過編譯的過程畜挥,可以進行依賴收集,進行依賴收集之后我們就把Data中的數(shù)據(jù)模型和視圖之間產(chǎn)生依賴關系婴谱,當模型發(fā)生變化的時候,我們就可以通知這些依賴的地方讓他們進行更新躯泰,這就是我們執(zhí)行編譯的目的谭羔,我們把這些界面全部編譯以后,更新操作麦向,我們就可以做到模型驅(qū)動視圖的變化這就是編譯的過程瘟裸。
雙向綁定的原理是什么?
我們在做雙向綁定的時候 通常會使用一個v-model這樣的指令放在input這樣的一個輸入元素上诵竭,我們在編譯的時候可以解析這個v-model 话告,我在做操作的時候有兩件事情兼搏,第一件事情我把當前vmodel所屬的這個元素上加了一個事件監(jiān)聽,這樣如果input會生變化的時候沙郭,我就可以把最新的值設置到vue的實例上佛呻,因為vue的實例已經(jīng)實現(xiàn)了數(shù)據(jù)的響應化,它的響應化的setter函數(shù)病线,會觸發(fā)頁面中所有依賴的更新吓著,所以跟這個數(shù)據(jù)相關的所有部分都更新了。