2023年12月3日 星期日

我受夠了react的hook,我想寫vue啊~

前言

你是否有跟我一樣的困擾呢,覺得react class component的setState寫法相當繁瑣,覺得react hook的的queue也不夠直覺,為什麼不能直接update state然後畫面就自動render需要透過一個setter,然後資料異動還不是同步的,也不是異步,竟然是透過queue......這是我一直以來的心聲。我今天沒有要戰,純粹是就職以來的一些小心得,作為一套老牌框架,我承認他的市佔率,但我覺得如果以便利性讓我挑選,我一定不會選react,但礙於現有的社群要他大改幾乎是不太可能,就算不論三大框架現在市場上也充斥著太多套創新的框架(qwik, svelte, solid, astro....),所以要用一個形容詞來形容這套框架只能說它“過時”了,but!最可怕的but來了,公司就愛用嘛~所以有沒有什麼辦法?

老實說我很驚訝竟然查不到相關的資料,還是大家都超滿意react,沒遇到什麼奇怪的坑,我是異類,總之~辦法還是有的,首先我在腦海中搜索想到了奶綠茶有介紹過preact/signal,奶綠大大事我很喜歡的一位工程師,我大學時還買過他的書,不過那是另外一個故事了

內文

preact跟react是不一樣的框架,但其中的api友87%像,signal使用起來就像是vue裡面的ref(太詳細的內容自己去看原始碼),然後看網誌說明signal無法直接使用在react,特別是底層是vite打包的狀況下(該死)

所以不得已我又開始尋找其他法,後來發現preact/signal有出preact/react-signal特別優化的版本,這樣應該是妥了吧,官方都直接出優化了,直接使用在公司專案結果爆出getSnapshop is undefined......我去查了一下原始碼也一無所獲,歸功於react亂七八糟......抱歉我不該戰,是莫名其妙...抱歉,是我孤陋寡聞不得而知的底層運作機制,所以我放棄了這麼一條路

難道我就該止步於此繼續使用這該死又難寫......抱歉我不該戰,我要繼續使用這我用了很痛苦的框架嗎?當然不是,既然連官方都靠不住,那就只能自己來寫了,一開始我嘗試使用Object.defineProperty去寫(沒錯就是vue2那套,想看原始碼也附註解在裡面),反正所有東西都丟到value下面就好了吧,但也發生了跟vue2一樣的問題,沒有註冊過的屬性就不會reactive,但如果使用Proxy怕太新UC那些奇怪的瀏覽器不支援

沒錯公司要求產品要支援UC,講到這裡我想花一點篇幅抱怨一下,建議看到這裡的工程師,珍惜生命,遠離UC,下次面試記得要問主管公司有沒有要求要支援UC,有人說UC就像手幾板的ie我覺得他說錯了,他比ie還不如!不僅沒辦法debug,之前發生一次白屏,在我一行一行除錯,最後問題竟然是html排序它不滿意,不是什麼沒有加上</>這種低級錯誤喔,就是單純排序它不滿意,當時的狀況就類似以下:
<div>
    hello
    <div>hello2</div>
<div>
然後我把它改成
<div>hello<div>
<div>hello2</div>
就正常了.....我還以為我又用什麼新的寫法或物件戳到它的G點,Fuck!這我真的沒辦法誒!!

好拉,工作還是要繼續下去,但這還在研發階段之不支援我就先不考慮,之後不支援大不了不用!所以你以為我就乖乖刻輪子嗎?當然沒有,我想到既然底層都是Proxy那我用signal就好拉,那畫面不會reactive的問題怎麼處理?奶綠茶大大的網誌有提到react18有出一個新的hook是useSyncExternalStore 這個hook可以控制畫面要不要渲染,網誌裡面有提到大大如何包裝這個function,我試著去改寫但我一直嘗試不成功,因為我一直把變數放在function內,所以在rerender時function會重跑一遍,所以變數每次都會被重置(Fuck我就說寫法很不直覺吧),當我想到這問題時已經好幾天過去了,如果不想變數被重置,那就使用原生的hook來解決吧,最後產出來的code就如下

沒錯,前面說這麼多我只是想要抱怨,最終結果在下面,大家如果為了自己身體著想,可以直接拿去使用

我模擬出一些比較常用的hook,其他我也很少用就不做了,畢竟這就像那些砲轟我的網友說的,就是vue皮react骨,我也沒有不承認這件事,但更新資料畫面就會自動更新就是爽,也不用在那邊setState後還要在useEffect等狀態更新才能在function裡面執行我想執行的內容,這寫法真的很蠢......抱歉我不該戰,這寫法真的很不直覺

