手把手教你剖析vue响应式原理,监听数据不再迷茫
Object.defineProperty實現vue響應式原理
- 一、組件化基礎
- 1、“很久以前”的組件化
- (1)asp jsp php 時代
- (2)nodejs
- 2、數據驅動視圖(MVVM,setState)
- (1)數據驅動視圖 - Vue MVVM
- (2)數據驅動視圖 - React setState
- (3)總結
- 二、Vue響應式
- 1、vue的響應式是什么
- 2、Object.defineProperty基本用法
- 3、Oject.defineProperty實現響應式
- (1)監聽對象
- (2)監聽數組
- (3)幾個缺點
- 四、結束語
近期在對 vue 的學習到一定階段之后,在想著自己能不能造些東西。于是身邊的小伙伴建議說可以從看 vue 的源碼開始,毫無頭緒的我原本遲遲不敢邁出這一步……(內心經歷了各種自我勸說后)最終,我開啟了我的源碼學習之路。
于是我搜刮了一些常見的原理來進行學習,我對 vue 源碼的第一步從 vue 的響應式原理開始。
下面的這篇文章中,將記錄我學習 vue 響應式原理的總結。一起來了解一下吧~🙋?
一、組件化基礎
1、“很久以前”的組件化
(1)asp jsp php 時代
在很久以前,也就是大概第一批接觸網頁開發的程序員,在他們的那個年代其實就已經有組件化了。
(2)nodejs
nodejs 比起 asp 、 jsp 和 php 來說,起步較晚,但是呢, nodejs 中也有類似的組件化,比如像 js 的模板引擎 ejs ,就可以實現組件化。
我們來看下 ejs 是怎么實現組件化的?:
<!-- 個人信息 --> <div class = "right-item"><%- include('widgets/user-info', {userInfo:userData.userInfo,isMe:userData.isMe,amIFollowed:userData.amIFollowed,atCount:userData.atCount});%> </div> <!-- 用戶列表 --> <%- include('widgets/user-list', {count:userData.count,userList:userData.list });%>通過以上代碼可以了解到, ejs 通過 <%- %> 的形式來定義一個組件,從而實現數據渲染。
雖說早期也有組件,但是對于傳統組件來說,也只是靜態渲染,并且它的更新還是要依賴于操作 DOM 。這樣子的話,用不用組件開發其實區別也不會差特別多。
因此,為了解決這個問題,就有了現在流行的 vue 和 react ,基于數據驅動視圖的開發。
2、數據驅動視圖(MVVM,setState)
(1)數據驅動視圖 - Vue MVVM
vue的組件化定義如下所示:
<template><div id="app"><imgalt="Vue logo"src="./assets/logo.png"><HelloWorldmsg="Welcome to your Vue.js App"/></div> </template>引用官方的圖片,我們來講下 Vue 的 MVVM 。
所謂 MVVM ,即 Model-View-ViewModel 。
View 即 視圖 ,也就是 DOM 。
Model 即 模型 ,可以理解為 Vue 中組件里面的 data 。
那么這兩者之間,就通過 ViewModel 來做關聯。而 ViewModel 可以做的事情有很多,比如說像監聽事件,監聽指令等。當 Model 層的數據發生修改時,就可以通過 ViewModel ,來把數據渲染到 View 視圖層上。反之,當 View 層觸發 DOM 事件時,就可以通過 ViewModel ,從而使得 Model 層實現數據的修改。
這就是 Vue 中的數據驅動視圖,通過修改 Model 層的數據,來驅動到 View 的視圖中來。
了解完基本概念,我們用一段代碼來剖析 Vue 中的 MVVM 是怎么樣的。
<template><div id="app"><p @click="changeName">{{name}}</p><ul><li v-for="(item, index) in list" :key="index">{{item}}</li></ul><button @click="addItem">添加一項</button></div> </template> <script> export default {name:'app',data(){return{name:'vue',list:['a', 'b', 'c']}},methods:{changeName(){this.name = 'monday';},addItem(){this.list.push(`${Date.now()}`);}} } </script>在上面的代碼中, template 部分就表示 view 層,而下面的 data 就表示 Model 層。之后呢,像 @click 這種點擊事件,點擊完之后觸發到具體的 methods ,這一部分就可以視為是 ViewModel 層,這樣的話,就可以理解為 ViewModel 層是連接 View 層和 Model 層的一個橋梁。
(2)數據驅動視圖 - React setState
React的組件化的定義如下所示:
function App(){return(<div className="App"><header className="AppHeader"><imgsrc={logo}className="App-logo"alt="logo"/><HelloWorldmsg="Welcome to Your React App"/></header></div>); }React 通過 setState 去操作數據驅動視圖。這里不對 react 的數據驅動視圖進行細講,大家可以根據自身需求進行資料查詢~
(3)總結
vue 和 react 幫助我們通過數據去渲染視圖,這也就讓我們在做 vue 和 react 開發時,更多的是關注業務邏輯,而不像傳統組件一樣要一直去考慮 DOM 更新的問題。
二、Vue響應式
1、vue的響應式是什么
所謂 vue 的響應式,即組件 data 的數據一旦變化,就會立刻觸發視圖的更新。實現數據驅動視圖的第一步,需要了解實現響應式的一個核心 API ,即 Object.defineProperty 。
2、Object.defineProperty基本用法
我們用一段代碼來演示 Object.defineProperty 的用法,如下所示:
const data = {} const name = 'friday' Object.defineProperty(data, "name", {get:function () {console.log('get')return name},set: function (newVal) {console.log('set')name = newVal} })// 測試 console.log(data.name) //get friday data.name = 'monday' //set通過上面的代碼可以看到,通過 Object.defineProperty ,我們可以實現對數據進行 get 和 set 操作,即獲取數據和修改數據的操作,從而達到對數據進行響應式的監聽。
那 Object.defineProperty 又是如何實現響應式的呢?接下來一起來一探究竟吧!
3、Oject.defineProperty實現響應式
(1)監聽對象
在了解響應式之前,需要大家對 js 的數據類型和深拷貝有一個了解。這里我之前寫過一篇文章,如有需要可前往查看~
我們都知道 js 的數據類型有基本數據類型和引用數據類型,接下來我們將來實現這兩種數據類型的響應式監聽。
基本數據類型:
// 觸發更新視圖 function updateView() {console.log('視圖更新') }// 重新定義屬性,監聽起來 function defineReactive(target, key, value) {// 深度監聽observer(value)// 核心 APIObject.defineProperty(target, key, {get() {return value},set(newValue) {if (newValue !== value) {// 深度監聽observer(newValue)// 設置新值// 注意,value 一直在閉包中,此處設置完之后,再次 get 時也是會獲取最新的值value = newValue// 觸發更新視圖updateView()}}}) }// 監聽對象屬性 function observer(target) {//判斷是基本數據類型 or 引用數據類型if (typeof target !== 'object' || target === null) {// 不是對象或數組return target}// 重新定義各個屬性(for in 也可以遍歷數組)for (let key in target) {defineReactive(target, key, target[key])} } // 準備數據 const data = {name: 'monday',age: 20 }// 監聽數據 observer(data)// 測試 data.name = 'lisi' data.age = 18 console.log('name', data.name) console.log('age', data.age)此時控制臺的打印效果如下:
從上圖可以看到,我們改變了兩個數據的值,數據也會實時更新。在控制臺中我們可以發現,改變了兩個數據的值,同時也顯示出兩個“視圖更新”,至此,則說明這兩個數據監聽成功。
閱讀代碼我們可以發現,當我們監聽的數據是基本數據類型時,會直接返回 target 的值,并且視圖進行實時更新。
同時,需要注意的是, Object.defineProperty() 在新增屬性和刪除屬性時,數據是監聽不到的。
什么意思呢?我們來演示一下。
依據上面的代碼,我們再增加以下兩行內容。
data.x = '100' // 新增屬性,監聽不到 —— 用 Vue.set 解決 delete data.name // 刪除屬性,監聽不到 —— 用 Vue.delete 解決此時控制臺的打印結果如下:
細心的小伙伴已經發現,加上這兩行代碼后運行效果跟原來是一樣的。所以,我們可以得出結論,在用 Object.defineProperty() 新增和刪除屬性時,數據是監聽不到的,這個時候即使數據修改了,視圖也監聽不到對應的數據,也就沒有辦法進行視圖更新。
引用數據類型:
同樣,依據基本數據類型第一段的代碼,我們來監聽引用數據類型的數據。測試代碼如下:
// 準備數據 const data = {name: 'monday',age: 20,info: {address: '福州' // 需要深度監聽},nums: ['打籃球', '出來玩', '打乒乓球'] }// 監聽數據 observer(data)// 測試 data.info.address = '上海' // 深度監聽 data.nums.push('神游') // 監聽數組此時瀏覽器的打印結果如下:
我們可以發現,只出現了一個視圖更新,沒有出現兩個。原因在于,對象 info 監聽到了,但是數組 nums 并沒有監聽到。這是為什么呢?
其實,從某種意義上來講, nums 雖然可以走到深度遍歷里面,但是呢, Object.defineProperty() 這個 API 本身是不具備監聽數組能力的,所以我們需要加工一層,讓其可以擁有監聽數組的能力。
(2)監聽數組
要想讓 Object.defineProperty() 這個 API 擁有監聽數組的能力,我們可以這么做。具體代碼如下:
// 觸發更新視圖 function updateView() {console.log('視圖更新') }// 重新定義數組原型 const oldArrayProperty = Array.prototype // 創建新對象,原型指向 oldArrayProperty ,再擴展新的方法不會影響原型 const arrProto = Object.create(oldArrayProperty); ['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {arrProto[methodName] = function () {updateView() // 觸發視圖更新oldArrayProperty[methodName].call(this, ...arguments)// Array.prototype.push.call(this, ...arguments)} })// 重新定義屬性,監聽起來 function defineReactive(target, key, value) {// 深度監聽observer(value)// 核心 APIObject.defineProperty(target, key, {get() {return value},set(newValue) {if (newValue !== value) {// 深度監聽observer(newValue)// 設置新值// 注意,value 一直在閉包中,此處設置完之后,再 get 時也是會獲取最新的值value = newValue// 觸發更新視圖updateView()}}}) }// 監聽對象屬性 function observer(target) {if (typeof target !== 'object' || target === null) {// 不是對象或數組return target}// 污染全局的 Array 原型(如果直接定義在這里面,會直接污染全局)// Array.prototype.push = function () {// updateView()// ...// }if (Array.isArray(target)) {target.__proto__ = arrProto}// 重新定義各個屬性(for in 也可以遍歷數組)for (let key in target) {defineReactive(target, key, target[key])} }// 準備數據 const data = {name: 'monday',age: 20,info: {address: '福州' // 需要深度監聽},nums: ['打籃球', '出來玩', '打乒乓球'] }// 監聽數據 observer(data)// 測試 data.info.address = '上海' // 深度監聽 data.nums.push('神游') // 監聽數組此時瀏覽器的打印效果如下:
我們可以看到,兩個數據對應的視圖都更新了。通過對數組原型的重新定義,我們就讓 Object.defineProperty() 實現了監聽數組的能力。
(3)幾個缺點
在讓 Object.defineProperty() 實現響應式功能以后,我們來總結下其存在的幾個缺點:
1)深度監聽,需要遞歸到底,一次性計算量大
在遍歷對象或數組時,需要進行深度監聽,即需要遞歸到底,這會使得一次性計算量非常大。(這個問題在 vue3.0 中已經解決,其解決原理是不一定要一次性遞歸,而是可以我們什么時候用,什么時候再遞歸。這個將放在后面的文章中講解)
2)無法監聽新增屬性/刪除屬性
Object.defineProperty() 在進行新增屬性和刪除屬性時,視圖是無法進行更新的,也就是數據監聽不到,這一點在平常的開發中需要特別注意!否則有時候我們在取數據時總會莫名其妙地都不知道自己錯在哪里。通常解決這個問題的方法是,使用 Vue.set 和 Vue.delete 來進行新增屬性和刪除屬性,這樣就可以解決數據無法監聽的問題。
3)無法原生監聽數組,需要特殊處理
Object.defineProperty() 這個 API 本身無法監聽原生數組,需要通過重新定義數組原型的方式,來對數組進行數據監聽。
四、結束語
對于 vue2.x 的響應式原理講到這里就結束啦!從上面的分析中我們可以發現, Object.defineProperty() 有它一定的好用之處,但同時也有一些缺點存在。因此 Vue3.0 用了 Proxy 來解決上述缺點中存在的問題,但是呢, proxy 到現在其實也還沒有推廣開來,因為 proxy 有兼容性的問題存在,如無法兼容 IE11 等問題,且 proxy 無法 polyfill ,所以 vue2.x 很長一段時間內應該還會存在。因此,對于 vue2.x 和 vue3.0 來說,這兩者都是得學的,而不是說出了 vue3.0 就不學 vue2.x 了,對于這兩者來說,更多的是相輔相成的一個結果。
閑談到此結束,對于 vue 原理的學習有深深感受到造輪子的快樂,但是啃源碼在開始學習時確實會比較枯燥。希望再接再厲,爭取啃下更多 vue 的源碼,讀懂更多原理!
- 關注公眾號 星期一研究室 ,不定期分享學習干貨,學習路上不迷路~
- 如果這篇文章對你有用,記得點個贊加個關注再走哦~
總結
以上是生活随笔為你收集整理的手把手教你剖析vue响应式原理,监听数据不再迷茫的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 荣耀董事长万飚回应离职传闻,称其目前仍在
- 下一篇: 特斯拉 Cybertruck 电动皮卡内