從 SimpleIntegerProperty 看 Java屬性綁定(property binding) 與 觀察者模式(Observable)


//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)

允許同步兩個屬性的值,其中一個修改時,另一個屬性的獲取值會同步更新。
有兩個綁定方法 bindbinBidirectional 分別對應兩種綁定方式:

  • 單向綁定(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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM