1. 程式人生 > >hyperapp.js 一個輕量級的 react 實現

hyperapp.js 一個輕量級的 react 實現

hyperapp 是什麼鬼?

hyperapp 是一個前端的應用構建庫。初見寫法,很有一種寫react的親切的感覺(其實就是一個套路),不過這肯定不能成為吸引廣發gay友從而在短短兩個月拿到 8K star的理由。更重要的一個原因是 官方宣稱的1kb。是的, hyperapp 的核心程式碼只有1kb,這對早已習慣react全家桶,同時對當今web應用一個頁面動輒3、4M毒害的gay友來說,的確是一個福音。基於此,官方給自己的定位是:

  • 更小:只要1kb,做到其他框架應該做的;
  • 更實用:主流的前端應用思想,不會對學習帶來額外負擔;
  • 開箱即用:完善的虛擬Dom、key更新、應用生命週期。
  • 以上個人翻譯,有吹噓成分

既然聽起來這麼厲害,今天就來一探究竟了……

簡單的使用

最簡單的使用方法就是看官網給的 計數器 示例,可以在 這裡 檢視最終效果:

<body>
<script src="https://unpkg.com/hyperapp"></script>
<script>

// ******劃重點

const { h, app } = hyperapp

const state = {
  count: 0
}

const actions = {
  down: value => state => ({ count: state.count - value }),
  up
: value => state => ({ count: state.count + value }) } const view = (state, actions) => h("div", {}, [ h("h1", {}, state.count), h("button", { onclick: () => actions.down(1) }, "–"), h("button", { onclick: () => actions.up(1) }, "+") ]) window.main = app(state, actions, view, document
.body)
// *****劃重點 </script> </body>

顯而易見,state 定義了應用的狀態, view 定義了應用的檢視,通過 h 方法生成一個虛擬Dom,也就是可以被瀏覽器解釋的結點樹,action 則定義了應用的一些行為邏輯,最後在通過 app 方法掛載到真實的Dom元素結點上。

當然這只是很簡單的使用。對於已經習慣了react寫法的我們來說,我們可能在 view 的部分更習慣寫純函式,或者說一些牽扯到生命週期的操作,當然這些在 hyperapp 中也是可以的。

具體的操作可以參考 官方文件

看原始碼吧還是

當然學習使用不是我們的目的,這些操作其他庫中都有實現,真正感興趣的是他說的1kb,所以還是來看原始碼吧(講真,原始碼寫的有點繞)。

核心的方法只有兩個,h 函式 和 app 函式,h函式很簡單,只是用來構建 dom 結點的。原始碼如下:

/**
 * 先來看h的用法,作用是生成一個虛擬dom節點
 * name 可以是 一個標籤名字串,如‘div’, 也可以是一個已經被渲染的component,如‘h(div,'',)’
 * props 標籤的屬性定義,如‘class’,事件等
 * 不定引數,都會當做當前節點的子節點計算
 */

export function h(name, props) {
  var node
  var stack = []
  var children = []

  for (var i = arguments.length; i-- > 2; ) {
    stack.push(arguments[i])
  }

  while (stack.length) {
    if (Array.isArray((node = stack.pop()))) {
      for (i = node.length; i--; ) {
        stack.push(node[i])
      }
    } else if (null == node || true === node || false === node) {
    } else {
      children.push(typeof node === "number" ? node + "" : node)
    }
  }

  return typeof name === "string"
    ? {
        name: name,
        props: props || {},
        children: children
      }
    : name(props || {}, children)
}

app 方法則是專案的入口,整個構建的操作其實在這裡執行。在app函式裡又定義了許多常用的工具方法,比如 createElement(建立元素),getKey(獲取元素結點的key),removeElement(移除元素)等等。又很多,這裡不在一一分析,重點方法只有兩個 init 方法和 patch方法。

init()

init的方法的呼叫還是挺有意思的,如下:

repaint(init([], (state = copy(state)), (actions = copy(actions))))

可理解成:

function a() { console.log('a'); setTimeout(b); }
function b() { console.log('b') }
function c() { console.log('c') };

a(c());

其實就是確保在入口的 repaint 方法每次被呼叫的時候先執行 init 方法。

