본문 바로가기

Javascript/Knockout

Computed Observables

Computed Observables

만약 여러분이 observable로 firstName과 lastName을 갖고 있고 full name을 보여주기 원한다면 어떻게 할까?  이러한 것으로 부터 computed observable들이  탄생하게 됐다.  computed observable은  함수들이고 이것들은 하나 또는 여러개의 다른 observable들과 종속관계를 갖고 있다. 즉 이런 종속관계에 있는것들이 변경이 일어나면 자동적으로 업데이트가 된다는 말이다.

예를 들어 다음과 같은 view model 클래스가 있으면

function AppViewModel() {
    this.firstName = ko.observable('Bob');
    this.lastName = ko.observable('Smith');
}

여러분은 full name을 반한하는 computed observable을 추가할 수 있다 : 


function AppViewModel() {
    // ... leave firstName and lastName unchanged ...
 
    this.fullName = ko.computed(function() {
        return this.firstName() + " " + this.lastName();
    }, this);
}
이제 여러분들은 UI 요소들에 바인드 할수 있다. 예 :


The name is <span data-bind="text: fullName"></span>

그리고 firstName또는 lastName이 변경되면 자동으로 fullName이 변경 된다.



 ‘this’ 관리하기


초보자들은 이 섹션을 스킵하기 원할것입니다. 만약 여러분들이 예제와 같은 패턴으로 코딩을 작성한다면 여러분은 이 섹션에 대해 알 필요가 없습니다.  

만약 여러분들이 ko.computed의 두번째 파라미터가 무엇인 궁금해 하신다면 그 두번째 파라미터는 computed observable을 계산할때 this의 값을 정의하기 위한 파라미터 입니다.  this키워드를 넘기지 않으면 this.firstName() 또는 this.lastname()를 참조할 수 없습니다. 경험이 많은 javascript 코더들은 이러한 것들을 잘 알고 있습니다. 그러나 여러분들이 Javascript을 알아가는 과정에 있다면 좀 이상하게 느껴질 것입니다. ( C#과 JAVA는 this라는 값을 넘기지 않습니다.그러나 JavaScript는 넘겨야 합니다. 왜냐면 기본적으로 function들은 어떠한 object에 속하지 않기 때문입니다.)


this를 넘기지 않기위해 (트랙킹하지않기위해) 옛날부터 해온 코딩이 있습니다. 즉 viewmodel constructor에 this를 다른 변수(전통적으로 self)에 카피합니다. 그러면 여러분들은 여러분들이 viewmodel에  self를 사용할수 있습니다. 그리고 여러분들은 다른 어떠한 것도 재정의 할 필요가 없습니다. 예를 들면 :

function AppViewModel() {
    var self = this;
 
    self.firstName = ko.observable('Bob');
    self.lastName = ko.observable('Smith');    
    self.fullName = ko.computed(function() {
        return self.firstName() + " " + self.lastName();
    });
}


왜냐하면 self는 function의 내에 있기 때문입니다. self는 계속 사용할수 있게 남아있고 어떠한 nested 함수(ko.computed 계산함수)에서도 사용할 수 있습니다. 이러한 방법은 이벤트 핸들러와 함께 사용하면 매우 유용하고 많은 예제를 통해서 볼수 있습니다.(http://knockoutjs.com/examples/)

Dependency chains just work(종속성의 사슬 관계의 동작)


여러분 마음대로 computed observable들의 체인들을 생성할수 있습니다. 예를 들면 여러분들은 다음과 같은 것들을 가지고 있을 것입니다 :

  • obserrvable - 어떤 아이템들 셋트를 보여주기위한 items
  • observable - 유저에 의해서 선택된 아이템들을 저장하기 위한 selectedIndexes
  • computed observable - selected indexes애 대응되는 item object array의 반환값 selectedItems 
  • computed obserbable - sselectedItems가 어떤 프로퍼티(새로운거 또는 저장안된거)를 가지고 있는지에 대한 반환값 true 또는 false. button 같은 UI요소들은 이러한 값에 따라 활성 비활성화 될수 있다.

다음으로 items의 변경 또는 selectedIndexes의 변경은 computed observable들로 인해 변경될수 있다.그리고 UI의 바인딩 된 값들을 순차적으로 업데이트 된다.

Writeable computed observables (쓰기 computed observables)


초보자는 아마 이 섹션을 넘어갈 것이다.  writeable  computed observable은  매우 고급기술이고 대부분의 상황에서는 필요없다.

여러분들이 배웠듯이 computed observable은 다른 observable들로부터 계산된 값을 갖는다. 무슨말이고 하면, computed observable들은 일반적으로 readonly라는 말이다. 그럼  computed observables writeable이 가능하다는것은 얼마나 놀라운 것인가.  여러분은 단지  callback 함수만 제공하면 된다. 이 함수는 어떤 적혀진 값들에 민감하게 반응해야 한다.

다음으로 여러분은 여러분의 writeable computed observable을 사용할수 있다. 마치 보통 observalbe처럼 - DOM요소에 양쪽(two-way data binding) 데이터 바인딩을 수행함으로여러분들은 custom 로직은 모든 read와 wirte의 로직을 갖는다. 이건 매우 강력한 기능이다 그리고 여러 부분에서 사용가능하다.

Example 1: Decomposing user input(사용자 입력을 분해하기)

"first name + last name = full name"의 예로 돌아가보자 여러분은 font로 돌아가서 fullName을 computed observable writeable로 만들수 있을 것이다. 그래서 유저들은 full name을 직접 수정하고 그 값들은 firstName과 lastName의 observable로 각각 분해되어 맵핑 될것이다 :

function MyViewModel() {
    this.firstName = ko.observable('Planet');
    this.lastName = ko.observable('Earth');
     
    this.fullName = ko.computed({
        read: function () {
            return this.firstName() + " " + this.lastName();
        },
        write: function (value) {
            var lastSpacePos = value.lastIndexOf(" ");
            if (lastSpacePos > 0) { // Ignore values with no space character
                this.firstName(value.substring(0, lastSpacePos)); // Update "firstName"
                this.lastName(value.substring(lastSpacePos + 1)); // Update "lastName"
            }
        },
        owner: this
    });
}
 
ko.applyBindings(new MyViewModel());

이 예에서 write 콜백핸들러는 들어온 값들을 "firstName" 과 "lastName" 요소들로 나눈다 그리고 원래 observable에게 값들을 쓴다. 여러분들은 view model을 DOM에 바인딩 할수 있습니다. 다음 처럼요 :

<p>First name: <span data-bind="text: firstName"></span></p>
<p>Last name: <span data-bind="text: lastName"></span></p>
<h2>Hello, <input data-bind="value: fullName"/>!</h2>

이 예는 정확하게 Hello World예제와 반대되는 예제이다. 여기서는 first 그리고 last name을 수정할수  없었지만 full name은 수정할수 있었다. 

이전의 view model 코드는 single 파라미터 computed observable들을 초기화 해주는 방법을 설명하였다. 여러분은 JavaScript 객체에 다음과 같은 프로퍼티들을 넘길수 있다.:

  • read — 필수. 이 함수는 computed observable들의 현재 값을 계산하는데 사용한다.
  • write — 선택사항. 만약 이 프로퍼티가 정해 졌다면 computed observable writable을 생성할 수 있다. 
  • owner — 선택사항. 막약 이 프로퍼티가 정해져 있다면, this값을 정의해라 언제? KO가 read또는 write 콜백함수를 호출할때. 
  • deferEvaluation — 선택사항. 만약 이 옵션이 true이면 computed observable 값은 어떤것이 실제적으로 엑섹스를 시도할때까지 계산되지 않을것이다. 기본으로 computed observable 생성되는 동안 즉시 값이 결정된다.
  • disposeWhen — 선택사항. 만약 이 프로퍼티가 정해져 있다면 이 함수는 실행될것이다  언제? 만약 computed observable이 꼭 해제되어야 하는 그 시점에 
  • disposeWhenNodeIsRemoved — 선택사항. 이 프로퍼티가 정해져 있다면 특정 DOM 노드가 KO에 의해 제거 될때 computed observable의 해제가 이뤄질 것이다. 

Example 2: A value converter

때때로 여러분들은 어떤 데이터를 다른 포멧으로 보여주고 싶을것이다. 예를 들어 float 가격을 저장하기 원하는데 사용자가 그 값을 화폐단위와 숫자를 같이써다면 여러분들은 writeable computed observable을 원하는 형태의 포맷으로 보여주기 위해 사용할수 있다. 다시말하면 받은 값을 다시 float형태의 값으로 맵핑하는 것이다.:

function MyViewModel() {
    this.price = ko.observable(25.99);
         
    this.formattedPrice = ko.computed({
        read: function () {
            return '$' + this.price().toFixed(2);
        },
        write: function (value) {
            // Strip out unwanted characters, parse as float, then write the raw data back to the underlying "price" observable
            value = parseFloat(value.replace(/[^\.\d]/g, ""));
            this.price(isNaN(value) ? 0 : value); // Write to underlying storage
        },
        owner: this
    });
}
 
ko.applyBindings(new MyViewModel());

포맷 가격을 텍스트 박스에 바인딩 한다:

<p>Enter bid price: <input data-bind="value: formattedPrice"/></p>

이제 사용자가 어떠한 가격을 입력해서 텍스트 박스는 곧바로 환폐단위로 업데이트 된다.  이러한 기능한 사용자가게 보다 좋은 UX환경을 보여준다. 왜냐면 사용자들은 자신이 입력한 값이 가격입을 더욱 확실히 알수 있기 때문이다.

Example 3: Filtering and validating user input

예제 1에서는 어떻게 writeable computed observable이 효과적으로 데이터들을 필터링하는지 보여줬다. 예제에서 full name값들에 스페이스가 포함되지 않은 것은 무시하게 된다.

좀더 자세히 보면 여러분들은 isValid 플래그를 가장 최신의 인풋값이 만족을 하는지에 따라 토글할수 있다. 그리고 UI에 맞게 메세지를 보여줄수 있다. 좀더 쉬운방법으로 validation을 구현하는걸 잠시후에 설명할것이다 하지만 아래의 예제를 먼저보자:

function MyViewModel() {
    this.acceptedNumericValue = ko.observable(123);
    this.lastInputWasValid = ko.observable(true);
     
    this.attemptedValue = ko.computed({
        read: this.acceptedNumericValue,
        write: function (value) {
            if (isNaN(value))
                this.lastInputWasValid(false);   
            else {
                this.lastInputWasValid(true);
                this.acceptedNumericValue(value); // Write to underlying storage
            }
        },
        owner: this
    });   
}
 
ko.applyBindings(new MyViewModel());

DOM요소와 함께:

<p>Enter a numeric value: <input data-bind="value: attemptedValue"/></p>
<div data-bind="visible: !lastInputWasValid()">That's not a number!</div>

이제  acceptedNumericValue 는 numeric값만 포함하게 된다. 그리고 어떤한 값이 들어오게 되면 validation message를 보여주게 된다.

Note: 위와 같은 numeric validation을 하기위해 이러한 기술은 사요하는것은 좀 과하다할수 있다. 차라니 jquery의 validation을 쓰는게 나을것이다. 

How dependency tracking works(종속객체의 트랙킹 작동)


이건 매우 간단. 트래킹 algorithm은 아래와 같다.:

  1. 여러분들이 어떤 computed observable을 선언할때만다 KO는 즉시 그것의 evaluator함수를 호출해 그 값을 초기화 한다.
  2. 여러분의 evaluator 함수가 동작할때에 KO는 어떠한 observable이든 로그를 쌓는다
  3. 여러분의 evaluator가 끝나면 KO는 가각의 observable에게 구독( subscriptions)을 설정한다. 구독( subscriptions) 콜백은 여러분의 evaluator을 다시 구동시키기 위해 설정된다, 그리하여 step1으로 돌아가서 모든 프로세스가 Looping한다.
  4. KO는 구독자들에게 여러분의 computed observable의 새로운 값들을 알려준다.

그리하여 KO는 여러분의 evaluator을 한번만 작동시킬뿐 아니라 매번 감지하여 작동시킨다. 이 의미는 예를들어 여러분의 dependecy들은 다양할수 있다는것이다 :  dependecy A는 B또는 C의 의존적이라는 것을 또한 결정할수 있다. 그럼 여러분은 B또는 C또는 A가 변경될때 evaluator을 작동시킬수 있다. 여러분은 dependency를 선언할 필요가 없다:  dependecy들은 런타임시 참조하게 된다.

binding지시어는 computed observables으로 구현되어있다. 그래서 만약 어떠한 binding이 observalbe의 갑을 읽으려면 그 바인딩은 그 observable에 종속적이 된다. 그래서 만약 observable 이변경되면 다시 evaluator가 다시 작동한다.

Determining if a property is a computed observable

 만약 여러분이 이게 computed observable로 핸들링 되는 건지 아닌지를 판단해야할 때가 있다. 그러한 상황에  ko.isComputed 를써라. 예를 들면 여러분은 서버로 보내야 하는데이터에서 computed observable를 제외시키고자 한다.

for (var prop in myObject) {
  if (myObject.hasOwnProperty(prop) && !ko.isComputed(myObject[prop])) {
      result[prop] = myObject[prop];
  }
}

게다가.  Knockout 유사항 기능들을 제공하고 있다.:

  • ko.isObservable - observable, observableArray,computed observable 이면 true를 반환
  • ko.isWriteableObservable - observable, observableArray, writeable computed observables 이면 true를 반환