深入淺出KnockoutJS


深入淺出KnockoutJS

  寫在前面,本文資料大多來源網上,屬於自己的學習筆記整理。

  其中主要內容來自learn.knockoutjs.com,源碼解析部分資料來自司徒正美博文《knockout.js學習筆記》系列。


 

1. Knockout初體驗

1.1 Before Knockout

  假設我們的頁面輸入區域有一個div用來展示一件物品的名字,同時有一個輸入框用來編輯這件物品的名字
<div id=”itemName”></div>
<input type=”text” id=”itemNameEdit”/>

  使用JQuery,上述交互邏輯可以如下實現

var item = {
    id: 88,
    name: "Apple Pie"
};
$("#itemName").text(item.name);
$("#itemNameEdit").val(item.name).change(function() {
    item.name = $(this).val();
    $("#itemName").text(item.name);
});

采用這種方式的缺點

  • 當UI和data的交互越來越多時,代碼量迅速增長到難以維護
  • •Dom Query Based
  • 上述代碼耦合度高,不可重用
  • Id、classname命名難以管理

1.2 Use Knockout

  HTML View如下
<div data-bind=”text:name”></div>
<input type=”text” data-bind=”value:name”/>

  Javascript如下

function ViewModel={
    this.id=88;
    this.name=ko.observable(“Apple”);
};
ko.applyBindings(new ViewModel());

現在,當輸入框中值發生變化時,div中顯示的值也會自動發送變化


 

2.  Knockout基礎

2.1 MVVM模式

  Knockoutjs遵循Model(M)—View(V)—ViewModel(VM)模式

2.2 單次綁定

  從ViewModel綁定至UI這一層只進行一次綁定,不追蹤數據在任何一方的變化,適用於數據展現

  Javascript與Html示例如下

function AppViewModel() {
    this.firstName = "Bert";
    this.lastName = "Bertington";
}
ko.applyBindings(new AppViewModel());
<p>First name: <strong data-bind="text: firstName"></strong></p>
<p>Last name: <strong data-bind="text: lastName"></strong></p>

  效果如下圖所示

 2.3 雙向綁定

  無論數據在ViewModel或者是UI中變化,將會更新另一方,最為靈活的綁定方式,同時代價最大

function AppViewModel() {
    this.firstName = ko.observable("Bert");
    this.lastName = ko.observable("Bertington");
}
ko.applyBindings(new AppViewModel());
<p>First name: <strong data-bind="text: firstName"></strong></p>
<p>Last name: <strong data-bind="text: lastName"></strong></p>
<p>First name: <input data-bind="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>

  上述綁定,當輸入框中值發生改變時,<p>標簽中顯示內容相應發生改變

2.4 依賴綁定

  以其它observable的值為基礎來組成新的值,新值也是雙向綁定的

function AppViewModel() {
    this.firstName = ko.observable("Bert");
    this.lastName = ko.observable("Bertington");

    this.fullName = ko.computed(function() {
        return this.firstName() + " " + this.lastName();    
    }, this);
}
ko.applyBindings(new AppViewModel());
<p>First name: <strong data-bind="text: firstName"></strong></p>
<p>Last name: <strong data-bind="text: lastName"></strong></p>
<p>First name: <input data-bind="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>
<p>Full name: <strong data-bind="text: fullName"></strong></p>

上述代碼示例中,fullName依賴於firstName和lastName,改變firstName和lastName任意值,fullName的顯示也相應改變

2.5 綁定數組

  可以為屬性綁定集合

// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
    var self = this;
    self.name = name;
    self.meal = ko.observable(initialMeal);
}

// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
    var self = this;

    // Non-editable catalog data - would come from the server
    self.availableMeals = [
        { mealName: "Standard (sandwich)", price: 0 },
        { mealName: "Premium (lobster)", price: 34.95 },
        { mealName: "Ultimate (whole zebra)", price: 290 }
    ];    

    // Editable data
    self.seats = ko.observableArray([
        new SeatReservation("Steve", self.availableMeals[0]),
        new SeatReservation("Bert", self.availableMeals[0])
    ]);
}

ko.applyBindings(new ReservationsViewModel());
<h2>Your seat reservations</h2>

<table>
    <thead><tr>
        <th>Passenger name</th><th>Meal</th><th>Surcharge</th><th></th>
    </tr></thead>
    <tbody data-bind="foreach: seats">
        <tr>
            <td data-bind="text: name"></td>
            <td data-bind="text: meal().mealName"></td>
            <td data-bind="text: meal().price"></td>
        </tr>    
    </tbody>
