Vue的思考擴展
1、Vue是如何實現數據雙向綁定的
1.1、實現雙向綁定的基本原理
Vue 采用數據劫持結合發布者-訂閱者模式的方式來實現數據的響應式,通過Object.defineProperty來劫持數據的setter、getter,在數據變動時發布消息給訂閱者,訂閱者收到消息后進行相應的處理,修改 dom 節點內容。
數據響應原理:當你把一個普通的 JavaScript 對象傳給 Vue 實例的 data選項,Vue 將遍歷此對象所有的屬性,并使用 Object.defineProperty 把這些屬性全部轉為 getter/setter,在屬性被訪問和修改時通知變化。每個組件實例都有相應的 watcher 實例對象,它會在組件渲染的過程中把屬性記錄為依賴,之后當依賴項的setter被調用時,會通知watcher重新計算,從而致使它關聯的組件得以更新。
正如上面所說,vue實現數據雙向綁定主要是采用數據劫持結合發布-訂閱者模式的方式。
數據劫持是通過 Object.defineProperty() 實現的,該函數為每個屬性添加setter、getter 的方法,在數據發生改變時 setter 方法會被觸發,然后發布消息給訂閱者,觸發相應的監聽回調函數。當把一個普通 Javascript 對象傳給 Vue 實例來作為它的 data 選項時,Vue 將遍歷它的屬性,用 Object.defineProperty() 為每個屬性添加 setter,getter 的方法。
vue的數據雙向綁定主要通過三個模塊完成:監聽者Observer、訂閱者Watcher、Compile解析模板指令。
執行順序如下:
- 首先利用 Object.defineProperty() 創建 Observer,劫持所有屬性。
- Compile() 初始渲染頁面、為節點綁定函數、添加watcher監聽者。Compile 會掃描和解析每個節點的相關指令,在遇到 {{}} 或 v-modle 等節點的時候,首先將模板中的變量替換成數據,根據初始數據渲染頁面視圖。并且將每個指令對應的節點綁定函數,一旦視圖發生交互,綁定的函數就被觸發,數據會發生變化。創建 Watcher 訂閱者,并將它存入 Dep,當數據發生變化時,改變節點的內容。
由此即實現了雙向綁定。當數據發生變化時,set 函數會觸發,然后通知 watcher 改變元素內容。當元素節點發生一些交互時,節點綁定的事件會觸發,此時會更新數據,set 函數會觸發,然后通知 watcher 改變元素內容。
(詳細可以查看下面的 1.3.2 查看代碼實現)

var vm = new Vue({ data: { obj: { a: 1 } }, created: function () { console.log(this.obj); } });

打印 Vue 實例的data里的某個數據的某個屬性,可以看到該屬性含有 setter、getter 方法,也就是對該屬性進行了監聽。當我們使用 vue 時,輸出某一個對象,當看到該對象的變量有 getter 和 setter,則意味著該對象的變量已經是響應式的了。
因為 vue 是在一開始時就對 data 里的所有屬性遍歷并添加 getter 和 setter,所以Vue 不能動態添加 data 屬性里的根級別的響應式屬性,并且由于通過直接修改數組索引的值無法觸發 setter 函數,所以要想修改數組的索引值只能使用一些變異方法來修改,這些方法 vue 已經進行了重寫。
1.2、原生JS實現簡單雙向綁定
1.2.1、命令式操作視圖(簡單的數據劫持示例)
<body> <div id="app"> <input type="text" id="txt"> <p id="show"></p> </div> </body> <script type="text/javascript"> var obj = {} Object.defineProperty(obj, 'txt', { get: function () { return obj }, set: function (newValue) { document.getElementById('txt').value = newValue document.getElementById('show').innerHTML = newValue } }) document.addEventListener('keyup', function (e) { obj.txt = e.target.value }) </script>
上面代碼中,首先 defineProperty 為每個屬性添加 getter、setter 方法,當數據發生改變, setter 方法被觸發,視圖也發生改變。setter 里面的執行命令可以看做是一個訂閱者 Watcher,將視圖和數據連接起來了。最下面的代碼為節點綁定方法可以看做是Compile的作用,為指令節點綁定方法,當發生視圖交互時,函數被觸發,數據被改變,Watcher 收到通知,視圖也將發生改變。
效果如下:

1.2.2、指令式操作視圖(數據劫持+發布訂閱者示例)
下面實現 v-text 聲明式的指令版本。一但data中的屬性值發生變化后,標記的 v-text 的文本內容就會立即得到更新。
(下面代碼只是先實現了數據劫持)
<div id="app"> <p v-text="name"></p> <input v-model="searchVal" /> </div> <script> let data = { name: 'myname', searchVal: 'aaa測試' } // 遍歷每一個屬性 Object.keys(data).forEach((key) => { defineReactive(data, key, data[key]) }) function defineReactive(data, key, value) { Object.defineProperty(data, key, { get() { return value }, set(newVal) { value = newVal // 數據發生變化,操作dom進行更新 compile() } }) } function compile() { let app = document.getElementById('app') // 1.拿到app下所有的子元素 const nodes = app.childNodes // [text, input, text] //2.遍歷所有的子元素 nodes.forEach(node => { // nodeType為1為元素節點 if (node.nodeType === 1) { const attrs = node.attributes // 遍歷所有的attrubites找到 v-model Array.from(attrs).forEach(attr => { const dirName = attr.nodeName const dataProp = attr.nodeValue if (dirName === 'v-text') { node.innerText = data[dataProp] } if (dirName === 'v-model') { node.value = data[dataProp] // 視圖變化反應到數據 無非是事件監聽反向修改 node.oninput = function(e) { data[dataProp] = e.target.value } } }) } }) } // 首次渲染 compile() </script>
效果如下:

在控制臺中直接修改 data 的屬性值,元素標簽的內容會同時發生改變,或者在輸入框中直接輸入值,data 中屬性值實際也會發生改變。這也就實現了 view -> model 和 model -> view 的一個雙向綁定的效果。
不管是指令也好,插值表達式也好,這些都是將數據反應到視圖的標記而已,通過標記我們可以把數據的變化響應式的反應到對應的dom位置上去。我們把這個找標記,把數據綁定到dom的過程稱之為binding。
上面的代碼有個問題,就是在 data 的某個屬性值發生改變時,整個 compile 函數都會被執行,所有的 dom 元素也都會被重新設置。下面我們可以通過發布-訂閱者模式來優化代碼,實現當 data 屬性值發生改變時,只改變訂閱了該屬性值的 dom 元素的內容,實現精準更新。
代碼如下:
(下面代碼實現了數據劫持+發布-訂閱者模式)
<div id="app"> <p v-text="name"></p> <input v-model="searchVal" /> </div> <script> let data = { name: 'myname', searchVal: 'aaa測試' } // 遍歷每一個屬性,利用 Object.defineProperty() 創建 Observer function observe(data) { if (!data || typeof data !== 'object') { return; } // 取出所有屬性遍歷 Object.keys(data).forEach(function (key) { defineReactive(data, key, data[key]); }); }; function defineReactive(data, key, value) { observe(value); // 監聽子屬性 Object.defineProperty(data, key, { get() { return value }, set(newValue) { // 更新視圖 if (newValue === value) { return; } value = newValue // 再次編譯要放到新值已經變化之后只更新當前的key dep.trigger(key) } }) } // 執行observe,監聽每一個 data 的屬性 observe(data) // 增加dep對象 用來添加訂閱者和通知訂閱者 const dep = { map: Object.create(null), // 添加訂閱者 collect(dataProp, updateFn) { if (!this.map[dataProp]) { this.map[dataProp] = [] } this.map[dataProp].push(updateFn) }, // 通知訂閱者 trigger(dataProp) { console.log('觸發了事件', dataProp); this.map[dataProp] && this.map[dataProp].forEach(updateFn => { updateFn() }) } } // 編譯函數 function compile() { let app = document.getElementById('app') // 1.拿到app下所有的子元素 const nodes = app.childNodes // [text, input, text] //2.遍歷所有的子元素 nodes.forEach(node => { // nodeType為1的是元素節點 if (node.nodeType === 1) { const attrs = node.attributes // 遍歷所有的attrubites找到對應的指令 Array.from(attrs).forEach(attr => { const dirName = attr.nodeName const dataProp = attr.nodeValue if (dirName === 'v-text') { node.innerText = data[dataProp] // 給對應的data屬性值添加訂閱者 dep.collect(dataProp, () => { node.innerText = data[dataProp] }) } if (dirName === 'v-model') { node.value = data[dataProp] // 一些特定的指令(比如v-model)需要給節點綁定事件 node.oninput = function (e) { data[dataProp] = e.target.value } // 給對應的data屬性值添加訂閱者 dep.collect(dataProp, () => { node.value = data[dataProp] }) } }) } }) } // 首次加載執行compile,以此初始化頁面;給節點綁定事件;給data屬性值添加訂閱者watcher,以便屬性值發生改變好修改節點內容 compile() </script>
當 data 屬性的值發生改變時,只有訂閱了該屬性值的 dom 元素的內容會隨之發生變化。
上面代碼也就吻合了雙向綁定原理的執行順序,如下:
- 首先利用 Object.defineProperty() 創建 Observer,劫持所有屬性。
- Compile() 初始渲染頁面、為節點綁定函數、添加watcher監聽者。Compile 會掃描和解析每個節點的相關指令,在遇到 {{}} 或 v-modle 等節點的時候,首先將模板中的變量替換成數據,根據初始數據渲染頁面視圖。并且將每個指令對應的節點綁定函數,一旦視圖發生交互,綁定的函數就被觸發,數據會發生變化。創建 Watcher 訂閱者,并將它存入 Dep,當數據發生變化時,改變節點的內容。
由此即實現了雙向綁定。當數據發生變化時,set 函數會觸發,然后通知 watcher 改變元素內容。當元素節點發生一些交互時,節點綁定的事件會觸發,此時會更新數據,set 函數會觸發,然后通知 watcher 改變元素內容。
1.3、vue3和vue2的響應式處理的區別
vue2.x 中的 data 配置項,只要放到了data里的數據,不管層級多深不管你最終會不會用到這個數據都會進行遞歸響應式處理。所以如果非必要,盡量不要添加太多的冗余數據在data中。
vue3.x中,解決了 vue2 中對于數據響應式處理的無端性能消耗,使用的手段是 Proxy 劫持對象整體 + 惰性處理(用到了才進行響應式轉換)。
2、瀏覽器渲染頁面過程

