vanjs-router
Advanced tools
+53
| ## Vanjs Router 的异常情况(2024 年 11 月 4 日) | ||
| Vanjs Router 的基本原理是创建一个 `now` 状态,通过监听 Window 的 `hashchange` 事件来更新 `now` 的值。每个路由内部通过 `van.derive(() => now.val)` 来监听路由的改变,而不是直接使用 `hashchange`。此外,`onLoad` 和 `onFirst` 也是在 `van.derive` 内部被执行的。接下来我们看下面的代码: | ||
| ```ts | ||
| const count = van.state(0) | ||
| // 可以监听到 count 值的变化 | ||
| van.derive(() => { | ||
| return count.val * 2 | ||
| }) | ||
| // 可以监听到 count 值的变化 | ||
| van.derive(() => { | ||
| count.val | ||
| setTimeout(() => { | ||
| count.val * 2 | ||
| }, 1000) | ||
| }) | ||
| // 可以监听到 count 值的变化 | ||
| van.derive(async () => { | ||
| count.val // 此处在同步上下文中保留了一个引用 | ||
| await new Promise((resolve) => setTimeout(resolve, 1000)) | ||
| return count.val * 2 // 该处不属于同步上下文,所以是无效引用,但由于有上面的引用,这个 derive 可以监听 | ||
| }) | ||
| // 无法监听到 count 值的变化 | ||
| van.derive(async () => { | ||
| await new Promise((resolve) => setTimeout(resolve, 1000)) | ||
| return count.val * 2 // 该处不属于同步上下文,且同步上下文中不存在状态值的引用,因此无法监听。 | ||
| }) | ||
| const func = async () => { | ||
| return count.val * 2 | ||
| } | ||
| van.derive(async () => { | ||
| return await func() // 可以监听到,因为 func 的同步上下文也是 derive 回调函数的同步上下文,因此可以被监听。 | ||
| }) | ||
| ``` | ||
| 可以看到,为了能监听到 `derive` 回调函数中的状态值变化,就必须让状态值位于回调函数的同步上下文中。 | ||
| 如果 `onLoad` 和 `onFirst` 的同步上下文中存在某些状态值的引用,当这些值被异步更新时,就会导致与 `onLoad` 和 `onFirst` 处于同一同步上下文的 `now` 值监听函数被触发,进而导致 `onLoad` 和 `onFirst` 被重复触发的 Bug。 | ||
| 因为函数的同步上下文会继承外部的同步上下文,而在继承上下文中的多个作用域中的状态值,都会被 `derive` 监听到。我们要解决的是确保 `onLoad` 和 `onFirst` 的同步上下文中存在的状态值引用不会触发外部的 `derive`,从而导致 `onLoad` 和 `onFirst` 被重复触发。 | ||
| 我们监听 `now` 的目的是实现对 `hashchange` 监听器的动态更新。只需创建一个全局的 `hashchange` 监听器,各个路由通过 `derive` 来共享 `hashchange` 的动态更新通知。期望 `derive` 的监听效果比 `hashchange` 更好,操作也更加便捷,且具备更好的可拓展性,比如可以将 `derive` 的返回值作为新的状态用于其他开发场景。 | ||
| 然而,我们要注意的是,写这样的 `derive` 来监听 `now` 的主要目的实际上就是监听 `hashchange`。创建多个 `hashchange` 监听器并不会对性能产生明显影响。如果确实需要用到 `Van State` 来监听路由变化,其实我们已经有 `now`,并且它被全局的 `hashchange` 自动更新。这个 `now` 本身就可以作为拓展性的接口状态值按需使用,而各个路由需要监听路由变化时,最好使用正常的独立 `hashchange` 监听器。这样可以实现最纯粹的路由变化监听,而无需担心 `derive` 在监听 `now` 的同时带来的数据和事件异常情况。 |
@@ -1,1 +0,1 @@ | ||
| var B=Object.defineProperty;var D=(t,e,s)=>e in t?B(t,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):t[e]=s;var o=(t,e,s)=>D(t,typeof e!="symbol"?e+"":e,s);let d=Object.getPrototypeOf,p,g,a,h,x={isConnected:1},J=1e3,v,A={},Q=d(x),H=d(d),c,M=(t,e,s,r)=>(t??(setTimeout(s,r),new Set)).add(e),W=(t,e,s)=>{let r=a;a=e;try{return t(s)}catch(n){return console.error(n),s}finally{a=r}},y=t=>t.filter(e=>{var s;return(s=e._dom)==null?void 0:s.isConnected}),k=t=>v=M(v,t,()=>{for(let e of v)e._bindings=y(e._bindings),e._listeners=y(e._listeners);v=c},J),b={get val(){var t;return(t=a==null?void 0:a._getters)==null||t.add(this),this.rawVal},get oldVal(){var t;return(t=a==null?void 0:a._getters)==null||t.add(this),this._oldVal},set val(t){var e;(e=a==null?void 0:a._setters)==null||e.add(this),t!==this.rawVal&&(this.rawVal=t,this._bindings.length+this._listeners.length?(g==null||g.add(this),p=M(p,this,U)):this._oldVal=t)}},G=t=>({__proto__:b,rawVal:t,_oldVal:t,_bindings:[],_listeners:[]}),m=(t,e)=>{let s={_getters:new Set,_setters:new Set},r={f:t},n=h;h=[];let l=W(t,s,e);l=(l??document).nodeType?l:new Text(l);for(let i of s._getters)s._setters.has(i)||(k(i),i._bindings.push(r));for(let i of h)i._dom=l;return h=n,r._dom=l},V=(t,e=G(),s)=>{let r={_getters:new Set,_setters:new Set},n={f:t,s:e};n._dom=s??(h==null?void 0:h.push(n))??x,e.val=W(t,r,e.rawVal);for(let l of r._getters)r._setters.has(l)||(k(l),l._listeners.push(n));return e},I=(t,...e)=>{for(let s of e.flat(1/0)){let r=d(s??0),n=r===b?m(()=>s.val):r===H?m(s):s;n!=c&&t.append(n)}return t},R=(t,e,...s)=>{var i;let[r,...n]=d(s[0]??0)===Q?s:[{},...s],l=t?document.createElementNS(t,e):document.createElement(e);for(let[f,_]of Object.entries(r)){let E=w=>w?Object.getOwnPropertyDescriptor(w,f)??E(d(w)):c,P=e+","+f,C=A[P]??(A[P]=((i=E(d(l)))==null?void 0:i.set)??0),F=f.startsWith("on")?(w,z)=>{let j=f.slice(2);l.removeEventListener(j,z),l.addEventListener(j,w)}:C?C.bind(l):l.setAttribute.bind(l,f),L=d(_??0);f.startsWith("on")||L===H&&(_=V(_),L=b),L===b?m(()=>(F(_.val,_._oldVal),l)):F(_)}return I(l,n)},T=t=>({get:(e,s)=>R.bind(c,t,s)}),K=(t,e)=>e?e!==t&&t.replaceWith(e):t.remove(),U=()=>{let t=0,e=[...p].filter(r=>r.rawVal!==r._oldVal);do{g=new Set;for(let r of new Set(e.flatMap(n=>n._listeners=y(n._listeners))))V(r.f,r.s,r._dom),r._dom=c}while(++t<100&&(e=[...g]).length);let s=[...p].filter(r=>r.rawVal!==r._oldVal);p=c;for(let r of new Set(s.flatMap(n=>n._bindings=y(n._bindings))))K(r._dom,m(r.f,r._dom)),r._dom=c;for(let r of s)r._oldVal=r.rawVal};const S={tags:new Proxy(t=>new Proxy(R,T(t)),T()),hydrate:(t,e)=>K(t,m(e,t)),add:I,state:G,derive:V},O=()=>location.hash?location.hash.slice(2):"home",u=S.state(O());window.addEventListener("hashchange",t=>{u.val=O()});class N{constructor(e){o(this,"rule");o(this,"args",[]);o(this,"Loader");o(this,"delayed",!1);o(this,"onFirst");o(this,"onLoad");o(this,"element");o(this,"isFirstLoad",!0);if(!e)throw new Error("config 不能为空");if(!e.rule)throw new Error("rule 不能为空");if(!e.Loader)throw new Error("Loader 不能为空");this.rule=e.rule,this.Loader=e.Loader,this.delayed=e.delayed||!1,this.onFirst=e.onFirst||(async()=>{}),this.onLoad=e.onLoad||(async()=>{}),this.element=this.Loader(),this.element.hidden=!0,S.derive(()=>{const s=this.matchHash();s?(async()=>(this.args.splice(0),this.args.push(...s.args),this.isFirstLoad&&(this.isFirstLoad=!1,await this.onFirst()),await this.onLoad(),this.delayed||this.show()))():this.hide()})}matchHash(){if(this.rule instanceof RegExp){const s=u.val.match(this.rule);return s?{hash:u.val,args:[...s].slice(1)}:!1}const e=u.val.split("/").filter(s=>s.length>0);return e.length<1&&e.push("home"),e[0]==this.rule?{hash:u.val,args:e.slice(1)}:!1}show(){this.element.hidden=!1}hide(){this.element.hidden=!0}}const $=t=>new N(t).element,q=(t,...e)=>{location.hash=t=="home"&&e.length==0?"":`/${[t,...e].join("/")}`},X=(t,e)=>{$({rule:t,Loader:S.tags.div,onLoad(){q(e)}})},Y={nowHash:O,now:u,Handler:N,Route:$,goto:q,redirect:X};Object.defineProperty(window,"router",{value:Y}); | ||
| var B=Object.defineProperty;var D=(t,e,s)=>e in t?B(t,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):t[e]=s;var o=(t,e,s)=>D(t,typeof e!="symbol"?e+"":e,s);let d=Object.getPrototypeOf,p,g,a,h,T={isConnected:1},J=1e3,v,j={},Q=d(T),x=d(d),c,H=(t,e,s,r)=>(t??(setTimeout(s,r),new Set)).add(e),M=(t,e,s)=>{let r=a;a=e;try{return t(s)}catch(n){return console.error(n),s}finally{a=r}},y=t=>t.filter(e=>{var s;return(s=e._dom)==null?void 0:s.isConnected}),W=t=>v=H(v,t,()=>{for(let e of v)e._bindings=y(e._bindings),e._listeners=y(e._listeners);v=c},J),b={get val(){var t;return(t=a==null?void 0:a._getters)==null||t.add(this),this.rawVal},get oldVal(){var t;return(t=a==null?void 0:a._getters)==null||t.add(this),this._oldVal},set val(t){var e;(e=a==null?void 0:a._setters)==null||e.add(this),t!==this.rawVal&&(this.rawVal=t,this._bindings.length+this._listeners.length?(g==null||g.add(this),p=H(p,this,U)):this._oldVal=t)}},k=t=>({__proto__:b,rawVal:t,_oldVal:t,_bindings:[],_listeners:[]}),m=(t,e)=>{let s={_getters:new Set,_setters:new Set},r={f:t},n=h;h=[];let l=M(t,s,e);l=(l??document).nodeType?l:new Text(l);for(let i of s._getters)s._setters.has(i)||(W(i),i._bindings.push(r));for(let i of h)i._dom=l;return h=n,r._dom=l},V=(t,e=k(),s)=>{let r={_getters:new Set,_setters:new Set},n={f:t,s:e};n._dom=s??(h==null?void 0:h.push(n))??T,e.val=M(t,r,e.rawVal);for(let l of r._getters)r._setters.has(l)||(W(l),l._listeners.push(n));return e},G=(t,...e)=>{for(let s of e.flat(1/0)){let r=d(s??0),n=r===b?m(()=>s.val):r===x?m(s):s;n!=c&&t.append(n)}return t},I=(t,e,...s)=>{var i;let[r,...n]=d(s[0]??0)===Q?s:[{},...s],l=t?document.createElementNS(t,e):document.createElement(e);for(let[_,f]of Object.entries(r)){let E=w=>w?Object.getOwnPropertyDescriptor(w,_)??E(d(w)):c,O=e+","+_,P=j[O]??(j[O]=((i=E(d(l)))==null?void 0:i.set)??0),C=_.startsWith("on")?(w,z)=>{let F=_.slice(2);l.removeEventListener(F,z),l.addEventListener(F,w)}:P?P.bind(l):l.setAttribute.bind(l,_),L=d(f??0);_.startsWith("on")||L===x&&(f=V(f),L=b),L===b?m(()=>(C(f.val,f._oldVal),l)):C(f)}return G(l,n)},A=t=>({get:(e,s)=>I.bind(c,t,s)}),R=(t,e)=>e?e!==t&&t.replaceWith(e):t.remove(),U=()=>{let t=0,e=[...p].filter(r=>r.rawVal!==r._oldVal);do{g=new Set;for(let r of new Set(e.flatMap(n=>n._listeners=y(n._listeners))))V(r.f,r.s,r._dom),r._dom=c}while(++t<100&&(e=[...g]).length);let s=[...p].filter(r=>r.rawVal!==r._oldVal);p=c;for(let r of new Set(s.flatMap(n=>n._bindings=y(n._bindings))))R(r._dom,m(r.f,r._dom)),r._dom=c;for(let r of s)r._oldVal=r.rawVal};const K={tags:new Proxy(t=>new Proxy(I,A(t)),A()),hydrate:(t,e)=>R(t,m(e,t)),add:G,state:k,derive:V},S=()=>location.hash?location.hash.slice(2):"home",u=K.state(S());window.addEventListener("hashchange",()=>{u.val=S()});class N{constructor(e){o(this,"rule");o(this,"args",[]);o(this,"Loader");o(this,"delayed",!1);o(this,"onFirst");o(this,"onLoad");o(this,"element");o(this,"isFirstLoad",!0);if(!e)throw new Error("config 不能为空");if(!e.rule)throw new Error("rule 不能为空");if(!e.Loader)throw new Error("Loader 不能为空");this.rule=e.rule,this.Loader=e.Loader,this.delayed=e.delayed||!1,this.onFirst=e.onFirst||(async()=>{}),this.onLoad=e.onLoad||(async()=>{}),this.element=this.Loader(),this.element.hidden=!0;const s=async()=>{const r=this.matchHash();r?(this.args.splice(0),this.args.push(...r.args),this.isFirstLoad&&(this.isFirstLoad=!1,await this.onFirst()),await this.onLoad(),this.delayed||this.show()):this.hide()};window.addEventListener("hashchange",s),s()}matchHash(){if(this.rule instanceof RegExp){const s=u.val.match(this.rule);return s?{hash:u.val,args:[...s].slice(1)}:!1}const e=u.val.split("/").filter(s=>s.length>0);return e.length<1&&e.push("home"),e[0]==this.rule?{hash:u.val,args:e.slice(1)}:!1}show(){this.element.hidden=!1}hide(){this.element.hidden=!0}}const $=t=>new N(t).element,q=(t,...e)=>{location.hash=t=="home"&&e.length==0?"":`/${[t,...e].join("/")}`},X=(t,e)=>{$({rule:t,Loader:K.tags.div,onLoad(){q(e)}})},Y={nowHash:S,now:u,Handler:N,Route:$,goto:q,redirect:X};Object.defineProperty(window,"router",{value:Y}); |
+15
-17
@@ -6,3 +6,3 @@ import van from 'vanjs-core'; | ||
| export const now = van.state(nowHash()); | ||
| window.addEventListener('hashchange', event => { | ||
| window.addEventListener('hashchange', () => { | ||
| now.val = nowHash(); | ||
@@ -40,6 +40,5 @@ }); | ||
| // 根据 Hash 的变化,自动更新路由状态 | ||
| van.derive(() => { | ||
| const func = async () => { | ||
| // 获取当前路由的命中状态 | ||
| const match = this.matchHash(); | ||
| const hidden = !match; | ||
| if (!match) { | ||
@@ -51,17 +50,16 @@ // 未被命中,刷新路由,页面隐藏。 | ||
| // 路由命中 | ||
| const func = async () => { | ||
| // 将接收到的路由参数保存起来 | ||
| this.args.splice(0); | ||
| this.args.push(...match.args); | ||
| if (this.isFirstLoad) { | ||
| this.isFirstLoad = false; | ||
| await this.onFirst(); | ||
| } | ||
| await this.onLoad(); | ||
| if (!this.delayed) | ||
| this.show(); | ||
| }; | ||
| func(); | ||
| // 将接收到的路由参数保存起来 | ||
| this.args.splice(0); // 清空存储的旧参数 | ||
| this.args.push(...match.args); | ||
| if (this.isFirstLoad) { | ||
| this.isFirstLoad = false; | ||
| await this.onFirst(); | ||
| } | ||
| await this.onLoad(); | ||
| if (!this.delayed) | ||
| this.show(); | ||
| } | ||
| }); | ||
| }; | ||
| window.addEventListener('hashchange', func); | ||
| func(); | ||
| } | ||
@@ -68,0 +66,0 @@ /** 判断当前 Hash 是否与本路由的规则匹配 */ |
+15
-15
| { | ||
| "devDependencies": { | ||
| "typescript": "^5.5.4", | ||
| "vite": "^5.3.5" | ||
| }, | ||
| "dependencies": { | ||
| "vanjs-core": "^1.5.1" | ||
| }, | ||
| "type": "module", | ||
| "name": "vanjs-router", | ||
| "version": "2.0.8", | ||
| "main": "js/router.js", | ||
| "types": "src/router.ts", | ||
| "scripts": { | ||
| "build": "vite build && tsc" | ||
| } | ||
| "devDependencies": { | ||
| "typescript": "^5.5.4", | ||
| "vite": "^5.3.5" | ||
| }, | ||
| "dependencies": { | ||
| "vanjs-core": "^1.5.1" | ||
| }, | ||
| "scripts": { | ||
| "build": "vite build && tsc" | ||
| }, | ||
| "type": "module", | ||
| "name": "vanjs-router", | ||
| "version": "2.1.0", | ||
| "main": "js/router.js", | ||
| "types": "src/router.ts" | ||
| } |
+5
-4
@@ -22,3 +22,4 @@ # vanjs-router | ||
| ```html | ||
| <script src="https://cdn.jsdelivr.net/npm/vanjs-router@latest/dist/vanjs-router.min.js"></script> | ||
| <script type="text/javascript" src="https://cdn.jsdelivr.net/gh/vanjs-org/van/public/van-1.5.2.nomodule.min.js"></script> | ||
| <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/vanjs-router@latest/dist/vanjs-router.min.js"></script> | ||
| <script> | ||
@@ -37,6 +38,6 @@ const { Route, goto } = router; | ||
| }, | ||
| onFirst() { | ||
| async onFirst() { | ||
| console.log("home onfirst"); | ||
| }, | ||
| onLoad() { | ||
| async onLoad() { | ||
| console.log("home onload"); | ||
@@ -56,3 +57,3 @@ }, | ||
| }, | ||
| onLoad() { | ||
| async onLoad() { | ||
| this.show(); | ||
@@ -59,0 +60,0 @@ }, |
+13
-15
@@ -9,3 +9,3 @@ import van, { State } from 'vanjs-core' | ||
| window.addEventListener('hashchange', event => { | ||
| window.addEventListener('hashchange', () => { | ||
| now.val = nowHash() | ||
@@ -65,6 +65,5 @@ }) | ||
| // 根据 Hash 的变化,自动更新路由状态 | ||
| van.derive(() => { | ||
| const func = async () => { | ||
| // 获取当前路由的命中状态 | ||
| const match = this.matchHash() | ||
| const hidden = !match | ||
| if (!match) { | ||
@@ -75,16 +74,15 @@ // 未被命中,刷新路由,页面隐藏。 | ||
| // 路由命中 | ||
| const func = async () => { | ||
| // 将接收到的路由参数保存起来 | ||
| this.args.splice(0) | ||
| this.args.push(...match.args) | ||
| if (this.isFirstLoad) { | ||
| this.isFirstLoad = false | ||
| await this.onFirst() | ||
| } | ||
| await this.onLoad() | ||
| if (!this.delayed) this.show() | ||
| // 将接收到的路由参数保存起来 | ||
| this.args.splice(0) // 清空存储的旧参数 | ||
| this.args.push(...match.args) | ||
| if (this.isFirstLoad) { | ||
| this.isFirstLoad = false | ||
| await this.onFirst() | ||
| } | ||
| func() | ||
| await this.onLoad() | ||
| if (!this.delayed) this.show() | ||
| } | ||
| }) | ||
| } | ||
| window.addEventListener('hashchange', func) | ||
| func() | ||
| } | ||
@@ -91,0 +89,0 @@ |
19687
21.39%9
12.5%63
1.61%253
-1.56%