iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 29
2

【React.js入門 - 22】 元件練習(上) - 在class利用遞迴+state實作動畫,雖然說這篇文的重點不是在講這個,但這種用 requestAnimationFrame 來做不知道會不會比較好?好像會比 setTimeout 好一點。

17. [FE] 為什麼現在的前端都在用「框架」?,好喜歡裡面前端御三家那張圖XD

我覺得在前端的發展裡面有兩點很重要,第一點是資料與畫面的同步,第二點是互動的體驗,從這兩點當中就可以知道為什麼現在的前端長這個樣子,底下簡單提一下,以後有興趣再來發展成長文。

先談資料與畫面的同步這件事,假設我們今天有一個 todo list,然後用 jQuery 或 vanilla js 來寫,新增 todo 的時候就 append 一個 DOM 物件,刪除的時候就刪掉,看起來沒什麼問題。

但如果今天你要在 JS 裡面同步保存 todo list 的「資料」呢?假設我們每五秒會存一次 todo list 的資料進去 localStorage 好了,你要怎麼達成這個需求?

在新增 todo 的時候你除了新增 DOM 物件,你還要對 JS 裡的 todos 這個陣列做改變,把資料 push 進去;刪除、修改的時候也是,你要改變 DOM 也要改變資料。那麻煩的事情是什麼?一旦有一個操作你忘記改變任何一方,兩邊的資料就不同步了,就會造成錯誤的結果。

所以就有人說:「那不如我們把 DOM 跟資料關聯在一起好了。當我把畫面上的東西改變時,資料就會跟著變。當我改變資料時,畫面上的東西也會變,這樣不就行了嗎?」

就利用程式來實做這個機制,我們通常就稱它為雙向綁定(話說這一塊我其實沒經歷過...應該找時間去惡補一下,怕誤人子弟)

接著又有一種我自己認為更簡單的方法出現了:「不不不,既然害怕資料跟畫面不同步,那就規定只能改變資料就好啦,只要資料一變,就 render 出相對的畫面,這樣就不會不同步了,因為畫面永遠是跟著資料走的」

在我出給學生的作業裡面就有這樣一題,實做起來大概會長這樣:

var list = []
function addTodo(todo) {
  list.push(todo)
  render()
}

function removeTodo(id) {
  list = list.filter(item => item.id !== id)
  render()
}

function render(){
  $('.todo-list').empty()
  $('.todo-list').append(list.map(item => `<li>${todo.content}</li>`)) // 示意
}

資料一變,就把畫面清空然後重新 render 一次,不就能保證畫面最新了嗎?

這時候你應該跳出來指出一個超級嚴重的缺點:「不對啊,這樣我每改變一次,整個畫面都要清空,也太沒有效率了吧?」

沒錯,上面的這個實作問題就在於背後的原理是「每次改變資料就整個畫面重新渲染」,所以根本不可行。但如果可以做到「改變資料的時候,只有改變的部分重新渲染」,那不就好了嗎?

這就是為什麼要有 virtual DOM,以及為什麼 virtual DOM 比對的演算法這麼重要,如果沒有這兩個,是沒辦法做到「改變資料的時候,只有改變的部分重新渲染」這樣子的。

簡單講一下為什麼要有 virtual DOM。

上面提到了「改變資料的時候,只有改變的部分重新渲染」,那第一點就是你必須知道「哪邊改變了」,才能跟新的這份比對嘛。可是難道你要把之前的 DOM 狀態整個存起來嗎?不可能嘛,所以我們用很像 JS object 的形式去存就好了,例如說:

<div id="hello">
    <span>yo</span>
</div>

換成 JS object 可能就長得像這樣:

{
  tag: 'div',
  content: {
      tag: 'span',
      content: 'yo'
  }
}

簡單來說就是可以把 DOM 表示成 JS 物件的樣子,這個就叫做 virtual DOM。所以在比對的時候,只要比較 virtual DOM 就好,不需要再去比較 DOM 元素。

然後經過 virtual DOM diff 演算法之後,就可以找出這兩個 virtual DOM 的差異在哪邊,接著才去實際操作 DOM。

所以如果有人問你說直接操作 DOM 跟使用 virtual DOM 哪個比較快,當然是直接操作 DOM 阿,中間少一層當然比較快。但為什麼我們必須使用 virtual DOM?因為我們想要達成的目標是「資料改變,畫面就自動重新 render」,所以我們必須也要達成「只改變差異的部分」,不然就只能像上面寫的範例那樣每一次都把畫面清空重來。