</table>

  上述代碼將seats對象綁定了一個集合對象,在html view中,通過foreach指令渲染視圖,效果如下下圖

2.6 增加添加和刪除元素功能

// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
    var self = this;
    self.name = name;
    self.meal = ko.observable(initialMeal);

    self.formattedPrice = ko.computed(function() {
        var price = self.meal().price;
        return price;        
    });    
}

// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
    var self = this;

    // Non-editable catalog data - would come from the server
    self.availableMeals = [
        { mealName: "Standard (sandwich)", price: 0 },
        { mealName: "Premium (lobster)", price: 34.95 },
        { mealName: "Ultimate (whole zebra)", price: 290 }
    ];    

    // Editable data
    self.seats = ko.observableArray([
        new SeatReservation("Steve", self.availableMeals[0]),
        new SeatReservation("Bert", self.availableMeals[0])
    ]);  

    // Operations
    self.addSeat = function() {
        self.seats.push(new SeatReservation("", self.availableMeals[0]));
    }
    self.removeSeat = function(seat) { self.seats.remove(seat) }
}

ko.applyBindings(new ReservationsViewModel());
<h2>Your seat reservations</h2>

<table>
    <thead><tr>
        <th>Passenger name</th><th>Meal</th><th>Surcharge</th><th></th>
    </tr></thead>
    <tbody data-bind="foreach: seats">
        <tr>
            <td><input data-bind="value: name" /></td>
            <td><select data-bind="options: $root.availableMeals, value: meal, optionsText: 'mealName'"></select></td>
            <td data-bind="text: formattedPrice"></td>
            <td><a href="#" data-bind="click: $root.removeSeat">Remove</a></td>
        </tr>    
    </tbody>
</table>

<button data-bind="click: addSeat">Reserve another seat</button>
  •   上述代碼中,為viewmodel添加了addSeat和removeSeat方法。
  •   調用addSeat方法時,為seats集合添加一個初始化SeatReservation對象
  •   調用removeSeat方法時,knockout將當前dom元素綁定的seat對象作為參賽傳入到方法中

  效果如圖

  更多綁定可訪問官網文檔, http :// knockoutjs.com/documentation/introduction.html

3. Knockout進階

3.1 Custom bindings

  •   Binding連接view和viewmodel,除了內置bindings,你可以創建自己的binding

  •   將待注冊的綁定,添加為ko.bindingHandlers的屬性,然后可以在任意dom元素中使用它
ko.bindingHandlers.yourBindingName = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        // This will be called when the binding is first applied to an element
        // Set up any initial state, event handlers, etc. here
    },
    update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        // This will be called once when the binding is first applied to an element,
        // and again whenever any observables/computeds that are accessed change
        // Update the DOM element based on the supplied values here.
    }
};
<div data-bind="yourBindingName: someValue"> </div>

  custom binding示例

// ----------------------------------------------------------------------------
// Reusable bindings - ideally kept in a separate file

ko.bindingHandlers.fadeVisible = {
    init: function(element, valueAccessor) {
        // Start visible/invisible according to initial value
        var shouldDisplay = valueAccessor();
        $(element).toggle(shouldDisplay);
    },
    update: function(element, valueAccessor) {
        // On update, fade in/out
        var shouldDisplay = valueAccessor();
        shouldDisplay ? $(element).fadeIn() : $(element).fadeOut();
    } 
};

// ----------------------------------------------------------------------------
// Page viewmodel

function Answer(text) { this.answerText = text; this.points = ko.observable(1); }

function SurveyViewModel(question, pointsBudget, answers) {
    this.question = question;
    this.pointsBudget = pointsBudget;
    this.answers = $.map(answers, function(text) { return new Answer(text) });
    this.save = function() { alert('To do') };
                   
    this.pointsUsed = ko.computed(function() {
        var total = 0;
        for (var i = 0; i < this.answers.length; i++)
            total += this.answers[i].points();
        return total;        
    }, this);
}

ko.applyBindings(new SurveyViewModel("Which factors affect your technology choices?", 10, [
   "Functionality, compatibility, pricing - all that boring stuff",
   "How often it is mentioned on Hacker News",    
   "Number of gradients/dropshadows on project homepage",        
   "Totally believable testimonials on project homepage"
]));
<h3 data-bind="text: question"></h3>
<p>Please distribute <b data-bind="text: pointsBudget"></b> points between the following options.</p>

<table>
    <thead><tr><th>Option</th><th>Importance</th></tr></thead>
    <tbody data-bind="foreach: answers">
        <tr>
            <td data-bind="text: answerText"></td>
            <td><select data-bind="options: [1,2,3,4,5], value: points"></select></td>
        </tr>    
    </tbody>
