Last active
August 10, 2023 10:26
-
-
Save AimWhy/ccdc88713fe3bcfd80b93971049e18e5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const HTML_TAGS = | |
`html,body,base,head,link,meta,style,title,address,article,aside,footer,header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot`.split( | |
"," | |
); | |
const specialBooleanAttrs = | |
`itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`.split( | |
"," | |
); | |
const makeMap = (list) => { | |
const map = Object.create(null); | |
for (let i = 0; i < list.length; i++) { | |
map[list[i]] = true; | |
} | |
return (val) => !!map[val]; | |
}; | |
const isHTMLTag = makeMap(HTML_TAGS); | |
const isSpecialBooleanAttr = makeMap(specialBooleanAttrs); | |
const includeBooleanAttr = (value) => !!value || value === ""; | |
const isTextElement = (element) => element.type === "text"; | |
const resolvedPromise = Promise.resolve(); | |
const queueMicrotask = | |
window.queueMicrotask || | |
((callback) => { | |
if (typeof callback !== "function") { | |
throw new TypeError("The argument to queueMicrotask must be a function."); | |
} | |
resolvedPromise.then(callback).catch((error) => | |
setTimeout(() => { | |
throw error; | |
}, 0) | |
); | |
}); | |
const uniqueSet = new Set(); | |
const queueMicrotaskOnce = (func) => { | |
if (!uniqueSet.has(func)) { | |
uniqueSet.add(func); | |
queueMicrotask(() => { | |
func(); | |
uniqueSet.delete(func); | |
}); | |
} | |
}; | |
let isMessageLoopRunning = false; | |
const scheduledCallbackQueue = []; | |
const performWork = () => { | |
if (scheduledCallbackQueue.length) { | |
try { | |
const work = scheduledCallbackQueue.shift(); | |
work(); | |
} finally { | |
if (scheduledCallbackQueue.length) { | |
schedulePerform(); | |
} else { | |
isMessageLoopRunning = false; | |
} | |
} | |
} else { | |
isMessageLoopRunning = false; | |
} | |
}; | |
const channel = new MessageChannel(); | |
channel.port1.onmessage = performWork; | |
const schedulePerform = () => channel.port2.postMessage(null); | |
const requestCallback = (callback) => { | |
scheduledCallbackQueue.push(callback); | |
if (!isMessageLoopRunning) { | |
isMessageLoopRunning = true; | |
schedulePerform(); | |
} | |
}; | |
function shallowEqual(object1, object2) { | |
if (object1 === object2) { | |
return true; | |
} | |
if ( | |
typeof object1 !== "object" || | |
typeof object2 !== "object" || | |
object1 === null || | |
object2 === null | |
) { | |
return false; | |
} | |
const keys1 = Object.keys(object1); | |
const keys2 = Object.keys(object2); | |
if (keys1.length !== keys2.length) { | |
return false; | |
} | |
for (const key of keys1) { | |
if (object1[key] !== object2[key]) { | |
return false; | |
} | |
} | |
return true; | |
} | |
const firstNextWithProps = (generatorFunction) => { | |
return (...args) => { | |
const generatorObject = generatorFunction(...args); | |
generatorObject.next(); | |
const result = { | |
next: (...args) => { | |
return generatorObject.next(...args); | |
}, | |
throw: (...args) => { | |
return generatorObject.throw(...args); | |
}, | |
return: (...args) => { | |
return generatorObject.return(...args); | |
}, | |
StatusLane: NoneLane, | |
}; | |
generatorObject.next(result); | |
return result; | |
}; | |
}; | |
const elementWalker = (element, fun) => { | |
let cursor = element; | |
if (!cursor.child) { | |
fun(cursor); | |
return; | |
} | |
while (true) { | |
while (cursor.child) { | |
cursor = cursor.child; | |
} | |
while (!cursor.sibling) { | |
fun(cursor); | |
if (cursor === element) { | |
return; | |
} | |
cursor = cursor.return; | |
} | |
fun(cursor); | |
if (cursor === element) { | |
return; | |
} | |
cursor = cursor.sibling; | |
} | |
}; | |
const renderList = new Set(); | |
const pushRenderElement = (generator) => { | |
renderList.add(generator); | |
}; | |
const getCommonRenderElement = () => { | |
const elements = [...renderList.values()].map((gen) => gen.element); | |
renderList.clear(); | |
const parentMap = new Map(); | |
for (const el of elements) { | |
let parent = el; | |
while (parent) { | |
const count = (parentMap.get(parent) || 0) + 1; | |
if (count === elements.length) { | |
return parent; | |
} | |
parentMap.set(parent, count); | |
parent = parent.return; | |
} | |
} | |
}; | |
const genCursorFix = () => { | |
const focusedElement = document.activeElement; | |
const start = focusedElement.selectionStart; | |
const end = focusedElement.selectionEnd; | |
return () => { | |
// 重新定位焦点, 恢复选择位置 | |
focusedElement.focus(); | |
focusedElement.selectionStart = start; | |
focusedElement.selectionEnd = end; | |
}; | |
}; | |
const forceRender = () => { | |
const cursorFix = genCursorFix(); | |
const element = getCommonRenderElement(); | |
const [startComment, endComment] = getCommentRang(element._key); | |
// 删除旧的注释节点 | |
const range = document.createRange(); | |
range.setStartAfter(startComment); | |
range.setEndAfter(endComment); | |
range.deleteContents(); | |
let returnStateNode; | |
if (element.return) { | |
returnStateNode = element.return.stateNode; | |
element.return.stateNode = null; | |
} | |
const existKeySet = new Set(); | |
elementWalker(element, (el) => { | |
existKeySet.add(el._key); | |
}); | |
innerRender(element, existKeySet); | |
if (element.return) { | |
element.return.stateNode = returnStateNode; | |
} | |
startComment.parentNode.insertBefore(element.stateNode, startComment); | |
startComment.remove(); | |
cursorFix(); | |
}; | |
const checkIfSnapshotChanged = ({ value, getSnapshot }) => { | |
try { | |
return value !== getSnapshot(); | |
} catch { | |
return true; | |
} | |
}; | |
function* withStateFun(func, innerRender) { | |
const self = yield; | |
let StateIndex = 0; | |
const hookQueue = []; | |
const isHostElement = Object.values(buildIn).includes(func); | |
const useState = (initialState) => { | |
const innerIndex = StateIndex++; | |
if (hookQueue.length <= innerIndex) { | |
if (typeof initialState === "function") { | |
initialState = initialState(); | |
} | |
hookQueue[innerIndex] = initialState; | |
} | |
return [ | |
hookQueue[innerIndex], | |
(newState) => { | |
if (typeof newState === "function") { | |
const oldState = hookQueue[innerIndex]; | |
newState = newState(oldState); | |
} | |
hookQueue[innerIndex] = newState; | |
pushRenderElement(self); | |
queueMicrotaskOnce(forceRender); | |
}, | |
]; | |
}; | |
const effects = []; | |
const cleanEffects = []; | |
const useEffect = (effect, deps) => { | |
const innerIndex = StateIndex++; | |
const oldEffect = hookQueue[innerIndex]; | |
hookQueue[innerIndex] = [effect, deps]; | |
if (deps == void 0) { | |
return effects.push(effect); | |
} | |
if (self.StatusLane === MountedLane) { | |
effects.push(effect); | |
effect.mountDep = !deps.length; | |
} else if (deps.length && !shallowEqual(deps, oldEffect[1])) { | |
effects.push(effect); | |
} | |
}; | |
const useSyncExternalStore = (subscribe, getSnapshot) => { | |
const value = getSnapshot(); | |
const [{ inst }, forceUpdate] = useState({ | |
inst: { value, getSnapshot }, | |
}); | |
useEffect(() => { | |
if (checkIfSnapshotChanged(inst)) { | |
forceUpdate({ inst }); | |
} | |
return subscribe(function handleStoreChange(_cur, _pre) { | |
if (checkIfSnapshotChanged(inst)) { | |
forceUpdate({ inst }); | |
} | |
}); | |
}, [subscribe]); | |
return value; | |
}; | |
self.flushEffects = function flushEffects() { | |
while (effects.length) { | |
const current = effects.shift(); | |
const clean = current(); | |
if (typeof clean === "function") { | |
clean.mountDep = current.mountDep; | |
cleanEffects.push(clean); | |
} | |
} | |
self.StatusLane = UpdatedLane; | |
}; | |
self.flushCleanEffects = function flushCleanEffects(isUnmounted) { | |
const temp = []; | |
while (cleanEffects.length) { | |
const clean = cleanEffects.shift(); | |
const isUnmountClean = clean.mountDep; | |
if (isUnmounted) { | |
clean(); | |
} else { | |
if (!isUnmountClean) { | |
clean(); | |
} else { | |
temp.push(clean); | |
} | |
} | |
} | |
if (isUnmounted) { | |
props = void 0; | |
effects.length = 0; | |
cleanEffects.length = 0; | |
hookQueue.length = 0; | |
} else { | |
cleanEffects.push(...temp); | |
} | |
}; | |
const hookMap = { | |
useState, | |
useEffect, | |
useSyncExternalStore, | |
}; | |
let props = void 0; | |
let newProps = (yield) || {}; | |
let result = null; | |
while (true) { | |
self.flushCleanEffects(); | |
if (isHostElement) { | |
newProps = fixProps(newProps); | |
} | |
StateIndex = 0; | |
result = func.call(hookMap, newProps, props, hookMap); | |
props = newProps; | |
newProps = yield result; | |
hookMap.instance = result; | |
} | |
} | |
const componentCreator = firstNextWithProps(withStateFun); | |
const optionsModifierRE = /(?:Once|Passive|Capture)$/; | |
function parseName(name) { | |
let options; | |
if (optionsModifierRE.test(name)) { | |
options = {}; | |
let m; | |
while ((m = name.match(optionsModifierRE))) { | |
name = name.slice(0, name.length - m[0].length); | |
options[m[0].toLowerCase()] = true; | |
} | |
} | |
const event = name.slice(2).toLowerCase(); | |
return [event, options]; | |
} | |
function onCompositionStart(e) { | |
e.target.composing = true; | |
} | |
function onCompositionEnd(e) { | |
const target = e.target; | |
if (target.composing) { | |
target.composing = false; | |
target.dispatchEvent(new Event("input")); | |
} | |
} | |
function inputWrap(fun) { | |
return function (e) { | |
if (!e.target.composing) { | |
fun(e); | |
} | |
}; | |
} | |
function fixProps(oldProps) { | |
const newProps = { ...oldProps }; | |
if ("onInput" in newProps) { | |
newProps["onCompositionstart"] = onCompositionStart; | |
newProps["onCompositionend"] = onCompositionEnd; | |
newProps["onChange"] = onCompositionEnd; | |
newProps["onInput"] = inputWrap(newProps["onInput"]); | |
} | |
return newProps; | |
} | |
const buildIn = {}; | |
const genBuildInFun = function ($tag) { | |
const func = function ( | |
props = {}, | |
oldProps = {}, | |
{ instance, useState, useEffect } | |
) { | |
const [invokers] = useState({}); | |
// console.log(`${$tag} 重用`, instance); | |
const element = instance || document.createElement($tag); | |
element.innerHTML = ""; | |
const deleteMap = { ...oldProps }; | |
for (const [pKey, pValue] of Object.entries(props)) { | |
if (pKey.match(/^on[A-Z]/)) { | |
const [eventName, options] = parseName(pKey); | |
if (!invokers[pKey]) { | |
invokers[pKey] = { | |
raw: pValue, | |
handler(event) { | |
invokers[pKey].raw(event); | |
}, | |
}; | |
element.addEventListener(eventName, invokers[pKey].handler, options); | |
} else { | |
invokers[pKey].raw = pValue; | |
} | |
delete deleteMap[pKey]; | |
continue; | |
} | |
delete deleteMap[pKey]; | |
if ((pKey === "children") | (pKey === "ref") || pKey === "key") { | |
continue; | |
} | |
if (pValue === oldProps[pKey]) { | |
continue; | |
} | |
const isBoolean = isSpecialBooleanAttr(pKey); | |
if (pValue == null || (isBoolean && !includeBooleanAttr(pValue))) { | |
element.removeAttribute(pKey); | |
} else { | |
element.setAttribute(pKey, isBoolean ? "" : pValue); | |
} | |
} | |
for (const [pKey, map] of Object.entries(deleteMap)) { | |
if (pKey.match(/^on[A-Z]/)) { | |
const [eventName, options] = parseName(pKey); | |
element.removeEventListener(eventName, map.handler, options); | |
} else { | |
element.removeAttribute(pKey); | |
} | |
} | |
useEffect( | |
() => () => { | |
for (const [pKey, map] of Object.entries(invokers)) { | |
const [eventName, options] = parseName(pKey); | |
element.removeEventListener(eventName, map.handler, options); | |
} | |
element.remove(element); | |
}, | |
[] | |
); | |
return element; | |
}; | |
Object.defineProperty(func, "name", { value: $tag }); | |
return func; | |
}; | |
HTML_TAGS.forEach((tag) => { | |
buildIn[tag] = genBuildInFun(tag); | |
}); | |
buildIn.text = function text(props, oldProps, { instance, useEffect }) { | |
const element = instance || document.createTextNode(props.content); | |
// console.log(`text 重用`, instance); | |
if (!oldProps || props.content !== oldProps.content) { | |
element.data = props.content; | |
} | |
useEffect( | |
() => () => { | |
element.remove(element); | |
}, | |
[] | |
); | |
return element; | |
}; | |
buildIn.comment = (props, oldProps) => { | |
return document.createComment(props.content); | |
}; | |
const Fragment = (props) => { | |
return document.createDocumentFragment(); | |
}; | |
const getCommentRang = (key) => { | |
const walker = document.createTreeWalker( | |
document.body, | |
NodeFilter.SHOW_COMMENT | |
); | |
const result = []; | |
let comment = walker.nextNode(); | |
while (comment) { | |
if (comment.data === "^" + key) { | |
result.push(comment); | |
} | |
if (comment.data === key + "$") { | |
result.push(comment); | |
break; | |
} | |
comment = walker.nextNode(); | |
} | |
return result; | |
}; | |
function jsx(type, props = {}, key = null) { | |
return { | |
key, | |
type, | |
props, | |
child: null, | |
sibling: null, | |
return: null, | |
index: 0, | |
stateNode: null, | |
get _key() { | |
const typeName = | |
typeof this.type === "string" ? this.type : this.type.name; | |
const pKey = this.return ? this.return._key : ""; | |
const cKey = this.key || `${typeName}_${this.index}`; | |
return `${pKey}:${cKey}`; | |
}, | |
get generator() { | |
if (!GeneratorPool[this._key]) { | |
GeneratorPool[this._key] = componentCreator( | |
typeof this.type === "string" ? buildIn[this.type] : this.type, | |
innerRender | |
); | |
GeneratorPool[this._key].element = this; | |
} | |
if (GeneratorPool[this._key].StatusLane & (NoneLane | UnMountedLane)) { | |
GeneratorPool[this._key].StatusLane = MountedLane; | |
} | |
return GeneratorPool[this._key]; | |
}, | |
}; | |
} | |
function beginWork(element) { | |
if (!element.stateNode) { | |
element.stateNode = document.createDocumentFragment(); | |
} else { | |
console.log('%c 更新的根节点"', "color:#0f0;", element); | |
} | |
if (typeof element.type === "function" && element.type !== Fragment) { | |
element.stateNode.appendChild( | |
buildIn.comment({ content: "^" + element._key }) | |
); | |
} | |
} | |
function finishedWork(element) { | |
console.log("finishedWork", element); | |
if (isTextElement(element)) { | |
element.stateNode = element.generator.next(element.props).value; | |
} else if (isHTMLTag(element.type) || element.type === Fragment) { | |
const temp = element.generator.next(element.props).value; | |
temp.appendChild(element.stateNode); | |
element.stateNode = temp; | |
} else { | |
element.stateNode.appendChild( | |
buildIn.comment({ content: element._key + "$" }) | |
); | |
} | |
if (element.type === Fragment && element.props.target) { | |
element.props.target.appendChild(element.stateNode); | |
} else if (element.return && element.return.stateNode) { | |
element.return.stateNode.appendChild(element.stateNode); | |
} | |
} | |
const toValidElement = (element) => { | |
if (element && element.type) { | |
return element; | |
} | |
if (typeof element === "string" || typeof element === "number") { | |
return jsx("text", { content: element }); | |
} | |
if (Array.isArray(element)) { | |
return jsx(Fragment, { children: element.flat(5) }); | |
} | |
return jsx("text", { content: "" }); | |
}; | |
function* postOrder(element) { | |
beginWork(element); | |
if (isTextElement(element)) { | |
yield element; | |
} else if (isHTMLTag(element.type) || element.type === Fragment) { | |
let tempChildren = element.props.children; | |
if (tempChildren) { | |
if (!Array.isArray(tempChildren)) { | |
tempChildren = [tempChildren]; | |
} | |
if (tempChildren.length) { | |
let index = 0; | |
let prevSibling = null; | |
for (const tempChild of tempChildren) { | |
const child = toValidElement(tempChild); | |
child.index = index; | |
child.return = element; | |
index = index + 1; | |
if (!prevSibling) { | |
element.child = child; | |
} else { | |
prevSibling.sibling = child; | |
} | |
prevSibling = child; | |
yield* postOrder(child); | |
} | |
} | |
} | |
yield element; | |
} else { | |
const tempInnerRoot = element.generator.next(element.props).value; | |
if (tempInnerRoot != null) { | |
const innerRootElement = toValidElement(tempInnerRoot); | |
element.child = innerRootElement; | |
innerRootElement.return = element; | |
innerRootElement.index = 0; | |
yield* postOrder(innerRootElement); | |
} | |
yield element; | |
} | |
} | |
const GeneratorPool = new Map(); | |
const NoneLane = 0b000001; | |
const MountedLane = 0b000010; | |
const UpdatedLane = 0b000100; | |
const UnMountedLane = 0b001000; | |
const innerRender = (element, deleteKeySet) => { | |
console.clear(); | |
console.log('%c innerRender"', "color:#0f0;", element); | |
for (const item of postOrder(element)) { | |
finishedWork(item); | |
deleteKeySet.delete(item._key); | |
if (item.generator.flushEffects) { | |
requestCallback(item.generator.flushEffects); | |
} | |
} | |
for (const item of deleteKeySet.keys()) { | |
if (GeneratorPool[item]) { | |
GeneratorPool[item].StatusLane = UnMountedLane; | |
GeneratorPool[item].flushCleanEffects(true); | |
} | |
} | |
console.log("deleteKeySet", deleteKeySet); | |
}; | |
function createRoot(container) { | |
const key = container.id || (Date.now() + Math.random()).toString(36); | |
return { | |
render(element) { | |
element.key = element.key || key; | |
innerRender(element, new Set()); | |
container.appendChild(element.stateNode); | |
}, | |
}; | |
} | |
/** 业务代码在下方 */ | |
function Hello(props, oldProps, { useState, useEffect }) { | |
const [state, setState] = useState("aimwhy"); | |
const [state2, setState2] = useState("tt"); | |
useEffect(() => { | |
console.log("%c Hello Mounted", "color:#0f0;"); | |
return () => { | |
console.log("%c Hello UnMounted", "color:#0f0;"); | |
}; | |
}, []); | |
useEffect(() => { | |
console.log("%c Hello Update", "color:#990;"); | |
}); | |
useEffect(() => { | |
console.log("%c Hello Dep Update", "color:#990;"); | |
}, [window.a]); | |
return jsx("div", { | |
children: [ | |
props.children(), | |
state, | |
jsx("div", { children: state2 }), | |
jsx("input", { | |
onInput: (e) => { | |
props.parentChange((v) => !v); | |
setState(e.target.value); | |
setState2((v) => "tt" + e.target.value.slice(-3)); | |
}, | |
value: state, | |
type: "text", | |
}), | |
], | |
}); | |
} | |
function Temp() { | |
return null; | |
} | |
function World(props, oldProps, { useState, useEffect }) { | |
const [state, setState] = useState("点击我"); | |
useEffect(() => { | |
console.log("%c World Mounted", "color:#0f0;"); | |
return () => { | |
console.log("%c World UnMounted", "color:#0f0;"); | |
}; | |
}, []); | |
useEffect(() => { | |
console.log("%c World Update", "color:#990;"); | |
}); | |
return jsx("div", { | |
onClickOnce: (e) => { | |
setState((v) => v + 2); | |
e.stopPropagation(); | |
}, | |
children: [state], | |
}); | |
} | |
function App(props, oldProps, { useState, useEffect }) { | |
const [state, setState] = useState(true); | |
const [state2, setState2] = useState(true); | |
useEffect(() => { | |
console.log("%c App Mounted", "color:#0f0;"); | |
return () => { | |
console.log("%c App UnMounted", "color:#0f0;"); | |
}; | |
}, []); | |
useEffect(() => { | |
console.log("%c App Update", "color:#990;"); | |
}); | |
return jsx("div", { | |
onClick: () => { | |
setState((v) => !v); | |
}, | |
children: [ | |
!state | |
? jsx( | |
Fragment, | |
{ | |
children: ["Portal-aa", "Portal-bb"], | |
target: document.body, | |
}, | |
"Portal" | |
) | |
: "Portal Origin", | |
jsx( | |
Hello, | |
{ | |
parentChange: setState2, | |
id: "hello", | |
children: () => | |
jsx("i", { | |
children: ["i ", state2], | |
}), | |
}, | |
"hello" | |
), | |
state ? jsx(World) : "销毁后的文案", | |
["github!", null, " aimwhy"], | |
], | |
}); | |
} | |
createRoot(document.querySelector("#app")).render(jsx(App)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment