6

我经常发现自己遇到问题,即控件的两个(相关)值被更新,并且两者都会触发昂贵的操作,或者控件可能会暂时处于不一致的状态。

例如,考虑一个数据绑定,其中两个值 (x,y) 彼此相减,最终结果用作某个其他属性 z 的除数:

z / (x - y)

如果 x 和 y 绑定到某个外部值,那么一次更新它们可能会导致意外除以零错误,这取决于首先更新哪个属性以及另一个属性的旧值是什么。更新属性 z 的代码只是监听 x 和 y 的变化——它无法提前知道另一个属性的另一个更新即将到来。

现在这个问题很容易避免,但还有其他类似的情况,比如设置宽度和高度......我是立即调整窗口大小还是等待另一个更改?我是立即为指定的宽度和高度分配内存还是等待?如果宽度和高度是 1 和 100 万,然后更新为 100 万和 1,那么暂时我将有 100 万 x 100 万的宽度和高度......

这可能是一个相当普遍的问题,尽管对我来说它特别适用于 JavaFX 绑定。我感兴趣的是如何处理这些情况,而不会遇到未定义的行为或执行一旦另一个绑定更改就需要重做的昂贵操作。

到目前为止,为了避免这些情况,我所做的事情是在设置新值之前首先清除与已知值的绑定,但这对更新绑定的代码来说是一个负担,它确实不需要知道。

4

3 回答 3

1

我现在才学习 JavaFX,所以对这个答案持保留态度......欢迎任何更正。我对此很感兴趣,所以做了一些研究。

失效监听器

这个问题的答案部分是InvalidationListener. 您可以在此处详细阅读文档,但本质是 aChangeLister会立即传播更改,而InvalidationListenera 会注意到值无效但将计算推迟到需要时。基于“z / (x - y)”计算的两种情况的示例:

首先,琐碎的东西:

import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.NumberBinding;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableNumberValue;
import javafx.beans.value.ObservableValue;

public class LazyExample
{
    public static void main(String[] args) {
        changeListenerCase();
        System.out.println("\n=====================================\n");
        invalidationListenerCase();
    }
    ...
}

2 种情况(更改和失效侦听器)将设置 3 个变量,x, y, z,计算表达式z / (x - y)和适当的侦听器。然后他们调用一个manipulate()方法来改变值。记录所有步骤:

    public static void changeListenerCase() {
        SimpleDoubleProperty x = new SimpleDoubleProperty(1);
        SimpleDoubleProperty y = new SimpleDoubleProperty(2);
        SimpleDoubleProperty z = new SimpleDoubleProperty(3);

        NumberBinding nb = makeComputed(x,y,z);

        nb.addListener(new ChangeListener<Number>() {
            @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                System.out.println("ChangeListener: " + oldValue + " -> " + newValue);
            }
        });

        // prints 3 times, each after modification
        manipulate(x,y,z);

        System.out.println("The result after changes with a change listener is: " + nb.doubleValue());
    }

    public static void invalidationListenerCase() {
        SimpleDoubleProperty x = new SimpleDoubleProperty(1);
        SimpleDoubleProperty y = new SimpleDoubleProperty(2);
        SimpleDoubleProperty z = new SimpleDoubleProperty(3);

        NumberBinding nb = makeComputed(x,y,z);

        nb.addListener(new InvalidationListener() {
            @Override public void invalidated(Observable observable) {
                System.out.println("Invalidated");
            }
        });

        // will print only once, when the result is first invalidated
        // note that the result is NOT calculated until it is actually requested
        manipulate(x,y,z);

        System.out.println("The result after changes with an invalidation listener is: " + nb.doubleValue());
    }

以及常用的方法:

    private static NumberBinding makeComputed(final ObservableNumberValue x, final ObservableNumberValue y, final ObservableNumberValue z) {
        return new DoubleBinding() {
            {
                bind(x,y,z);
            }
            @Override protected double computeValue() {
                System.out.println("...CALCULATING...");
                return z.doubleValue() / (x.doubleValue()-y.doubleValue());
            }
        };
    }

    private static void manipulate(SimpleDoubleProperty x, SimpleDoubleProperty y, SimpleDoubleProperty z) {
        System.out.println("Changing z...");
        z.set(13);
        System.out.println("Changing y...");
        y.set(1);
        System.out.println("Changing x...");
        x.set(2);
    }

输出是:

...CALCULATING...
Changing z...
...CALCULATING...
ChangeListener: -3.0 -> -13.0
Changing y...
...CALCULATING...
ChangeListener: -13.0 -> Infinity
Changing x...
...CALCULATING...
ChangeListener: Infinity -> 13.0
The result after changes with a change listener is: 13.0

