跳至主要内容

關注點分離(Separation of Concerns)

TL;DR

圖片及資料來源:六角學院Vue影音課程

參考資料

相關連結


什麼是關注點分離

一開始在開發的時候,我們都會使用js根據需要的功能去組合出html的結構,接著再渲染到畫面上。只是這種方法程式碼中會混雜資料處理的邏輯判斷以及畫面的渲染等等。這樣如果在需要調整資料內容的時候就會很麻煩。 資料以及畫面的程式碼混雜在一起

我們在使用框架的時候,通常都是將資料給拆分的。這樣開發者就只要專注在資料的處理上。如果今天需要新增或刪除一筆資料,就只要調整資料的內容,處理完資料之後再重新渲染到畫面上即可。就算今天需要將資料使用在別的地方,也很容易將資料取出,因為原本資料就是分離的狀態。 資料分離

使用框架時的關注點分離實作

在使用Vue等等的框架時,渲染畫面是由框架去處理的。開發者只需要專注在資料以及生命週期上即可。

關注點分離範例

程式碼及展示

以下透過原生JS簡單示範關注點分離的概念,並附上codepen連結

codepen:範例連結

// #1 資料、畫面、方法分離
// 畫面 = html
// 資料 = component.data
// 方法 = 物件內的其它函式

// #2 元件結構
// 1. 資料
// 2. 方法、觸發器
// 3. 生命週期(初始化)

const component = {
data: [ // 資料
'這是第一句話',
'這是第二句話',
'這是第三句話'
],
removeData(id) {
this.data.splice(id, 1);
this.render();
},
render() { // 渲染方法
const list = document.querySelector('.component ul');
let content = '';
this.data.forEach((item, i) => {
content = `${content}<li>${item} <button type="button" class="btn" data-id="${i}">移除</button></li>`
});
// 縮寫優化
// const content = component.data.map(item => `<li>${item}</li>`).join('');
list.innerHTML = content;

// 加入監聽
const btns = document.querySelectorAll('.btn');
btns.forEach(btn => btn.addEventListener('click', (e)=> {
// #2 重點,移除項目是先移除資料,而不是直接移除 DOM
// 如果要進行 AJAX 或更複雜行為,不會因為 DOM 與資料混合而難以運作
const id = e.target.dataset.id;
this.removeData(id)
}))
},
init() {
this.render();
}
}

component.init();

程式碼說明

首先宣告一個元件,接著在裡面建立資料、方法。 方法這邊主要只有撰寫渲染以及移除,渲染畫面的部分通常只會固定撰寫一個,並且也只有這支方法裡面會有html結構。

需注意的地方

  1. Line:25 的地方,button要記得加上type="button",否則預設會是submit的功能。
  2. LINE:33 ,這邊的事件監聽如果裡面撰寫的是傳統函式,則this的指向會指向觸發的button本身,而不是component這個物件
    btns.forEach(btn => btn.addEventListener('click',function() {
    ...
    }))
    這是因為事件監聽內的function是一個callback function,但是他的this會被重新調整過,所以我們這邊需要透過修改成箭頭函式的方法讓他可以吃到外層函式的this,或是另外在宣告一個vm指向component物件本身。

練習-新增資料

程式碼及展示

程式碼以及codepen範例:

codepen:範例連結

// #1 資料、畫面、方法分離
// 畫面 = html
// 資料 = component.data
// 方法 = 物件內的其它函式

// #2 元件結構
// 1. 資料
// 2. 方法、觸發器
// 3. 生命週期(初始化)
const component = {
data: [ // 資料
'這是第一句話',
'這是第二句話',
'這是第三句話'
],
removeData(id) {
this.data.splice(id, 1);
this.render();
},
addData(value){
this.data.push(value);
this.render();
},
render() { // 渲染方法
const list = document.querySelector('.component ul');
let content = '';
this.data.forEach((item, i) => {
content = `${content}<li>${item} <button type="button" class="btn" data-id="${i}">移除</button></li>`
});
// 縮寫優化
// const content = component.data.map(item => `<li>${item}</li>`).join('');
list.innerHTML = content;

// 加入監聽
const btns = document.querySelectorAll('.btn');
btns.forEach(btn => btn.addEventListener('click', (e)=> {
// #2 重點,移除項目是先移除資料,而不是直接移除 DOM
// 如果要進行 AJAX 或更複雜行為,不會因為 DOM 與資料混合而難以運作
const id = e.target.dataset.id;
this.removeData(id)
}))

},
init() {
const input=document.querySelector('.inputData');
const addBtn=document.querySelector('.addBtn');
addBtn.addEventListener('click',()=>{
if(input.value){
this.addData(input.value);
}else{
alert('請輸入資料')
}
})
this.render();
}
}
component.init();

程式碼說明

透過關注點分離的概念,我們可以專注在處理資料上。這邊處理資料的方式很簡單,直接使用push將input內的資料加到data陣列內即可。 Line:47 同樣要使用箭頭函式這樣this的指向才會是component物件而不是事件監聽的對象addBtn本身

如果不使用箭頭函式,this會指向事件監聽的對象

如果將上述 Line:47 內的程式碼改成傳統函式,並修改程式碼如下

  init() {
const input=document.querySelector('.inputData');
const addBtn=document.querySelector('.addBtn');
addBtn.addEventListener('click',function(){
console.log(this);
})
this.render();
}

這時候去點擊按鈕觀察this會印出事件監聽的按鈕,這是因為他的this指向已經被重新調整過了,所以既不是window也不是component 印出按鈕

需注意的地方

會在init()內就定義事件監聽以及事件觸發時執行的函式是有原因的。因為如果將 Line:45 ~ Line:53 移動到 Line:42 ,會在每次render();執行的時候重新綁定監聽事件,這樣會造成重複綁定。所以第一次新增資料時會載入一筆資料,但是因為載入資料的時候在addData內需要重新渲染畫面,會執行render();,造成addBtn又被綁定了第二個相同的事件。

所以當再次添加資料的時候,第一次綁定的事件以及新增資料後再次渲染所綁定的事件都會執行。會導致相同資料添加兩次。並且又因為每個事件觸發的時候都會執行addData();,addData();又會再去重新渲染畫面,所以綁定的事件會成指數成長。

要避免這種狀況有兩種做法,一種是每次render();的時候移除之前所綁定的事件;另一種做法就是直接在初始化的時候就加入,這樣渲染畫面的時候就不需要重新綁定事件,也就不會造成重新綁定的問題。