//TODO:ExpressionHelper 、bindBidirectional雙向綁定、以及IntegerExpression的一系列算術方法和返回的IntegerBinding暫未詳細解析(比如,通過 sip.divide(2) 返回的IntegerBinding對象,是如何實現當sip修改時,其get方法的值也能做到除2【隨便猜測可能就類似於單向綁定一樣,維護observable並記錄算術操作,在get時,調用observable.get並加上算術操作】)
//注:關於觀察者模式和事件監聽模式(具體有沒有這個定義都還待定),雖然表現不太一樣但實現邏輯都一樣的,觀察者模式說一對多的依賴關系,當改變時其他相關依賴對象都對得到通知並更新,其實就等於調用監聽器的監聽方法
一、背景
使用過 SimpXXXProperty 系列的類都知道,這些類是支持屬性綁定以及改變監聽的,在實際開發中這種機制非常有用。
但包括Observable接口在內的這一系列類,均是由javafx所引入,在javafx包下。為了避免包引入看起來不論不類、也加深自己的理解,以SimpleIntegerProperty為例學習下實現原理。
二、使用示例
2.1 屬性綁定示例
例1:javafx窗口界面中有一個圓,若想實現無論怎么拉伸,使圓均處於窗口中心位置的話,就可以使用綁定機制
circle.centerXProperty().bind(stage.widthProperty().divide(2));
例2:小demo,讓一個屬性始終為另一個的一半
SimpleIntegerProperty half = new SimpleIntegerProperty();
SimpleIntegerProperty target = new SimpleIntegerProperty(8);
half.bind(target .divide(2));
System.out.println(half.get());
2.2 修改監聽示例
需求:比如做響應式頁面,當窗口寬度小於某個閾值時,執行某些操作。
stage.widthProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if(newValue < 333)
System.out.println("當前小於333");
}
});
三、機制概述
從表現上來看有兩個特性
3.1 屬性綁定(property binding)
允許同步兩個屬性的值,其中一個修改時,另一個屬性的獲取值會同步更新。
有兩個綁定方法 bind
與 binBidirectional
分別對應兩種綁定方式:
- 單向綁定(Unidirectional binding):比如屬性A綁定B,當B屬性改變時,A的獲取值會同步更新。且A將無法手動修改,只能修改B,否則會報異常
RuntimeException: A bound value cannot be set
- 雙向綁定(Bidirectional binding):只要A、B其中一個修改,另一個的獲取值將同步更新。
3.2 修改監聽(ChangeListener)
為屬性設置修改事件監聽器,當屬性值修改時,自動回調傳入監聽器方法。
四、實現原理解析
與我們熟知的觀察者模式不同,通過源碼我們可以看到在Observable
接口中定義的是InvalidationListener
類型監聽器添加方法,而在ObservableValue
接口中才定義了ChangeListener
。
由此引出疑問:什么是失效監聽器(Invalidation Listener)?這涉及到JavaFx屬性綁定的 延遲計算(lazy evaluation) 機制。
4.1 屬性綁定原理
如 A.bind(B),當綁定目標對象B更新時,並不是通過修改A自身的值來實現同步的。而是在使用bind()進行綁定時,通過傳入的綁定目標對象(無論是直接的 SimpleIntegerProeprty 或是通過 add、divide等方法返回的IntegerBinding對象)來構建維護 observable 字段。當調用get()嘗試獲取A的值時,則調用 observable.get()來獲取。
注:由上述屬性綁定邏輯我們可知,當綁定目標改變發生時並不直接重新計算,而是只有當此值被get()請求時,才調用 observable.get() 來返回最新值。因此在剛發生綁定操作或綁定目標修改后,還未get()使用前,則存在“失效”狀態【具體邏輯參考后面源碼解析】
4.2 監聽機制原理
調用 addListener
時,通過自身字段 ExpressionHelper helper
來附加存儲監聽器,當屬性值修改或是解綁時,則通過 markInvalid()
方法調用 ExpressionHelper.fireValueChangedEvent(helper)
,來回調所有附加的監聽器方法。
五、源碼解析
絕大部分字段(即類的成員變量,為了避免與 '屬性' 混淆,用字段一詞代替)都定義在抽象類 IntegerPropertyBase
中,而SimpleIntegerProperty則繼承自該抽象類。
public abstract class IntegerPropertyBase extends IntegerProperty {
private int value; //在非綁定情況下,類本身的值
//當使用bind()方法時,以傳入對象為基礎構建的目標綁定實例【具體邏輯參考下面bind方法源碼】,在get()等方法中用到,參考下面方法解釋。
private ObservableIntegerValue observable = null;
//失效監聽器,進行bind()時,則自動構建該監聽器。
//作用:【參考下面Listener源碼】作為InvalidationListener添加到綁定目標observable中,實現當observable改變時,將本實例設置為Invalid(失效)狀態的效果。
//創建時機:【參考下面bind代碼】當使用bind()方法時,以自身實例(this)作為參數構建 Listener 對象【Listener 為內部類繼承自 InvalidationListener,參考下面代碼】
private InvalidationListener listener = null;
//當前實例是否有效,創建實例時默認為有效
private boolean valid = true;
//用於存儲添加的失效或改變監聽器
private ExpressionHelper<Number> helper = null;
// 獲取值的get方法,其邏輯為:若進行過綁定,則調用observable來獲取綁定值;若未綁定,則返回本身的值
@Override
public int get() {
valid = true;
return observable == null ? value : observable.get();
}
// 根據observable是否為空判斷當前是否綁定
@Override
public boolean isBound() {
return observable != null;
}
// 解綁方法
@Override
public void unbind() {
if (observable != null) {
value = observable.get(); //解綁時將自身值更新到最新狀態
observable.removeListener(listener); //移除失效監聽器【關於"失效監聽器"參考下面源碼解釋】
observable = null; //將綁定目標observable置null
}
}
//綁定方法
@Override
public void bind(final ObservableValue<? extends Number> rawObservable){
ObservableIntegerValue newObservable;
// …省略newObservable的構建代碼。邏輯為:若傳入對象是ObservableIntegerValue類型實例則為傳入對象本身;否則則以傳入對象為基礎構建IntegerBinding實例)
if (!newObservable.equals(observable)) {
unbind(); //先解綁,若本身未綁定則等於沒執行
observable = newObservable; //為綁定目標字段賦值
if (listener == null) {
listener = new Listener(this); //以自身實例為參數構建Listener失效監聽器
}
observable.addListener(listener);//將失效監聽器添加到綁定目標中
markInvalid(); //設置為失效狀態(因為延遲計算機制)
}
}
//標記為失效的方法
private void markInvalid() {
if (valid) {
valid = false;
invalidated(); //默認為空實現,子類繼承時可自行重寫實現(SimpleIntegerProperty未重寫實現)
fireValueChangedEvent(); //激活hepler
}
}
//官方注釋:給所有注冊的InvalidationListener和ChangeListeners發送通知(即開始回調各個監聽器方法)
protected void fireValueChangedEvent() {
ExpressionHelper.fireValueChangedEvent(helper);
}
//屬性修改方法
//修改后會調用markInvalid()方法標記為失效,並回調所有附加的監聽器
@Override
public void set(int newValue) {
if (isBound()) {
throw new java.lang.RuntimeException((getBean() != null && getName() != null ?
getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set.");
}
if (value != newValue) {
value = newValue;
markInvalid();
}
}
//內部類 失效監聽器,在bind()方法中被使用
private static class Listener implements InvalidationListener {
private final WeakReference<IntegerPropertyBase> wref;
public Listener(IntegerPropertyBase ref) {
this.wref = new WeakReference<>(ref);
}
//失效監聽器邏輯很簡單,直接調用傳入實例的markInvalid方法
//即實現了:當綁定目標修改時,則回調該監聽器來將"綁定發起屬性"置為失效。
@Override
public void invalidated(Observable observable) {
IntegerPropertyBase ref = wref.get();
if (ref == null) {
observable.removeListener(this);
} else {
ref.markInvalid();
}
}
}
//…
}
參考
https://www.dummies.com/programming/java/javafx-binding-properties/
http://www.javafxchina.net/blog/2015/08/javafx-properties-binding/