=====================================

...CALCULATING...
Changing z...
Invalidated
Changing y...
Changing x...
...CALCULATING...
The result after changes with an invalidation listener is: 13.0

所以在第一种情况下有过多的计算和一个infinity案例。在第二种情况下,数据在第一次更改时被标记为无效,然后仅在需要时重新计算。

脉搏

绑定图形属性怎么样,例如某物的宽度和高度(如您的示例中)?JavaFX 的基础结构似乎不会立即将更改应用于图形属性,而是根据称为Pulse的信号。脉冲是异步调度的,在执行时,将根据节点属性的当前状态更新 UI。动画中的每一帧和 UI 属性的每次更改都会安排一个脉冲运行。

我不知道在您的示例情况下会发生什么,初始宽度 = 1 像素和高度 = 10 6像素,代码设置宽度 = 10 6像素(一步,调度脉冲),然后高度 = 1 像素(第二步)。如果第一步尚未处理,第二步是否会发出另一个脉冲?从 JavaFX 的角度来看,合理的做法是让管道只处理 1 个脉冲事件,但我需要一些参考。但是,即使处理了两个事件,第一个事件也应该处理整个状态变化(宽度和高度),因此变化发生在一个视觉步骤中。

开发人员必须考虑我相信的架构。假设有一个单独的任务(伪代码):

width = lengthyComputation();
Platform.runLater(node.setWidth(width));
height = anotherLengthyComputation();
Platform.runLater(node.setHeight(height));

如果第一个脉冲事件有机会运行,那么用户会看到宽度的变化 - 暂停 - 高度的变化。最好把它写成(同样,总是在后台任务中)(伪代码):

width = lengthyComputation();
height = anotherLengthyComputation();
Platform.runLater(node.setWidth(width));
Platform.runLater(node.setHeight(height));

更新(来自 john16384 的评论):据此,不可能直接听脉冲。但是,可以扩展某些方法,javafx.scene.Parent每个脉冲运行一次并达到相同的效果。因此layoutChildren(),如果不需要对子树进行更改,或者扩展computePrefHeight(double width)/ computePrefWidth(double height),如果子树将被修改,则您可以扩展。

于 2013-11-05T11:08:56.370 回答
0

根据我之前的回答,我进一步讨论了一些问题,我想出了一些我认为可以解决问题的代码。它确实需要改进,因为它只是为了展示这些原则。

不需要很长时间的昂贵手术

这将是内存分配的情况。它不会(?)花费很长时间,但它很昂贵,因为您不希望发生过多的分配。响应尺寸变化运行的代码在主线程中运行(这可能不正确,请纠正我)。

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.ObjectBinding;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Dimension2D;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class ExpensiveQuickCalculationExample extends Application
{
    @Override
    public void start(final Stage primaryStage) {
        primaryStage.setTitle("Lazy Example");

        Button btn = new Button();
        btn.setText("Manipulate");
        btn.setOnAction(new EventHandler<ActionEvent>() {
            @Override public void handle(ActionEvent event) {
                //////////////////////////////////
                // DIMENSIONS MANIPULATION HERE //
                //////////////////////////////////
                primaryStage.setHeight(150);
                primaryStage.setWidth(150);
            }
        });

        StackPane root = new StackPane();
        root.getChildren().add(btn);
        primaryStage.setScene(new Scene(root, 300, 250));
        primaryStage.show();


        final ObjectBinding<Dimension2D> stageDimBinding = makeWindowDimensionsBinding(primaryStage);
        stageDimBinding.addListener(new InvalidationListener() {
            @Override public void invalidated(Observable observable) {
                System.out.println("---> Dimensions INVALIDATED");
                Platform.runLater(new Runnable() {
                    @Override public void run() {
                        expensiveQuickCalculation(stageDimBinding.get());
                    }
                });
            }
        });
    }

    private ObjectBinding<Dimension2D> makeWindowDimensionsBinding(final Stage stage) {
        return new ObjectBinding<Dimension2D>() {
            {
                bind(stage.widthProperty(), stage.heightProperty());
            }
            @Override
            protected Dimension2D computeValue() {
                System.out.println("Dimensions computed");
                return new Dimension2D(stage.widthProperty().doubleValue(), stage.heightProperty().doubleValue());
            }
        };
    }

    private void expensiveQuickCalculation(Dimension2D d) {
        System.out.println("-=< EXPENSIVE CALCULATION >=-");
    }

    public static void main(String[] args) {
        launch(args);
    }
}

