Разработка

Subscriptions in viewmodel knockout.js

Правильно было бы оставить пару ссылок на предысторию этой статьи, но я обойдусь сухими выжимками кода и вольным переводом частей документации по Knockout.js.

Часто ли вам приходится связывать части своего приложения? Задумываетесь ли вы, насколько сильны эти связи, и что стоит позади тех привычных методов и классов, которые вы используете?

Рассмотрим пример, небольшой, но отражающий суть необходимости событий  и их роли в вашем веб-приложении. Итак, пусть у нас есть список именованых блоков. В каждом блоке есть название и галочка.  Должна быть реализована поддержка «выбрать/снять выделение со всех», а так же некая возможность изменить весь список.

Описание решения

Я создам две модели для одного блока и для всего приложения в целом. В модели блока будут 2 поля: имя и текущее состояние, а так же подписка на событие ‘поменять значение’. Модель страницы (всего списка) содержит одну галочку для возможности выбрать все, то самое событие будет подпиской на состояние этой галочки. Плюс кнопка, которая обновит список элементов.

Вот моя модель одного элемента. Тут используются две js-библиотеки knockout для двусторонней связи модели и представления и amplify для реализации механизма  событий и подписок.

function item(settings){
    var name = settings.name;
    var selected = ko.observable(settings.selected || false);
    
    amplify.subscribe(consts.events.setState, updateState);
    
    function updateState(state) {
        selected(state);
        console.log('Subscribe in ', name);
    }
    
    return {
        name: name,
        selected: selected                  
    };
}

В коде можно видеть ссылку на константу. Буду оставаться честным, вот объявление внешних констант.

var consts = {
    events:{
        setState: 'items.state.set'
    }
};

Теперь поговорим о модели, которая внутри будет оперировать описанными элементами.

function list(){
    var items = ko.observableArray();
   
    var selectAll = ko.observable(false);
    selectAll.subscribe(function (newVal) {
        amplify.publish(consts.events.setState, newVal);
    });
    
    function change () {                    
        items([]);
        items.push(item({name:'Item 4', selected:false}));
    }
    
    items.push(item({name:'Item 1', selected:true}));
    items.push(item({name:'Item 2', selected:false}));
    items.push(item({name:'Item 3', selected:false}));
    
    return {
        items:items,
        selectAll:selectAll,
        change:change
    };
}

В заключение всем хочется видеть разметку 🙂

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
    
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Ko items list</title>
    <link rel="stylesheet" href="../vendor/css/bootstrap.css">
    <link rel="stylesheet" href="../vendor/css/bootstrap-theme.css">
    <script type="text/javascript" src="../vendor/js/jquery.js"></script>
    <script type="text/javascript" src="../vendor/js/bootstrap.js"></script>
    <script type="text/javascript" src="../vendor/js/amplify.js"></script>
    <script type="text/javascript" src="../vendor/js/knockout.js"></script>
</head>
<body>
    <div class="checkbox">
        <label>
            <input type="checkbox" data-bind="checked:selectAll" />Select all</label>
    </div>
    <div data-bind="foreach:items">
        <div class="checkbox">
            <label>
                <input type="checkbox" data-bind="checked:selected" />
                <span data-bind="text:name"></span>
            </label>
        </div>
    </div>
    <input type="button" data-bind="click:change" value="Change list" />

    <script type="text/javascript">
        var model = list();        
        ko.applyBindings(model);
    </script>
</body>
</html>

Вот и всё решение. Теперь давайте рассмотрим, что же происходит в момент срабатывания события, какие элементы обрабатывают его.

    console.log('Subscribe in ', name);

Что выведет это логирование при нажатии на «Select all» при запуске приложения и после изменения списка?

index.html:44 Subscribe in  Item 1
index.html:44 Subscribe in  Item 2
index.html:44 Subscribe in  Item 3

Пока всё логично и верно. Но вот после изменения списка мы увидим следующую картину.

index.html:44 Subscribe in  Item 1
index.html:44 Subscribe in  Item 2
index.html:44 Subscribe in  Item 3
index.html:44 Subscribe in  Item 4

Дальнейшие изменения списка будут только пополнять этот список новыми элементами с номером 4.

index.html:44 Subscribe in  Item 1
index.html:44 Subscribe in  Item 2
index.html:44 Subscribe in  Item 3
2х index.html:44 Subscribe in  Item 4

Как избежать такого поведения? Что предпринять и как обезопасить своё приложение от ложных действий?

Избежать просто, необходимо следить за всеми подписками, computed переменными и методами вызываемыми через setInterval.

Разработчики knockout позаботились о нас и с введением в 3 версии компонент добавили поддержку метода dispose у модели. К сожалению, для обычной модели и представления, не обёрнутых в компоненты, такого выполнить не удастся, и решением здесь будет только самостоятельный контроль перед удалением.

function item(settings){
    var name = settings.name;
    var selected = ko.observable(settings.selected || false);
    
    amplify.subscribe(consts.events.setState, updateState);
    
    function updateState(state) {
        selected(state);
        console.log('Subscribe in ', name);
    }
    
    function dispose(){        
        amplify.unsubscribe(consts.events.setState, updateState);
    }
    return {
        name: name,
        selected: selected,
        dispose: dispose                  
    };
}

Вот пример как можно выполнить отписку от событий в нужное для нас время.

function change () {      
    _.each(items(), function(item){
        item.dispose();
    });
                  
    items([]);
    items.push(item({name:'Item 4', selected:false}));
}

Для компонент, повторюсь, это будет выполнено автоматически при удалении элемента из разметки. Вам достаточно вынести метод dispose в интерфейс Вашей модели.

Исходный код всего примера тут

 

Leave a Reply

Your email address will not be published. Required fields are marked *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.