我們來看 init 方法的主體部分:

// actions 有兩種情況,一種是引數只存在state的情況,一種是引數存在state和action的情況,又是討厭的遞迴
  function init(path, slice, actions) {
    for (var key in actions) {
      typeof actions[key] === "function"
        ? (function(key, action) {
            actions[key] = function(data) {

              // 第一次初始化的時候,path為[],所以得到的還是初始傳入的state
              slice = get(path, state)  

              // actions引數中存在action的情況,同時執行重新渲染一次
              if (typeof (data = action(data)) === "function") {
                data = data(slice, actions)
              }

              if (data && data !== slice && !data.then) {
                repaint((state = set(path, copy(slice, data), state, {})))
              }

              return data
            }
          })(key, actions[key])
        : init(
            path.concat(key),
            (slice[key] = slice[key] || {}),
            (actions[key] = copy(actions[key]))
          )
    }
  }

其實 init 方法的目的就是確保了兩種執行 repaint 方法的不同情況(有個看原始碼的小技巧就是去看官方提供的單元測試,來反推某個方法的用法)。init 方法的目的是執行 repaint 方法(真實渲染的方法入口,最終會執行 patch 方法)。

patch()

function patch(parent, element, oldNode, node, isSVG, nextSibling) {
    if (node === oldNode) {
    } else if (null == oldNode) {
      element = parent.insertBefore(createElement(node, isSVG), element)
    } else if (node.name && node.name === oldNode.name) {
      updateElement(element, oldNode.props, node.props)

      var oldElements = []
      var oldKeyed = {}
      var newKeyed = {}

      for (var i = 0; i < oldNode.children.length; i++) {
        oldElements[i] = element.childNodes[i]

        var oldChild = oldNode.children[i]
        var oldKey = getKey(oldChild)

        if (null != oldKey) {
          oldKeyed[oldKey] = [oldElements[i], oldChild]
        }
      }

      var i = 0
      var j = 0

      while (j < node.children.length) {
        var oldChild = oldNode.children[i]
        var newChild = node.children[j]

        var oldKey = getKey(oldChild)
        var newKey = getKey(newChild)

        if (newKeyed[oldKey]) {
          i++
          continue
        }

        if (null == newKey) {
          if (null == oldKey) {
            patch(element, oldElements[i], oldChild, newChild, isSVG)
            j++
          }
          i++
        } else {
          var recyledNode = oldKeyed[newKey] || []

          if (oldKey === newKey) {
            patch(element, recyledNode[0], recyledNode[1], newChild, isSVG)
            i++
          } else if (recyledNode[0]) {
            patch(
              element,
              element.insertBefore(recyledNode[0], oldElements[i]),
              recyledNode[1],
              newChild,
              isSVG
            )
          } else {
            patch(element, oldElements[i], null, newChild, isSVG)
          }

          j++
          newKeyed[newKey] = newChild
        }
      }

      while (i < oldNode.children.length) {
        var oldChild = oldNode.children[i]
        if (null == getKey(oldChild)) {
          removeElement(element, oldElements[i], oldChild)
        }
        i++
      }

      for (var i in oldKeyed) {
        if (!newKeyed[oldKeyed[i][1].props.key]) {
          removeElement(element, oldKeyed[i][0], oldKeyed[i][1])
        }
      }
    } else if (node.name === oldNode.name) {
      element.nodeValue = node
    } else {
      element = parent.insertBefore(
        createElement(node, isSVG),
        (nextSibling = element)
      )
      removeElement(parent, nextSibling, oldNode)
    }
    return element
  }

具體的方法什麼意思就不一一解釋了,有一點要注意的是,這個庫用了很多小套路,如果想要理解的話,最好先去好好理解下 JS 中的()是什麼意思?

原始碼

太長就不放了,放個連結吧。

其他類似的

其實類似的實現還有 preact ,不過 preact 大了一丟丟,但是在知名度和可靠性上肯定是 preact
遙遙領先的,本文只是用來學習,真正專案使用的話還是要慎重考慮的,優先考慮 react 和 preact 這些。

總結

寫到這裡感覺自己也是似懂非懂的了,一定是原始碼看的太少了……

以後繼續加油,拜拜