LoginSignup
0
2

More than 5 years have passed since last update.

knockoutのcomputed property

Posted at
この記事は、knockout.js Advent Calendar 2015の7日目の記事です。 先に6日目に目を通すことを推奨しています。
knockout , knockout-es5 , knockout.punches環境を想定しています。

knockoutでも、あるpropertyに依存した別のpropertyを定義することができます。
例えば、商品の小計を考えてみましょう。
ここでの小計は単価 × 数量とします。
つまり、小計単価数量に依存しています。

これをバニラのjavascriptで書くと、

javascript
function Order(){
  this.unitPrice=150;
  this.quantity=3;
  Object.defineProperty(this,'price',{
    get:function(){
      return this.unitPrice * this.quantity;
    }
  });
}

このようになります。
これについての説明は、MDNなどを参照するといいです。

では、knockoutではどう書けばいいんでしょうか?

knockoutもまったく同じでOKです。

もちろん、このpropertyにさらに依存したpropertyも定義できます。

なお、knockout es5では、knockout用にカスタムしたko.definePropertyという機構も提供しています。
Object.definePropertyではなく、あえてko.definePropertyを採用する利点は、
Object.definePropertyの場合は、propertyへアクセスする度に値が再計算されるのに対して、
ko.definePropertyは、依存先の値が変化するまでは、計算された値をキャッシュとして保持することで、余計な再計算を防ぐことができます。
複雑な計算や、膨大な計算量がある場合には、ko.definePropertyを使うべきでしょう。


では、これまでの復習とより実践的な例として、次のようなものを考えてみます。

とあるFruitShopの商品注文を想定します。このShopでは、下記の要件をクリアする必要があります。

  • Shopでは複数のItem(商品)を扱っている。
  • Itemは、name(商品名)とprice(単価)の情報を持っている。
  • Shopでは、1つのOrder(注文)を受け付けている。
  • Orderは、複数のOrderDetail(注文内訳)を持っていて、そのtotalPrice(合計価格)の情報を持っている。
  • OrderDetailは Itemとquantity(個数)とprice(小計)の情報を持っている。

これらを実現するサンプルがこちらです。

index.html
<h1>Fruit Shopping Order</h1>
<div>商品リスト:
    <table>
        <thead>
            <tr>
                <th>商品名</th>
                <th>単価</th>
                <th>注文</th>
            </tr>
        </thead>
        <tbody data-bind="foreach:items">
            <tr>
                <td>{{name}}</td>
                <td>{{price}}</td>
                <td>
                    <button type="button" data-bind="click:$parent.order.add">注文</button>
                </td>
            </tr>
        </tbody>
    </table>
</div>
<br>
<div>注文票:
    <table data-bind="with:order">
        <thead>
            <tr>
                <th>商品名</th>
                <th>単価</th>
                <th>数量</th>
                <th>小計</th>
                <th></th>
            </tr>
        </thead>
        <tbody data-bind="foreach:details">
            <tr>
                <td>{{item.name}}</td>
                <td>{{item.price}}</td>
                <td><input type="number" data-bind="value:quantity" min="1" style="width:3em;" /></td>
                <td>{{price}}</td>
                <td><button type="button" data-bind="on.click:$parent.details.splice($index(),1)">キャンセル</button></td>
            </tr>
        </tbody>
        <tbody data-bind="if:!details.length">
            <tr>
                <td colspan="5" style="text-align:center">Empty</td>
            </tr>
        </tbody>
        <tfooter>
            <tr>
                <td colspan="3">合計</td>
                <td>{{totalPrice}}</td>
            </tr>
        </tfooter>
    </table>
</div>
script.js
function Item(name,price){
    this.name = name;
    this.price = price;
    ko.track(this);
}
function Shop(){
    this.items = [
        new Item("apple",60),
        new Item("banana",25),
        new Item("cinnamon",80),
        new Item("dragonfruit",120)
    ];
    this.order = new Order();
    ko.track(this);
}
function OrderDetail(item,quantity){
    this.item=item;
    this.quantity=quantity;
    ko.track(this);
    Object.defineProperty(this,'price',{get:function(){
        return this.item.price * this.quantity
    }});
}
function Order(){
    this.details = [];
    ko.track(this);
    Object.defineProperty(this,'totalPrice',{get:function(){
        var total = 0;
        this.details.forEach(function(detail){
            total += detail.price;
        });
        return total;
    }});
}
Order.prototype.add=function(item,event){
    var quantity = 1;
    this.details.push(new OrderDetail(item,quantity));
}

var vm = new Shop();

ko.punches.enableAll();
ko.applyBindings(vm);

蛇足ですが、そろそろ型チェックが恋しくなってきたのではないでしょうか? ぜひTypeScriptで書きましょう。

script.ts
class Item{
    constructor(
        public name:string,
        public price:number
    ){
        ko.track(this);
    }
}
class Shop{
    public items:Item[]=[];
    public order=new Order();
    constructor(){
        this.items = [
            new Item("apple",60),
            new Item("banana",25),
            new Item("cinnamon",80),
            new Item("dragonfruit",120)
        ];
        ko.track(this);
    }
}
class OrderDetail{
    public price:number;
    constructor(
        public item:Item,
        public quantity:number
    ){
        ko.track(this);
    }
    public get price():number{
        return this.item.price * this.quantity
    }
}
class Order{
    public totalPrice:number;
    public details:OrderDetail[] = [];
    constructor(){
        ko.track(this);
    }
    public get totalPrice():number{
        var total = 0;
        this.details.forEach((detail)=>{
            total += detail.price;
        });
        return total;
    }
    public add(item:Item,event){
        var quantity = 1;
        this.details.push(new OrderDetail(item,quantity));
    }
}

var vm = new Shop();

ko.punches.enableAll();
ko.applyBindings(vm);
0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2