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 這些。
總結
寫到這裡感覺自己也是似懂非懂的了,一定是原始碼看的太少了……
以後繼續加油,拜拜