好吧廢話不多說直接上程式碼

import { useReactive } from "@reactivedata/react"
import { useEffect, useMemo, useRef, useSyncExternalStore } from "react"
import { signal, effect, computed as preactComputed } from "@preact/signals-core"

export const onMounted = (fn) => {
useEffect(() => {
fn()
}, [])
}
export const onUnmounted = (fn) => {
useEffect(() => {
return () => {
fn()
}
}, [])
}

export const ref = (initValue) => {
const valueRef = useRef(initValue)
const versionRef = useRef(0)
function createStore(signal) {
let onChangeNotifyReact
const unsubscribe = effect(() => {
valueRef.current = signal.value
versionRef.current++
onChangeNotifyReact?.()
})
return {
subscribe(listener) {
onChangeNotifyReact = listener
return () => {
unsubscribe()
}
},
getSnapshop() {
return versionRef.current
},
}
}
const res = signal(valueRef.current)
const store = useMemo(() => {
return createStore(res)
}, [res])
useSyncExternalStore(store.subscribe, store.getSnapshop)
return res
// const _value = useRef(initValue)
// const [ref, setRef] = useState({ value: initValue })
// Object.defineProperty(ref, "value", {
// get() {
// return _value.current
// },
// set(newValue) {
// _value.current = newValue
// setRef({ value: newValue })
// },
// })
// return ref
}
export const reactive = useReactive
export const computed = (computeFn) => {
const res = preactComputed(computeFn)
function createStore(res) {
let onChangeNotifyReact
const unsubscribe = effect(() => {
res.value
onChangeNotifyReact?.()
})
return {
subscribe(listener) {
onChangeNotifyReact = listener
return () => {
unsubscribe()
}
},
getSnapshop() {
return res.value
},
}
}
const store = useMemo(() => {
return createStore(res)
}, [res])
useSyncExternalStore(store.subscribe, store.getSnapshop)
return res.value
}

export const watch = (sSatcher, fn) => {
const watcher = typeof sSatcher === "object" ? sSatcher : [sSatcher]
const value = useMemo(
() =>
watcher.map((x) => {
return typeof x == "function" ? x() : x
}),
[watcher]
)
const ref = useRef(value)
if (value.toString() !== ref.current.toString()) {
ref.current = value
}
useEffect(fn, ref.current)
}

export { effect }

這樣onMounted, onUnmounted也解決了useEffect裡面不能放async的function,那警告看了真的很礙眼,支援就不要吵,不支援就直接抱錯,在那邊吵什麼吵,然後為什麼不多包一層就好,我也不知道,可能礙於什麼東方神秘力量

reactive就直接用別人寫好的吧

watch就是watch

computed這麼好用的功能怎麼能不實現(但我沒實現getter、setter,沒錯因為我不常用)

ref就用useRef去解決變數被重置問題,我們來比較一下差別
// normal way
const [value, setValue] = useState(0)
setValue(1)
console.log(value) // 0 wtf!
useEffect(() => {
    console.log(value) // 1
}, [value])

// better way
const [value, setValue] = useState(0)
const newValue = 1
setValue(newValue)
console.log(newValue) // 1 .....why so troublesome?

// new way
const state = ref(0)
state.value++
console.log(state.value) // 1
// 484很直覺!484很爽!484很好寫


後記

最重要UC, QQ這些中國大牌瀏覽器到底有沒有支援Proxy呢~~答案是有的,至少上線到現在沒有傳出什麼災情
謎之聲:連Promise.race跟any都可以拔掉,竟然支援Proxy,真是可喜可賀~
不過想想也是拉,現在vue3都用Proxy寫了,如此泱泱大國總不會想跟自國工程師對著幹吧!

最後,我沒有實際測過使用起來的效能,我相信應該不會太好,畢竟有內容更新畫面就要重新渲染一次,原本react18 queue的優化都沒用了,但我才不管,開發體驗最重要!!事實上,你如果注重效能就不會選擇react框架了,那這篇就先到這裡拉,希望大家都可以準時下班,see u later~

12/18更新

computed並非是及時的,useSyncExternalStore只用來更新畫面,所以如果要用在邏輯演算中,用useMemo效果可能比較好,之後再來思考看有沒有機會debug,下面有個使用範例,這真令人有點難過

const users = [{ birthday: 2020-01-01}, {birthday:"2022-02-02"}]

let index = 0

const user = computed(() => users[index].birthday) 

console.log(user.birthday) // 2020-01-01

index = 1

console.log(user.birthday) // 2020-01-01 來不及更新


沒有留言:

張貼留言