Variable Dependency with knockoutJS

2019-07-23 11:25发布

问题:

I'm building an application with KnockoutJS with a component that essentially acts as a sequential spreadsheet. On different lines users may define variables or use them to represent a value.

So for example

x =2 
x  //2
x = 4
x  //4

I have this working in the straightforward case of continuing adding new lines. The output function for each line checks and iterates backwards to see if the variable was ever defined previously. If it was it uses the first example it finds and sets that as the value. This works when initially defining the lines, and also works when you edit a line after a previous line has changed.

However, I would like variables to update if a previous definition of that variable has changed, been removed, or been added. That behavior does not exist right now. I have tried adding my own custom dependency handling code using a map to track the variables, but it badly impacted performance. I would like to tap into Knockouts dependency management to solve this, but I'm not sure of the best way to do so. Here is a brief summary of my code structure, I would be happy to add more detail if needed.

calcFramework is the view-model object I bind to the map. It consists of an observable list of Lines, a varMap, and other unrelated properties and functions

Line is a custom object. The relevant code is below

var Line = function (linenum,currline) {
    var self = this;
    self.varMap = {};
    self.input = ko.observable("");
    self.linenum = ko.observable(linenum); 

    self.lnOutput = ko.computed({
        read:function(){
           return outputFunction(self,self.input());
        },
        write:function(){},
        owner:self
    });
};

function  outputFunction(self,input) {
    try{
        var out = EQParser.parse(input,10,self);
        return  out.toString();
    }
    catch(ex){
        //error handling
    }
}

Line.prototype.getVar = function (varName, notCurrentLine) {
    if(typeof varName === "undefined"){
        return null;
    }
    //Actually don't want ones set in the current varMap, only past lines
    if(varName in this.varMap && notCurrentLine){
        return this.varMap[varName];
    }

    if (this.linenum() > 0) {
        var nextLine = calcFramework.lines()[this.linenum() - 1];
        return nextLine.getVar(varName,true);
    } else {
        //eventually go to global
        return calcFramework.varMap[varName];
    }
};

Line.prototype.setVar = function(varName,value){
    this.varMap[varName] = value;
};

SetVar and getVar are passed to eqParser, which gets the value of the expression, calling those functions as needed if a variable is referenced. So the variable value is not explicitly passed to the function and thus knockout does not view it as a dependency. But I'm not sure how I would pass the variable as a parameter without traversing the list every time.

So my question is, given this setup, what is the best way to track changes to a variable assignment (and/or new assignments) and update the lines that reference that variable, while maintaining good performance.

I recognize my question is lengthy and I have attempted to trim out all unnecessary detail. Thanks for your patience in reading.

回答1:

I would be tempted to use a publish/subscribe model, using something like Peter Higgins' PubSub jquery plugin

Your overall app would subscribe/listen out for lines publishing an event that they have a variable definition. This would store any variable names in a standard javascript hashtable, along with the value. When a variable found event is published by a line, the app would check through all the known variables, and if it finds that it is a change to an existing variable value, it would publish a variable changed event. All the lines would subscribe to that event. They can then check whether they have a variable matching that name, and update the value accordingly.

Here's some untested code to give you an idea of what I mean:

var app = function()
{
    var self = this;

    self.variables = {};
    $.subscribe('/variableAssigned', function (key, value)
        {
            // I think that this is the best way of checking that there is a variable
            // in the object
            if(self.variables.hasOwnProperty(key))
            {
                if(self.variables[key] !== value)
                {
                    $.publish('/variableChanged', [ key, value ]);
                }
            }
        });
}

In your Line object:

$.subscribe('/variableChanged', function (key, value)
    {
        // loop through varMap and see if any of them need updating.
    });