『翻譯』深入理解Vue.js響應式原理

FREAKFILTH發表於2017-08-31

作者從 Java 與 C# 中經典的 Getters/Setters 引入,討論了 Vue.js 中從元件渲染函式、資料的 Getter、Setter 劫持、監聽器的控制以及重渲染觸發整個生命流程。
原文連結:Understanding Vue.js Reactivity in Depth with Object.defineProperty()

引子

本人是Java背景,許多年前剛接觸JavaScript時有點怪怪的,因為它沒有 getterssetters。隨著時間的推移,我開始喜歡上這個缺失的特性,因為相比Java大量的 gettersetter,它讓程式碼更簡潔。例如,我們看看下面的Java程式碼:

class Person{
  String firstName;
  String lastName;

  // 這個Demo中省略了一些構造器程式碼 :)

  public void setFirstName(firstName) {
    this.firstName = firstName;
  }

  public String getFirstName() {
    return firstName;
  }

  public void setLastName(lastName) {
    this.lastName = lastName;
  }

  public String getLastName() {
    return lastName;
  }
}

// Create instance
Person bradPitt = new Person();
bradPitt.setFirstName("Brad");
bradPitt.setLastName("Pitt");複製程式碼

JavaScript開發人員永遠不會這樣做,相反他們會這樣:

var Person = function () {
};

var bradPitt = new Person();
bradPitt.firstName = 'Brad';
bradPitt.lastName = 'Pitt';複製程式碼

這要簡潔的多。通常簡潔更好,不是嗎?

的確如此,但有時我想獲取一些可以被修改的屬性,但我不用知道這些屬性是什麼。例如,我們在Java程式碼中擴充套件一個新的方法 getFullName()

class Person{
  private String firstName;
  private String lastName;

  // 這個Demo中省略了一些構造器程式碼 :)

  public void setFirstName(firstName) {
    this.firstName = firstName;
  }

  public String getFirstName() {
    return firstName;
  }

  public void setLastName(lastName) {
    this.lastName = lastName;
  }

  public String getLastName() {
    return lastName;
  }

  public String getFullName() {
    return firstName + " " + lastName;
  }
}

Person bradPitt = new Person();
bradPitt.setFirstName("Brad");
bradPitt.setLastName("Pitt");

// Prints 'Brad Pitt'
System.out.println(bradPitt.getFullName());複製程式碼

在上面例子中, fullName 是一個計算過的屬性,它不是私有屬性,但總能返回正確的結果。

C# 和隱式的 getter/setters

我們來看看 C# 特性之一:隱式的 getters/setters,我真的很喜歡它。在 C# 中,如果需要,你可以定義 getters/setters,但是並不用這樣做,但是如果你決定要這麼做,呼叫者就不必呼叫函式。呼叫者只需要直接訪問屬性,getter/setter 會自動在鉤子函式中執行:

public class Foo
{
    public string FirstName {get; set;}
    public string LastName {get; set;}
    public string FullName {get { return firstName + " " + lastName }; private set;}
}複製程式碼

我覺得這很酷...

現在,如果我想在JavaScript中實現類似的功能,我會浪費很多時間,比如:

var person0 = {
  firstName: 'Bruce',
  lastName: 'Willis',
  fullName: 'Bruce Willis',
  setFirstName: function (firstName) {
    this.firstName = firstName;
    this.fullName = `${this.firstName} ${this.lastName}`;
  },
  setLastname: function (lastName) {
    this.lastName = lastName;
    this.fullName = `${this.firstName} ${this.lastName}`;
  },
};
console.log(person0.fullName);
person0.setFirstName('Peter');
console.log(person0.fullName);複製程式碼

它會列印出:

"Bruce Willis"
"Peter Willis"複製程式碼

但使用 setXXX(value) 的方式並不夠'javascripty'(是個玩笑啦)。

下面的方式可以解決這個問題:

var person1 = {
  firstName: 'Brad',
  lastName: 'Pitt',
  getFullName: function () {
    return `${this.firstName} ${this.lastName}`;
  },
};
console.log(person1.getFullName()); // 列印 "Brad Pitt"複製程式碼

現在我們回到被計算過的 getter。你可以設定 first 或 last
name,並簡單的合併它們的值:

person1.firstName = 'Peter'
person1.getFullName(); // 返回 "Peter Pitt"複製程式碼

這的確更方便,但我還是不喜歡它,因為我們要定義一個叫“getxxx()”的方法,這也不夠'javascripty'。許多年來,我一直在思考如何更好的使用 JavaScript。

然後 Vue 出現了

在我的Youtube頻道,很多和Vue教程有關的視訊都講到,我習慣響應式開發,在更早的Angular1時代,我們叫它:資料繫結(Data Binding)。它看起來很簡單。你只需要在Vue例項的 data() 塊中定義一些資料,並繫結到HTML:

var vm = new Vue({
  data() {
    return {
      greeting: 'Hello world!',
    };
  }
})複製程式碼
<div>{greeting}</div>複製程式碼

顯然它會在使用者介面列印出 “Hello world!”。

現在,如果你改變“greeting”的值,Vue引擎會對此作出反應並相應地更新檢視。

methods: {
  onSomethingClicked() {
    this.greeting = "What's up";
  },
}複製程式碼

很長一段時間我都在想,它是如何工作的?當某個物件的屬性發生變化時會觸發某個事件?或者Vue不停的呼叫 setInterval 去檢查是否更新?

通過閱讀Vue官方文件,我才知道,改變一個物件屬性將隱式呼叫getter/setter,再次通知觀察者,然後觸發重新渲染,如下圖,這個例子來自官方的vue.js文件:

但我還想知道:

  • 怎麼讓資料自帶getter/setters

  • 這些隱式呼叫內部是怎樣的?

第一個問題很簡單:Vue為我們準備好了一切。當你新增新資料,Vue將會通過其屬性為其新增 getter/setters。但是我讓 foo.bar = 3? 會發生什麼?

這個問題的答案出現在我和SVG & Vue專家Sarah Drasner的Twitter對話中:

Timo: foo.bar=value;是怎麼做到實時響應的?
Sarah: 這個問題很難在Twitter說清楚,可以看這篇文章
Timo: 但這篇文章並沒有解釋上面提到的問題。
Timo: 它們就像:分配一個值->呼叫setter->通知觀察者,不理解為什麼在不使用setInterval和Event的情況下,setter/getter就存在了。
Sarah: 我的理解是:你獲取的所有資料都在Vue例項data{}中被代理了。

顯然,她也是參考的官方文件,之前我也讀過,所以我開始閱讀Vue原始碼,以便更好的理解發生了什麼。過了一會我想起在官方文件看到一個叫 Object.defineProperty() 的方法,我找到它,如下:

/**
 * 給物件定義響應的屬性
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // 預定義getter/setters
  const getter = property && property.get
  const setter = property && property.set

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* 禁用eslint 不進行自我比較 */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* 開啟eslint 不進行自己比較 */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}複製程式碼

所以答案一直存在於文件中:

把一個普通 JavaScript 物件傳給 Vue 例項的 data 選項,Vue 將遍歷此物件所有的屬性,並使用 Object.defineProperty 把這些屬性全部轉為 getter/setter。Object.defineProperty 是僅 ES5 支援,且無法 shim 的特性,這也就是為什麼 Vue 不支援 IE8 以及更低版本瀏覽器的原因。

我只想簡單的瞭解 Object.defineProperty() 做了什麼,所以我用一個例子簡單的給你講解一下:

var person2 = {
  firstName: 'George',
  lastName: 'Clooney',
};
Object.defineProperty(person2, 'fullName', {
  get: function () {
    return `${this.firstName} ${this.lastName}`;
  },
});
console.log(person2.fullName); // 列印 "George Clooney"複製程式碼

還記得文章開頭C#的隱式 getter 嗎?它們看起來很類似,但ES5才開始支援。你需要做的是使用 Object.defineProperty() 定義現有物件,以及何時獲取這個屬性,這個getter被稱為響應式——這實際上就是Vue在你新增新資料時背後所做的事。

Object.defineProperty()能讓Vue變的更簡化嗎?

學完這一切,我一直在想,Object.defineProperty() 是否能讓Vue變的更簡化?現今越來越多的新術語,是不是真的有必要把事情變得過於複雜,變的讓初學者難以理解(Redux也是同樣):

  • Mutator - 或許你在說(隱式)setter

  • Getters - 為什麼不用 Object.defineProperty() 替換成(隱式)
    getter

  • store.commit() - 為什麼不簡化成 foo = bar,而是 store.commit("setFoo", bar);

你是怎麼認為的?Vuex必須是複雜的還是可以像 Object.defineProperty() 一樣簡單?

本文譯者:餘震(Freak)
譯文出處:Rockjins Blog
版權宣告:本部落格所有文章除特別宣告外,均採用 CC BY-NC-SA 3.0 CN許可協議。轉載請註明出處!

相關文章