</table>

<h3 data-bind="fadeVisible: pointsUsed() > pointsBudget">You've used too many points! Please remove some.</h3>
<p>You've got <b data-bind="text: pointsBudget - pointsUsed()"></b> points left to use.</p>
<button data-bind="enable: pointsUsed() <= pointsBudget, click: save">Finished</button>

  上述代碼定義了一個fadeVisible綁定,用來控制元素顯示動畫效果。init方法根據dom元素傳入參數當前狀態設置初始顯示效果;update方法在pointsUsed 每次發生更新時觸發,更新元素顯示效果 

3.2 Template binding

  模板綁定用模板的渲染結果來填充關聯的DOM元素,構建復制UI架構、可復用、可嵌套
  knockout 支持兩種類型模板
  •   Native templating:內置,用於加強控制流程的綁定
  •   String-based templating:集成第三方模板引擎的方式,原理是將model value傳遞給第三方模板引擎,將結果字符串注入到當前document

  Native templating示例

<h2>Participants</h2>
Here are the participants:
<div data-bind="template: { name: 'person-template', data: buyer }"></div>
<div data-bind="template: { name: 'person-template', data: seller }"></div>
 
<script type="text/html" id="person-template">
    <h3 data-bind="text: name"></h3>
    <p>Credits: <span data-bind="text: credits"></span></p>
</script>
 
<script type="text/javascript">
     function MyViewModel() {
         this.buyer = { name: 'Franklin', credits: 250 };
         this.seller = { name: 'Mario', credits: 5800 };
     }
     ko.applyBindings(new MyViewModel());
</script>

 3.3 Components and Custom Elements

  組件是將UI代碼組織成可復用模塊的方法

  使用ko.components.register方法注冊組件,組件定義包含viewModel和template

ko.components.register('some-component-name', {
    viewModel: <see below>,
    template: <see below>
});

  一個like/dislike組件示例

ko.components.register('like-widget', {
    viewModel: function(params) {
        // Data: value is either null, 'like', or 'dislike'
        this.chosenValue = params.value;
         
        // Behaviors
        this.like = function() { this.chosenValue('like'); }.bind(this);
        this.dislike = function() { this.chosenValue('dislike'); }.bind(this);
    },
    template:
        '<div class="like-or-dislike" data-bind="visible: !chosenValue()">\
            <button data-bind="click: like">Like it</button>\
            <button data-bind="click: dislike">Dislike it</button>\
        </div>\
        <div class="result" data-bind="visible: chosenValue">\
            You <strong data-bind="text: chosenValue"></strong> it\
        </div>'
});

function Product(name, rating) {
    this.name = name;
    this.userRating = ko.observable(rating || null);
}
 
function MyViewModel() {
    this.products = [
        new Product('Garlic bread'),
        new Product('Pain au chocolat'),
        new Product('Seagull spaghetti', 'like') // This one was already 'liked'
    ];
}
 
ko.applyBindings(new MyViewModel());
<ul data-bind="foreach: products">
    <li class="product">
        <strong data-bind="text: name"></strong>
        <like-widget params="value: userRating"></like-widget>
    </li>
</ul>

  viewModel中,為products單項綁定了一個Product集合,並為第三個Product對象userRating屬性設置為like

  html view中,使用like-widget指令使用上述定義的組件

  效果如下圖

4. Knockout實戰

4.1 knockout版todo app

  效果如下,在線體驗http://todomvc.com/examples/knockoutjs/
  觀察各項功能,可以對這一todo app做出如下分析
  •   需要一個todo對象作為 Model
  •   需要一個todos 的集合用來存儲各個todo對象
  •   需要filterTodos對象,根據All,Active,Completed過濾todos集合
  •   需要添加、刪除、編輯、清除等各種事件方法
  同時,為了良好體驗,還可以提供自定義綁定,提供對鍵盤快捷鍵的支持,如按下回車鍵時保存
  為了體驗的完整,還需要使用localstorage將todos存儲至本地
  

4.2 todo app主要代碼分析

  •   Todo Model,包含3 個屬性分別是title,completed,editing
// represent a single todo item
    var Todo = function (title, completed) {
        this.title = ko.observable(title);
        this.completed = ko.observable(completed);
        this.editing = ko.observable(false);
    };
  •   todos Array、filteredTodos
// map array of passed in todos to an observableArray of Todo objects
this.todos = ko.observableArray(todos.map(function (todo) {
    return new Todo(todo.title, todo.completed);
}));

// store the new todo value being entered
this.current = ko.observable();

this.showMode = ko.observable('all');

this.filteredTodos = ko.computed(function () {
    switch (this.showMode()) {
    case 'active':
        return this.todos().filter(function (todo) {
            return !todo.completed();
        });
    case 'completed':
        return this.todos().filter(function (todo) {
            return todo.completed();
        });
    default:
        return this.todos();
    }
}.bind(this));
  •   Events binding
  •   Custom binding

  提供了對鍵盤回車鍵ENTER_KEY、取消鍵ESCAPE_KEY的事件綁定

  當為dom元素綁定enter_key、escape_key事件時,會以當前dom元素作用域執行賦予的valueAccessor函數

 

  在selectAndFocus自定義綁定中,同時定義了init方法和update方法

  在init中為dom元素注冊了foucs方法,在update方法中來觸發元素的focus,其目的是為了在選中todo元素,可以立即進入可編輯的狀態

function keyhandlerBindingFactory(keyCode) {
    return {
        init: function (element, valueAccessor, allBindingsAccessor, data, bindingContext) {
            var wrappedHandler, newValueAccessor;

            // wrap the handler with a check for the enter key
            wrappedHandler = function (data, event) {
                if (event.keyCode === keyCode) {
                    valueAccessor().call(this, data, event);
                }
            };

            // create a valueAccessor with the options that we would want to pass to the event binding
            newValueAccessor = function () {
                return {
                    keyup: wrappedHandler
                };
            };

            // call the real event binding's init function
            ko.bindingHandlers.event.init(element, newValueAccessor, allBindingsAccessor, data, bindingContext);
        }
    };
}

// a custom binding to handle the enter key
ko.bindingHandlers.enterKey = keyhandlerBindingFactory(ENTER_KEY);

// another custom binding, this time to handle the escape key
ko.bindingHandlers.escapeKey = keyhandlerBindingFactory(ESCAPE_KEY);

// wrapper to hasFocus that also selects text and applies focus async
ko.bindingHandlers.selectAndFocus = {
    init: function (element, valueAccessor, allBindingsAccessor, bindingContext) {
        ko.bindingHandlers.hasFocus.init(element, valueAccessor, allBindingsAccessor, bindingContext);
        ko.utils.registerEventHandler(element, 'focus', function () {
            element.focus();
        });
    },
    update: function (element, valueAccessor) {
        ko.utils.unwrapObservable(valueAccessor()); // for dependency
        // ensure that element is visible before trying to focus
        setTimeout(function () {
            ko.bindingHandlers.hasFocus.update(element, valueAccessor);
        }, 0);
    }
};
  •   HTML View
<section id="todoapp">
    <header id="header">
        <h1>todos</h1>
        <input id="new-todo" data-bind="value: current,  enterKey: add" placeholder="What needs to be done?" autofocus>
    </header>
    <section id="main" data-bind="visible: todos().length">
        <input id="toggle-all" data-bind="checked: allCompleted" type="checkbox">
        <label for="toggle-all">Mark all as complete</label>
        <ul id="todo-list" data-bind="foreach: filteredTodos">
            <li data-bind="css: { completed: completed, editing: editing }">
                <div class="view">
                    <input class="toggle" data-bind="checked: completed" type="checkbox">
                    <label data-bind="text: title, event: { dblclick: $root.editItem }"></label>
                    <button class="destroy" data-bind="click: $root.remove"></button>
                </div>
                <input class="edit" data-bind="value: title,  enterKey: $root.saveEditing, 
                escapeKey: $root.cancelEditing, selectAndFocus:editing, event: { blur: $root.cancelEditing }">
            </li>
        </ul>
    </section>
    <footer id="footer" data-bind="visible: completedCount() || remainingCount()">
        <span id="todo-count">
            <strong data-bind="text: remainingCount">0</strong>
            <span data-bind="text: getLabel(remainingCount)"></span> left
        </span>
        <ul id="filters">
            <li>
                <a data-bind="css: { selected: showMode() == 'all' }" href="#/all">All</a>
            </li>
            <li>
                <a data-bind="css: { selected: showMode() == 'active' }" href="#/active">Active</a>
            </li>
            <li>
                <a data-bind="css: { selected: showMode() == 'completed' }" href="#/completed">Completed</a>
            </li>
        </ul>
        <button id="clear-completed" data-bind="visible: completedCount, click: removeCompleted">
            Clear completed (<span data-bind="text: completedCount"></span>)
        </button>
    </footer>
</section>

 

5. Knockout源碼解析

5.1 ko.observable是什么

this.firstName=ko.observable(“Bert”);
this.firstName();
this.firstName(“test”);

  調用上面代碼發生了什么

$.observable = function(value){
    var v = value;//將上一次的傳參保存到v中,ret與它構成閉包
    function ret(neo){
        if(arguments.length){ //setter
            if(v !== neo ){
                v = neo;
            }
            return ret;
        }else{                //getter
            return v;
        }
    }
    return ret
}

5.2 ko.computed是什么

this.fullName = ko.computed(function() {
        return this.firstName() + " " + this.lastName();    
    }, this);
$.computed = function(obj, scope){
    //computed是由多個$.observable組成
    var getter, setter
    if(typeof obj == "function"){
        getter = obj
    }else if(obj && typeof obj == "object"){
        getter = obj.getter;
        setter = obj.setter;
        scope  = obj.scope;
    }
    var v
    var ret = function(neo){
        if(arguments.length ){
            if(typeof setter == "function"){//setter不一定存在的
                if(v !== neo ){
                    setter.call(scope, neo);
                    v = neo;
                }
            }
            return ret;
        }else{
            v = getter.call(scope);
            return v;
        }
    }
    return ret;
}

5.3 屬性依賴如何實現

  調用observable中getter方法時,ret函數對象收集所有對自身的依賴對象

  調用observable中setter方法時,ret函數對象想依賴對象發生通知

  

  調用computed中getter方法時,ret函數對象將自身傳遞給依賴探測的begin方法

  然后通過call()方法獲取函數值,這時,會觸發observable中相對應的getter的調用,從而收集到computed中的ret函數對象

  在調用完成后,再將自身移除

$.dependencyDetection = (function () {
    var _frames = [];
    return {
        begin: function (ret) {
            _frames.push(ret);
        },
        end: function () {
            _frames.pop();
        },
        collect: function (self) {
            if (_frames.length > 0) {
                self.list = self.list || [];
                var fn = _frames[_frames.length - 1];
                if ( self.list.indexOf( fn ) >= 0)
                    return;
                self.list.push(fn);
            }
        }
    };
})();
$.valueWillMutate = function(observable){
    var list = observable.list
    if($.type(list,"Array")){
        for(var i = 0, el; el = list[i++];){
            el();
        }
    }
}

5.4 雙向綁定如何實現

The name is <span data-bind="text: fullName" id="node"></span>
$.buildEvalWithinScopeFunction =  function (expression, scopeLevels) {
    var functionBody = "return (" + expression + ")";
    for (var i = 0; i < scopeLevels; i++) {
        functionBody = "with(sc[" + i + "]) { " + functionBody + " } ";
    }
    return new Function("sc", functionBody);
}
$.applyBindings = function(model, node){       
   
    var nodeBind = $.computed(function (){
        var str = "{" + node.getAttribute("data-bind")+"}"
        var fn = $.buildEvalWithinScopeFunction(str,2);
        var bindings = fn([node,model]);
        for(var key in bindings){
            if(bindings.hasOwnProperty(key)){
                var fn = $.bindingHandlers["text"]["update"];
                var observable = bindings[key]
                $.dependencyDetection.collect(observable);//綁定viewModel與UI
                fn(node, observable)
            }
        }
    },node);
    return nodeBind
     
}
$.bindingHandlers = {}
$.bindingHandlers["text"] = {
    'update': function (node, observable) {
        var val = observable()
        if("textContent" in node){
            node.textContent = val;
        }
    }
}
window.onload = function(){
    var model = new MyViewModel();
    var node = document.getElementById("node");
    $.applyBindings(model, node);
}

   上述代碼中,$.buildEvalWithinScopeFunction(str,2)返回一個匿名函數

function anonymous(sc/**/) {with(sc[1]) { with(sc[0]) { return ({text: fullName}) }  } }

  通過var bindings = fn([node,model]),bindings得到一個{text:fullName函數對象}的對象,其中,fullName是一個組合依賴屬性,即fullName是一個computed中ret函數對象

6. 總結

6.1 優點

  • 專注於data-binding,UI自動刷新,model依賴跟蹤
  • 簡單易上手,學習成本低
  • 輕量,方便與其他第三方JS框架集成
  • 可擴展,支持自定義定制
  • 瀏覽器兼容度高,幾乎支持所有現代瀏覽器

6.2 不足

  • 是一個MVVM library,不是一個前端解決方案
  • 缺少Router等重要模塊支持
  • 缺少可測試性支持

參考資料

Knockoutjs Tutorial  http://learn.knockoutjs.com
knockout.js學習筆記  http://www.cnblogs.com/rubylouvre/archive/2012/06/17/2551907.html
Knockout todo mvc源碼  https://github.com/tastejs/todomvc
 

如果您覺得本文對您有幫助,請【推薦】,謝謝。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM