Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
2
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

@MKGaru

knockoutのcomputed property

この記事は、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);
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
2
Help us understand the problem. What are the problem?