(瀏覽器渲染引擎的渲染流程)
2.1、關鍵渲染路徑
關鍵渲染路徑是指瀏覽器從最初接收請求來的HTML、CSS、javascript等資源,然后解析、構建樹、渲染布局、繪制,最后呈現給客戶能看到的界面這整個過程。
所以瀏覽器的渲染過程主要包括以下幾步:
- 解析HTML生成DOM樹。
- 解析CSS生成CSSOM規則樹。
- 將DOM樹與CSSOM規則樹合并在一起生成渲染樹。
- 遍歷渲染樹開始布局,計算每個節點的位置大小信息。
- 將渲染樹每個節點繪制到屏幕。
3、JS操作真實DOM的代價!
4、虛擬DOM的作用
虛擬DOM就是為了解決瀏覽器性能問題而被設計出來的。假如像上面所說的,若一次操作中有10次更新DOM的動作,會生成一個新的虛擬DOM,將新的虛擬DOM和舊的進行比較,然后將10次更新的 diff 內容保存到一個JS對象中,最終通過這個JS對象來更新真實DOM,由此只進行了一次操作真實DOM,避免大量無謂的計算量。所以,虛擬DOM的作用是將多個DOM操作合并成一個,并且將DOM操作先全部反映在JS對象中(操作內存中的JS對象比操作DOM的速度要更快),再將最終的JS對象映射成真實的DOM,交由瀏覽器去繪制。
5、實現虛擬DOM
虛擬DOM就是是用JS對象來代表節點,每次渲染都會生成一個VNode。當數據發生改變時,生成一個新的的VNode,通過 diff 算法和上一次渲染時用的VNode進行對比,生成一個對象記錄差異,然后根據該對象來更新真實的DOM。原本要操作的DOM在vue這邊還是要操作的,不過是統一計算出所有變化后統一更新一次DOM,進行瀏覽器DOM的一次性更新。
參考:https://baijiahao.baidu.com/s?id=1593097105869520145&wfr=spider&for=pc、https://www.jianshu.com/p/af0b398602bc
5.1、diff 算法
主流框架中多采用VNode更新結點,更新規則為diff算法。
diff 算法的原理:框架會將所有的結點先轉化為虛擬節點Vnode,在發生更改后將VNode和原本頁面的OldNode進行對比,然后以VNode為基準,在oldNode上進行準確的修改。(修改準則:原本沒有新版有,則增加;原本有新版沒有,則刪除;都有則進行比較,都為文本結點則替換值;都為靜態資源不處理;都為正常結點則替換)
6、Vue 中路由的hash模式和history模式
Vue 中路由有 hash 模式和 history 模式,hash 模式帶 # 號,history 沒有這個 # 號,就是普通的 url 。可以通過在 router 中配置 mode 選項來切換模式。
Vue 中的路由是怎么實現的可以參考:https://segmentfault.com/a/1190000011967786
Vue 中路由的實現是通過監聽 url 的改變,然后通過解析 url ,匹配上對應的組件進行渲染實現的。
在 hash 模式下,跳轉路由導致后面 hash 值的變化,但這并不會導致瀏覽器向服務器發出請求。另外每次 hash 值的變化,還會觸發 hashchange 這個事件,通過監聽這個事件就能知道 hash 值的改變,并能解析出 url。在 hash 模式下,刷新頁面和直接輸入鏈接都不會導致瀏覽器發出請求。
history 模式的實現原理是通過HTML5中的兩個方法:pushState 和 replaceState,這兩個方法可以改變 url 地址且不會發送請求,由此可以跳轉路由而不刷新頁面,不發出請求。但是在 history 模式下,用戶如果直接輸入鏈接或者手動刷新時,瀏覽器還是會發出請求,而會導致服務器尋找該 url 路徑下的對應的文件,而該路徑下的文件往往不存在,所以會返回 404。為了避免這種情況,在使用 history 模式時,需要后端進行配合使用,配置在URL 匹配不到任何靜態資源時應該返回什么東西,比如可以配置在找不到文件時返回項目的主頁面。
參考:http://www.rzrgm.cn/xufeimei/p/10745353.html
7、Vue 列表為什么加 key?
1)為了性能優化
key 是為 Vue 中 vnode 的唯一標記,通過這個 key,我們的 diff 操作可以更準確、更快速。
vue是虛擬DOM,更新DOM時用diff算法對節點進行一一比對。比如有很多li元素,要在某個位置插入一個li元素,但沒有給li上加key,那么在進行運算的時候,就會將所有li元素重新渲染一遍。但是如果有key,那么它就會按照key一一比對li元素,只需要創建新的li元素,插入即可,不需要對其他元素進行修改和重新渲染。
2)解決就地復用問題
通過 key 可以解決 “就地復用問題”,即下一個元素復用了上一個在當前位置元素的狀態。
key也不能是li元素的index,因為假設我們給數組前插入一個新元素,它的下標是0,那么和原來的第一個元素重復了,整個數組的key都發生了改變,這樣就跟沒有key的情況一樣了。

浙公網安備 33010602011771號