wxy-micro-dom
Version:
一个类似jquery调用方法、性能超越react的虚拟轻量级dom库
2 lines (1 loc) • 17.6 kB
JavaScript
(function(){"use strict";class ReactiveSystem{constructor(){this.subscribers=new Map;this.currentComputed=null;this.proxyCache=new WeakMap;this.computedCache=new WeakMap;this.effectQueue=new Set;this.batchDepth=0;this.effectId=0}reactive(data){if(typeof data!=="object"||data===null)return data;if(this.proxyCache.has(data)){return this.proxyCache.get(data)}const self=this;const proxy=new Proxy(data,{get(target,key){self.track(target,key);const result=Reflect.get(target,key);return typeof result==="object"?self.reactive(result):result},set(target,key,value){const oldValue=target[key];const result=Reflect.set(target,key,value);if(oldValue!==value){self.trigger(target,key)}return result},deleteProperty(target,key){const hadKey=Object.prototype.hasOwnProperty.call(target,key);const result=Reflect.deleteProperty(target,key);if(hadKey){self.trigger(target,key)}return result}});this.proxyCache.set(data,proxy);return proxy}track(target,key){if(!this.currentComputed)return;const targetKey=`${target}_${key}`;if(!this.subscribers.has(targetKey)){this.subscribers.set(targetKey,new Set)}this.subscribers.get(targetKey).add(this.currentComputed)}trigger(target,key){const targetKey=`${target}_${key}`;if(this.subscribers.has(targetKey)){this.subscribers.get(targetKey).forEach(effect=>{if(this.batchDepth>0){this.effectQueue.add(effect)}else{this.runEffect(effect)}})}}runEffect(effect){if(!effect.active)return;if(typeof effect==="function"){effect()}else if(effect.update){effect.update()}}batch(fn){this.batchDepth++;try{fn()}finally{this.batchDepth--;if(this.batchDepth===0){this.flushEffects()}}}flushEffects(){const effects=Array.from(this.effectQueue);this.effectQueue.clear();effects.sort((a,b)=>(b.priority||0)-(a.priority||0));effects.forEach(effect=>this.runEffect(effect))}computed(fn,options={}){const cacheKey=options.cacheKey||fn;if(this.computedCache.has(cacheKey)){return this.computedCache.get(cacheKey)}const computedObj={id:this.effectId++,value:null,active:true,priority:options.priority||0,update(){const newValue=fn();if(newValue!==this.value){this.value=newValue;if(this.callback){this.callback(this.value)}}}};this.currentComputed=computedObj;computedObj.update();this.currentComputed=null;const computedWrapper={get value(){return computedObj.value},onUpdate(callback){computedObj.callback=callback;return this},dispose(){computedObj.active=false}};this.computedCache.set(cacheKey,computedWrapper);return computedWrapper}watch(getter,callback,options={}){const watcher={id:this.effectId++,value:null,active:true,priority:options.priority||0,update(){const newValue=getter();if(newValue!==this.value||options.deep){const oldValue=this.value;this.value=newValue;callback(newValue,oldValue)}}};this.currentComputed=watcher;watcher.update();this.currentComputed=null;return{dispose(){watcher.active=false}}}effect(fn,options={}){const effect={id:this.effectId++,active:true,priority:options.priority||0,run:fn};this.currentComputed=effect;fn();this.currentComputed=null;return{dispose(){effect.active=false}}}}const reactiveSystem=new ReactiveSystem;class VNode{constructor(tag,props={},children=[],key,isStatic=false){this.tag=tag;this.props=props;this.children=children;this.key=key;this.el=null;this.reactiveProps={};this.computedValues=[];this.isStatic=isStatic;this._events={};this.staticNodes=new Map;this.memoizedProps=null;this.memoizedChildren=null}createElement(){if(this.isStatic&&this.staticNodes.has(this.getStaticKey())){return this.staticNodes.get(this.getStaticKey()).cloneNode(true)}if(typeof this.tag==="string"){this.el=document.createElement(this.tag);this.processProps();this.processChildren();if(this.isStatic){this.staticNodes.set(this.getStaticKey(),this.el.cloneNode(true))}return this.el}return document.createTextNode("")}getStaticKey(){return this.key||this.tag+JSON.stringify(this.props)}processProps(){const staticProps={};const reactiveProps={};for(const[key,value]of Object.entries(this.props)){if(key.startsWith("r:")){reactiveProps[key.substring(2)]=value}else{staticProps[key]=value}}this.setElementProps(staticProps);this.processReactiveProps(reactiveProps)}processReactiveProps(props){for(const[key,value]of Object.entries(props)){if(typeof value==="function"){const computed=reactiveSystem.computed(value,{cacheKey:`${this.tag}_${key}_${this.key||""}`});computed.onUpdate(newValue=>{this.reactiveProps[key]=newValue;this.setElementAttribute(key,newValue)});this.reactiveProps[key]=computed.value;this.computedValues.push(computed)}else{this.reactiveProps[key]=value}this.setElementAttribute(key,this.reactiveProps[key])}}processChildren(){const fragment=document.createDocumentFragment();this.children.forEach(child=>{if(child instanceof VNode){if(child.isStatic){const staticEl=child.createElement();fragment.appendChild(staticEl)}else{fragment.appendChild(child.createElement())}}else if(child instanceof DOMWrapper){fragment.appendChild(child.el)}else if(typeof child==="function"){fragment.appendChild(this.createReactiveTextNode(child))}else{fragment.appendChild(document.createTextNode(String(child)))}});this.el.appendChild(fragment)}createReactiveTextNode(getter){const textNode=document.createTextNode("");const computed=reactiveSystem.computed(getter,{cacheKey:`text_${this.tag}_${this.key||""}_${getter.toString()}`});computed.onUpdate(newValue=>{textNode.textContent=newValue});this.computedValues.push(computed);textNode.textContent=computed.value;return textNode}setElementProps(props){for(const[key,value]of Object.entries(props)){this.setElementAttribute(key,value)}}setElementAttribute(key,value){if(!this.el)return;if(key==="style"&&typeof value==="object"){Object.assign(this.el.style,value)}else if(key.startsWith("on")){const eventName=key.substring(2).toLowerCase();if(this._events[eventName]){this.el.removeEventListener(eventName,this._events[eventName])}this.el.addEventListener(eventName,value);this._events[eventName]=value}else if(value===true){this.el.setAttribute(key,"")}else if(value===false||value===null||value===undefined){this.el.removeAttribute(key)}else{this.el.setAttribute(key,value)}}updateElement(parentEl,newNode,oldNode,index=0){if(newNode.isStatic&&oldNode.isStatic&&newNode.tag===oldNode.tag&&newNode.key===oldNode.key){newNode.el=oldNode.el;return}if(!oldNode){parentEl.appendChild(newNode.createElement());return}if(!newNode){if(parentEl.childNodes[index]){parentEl.removeChild(parentEl.childNodes[index])}return}if(newNode.tag!==oldNode.tag||newNode.key!==oldNode.key){parentEl.replaceChild(newNode.createElement(),oldNode.el);return}newNode.el=oldNode.el;this.updateAttributes(newNode,oldNode);this.updateChildren(newNode,oldNode)}updateAttributes(newNode,oldNode){const newProps={...newNode.props,...newNode.reactiveProps};const oldProps={...oldNode.props,...oldNode.reactiveProps};const el=oldNode.el;const allKeys=new Set([...Object.keys(newProps),...Object.keys(oldProps)]);allKeys.forEach(key=>{const newValue=newProps[key];const oldValue=oldProps[key];if(newValue===oldValue)return;if(newValue===undefined){if(key==="style"){el.style=""}else if(key.startsWith("on")){el.removeEventListener(key.substring(2).toLowerCase(),oldValue)}else{el.removeAttribute(key)}}else{this.setElementAttribute(key,newValue)}})}updateChildren(newNode,oldNode){const newCh=newNode.children;const oldCh=oldNode.children;const el=oldNode.el;if(newCh.length===0&&oldCh.length===0)return;if(newCh.length===1&&oldCh.length===1&&typeof newCh[0]==="string"&&typeof oldCh[0]==="string"){if(newCh[0]!==oldCh[0]){el.textContent=newCh[0]}return}this.diffChildren(el,newCh,oldCh)}diffChildren(parentEl,newCh,oldCh){let oldStartIdx=0,newStartIdx=0;let oldEndIdx=oldCh.length-1,newEndIdx=newCh.length-1;let oldStartNode=oldCh[oldStartIdx],oldEndNode=oldCh[oldEndIdx];let newStartNode=newCh[newStartIdx],newEndNode=newCh[newEndIdx];let keyToIdx,idxInOld,elmToMove;while(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){if(!oldStartNode){oldStartNode=oldCh[++oldStartIdx]}else if(!oldEndNode){oldEndNode=oldCh[--oldEndIdx]}else if(this.isSameVNode(newStartNode,oldStartNode)){this.updateElement(parentEl,newStartNode,oldStartNode);newStartNode=newCh[++newStartIdx];oldStartNode=oldCh[++oldStartIdx]}else if(this.isSameVNode(newEndNode,oldEndNode)){this.updateElement(parentEl,newEndNode,oldEndNode);newEndNode=newCh[--newEndIdx];oldEndNode=oldCh[--oldEndIdx]}else if(this.isSameVNode(newStartNode,oldEndNode)){parentEl.insertBefore(oldEndNode.el,oldStartNode.el);this.updateElement(parentEl,newStartNode,oldEndNode);newStartNode=newCh[++newStartIdx];oldEndNode=oldCh[--oldEndIdx]}else if(this.isSameVNode(newEndNode,oldStartNode)){parentEl.insertBefore(oldStartNode.el,oldEndNode.el.nextSibling);this.updateElement(parentEl,newEndNode,oldStartNode);newEndNode=newCh[--newEndIdx];oldStartNode=oldCh[++oldStartIdx]}else{if(!keyToIdx)keyToIdx=this.createKeyToOldIdx(oldCh,oldStartIdx,oldEndIdx);idxInOld=newStartNode.key?keyToIdx[newStartNode.key]:null;if(!idxInOld){parentEl.insertBefore(newStartNode.createElement(),oldStartNode.el)}else{elmToMove=oldCh[idxInOld];if(this.isSameVNode(newStartNode,elmToMove)){this.updateElement(parentEl,newStartNode,elmToMove);oldCh[idxInOld]=undefined;parentEl.insertBefore(elmToMove.el,oldStartNode.el)}else{parentEl.insertBefore(newStartNode.createElement(),oldStartNode.el)}}newStartNode=newCh[++newStartIdx]}}if(oldStartIdx>oldEndIdx){const refNode=newCh[newEndIdx+1]?newCh[newEndIdx+1].el:null;for(let i=newStartIdx;i<=newEndIdx;i++){parentEl.insertBefore(newCh[i].createElement(),refNode)}}else if(newStartIdx>newEndIdx){for(let i=oldStartIdx;i<=oldEndIdx;i++){if(oldCh[i]){parentEl.removeChild(oldCh[i].el)}}}}createKeyToOldIdx(children,beginIdx,endIdx){const map={};for(let i=beginIdx;i<=endIdx;i++){const key=children[i]?.key;if(key)map[key]=i}return map}isSameVNode(a,b){return a?.tag===b?.tag&&a?.key===b?.key}}const batchUpdateQueue=new Set;let isBatchUpdating=false;let rafId=null;function enqueueUpdate(updateFn){batchUpdateQueue.add(updateFn);if(!isBatchUpdating){isBatchUpdating=true;if(typeof queueMicrotask!=="undefined"){queueMicrotask(()=>{requestAnimationFrame(flushUpdates)})}else if(typeof Promise!=="undefined"){Promise.resolve().then(()=>{requestAnimationFrame(flushUpdates)})}else{rafId=requestAnimationFrame(flushUpdates)}}}function flushUpdates(){try{const updates=Array.from(batchUpdateQueue);batchUpdateQueue.clear();updates.sort((a,b)=>{const aIsStyle=a.toString().includes("style");const bIsStyle=b.toString().includes("style");return bIsStyle-aIsStyle});reactiveSystem.batch(()=>{updates.forEach(update=>update())})}finally{isBatchUpdating=false;if(rafId){cancelAnimationFrame(rafId);rafId=null}}}class DOMWrapper{constructor(selector){if(typeof selector==="string"){this.el=document.querySelector(selector)}else if(selector instanceof Element){this.el=selector}else if(selector instanceof VNode){this.vnode=selector;this.el=selector.createElement()}else{this.el=null}this.vnode=null;this._events={};this._onMountCallbacks=[];this._onUnmountCallbacks=[];this._disposables=[]}vdom(tag,props,children){this.vnode=new VNode(tag,props,children);return this}reactiveVdom(tag,props,children){const vnode=new VNode(tag,props,children);for(const[key,value]of Object.entries(props)){if(key.startsWith("w:")){const propName=key.substring(2);if(typeof value==="function"){const computed=this.computed(value);computed.onUpdate(newValue=>{vnode.reactiveProps[propName]=newValue;if(vnode.el){vnode.setElementAttribute(propName,newValue)}});vnode.reactiveProps[propName]=computed.value}}}this.vnode=vnode;return this}static(isStatic=true){if(this.vnode){this.vnode.isStatic=isStatic}return this}mount(container){if(!this.vnode)return this;if(container){if(typeof container==="string"){container=document.querySelector(container)}else if(container instanceof DOMWrapper){container=container.el}if(container){enqueueUpdate(()=>{if(container.firstChild){this.vnode.updateElement(container,this.vnode,new VNode(container.firstChild))}else{container.appendChild(this.vnode.createElement())}this._onMountCallbacks.forEach(cb=>cb())})}}else if(this.el){enqueueUpdate(()=>{this.vnode.updateElement(this.el.parentNode,this.vnode,new VNode(this.el));this._onMountCallbacks.forEach(cb=>cb())})}return this}onMount(callback){if(typeof callback==="function"){this._onMountCallbacks.push(callback)}return this}onUnmount(callback){if(typeof callback==="function"){this._onUnmountCallbacks.push(callback)}return this}dispose(){this._disposables.forEach(dispose=>dispose());this._disposables=[];this._onUnmountCallbacks.forEach(cb=>cb());return this}set html(content){enqueueUpdate(()=>{if(this.el)this.el.innerHTML=content});return this}get html(){return this.el?.innerHTML}set text(content){enqueueUpdate(()=>{if(this.el)this.el.textContent=content});return this}get text(){return this.el?.textContent}append(child){enqueueUpdate(()=>{if(!this.el)return;if(child instanceof DOMWrapper){this.el.appendChild(child.el)}else if(child instanceof Element){this.el.appendChild(child)}else if(child instanceof VNode){this.el.appendChild(child.createElement())}else{this.el.appendChild(document.createTextNode(String(child)))}});return this}on(event,handler){if(this.el){if(this._events[event]){this.el.removeEventListener(event,this._events[event])}this.el.addEventListener(event,handler);this._events[event]=handler}return this}off(event){if(this.el){this.el.removeEventListener(event,this._events[event]);delete this._events[event]}return this}delegate(selector,event,handler){if(!this.el)return this;const wrappedHandler=e=>{let target=e.target;while(target&&target!==this.el){if(target.matches(selector)){handler.call(target,e);break}target=target.parentNode}};this.on(event,wrappedHandler);this._disposables.push(()=>{this.off(event,wrappedHandler)});return this}css(styles){if(this.el){enqueueUpdate(()=>{Object.assign(this.el.style,styles)})}return this}addClass(className){if(this.el){enqueueUpdate(()=>{this.el.classList.add(className)})}return this}removeClass(className){if(this.el){enqueueUpdate(()=>{this.el.classList.remove(className)})}return this}toggleClass(className){if(this.el){enqueueUpdate(()=>{this.el.classList.toggle(className)})}return this}attr(name,value){if(value===undefined)return this.el?.getAttribute(name);enqueueUpdate(()=>{if(this.el)this.el.setAttribute(name,value)});return this}delAttr(name){enqueueUpdate(()=>{if(this.el)this.el.removeAttribute(name)});return this}find(selector){return this.el?$(this.el.querySelector(selector)):$(null)}get parent(){return this.el?$(this.el.parentNode):$(null)}get children(){if(!this.el)return[];return Array.from(this.el.children).map(el=>$(el))}show(){if(this.el){enqueueUpdate(()=>{this.el.style.display=""})}return this}hide(){if(this.el){enqueueUpdate(()=>{this.el.style.display="none"})}return this}reactive(data){return reactiveSystem.reactive(data)}computed(fn){const computed=reactiveSystem.computed(fn);this._disposables.push(()=>computed.dispose());return computed}watch(getter,callback){const watcher=reactiveSystem.watch(getter,callback);this._disposables.push(()=>watcher.dispose());return watcher}effect(fn){const effect=reactiveSystem.effect(fn);this._disposables.push(()=>effect.dispose());return effect}model(reactiveObj,propName){if(this.el&&this.el.tagName==="INPUT"){this.el.value=reactiveObj[propName]||"";this.on("input",()=>{reactiveObj[propName]=this.el.value});this.watch(()=>reactiveObj[propName],newVal=>{if(this.el.value!==newVal){this.el.value=newVal}})}return this}animate(keyframes,options){if(this.el){return this.el.animate(keyframes,options)}return null}frontUpdate(fn){reactiveSystem.batch(()=>{const prevBatch=isBatchUpdating;isBatchUpdating=true;try{fn()}finally{isBatchUpdating=prevBatch;if(!isBatchUpdating){flushUpdates()}}});return this}mountList(items,renderItem){if(!this.el)return this;const fragment=document.createDocumentFragment();const vnodes=items.map((item,index)=>{const vnode=renderItem(item,index);if(vnode instanceof VNode){fragment.appendChild(vnode.createElement())}return vnode});this.empty();this.el.appendChild(fragment);this.vnode=new VNode("div",{},vnodes);return this}}const createNegativeArray=arr=>new Proxy(arr,{get(target,prop){if(String(prop).startsWith("-")){const index=Number(prop);if(isNaN(index))return target[prop];return target[target.length+index]}return target[prop]},set(target,prop,value){if(String(prop).startsWith("-")){const index=Number(prop);if(!isNaN(index)){target[target.length+index]=value;return true}}target[prop]=value;return true}});function $(tag,props,children,key,isStatic){if(key===undefined&&props===undefined&&children===undefined&&isStatic===undefined){if(typeof tag==="string"){return new DOMWrapper(tag)}else if(typeof tag==="function"){reactiveSystem.batch(tag)}}else{return new DOMWrapper(new VNode(tag,props||{},children||[],key,isStatic||false))}}function $$(selector){return createNegativeArray(Array.from(document.querySelectorAll(selector)).map(it=>new DOMWrapper(it)))}$.static=(tag,props={},children=[])=>{const vnode=new VNode(tag,props,children,null,true);return new DOMWrapper(vnode)};$.memo=fn=>{let lastArgs,lastResult;return(...args)=>{if(lastArgs&&args.every((arg,i)=>arg===lastArgs[i])){return lastResult}lastArgs=args;lastResult=fn(...args);return lastResult}};$.reactive=data=>reactiveSystem.reactive(data);$.computed=fn=>reactiveSystem.computed(fn);$.watch=(getter,callback)=>reactiveSystem.watch(getter,callback);$.effect=fn=>reactiveSystem.effect(fn);$.rdom=(tag,props,children)=>{const dom=new DOMWrapper;return dom.reactiveVdom(tag,props,children)};$.RS=reactiveSystem;window.$=$;window.$$=$$})();