昂贵且缓慢的操作

与上述情况相比,操作速度较慢。我们不想阻塞 UI 线程,所以我们使用javafx.concurrent.Service. 缓慢的操作是用 模拟的Thread.sleep(),所以请注意控制台并耐心等待:)

import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Dimension2D;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class ExpensiveSlowCalculationExample extends Application
{
    private static class SlowCalculationTask extends Task<Void>
    {
        private Dimension2D dimension;

        public SlowCalculationTask(Dimension2D dimension) {
            this.dimension = dimension;
        }

        @Override
        protected Void call() throws Exception {
            expensiveSlowCalculation();
            if( !isCancelled() ) {
                /////////////////////////////////////
                //        UPDATE STATE HERE        //
                // I would use Platform.runLater() //
                /////////////////////////////////////
                System.out.println("-=< UPDATING STATE >=-");
            }
            return null;
        }

        private void expensiveSlowCalculation() {
            System.out.println("-=< EXPENSIVE SLOW CALCULATION STARTED " + dimension + ">=-");
            try {
                Thread.sleep(5000);
                System.out.println("-=< EXPENSIVE SLOW CALCULATION DONE >=-");
            }
            catch (InterruptedException e) {
                if( isCancelled() ) System.out.println("-=< EXPENSIVE SLOW CALCULATION *CANCELLED* >=-");
                else throw new RuntimeException(e);
            }
        }
    }

    private static class SlowCalculationService extends Service<Void>
    {
        private ObjectProperty<Dimension2D> dimensions = new SimpleObjectProperty<>();
        public void setDimensions(Dimension2D dimensions) { this.dimensions.set(dimensions); }

        @Override
        protected Task<Void> createTask() {
            return new SlowCalculationTask(dimensions.get());
        }
    }



    private SlowCalculationService calculationService;


    @Override
    public void start(final Stage primaryStage) {
        primaryStage.setTitle("Lazy Example");

        Button btn = new Button();
        btn.setText("Manipulate");
        btn.setOnAction(new EventHandler<ActionEvent>() {
            @Override public void handle(ActionEvent event) {
                //////////////////////////////////
                // DIMENSIONS MANIPULATION HERE //
                //////////////////////////////////
                primaryStage.setHeight(150);
                primaryStage.setWidth(150);
            }
        });

        StackPane root = new StackPane();
        root.getChildren().add(btn);
        primaryStage.setScene(new Scene(root, 300, 250));
        primaryStage.show();


        final ObjectBinding<Dimension2D> stageDimBinding = makeWindowDimensionsBinding(primaryStage);
        stageDimBinding.addListener(new InvalidationListener() {
            @Override public void invalidated(Observable observable) {
                System.out.println("---> Dimensions INVALIDATED");
                startService(stageDimBinding.get());
            }
        });
    }

    private ObjectBinding<Dimension2D> makeWindowDimensionsBinding(final Stage stage) {
        return new ObjectBinding<Dimension2D>() {
            {
                bind(stage.widthProperty(), stage.heightProperty());
            }
            @Override
            protected Dimension2D computeValue() {
                System.out.println("Dimensions computed");
                return new Dimension2D(stage.widthProperty().doubleValue(), stage.heightProperty().doubleValue());
            }
        };
    }

    private void startService(Dimension2D d) {
        if( calculationService == null ) calculationService = new SlowCalculationService();
        calculationService.setDimensions(d);
        calculationService.restart();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

测试

启动应用程序并按下按钮。尽管操作取决于宽度和高度,但消息“已启动昂贵的慢速计算”、“已完成昂贵的慢速计算”会出现一次。

然后手动调整窗口大小。您将不可避免地收到更多“昂贵的缓慢计算开始”消息。但是所有这些操作都将被取消,只有最后一个被允许完成。

于 2013-11-07T14:22:29.853 回答
0

这些案例表明数据绑定不能在任何地方使用。它基于事件,因此您最终会遇到带有竞争条件等的事件驱动架构的所有麻烦......

所以我想说:不要在副作用不可预测和致命的情况下使用它。例如,在您的情况下,坚持使用一种基本方法,该方法根据彼此和旧值设置两个属性,您可以在安全时间甚至定期调用旧值,甚至通过脏检查值。如果您可以同时执行这两个操作,则计算量很小,因此这应该不是问题。

数据绑定很棒,但并不适用于所有事情。它不是一种宗教(仍然是一个很好的工具)。不要过度使用它:这里应该应用简单而不是聪明,不要破解它,它很快就会变得无法调试。

于 2013-11-04T17:22:44.803 回答