那既然必須「只改變差異的部分」,你不太可能靠人腦去處理這種東西,所以一定要靠演算法來幫你做,而你要比對就要使用 virtual DOM 來比對,而不是使用真正的 DOM。所以 virtual DOM 是必須的。

上面提的這模式基本上就是 React 的核心理念:只要資料改變,畫面就跟著改變。

突然發現上面打這些只講到第一點:資料與畫面的同步,第二點還沒講,那就以後再說吧,我目前懶得寫了XDD

[番外篇] 再談 Redux,剛好提到我比較擅長的領域,可以來補充一下。

之前我面試只要面到有用過 redux 的,就會問他說:「你用哪一個 middleware 來處理 side effect?」,然後就會跟著一題延伸題:「那你知道 redux-thunk、redux-saga 跟 redux-observable 的差異嗎?」

這剛好我前天才寫給我學生看,直接複製過來:

先來說說 Redux 為什麼要有 middleware,以及 middleware 到底是什麼。

簡單來說呢,你的 action 在 dispatch 以後,在抵達 reducer 之前,就會經過 middleware,因此流程是這樣的:

action -> middleware1 -> middleware2 -> reducer

所以你可以透過 middleware 對某些 action 做一些事情。

那為什麼需要 middleware?因為有些東西不適合放在 component 裡面去做,例如說直接去 call API。為什麼呢?因為這樣子 component 的邏輯就跟 api 的邏輯綁在一起了,很難拆分開來。或是從另一個角度去講,你很難測試。

考慮底下這個簡單的 component:

class Posts extends React.Component {
  componentDidMount() {
    WebAPI.getPosts().then((posts) => {
      this.setState({
        posts
      })
    })
  }

  render() {
    return (
      this.props.posts.map(post => <Post {...post}/>)
    )
  }
}

只要這個 component mount,就會去呼叫 API。

我剛剛提到的測試是什麼意思?

我們可能會想測試說:「這個 component render 以後是不是真的會去呼叫 API」。

你可能會想說:「那現在這樣不是很好嗎?」

No no no,我們其實不用真的去測試「送出 request」這一塊,只要能確定「WebAPI.getPosts」這個 fucntion 有被呼叫就行了。為什麼呢?因為我們還會對「WebAPI.getPosts」這個 function 寫另外的測試,確保它有送出 request。

簡單來說,測試是會分層的,你不應該把所有層次都混在一起。

以上面的例子來說,混在一起就是代表說當我測試 Posts 會不會呼叫 API 的時候,我測了兩個東西:

  1. Posts 會不會呼叫 WebAPI.getPosts
  2. WebAPI.getPosts 是否真的會送出 request

但其實我們只要測試第一個就好了,因為第二個是 WebAPI 這個 module 的事情,不關我們的事。

那怎樣比較好?我們來看個範例:

class Posts extends React.Component {
  componentDidMount() {
    this.props.getPosts()
  }

  render() {
    return (
      this.props.posts.map(post => <Post {...post}/>)
    )
  }
}

這邊不是直接去呼叫 API,而是利用 props 提供的 function 去做呼叫。

這樣的好處是什麼?

好處是我只要檢查 component 是不是有呼叫 getPosts 就好了,一切就結束了。

那 getPosts 又要怎麼寫呢?最簡單的寫法是這樣:

const mapDispatchToProps = dispatch => {
  return {
    getPosts: function() {
      WebAPI.getPosts().then((posts) => {
        dispatch(actions.setPosts(posts))
      })
    }
  }
}

把邏輯寫在這個地方,這樣就可以在呼叫 action 的時候去 call API 了。

可是這樣也很奇怪啊,會造成呼叫 API 的邏輯跟 mapDispatchToProps 綁在一起了,而且四散各地,假設你有 10 個不同的 component 都要呼叫各自的 API,你的呼叫 API 的邏輯就散佈在十個檔案之中。

那要怎樣才能夠拆開呢?

這就要靠 redux 的 middleware 啦,先來介紹耳熟能詳的 redux-thunk,概念很簡單,就是把 action 變成 function,然後 thunk 會幫你執行這個 function。

