你真的瞭解vue嗎?vue2.0響應式源碼實踐

@TOChtml

殺生丸.jpg


寫在前面

震驚!!! 2019年10月5日,尤小右公開了 Vue 3.0 的源代碼。源碼地址:vue-next,這次更新的主要內容除了自行查看源碼還能夠在知乎上進行了解尤小右 3.0 RFC,在這兩篇的基礎上,接下來我將爲你們展現最近學習到3.0的內容解讀vue

瞭解3.0的進步,咱們得先了解2.0的響應式原理,若是已經知道其優點劣勢的大佬自行跳過~~react

vue2.0響應式源碼實現

看過官方文檔的同窗都知道Vue 響應式系統的解釋: 當你把一個普通的 JavaScript 對象傳入 Vue 實例做爲 data 選項,Vue 將遍歷此對象全部的屬性,並使用 Object.defineProperty 把這些屬性所有轉爲getter/setterObject.defineProperty 是 ES5 中一個沒法 shim 的特性,這也就是 Vue 不支持 IE8 以及更低版本瀏覽器的緣由git

下面咱們將大概先實現vue2.0響應github

原理:使用 Object.defineProperty 能夠從新定義屬性,而且給屬性增長 getter 和setter;數組

1. 先建立一個對象

// 咱們先建立一個對象,而後經過某個方法去監聽這個對象,當對象的值改變時,觸發操做
let defalutName = ''
let data = {name:''}
// observer監聽函數
observer(data)
console.log(data.name);
// expected output: Magic Eno

// 給data裏面的name賦值 = "Eno"
data.name = 'Eno'
console.log(data.name);   // expected output: Eno

console.log(defalutName); // expected output: Eno

複製代碼

2.實現observer方法

observer的效果要求很簡單,就是監聽data對象,當data裏面的屬性值改變時,監聽到其改變; 下面實現一個簡陋的雙向數據綁定,即data的name改變時,defaultName也要改變,實現雙向數據綁定,即defalutName與data對象的name雙向綁定了瀏覽器

let defalutName = ''
let data = {name:''};

function observer (data) {
  //Object.defineProperty直接在對象上定義新屬性,或修改對象上的現有屬性,而後返回對象。
//不瞭解的請轉MDN文檔 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
  Object.defineProperty(data, 'name', {
    get(){
      return defalutName
    },
    set(newValue){
      defalutName = newValue
    }
  });
}
// 
observer(data)
console.log(data.name);
// expected output: ''

// 給data裏面的name賦值 = "Eno"
data.name = 'Eno'
console.log(data.name);   // expected output: Eno

console.log(defalutName); // expected output: Eno
複製代碼

3.接下來咱們對observer函數進行改造

上面咱們的observer對象並無對data的全部值進行監聽,接下來咱們完善oberver函數以下:bash

function observer(data){
  // 判斷是否爲對象 若是不是則直接返回,Object.defineProperty是對象上的屬性
  if(typeof data !== 'object' || data == null){
    return data;
  }
  for(let key in data){
    defineReactive(data,key,data[key]);
  }
}
function defineReactive(data,key,value){
  //Object.defineProperty直接在對象上定義新屬性,或修改對象上的現有屬性,而後返回對象。
  //不瞭解的請轉MDN文檔 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
  Object.defineProperty(data, key, {
      get(){
         console.log('獲取了值') // 在此作依賴收集的操做
          return value 
      },
      set(newValue){
          if(newValue !== value){
              console.log('設置了值')
              value = newValue
          }
      }
  });
}

let data = {name:'',age:18};
observer(data)

// 給data裏面的name賦值 = "Eno"
data.name = 'Eno'
console.log(data.name);  
// 設置了值
// 獲取了值
// Eno
 data.age = 12
 console.log(data.age); 
// 設置了值
// 獲取了值
// 12
複製代碼
  • 輸入如圖
    image.png
    此時更新data裏面的全部值都觸發了defineProperty 的get和set方法

補充:什麼是依賴收集? 咱們都知道,當一個可觀測對象的屬性被讀寫時,會觸發它的getter/setter方法。若是咱們能夠在可觀測對象的getter/setter裏面,執行監聽器裏面的update()方法;不就可以讓對象主動發出通知了嗎?app

依賴收集.png

4. 假如給data添加不存在key會如何呢?

// ...
let data = {name:'',age:18};
observer(data)

// 給data裏面的name賦值 = "Eno"
// data.name = 'Eno'
// console.log(data.name);  
//  data.age = 12
//  console.log(data.age); 
data.gender = '男'

複製代碼

輸出結果以下: ide

image.png

由圖可知,並無觸發set和get,這個由於,在咱們對data進行監測的時候是沒有gender這個屬性值的,所以咱們若是想要對新增的屬性進行監聽的話,須要在賦值後再進行一次監聽,即vm.$set的效果;咱們能夠建立一個reactiveSet函數以下:

function reactiveSet (data,key,value) {
  data[key] = value
  observer(data)
}


let data = {name:'',age:18};
observer(data)

// 給data裏面的name賦值 = "Eno"
// data.name = 'Eno'
// console.log(data.name);  
//  data.age = 12
//  console.log(data.age); 
// 經過reactiveSet添加屬性
reactiveSet(data,'gender','男')
console.log(data.gender)

複製代碼

執行結果以下:

image.png

此時是能夠響應的,不過vue並非這樣作的,下面能夠看vue的源碼 vuejs in github 裏面是這樣判斷的,若是這個key目前沒有存在於對象中,那麼會進行賦值並監聽。可是這裏省略了ob的判斷;

補充: ob是什麼呢? vue初始化的數據(如data中的數據)在頁面初始化的時候都會被監聽,而被監聽的屬性都會被綁定__ob__屬性,下圖就是判斷這個數據有沒有被監聽的。若是這個數據沒有被監聽,那麼就默認你不想監聽這個數據,因此直接賦值並返回

image.png

5. 假如data裏面的數據是多層嵌套對象呢?

目前,咱們是對data一個簡單對象進行監聽,思考🤔一下假如是多層對象該如何調整及修改observer呢?

  • 其實很簡單, 咱們只須要在observer調用的defineReactive函數裏邊對value值進行遞歸監聽就能夠實現,但這種方式,會有必定性能問題;defineReactive修改以下:
// ...
function defineReactive(data,key,value){
  observer(value); // 遞歸 繼續對當前value進行攔截
  
  //Object.defineProperty直接在對象上定義新屬性,或修改對象上的現有屬性,而後返回對象。
  //不瞭解的請轉MDN文檔 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
  Object.defineProperty(data, key, {
      get(){
          console.log('獲取了值') // 在此作依賴收集的操做
          return value 
      },
      set(newValue){
          if(newValue !== value){
              // 對於新增的值也須要監聽
              observer(newValue)
              console.log('設置了值')
              value = newValue
          }
      }
  });
}
// ...
複製代碼

2019年10月21日更新

6. 假如data裏面的數據是多層嵌套數組呢?

假如data裏面的對象裏面有數組,那麼須要對數組進行攔截,若是數組裏面是多維數組,還需和5.嵌套對象的作法一致,還須要進行遞歸監聽,observer修改以下:

function observer(data){
  // 判斷是否爲對象 若是不是則直接返回,Object.defineProperty是對象上的屬性
  if(typeof data !== 'object' || data == null){
    return data;
  }
  if(Array.isArray(data)){ // 若是是數組,則對數據進行遍歷並對其value進行遞歸監聽
    for(let i = 0; i< data.length ;i++){
        observer(data[i]);
    }
  } else {
    for(let key in data){
      defineReactive(data,key,data[key]);
    }
  }
}
// ...
// ...

let data = {name:'',age:18};
observer(data)

// 給data裏面的name賦值 = "Eno"
// data.name = 'Eno'
// console.log(data.name);  
//  data.age = 12
//  console.log(data.age); 
// reactiveSet(data,'gender','男')
// console.log(data.gender)


reactiveSet(data,'attr',[1,2,3,4,5,100])
console.log(data.attr)
複製代碼
  • 執行輸出結果以下:
    image.png

從上圖能夠看出,在reactiveSet的狀況下,即便給data設置了不存在的數組,也可以獲得監聽,接下來嘗試對數組進行修改測試;

// ...
let data = {name:'',attr:[1,2,3,4,5,100]};
observer(data)

// 給data裏面的name賦值 = "Eno"
// data.name = 'Eno'
// console.log(data.name);  
//  data.age = 12
//  console.log(data.age); 
// reactiveSet(data,'gender','男')
// console.log(data.gender)
// console.log(data.attr)
data.attr.push(10000); 
data.attr.splice(0,1); 
複製代碼
  • 結果輸出以下: 此時對數據push或者刪除其中某個元素,很明顯observer並未監測到其變化: image.png

究其緣由:因爲數組的方法在對數組的增刪查改過程當中,vue並沒其操做更新視圖的操做,故此時是不能響應式的,所以若是須要對此類方法的調用時,經過視圖更新,則須要對數組方法重寫,查看vue/array.js源碼可知:

image.png

  • 其中:def的源碼以下: 即對obj的屬性進行了重寫或者稱之爲元素的屬性從新定義

    image.png

  • 接下來咱們寫一個簡陋版的數組重寫:

const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(method=>{
  arrayMethods[method] = function(){ 
    // 函數劫持 把函數進行重寫 
    // 而內部實際上繼續調用原來的方法但在這裏咱們能夠去調用更新視圖的方法
     console.log('數組 更新啦...')
      arrayProto[method].call(this,...arguments)
  }
});
複製代碼
  • 而且在observer中設置新的數組方法;
function observer(data){
  // 判斷是否爲對象 若是不是則直接返回,Object.defineProperty是對象上的屬性
  if(typeof data !== 'object' || data == null){
    return data;
  }
  if(Array.isArray(data)){ // 若是是數組,則對數據進行遍歷並對其value進行遞歸監聽
    // 在這裏對數組方法進行重寫 即函數劫持
    Object.setPrototypeOf(data, arrayMethods); 

    for(let i = 0; i< data.length ;i++){
        observer(data[i]);
    }
  } else {
    for(let key in data){
      defineReactive(data,key,data[key]);
    }
  }
}
複製代碼
  • 此時,全部代碼以下:
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(method=>{
  arrayMethods[method] = function(){ 
    //函數劫持 把函數進行重寫 
    // 而內部實際上繼續調用原來的方法但在這裏咱們能夠去調用更新視圖的方法
     console.log('數組 更新啦...')
      arrayProto[method].call(this,...arguments)
  }
});


function observer(data){
  // 判斷是否爲對象 若是不是則直接返回,Object.defineProperty是對象上的屬性
  if(typeof data !== 'object' || data == null){
    return data;
  }
  if(Array.isArray(data)){ // 若是是數組,則對數據進行遍歷並對其value進行遞歸監聽
    // 在這裏對數組方法進行重寫 即函數劫持
    Object.setPrototypeOf(data, arrayMethods); 
    for(let i = 0; i< data.length ;i++){
        observer(data[i]);
    }
  } else {
    for(let key in data){
      defineReactive(data,key,data[key]);
    }
  }
}
function defineReactive(data,key,value){
  observer(value); // 遞歸 繼續對當前value進行攔截

  //Object.defineProperty直接在對象上定義新屬性,或修改對象上的現有屬性,而後返回對象。
  //不瞭解的請轉MDN文檔 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
  Object.defineProperty(data, key, {
      get(){
          console.log('獲取了值') // 在此作依賴收集的操做
          return value 
      },
      set(newValue){
          if(newValue !== value){
              // 對於新增的值也須要監聽
              observer(newValue)
              console.log('設置了值')
              value = newValue
          }
      }
  });
}

function reactiveSet (data,key,value) {
  data[key] = value
  observer(data)
}


let data = {name:'',attr:[1,2,3,4,5,100]};
observer(data)

// 給data裏面的name賦值 = "Eno"
// data.name = 'Eno'
// console.log(data.name);  
//  data.age = 12
//  console.log(data.age); 
// reactiveSet(data,'gender','男')
// console.log(data.gender)
// console.log(data.attr)
data.attr.push(10000); 
data.attr.splice(0,1); 

複製代碼

運行結果以下:

image.png

看到視圖真的更新了,不得不佩服尤大大真的厲害,據說2020年第一季度就要出vue3.0了,接下來要寫篇vue3.0的初步學習文章,但願各位看官支持,不要忘記點贊喔;

文章源碼:github wLove-c


總結: 能力有限,暫時先寫這麼多,接下來有時間會寫一篇vue-next的源碼實踐和理解, 但願各位看官大人不要忘記點贊哈,寫的很差的地方歡迎指正;

vue3.0使用小測

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
  <div id="app"></div>
  <script src="vue.global.js"></script>
  <script>
    console.log('Vue====',Vue)
    const App = {
        setup() {
          // reactive state
          let count =  Vue.reactive({value:1}) // 知乎上尤大大推薦的是使用 const count = value(0) 但目前這個版本是沒有value的 先用reactive作響應
          // computed state
          const plusOne = Vue.computed(() => count.value * 2)
          // method
          const increment = () => {
             count.value++ 

            }
          // watch
          Vue.watch(() => count.value * 2, val => {
            console.log(`value * 2 is ${val}`)
          })
          // lifecycle
          Vue.onMounted(() => {
            console.log(`mounted`)
          })
          // expose bindings on render context
          return {
            count,
            plusOne,
            increment
          }
        },
        template: `
          <div>
            <div>count is {{ count.value }}</div>
            <span>plusOne is {{ plusOne }}</span>
            <button @click="increment">count++</button>
          </div>
        `,
      }
    Vue.createApp().mount(App,app)
  </script>
</body>
</html>
複製代碼
相關文章
相關標籤/搜索