以上面的例子來改寫,會變成這樣:

const mapDispatchToProps = dispatch => {
  return {
    getPosts: function() {
      dispatch(actions.getPosts())
    }
  }
}

// actions.js
function getPosts() {
  // redux-thunk 會幫你把 dispatch 傳進來
  return function(dispatch) {
    WebAPI.getPosts.then(posts => {
      dispatch(actions.setPosts(posts))
    })
  }
}

如此一來,你就把 call API 的邏輯移到了 action 裡面,就不會四散於各地,不會存在於 component 當中。

其實很推薦大家可以看看 redux-thunk 的原始碼,只有 14 行而已:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

重點在第三行,判斷如果 action 是個 function 就幫你執行,然後把參數傳給你,就是這麼簡單而已。

好,看起來 redux-thunk 的解法不錯啊,把 action 變成一個 function,就可以把相關邏輯都放在這邊了。

但其實還可以更好,那就是把邏輯從 action 裡面拿掉,再拆分出來一層。這樣就可以保持 action 都是 pure action(純 JS 物件),會更方便測試。

而這樣做的 middleware 有兩套,一套叫做 redux-saga,另一套叫做 redux-observable

其實這兩套的底層概念是一樣的,就是 pure action in, pure action out。

再次舉上面同樣的例子,改寫成 redux-saga 或是 redux-observable 之後,action 不再是個 function,而是純粹物件形式的 action:

const mapDispatchToProps = dispatch => {
  return {
    getPosts: function() {
      dispatch(actions.getPosts())
    }
  }
}

// actions.js
// pure action object
function getPosts() {
  return {
    type: 'GET_POSTS'
  }
}

那要在哪邊呼叫 API 呢?在 middleware 裡面去針對不同的 action 來做出反應。middleware 的概念會像這樣(非正式程式碼):

function handleAction(action, dispatch) {
  if (action.type === 'GET_POSTS') {
    WebAPI.getPosts.then(posts => {
      dispatch(actions.setPosts(posts))
    })
  }
}

在 middleware 裡面執行任何會造成 side effect 的程式碼(side effect 就是副作用,以程式來講就是像 call API、寫 cookie 或是寫入 local storage 這些額外的操作)

如此一來,在 component 裡面就只需要 dispatch 一個 pure action,而非同步的處理就在 middleware 裡面處理就好,就不需要再把 action 變成 function。

這是 redux-saga 與 redux-observable 這兩套與 redux-thunk 跟 redux-promise 最不一樣的地方。在這兩套裡面,action 一定是 pure action,不會是 function 也不會是 promise。

更多細節可以參考我之前 Modern Web 2018 的演講:輕鬆應付複雜的非同步操作:RxJS Redux Observable - 胡立 (huli),裡面有附上投影片,可以直接跳到最後面的地方(113~119 頁),忽略前面 RxJS 的部分。

話說我發現很多東西後來都要以「容易測試」的角度去理解,這樣會超級好瞭解而且知道為什麼要這樣做。再次感受到測試的重要性。


原本其實是要搭歐洲統聯 flixbus 從蘇黎世去米蘭,但突然發現要搭四個小時,上次從慕尼黑搭到蘇黎世三個半小我就快受不了了,何況是這一次。所以前一晚改訂火車票,決定搭火車去。

火車雖然也要頗久,大概三個半小,但至少有風景看,而且座位比較不會晃,整體的感覺還是好滿多的。

到米蘭之後找住宿 check in,去超市買點吃的,今天就這樣了,明天再去一些知名景點看看好了。

話說今天這標題是因為看到朋友的推特問到 i18n 這種縮寫的源頭是哪個,我無聊查了一下 wiki,裡面附了這個來源:http://i18nguy.com/origini18n.html

看起來 i18n 在 1985 左右開始使用,然後始祖是一個叫做「Jan Scherpenhuizen」的人
因為名字太長沒辦法當帳號,所以就簡寫叫做 S12n

這就是標題來源,也附贈給大家當做小知識
更詳細的可以看:https://en.wikipedia.org/wiki/Numeronym


上一篇
後設鐵人 Day28:FREITAG 瑞士蘇黎世總部
下一篇
後設鐵人 Day30:輕鬆完賽
系列文
後設鐵人:我從其他鐵人們身上學到的事30

尚未有邦友留言

立即登入留言