├── .prettierrc ├── README.md ├── trim.js ├── .gitignore ├── .vscode └── launch.json ├── curry-add.js ├── html-compiler ├── transform.js ├── generate.js ├── index.html └── parse.js ├── shop-combine.js ├── new.js ├── flex.html ├── es6-extends.js ├── dfs-path.js ├── max-requests.js ├── promise-easy.js ├── create-flow.js ├── async.js ├── tree-convert.js ├── markdown-title-parser.js ├── useMap.test.tsx ├── fetch-dev-cache.ts ├── useMap.ts └── parse-json.js /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # javascript-codes 2 | 3 | 写一些笔试题,或者随便实现一些灵感,whatever~ 4 | -------------------------------------------------------------------------------- /trim.js: -------------------------------------------------------------------------------- 1 | function trim(str) { 2 | let i = 0 3 | let j = str.length - 1 4 | 5 | function isWhite(s) { 6 | return /\s/.test(s) 7 | } 8 | 9 | while (isWhite(str[i])) { 10 | i++ 11 | } 12 | while (isWhite(str[j])) { 13 | j-- 14 | } 15 | 16 | return str.substring(i, j + 1) 17 | } 18 | 19 | console.log(trim(' as d ')) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | ._* 3 | .cache 4 | .project 5 | .settings 6 | .tmproj 7 | *.esproj 8 | *.sublime-project 9 | *.sublime-workspace 10 | nbproject 11 | thumbs.db 12 | *.iml 13 | 14 | # Folders to ignore 15 | .hg 16 | .svn 17 | .CVS 18 | .idea 19 | node_modules/ 20 | jscoverage_lib/ 21 | bower_components/ 22 | dist/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "启动程序", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${file}" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /curry-add.js: -------------------------------------------------------------------------------- 1 | function add(...nums) { 2 | let res = sum(nums); 3 | 4 | queueMicrotask(() => { 5 | console.log(res) 6 | }) 7 | 8 | function sum(nums) { 9 | return nums.reduce((a, b) => a + b); 10 | } 11 | 12 | return function addCurry(...nums) { 13 | if (nums.length === 0) return res; 14 | 15 | res += sum(nums); 16 | 17 | return addCurry; 18 | }; 19 | } 20 | 21 | add(5, 4)(3, 2)(1)(3); 22 | -------------------------------------------------------------------------------- /html-compiler/transform.js: -------------------------------------------------------------------------------- 1 | export const transform = (ast, visitors) => { 2 | function traverseNode(node, parent) { 3 | switch (node.type) { 4 | case "tag": { 5 | visitors.tag?.(node, parent) 6 | return travserChildren(node.children, node) 7 | } 8 | } 9 | } 10 | 11 | function travserChildren(nodes, parent) { 12 | nodes?.forEach?.((node) => { 13 | traverseNode(node, parent) 14 | }) 15 | } 16 | 17 | travserChildren(ast.children, null) 18 | } 19 | -------------------------------------------------------------------------------- /shop-combine.js: -------------------------------------------------------------------------------- 1 | let names = ["iPhone X", "iPhone XS"] 2 | 3 | let colors = ["黑色", "白色"] 4 | 5 | let storages = ["64g", "256g"] 6 | 7 | let combine = function (...chunks) { 8 | let res = [] 9 | 10 | let helper = function (chunkIndex, prev) { 11 | let chunk = chunks[chunkIndex] 12 | let isLast = chunkIndex === chunks.length - 1 13 | for (let val of chunk) { 14 | let cur = prev.concat(val) 15 | if (isLast) { 16 | // 如果已经处理到数组的最后一项了 则把拼接的结果放入返回值中 17 | res.push(cur) 18 | } else { 19 | helper(chunkIndex + 1, cur) 20 | } 21 | } 22 | } 23 | 24 | helper(0, []) 25 | 26 | return res 27 | } 28 | 29 | console.log(combine(names, colors, storages)) 30 | -------------------------------------------------------------------------------- /new.js: -------------------------------------------------------------------------------- 1 | function myNew(Ctor, ...args) { 2 | if (!Ctor.prototype) { 3 | throw new Error(`${Ctor.toString()} is not a constructor`) 4 | } 5 | 6 | myNew.target = Ctor 7 | 8 | const instance = {} 9 | Object.setPrototypeOf(instance, Ctor.prototype) 10 | 11 | const res = Ctor.apply(instance, args) 12 | 13 | const isObject = typeof res === "object" && res !== null 14 | const isFunction = typeof res === "function" 15 | 16 | if (isObject || isFunction) { 17 | return res 18 | } else { 19 | return instance 20 | } 21 | } 22 | 23 | function Dog(name) { 24 | this.name = name 25 | } 26 | 27 | Dog.prototype.bark = function () { 28 | console.log(`我是${this.name},汪汪汪`) 29 | } 30 | 31 | let dog = myNew(Dog, "Husky") 32 | 33 | dog.bark() 34 | -------------------------------------------------------------------------------- /flex.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | -------------------------------------------------------------------------------- /es6-extends.js: -------------------------------------------------------------------------------- 1 | function inherits(Sub, Sup) { 2 | // 静态方法继承 3 | Sub.__proto__ = Sup 4 | 5 | // 原型链继承 6 | Sub.prototype = Object.create(Sup.prototype) 7 | Sub.prototype.constructor = Sub 8 | } 9 | 10 | function Animal(name) { 11 | this.name = name 12 | } 13 | 14 | Animal.staticSay = function() { 15 | console.log("我是静态方法") 16 | } 17 | 18 | Animal.prototype.say = function () { 19 | console.log("我会说话") 20 | } 21 | 22 | function Dog(name) { 23 | if (new.target !== Dog) { 24 | throw new TypeError("Class constructor A cannot be invoked without 'new'") 25 | } 26 | Animal.call(this, name) 27 | this.type = "Dog" 28 | } 29 | 30 | inherits(Dog, Animal) 31 | 32 | Dog.prototype.bark = function () { 33 | console.log("汪汪汪") 34 | } 35 | 36 | let dog = new Dog("wangcai") 37 | 38 | dog.bark() 39 | dog.say() 40 | 41 | Dog.staticSay() -------------------------------------------------------------------------------- /dfs-path.js: -------------------------------------------------------------------------------- 1 | const test = { 2 | a: { 3 | c: { 4 | w: 2, 5 | r: { 6 | q: 4, 7 | }, 8 | }, 9 | d: 1, 10 | f: 2, 11 | e: 3, 12 | }, 13 | b: { 14 | h: 4, 15 | i: { 16 | p: { 17 | q: 5, 18 | }, 19 | }, 20 | }, 21 | }; 22 | 23 | let find = (obj, key, value) => { 24 | let res; 25 | let helper = (subObj, path) => { 26 | if (res) { 27 | return; 28 | } 29 | if (subObj[key] === value) { 30 | res = path.concat(key); 31 | } 32 | Object.keys(subObj).forEach((subKey) => { 33 | let sub = subObj[subKey]; 34 | if (typeof sub === "object") { 35 | helper(sub, [...path, subKey]); 36 | } 37 | }); 38 | }; 39 | helper(obj, []); 40 | return res || null; 41 | }; 42 | 43 | console.log(find(test, "q", 5)); 44 | console.log(find(test, "q", 4)); 45 | console.log(find(test, "m", 100)); 46 | 47 | 48 | -------------------------------------------------------------------------------- /html-compiler/generate.js: -------------------------------------------------------------------------------- 1 | export const generate = (ast) => { 2 | function generateNode(node) { 3 | const { type } = node 4 | if (type === "tag") { 5 | return generateTag(node) 6 | } else if (type === "text") { 7 | return generateText(node) 8 | } 9 | } 10 | 11 | function generateTag(node) { 12 | const { tag, attrs, children } = node 13 | 14 | const tagStart = `<${tag}` 15 | const tagAttrs = generateAttrs(attrs.value) 16 | const tagChildren = generateChildren(children) 17 | const tagEnd = `` 18 | 19 | return `${tagStart}${tagAttrs}>${tagChildren}${tagEnd}` 20 | } 21 | 22 | function generateText(node) { 23 | return node.value 24 | } 25 | 26 | function generateAttrs(attrs = []) { 27 | const keys = Object.keys(attrs) 28 | return keys.reduce((str, key) => { 29 | const value = attrs[key] 30 | return `${str} ${key}="${value}"` 31 | }, "") 32 | } 33 | 34 | function generateChildren(children = []) { 35 | return children.map(generateNode).join("") 36 | } 37 | 38 | return generateChildren(ast.children) 39 | } 40 | -------------------------------------------------------------------------------- /max-requests.js: -------------------------------------------------------------------------------- 1 | const wait = () => 2 | new Promise((resolve) => { 3 | setTimeout(resolve, 1000 * Math.random()) 4 | }) 5 | 6 | const multiRequest = (urls, max = 1) => { 7 | return new Promise((resolve, reject) => { 8 | let rest = urls.slice() 9 | let finished = 0 10 | let currentIndex = 0 11 | let res = [] 12 | 13 | const request = async (idx) => { 14 | if (rest.length === 0) { 15 | return 16 | } 17 | 18 | // 维护本次请求对应数组的下标 19 | // 由于请求一定是一个换一个的 所以这个下标是可以对应上的 20 | currentIndex++ 21 | 22 | const url = rest.shift() 23 | try { 24 | await wait() 25 | } catch (error) { 26 | reject(error) 27 | } 28 | 29 | console.log('res: ', res); 30 | res[idx] = url 31 | 32 | // 请求 33 | finished++ 34 | if (finished === urls.length) { 35 | return resolve(res) 36 | } 37 | 38 | request(currentIndex) 39 | } 40 | 41 | for (let i = 0; i < max; i++) { 42 | request(i) 43 | } 44 | }) 45 | } 46 | 47 | console.log( 48 | multiRequest(["1", "2", "3", "4", "5", 6, 7, 8, 9, 10], 3).then((res) => 49 | console.log("res", res), 50 | ), 51 | ) 52 | -------------------------------------------------------------------------------- /promise-easy.js: -------------------------------------------------------------------------------- 1 | class Promise { 2 | constructor(exec) { 3 | this.status = "pending" 4 | this.value = undefined 5 | this.reason = undefined 6 | 7 | this.onResolvedCallbacks = [] 8 | this.onRejectedCallbacks = [] 9 | 10 | const resolve = (value) => { 11 | queueMicrotask(() => { 12 | this.value = value 13 | this.status = "resolved" 14 | this.onResolvedCallbacks.forEach((callback) => callback()) 15 | }) 16 | } 17 | 18 | const reject = (reason) => { 19 | queueMicrotask(() => { 20 | this.reason = reason 21 | this.status = "rejected" 22 | this.onRejectedCallbacks.forEach((callback) => callback()) 23 | }) 24 | } 25 | 26 | try { 27 | exec(resolve, reject) 28 | } catch (error) { 29 | reject(error) 30 | } 31 | } 32 | 33 | then(callback) { 34 | return new Promise((resolve, reject) => { 35 | this.onResolvedCallbacks.push(() => { 36 | let result 37 | try { 38 | result = callback(this.value) 39 | } catch (error) { 40 | return reject(error) 41 | } 42 | if (result instanceof Promise) { 43 | result.then(resolve) 44 | } else { 45 | resolve(result) 46 | } 47 | }) 48 | }) 49 | } 50 | } 51 | 52 | new Promise(resolve => { 53 | resolve(2) 54 | }).then(res => { 55 | console.log('res: ', res); 56 | return new Promise(resolve => { 57 | setTimeout(() => { 58 | resolve(res + 1) 59 | }, 1000); 60 | }) 61 | }).then(res => { 62 | console.log(res) 63 | }) -------------------------------------------------------------------------------- /create-flow.js: -------------------------------------------------------------------------------- 1 | /** 2 | 蚂蚁金服面试题 3 | const log = jest.fn(); 4 | const subFlow = createFlow([() => log('subFlow')]), 5 | 6 | createFlow([ 7 | () => log('a'), 8 | () => log('b'), 9 | subFlow, 10 | [ 11 | () => delay().then(() => log('c')), 12 | () => log('d'), 13 | ] 14 | ]).run(null, () => { 15 | expect(log.mock.calls[0][0]).toBe('a') 16 | expect(log.mock.calls[1][0]).toBe('b') 17 | expect(log.mock.calls[2][0]).toBe('subFlow') 18 | expect(log.mock.calls[3][0]).toBe('c') 19 | expect(log.mock.calls[4][0]).toBe('d') 20 | }) 21 | 22 | 23 | 24 | 按照上面的测试用例,实现 createFlow: 25 | 26 | flow 是指一些列 effects(这里是普通的函数)组成的逻辑片段 27 | flow 支持嵌套 28 | effects 的执行只需要支持串行 29 | */ 30 | 31 | function createFlow(effects = []) { 32 | let sources = effects.slice().flat(); 33 | function run(callback) { 34 | while (sources.length) { 35 | const task = sources.shift(); 36 | if (typeof task === "function") { 37 | const res = task(); 38 | if (res?.then) { 39 | res.then(createFlow(sources).run); 40 | break; 41 | } 42 | } else if (task?.isFlow) { 43 | task.run(createFlow(sources).run); 44 | break; 45 | } 46 | } 47 | callback?.(); 48 | } 49 | return { 50 | run, 51 | isFlow: true, 52 | }; 53 | } 54 | 55 | const delay = () => new Promise((resolve) => setTimeout(resolve, 1000)); 56 | createFlow([ 57 | () => console.log("a"), 58 | () => console.log("b"), 59 | createFlow([() => console.log("c")]), 60 | [() => delay().then(() => console.log("d")), () => console.log("e")], 61 | ]).run(); 62 | -------------------------------------------------------------------------------- /html-compiler/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 48 | 49 | 50 |
51 |
你好再见
52 |
53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /async.js: -------------------------------------------------------------------------------- 1 | /** 2 | * async的执行原理 3 | * 其实就是自动执行generator函数 4 | * 暂时不考虑genertor的编译步骤(更复杂) 5 | */ 6 | 7 | const getData = () => 8 | new Promise(resolve => setTimeout(() => resolve("data"), 1000)) 9 | 10 | // 这样的一个async函数 应该再1秒后打印data 11 | async function test() { 12 | const data = await getData() 13 | 14 | console.log(data) 15 | return data 16 | } 17 | 18 | // async函数会被编译成generator函数 (babel会编译成更本质的形态,这里我们直接用generator) 19 | function* testG() { 20 | // await被编译成了yield 21 | const data = yield getData() 22 | console.log('data: ', data); 23 | const data2 = yield getData() 24 | console.log('data2: ', data2); 25 | return data + '123' 26 | } 27 | 28 | function asyncToGenerator(generatorFunc) { 29 | return function() { 30 | const gen = generatorFunc.apply(this, arguments) 31 | 32 | return new Promise((resolve, reject) => { 33 | function step(key, arg) { 34 | let generatorResult 35 | try { 36 | generatorResult = gen[key](arg) 37 | } catch (error) { 38 | return reject(error) 39 | } 40 | 41 | const { value, done } = generatorResult 42 | 43 | if (done) { 44 | return resolve(value) 45 | } else { 46 | return Promise.resolve(value).then( 47 | function onResolve(val) { 48 | step("next", val) 49 | }, 50 | function onReject(err) { 51 | step("throw", err) 52 | }, 53 | ) 54 | } 55 | } 56 | step("next") 57 | }) 58 | } 59 | } 60 | 61 | const testGAsync = asyncToGenerator(testG) 62 | testGAsync().then(result => { 63 | console.log(result) 64 | }) -------------------------------------------------------------------------------- /tree-convert.js: -------------------------------------------------------------------------------- 1 | // https://shimo.im/forms/yXUgGENQI5kRr88f 2 | 3 | /** 4 | * 假设后端同学通过接口向前端返回了天猫的行业信息,为了取用方便,我们希望可以将其转换为树状格式,例如: 5 | { 6 | "数码": { 7 | "电脑配件": { 8 | "内存" : {} 9 | } 10 | }, 11 | "女装" : { 12 | "连衣裙": {}, 13 | "半身裙": {}, 14 | "A字裙": {} 15 | } 16 | } 17 | 实现一个方法完成这个转换 18 | function convert_format(data) 19 | */ 20 | 21 | let list = [ 22 | { 23 | parent_ind: '女装', 24 | name: '连衣裙', 25 | }, 26 | { 27 | name: '女装', 28 | }, 29 | { 30 | parent_ind: '女装', 31 | name: '半身裙', 32 | }, 33 | { 34 | parent_ind: '女装', 35 | name: 'A字裙', 36 | }, 37 | { 38 | name: '数码', 39 | }, 40 | { 41 | parent_ind: '数码', 42 | name: '电脑配件', 43 | }, 44 | { 45 | parent_ind: '电脑配件', 46 | name: '内存', 47 | }, 48 | ] 49 | 50 | let convert = list => { 51 | let res = {} 52 | let copy = list.slice() 53 | let nodeMap = {undefined: res} 54 | 55 | while (copy.length) { 56 | let cachedLen = copy.length 57 | 58 | for (let i = copy.length - 1; i >= 0; i--) { 59 | let item = copy[i] 60 | let {parent_ind: parentName, name} = item 61 | let parent = nodeMap[parentName] 62 | if (parent) { 63 | let node = {} 64 | parent[name] = node 65 | nodeMap[name] = node 66 | copy.splice(i, 1) 67 | } 68 | } 69 | 70 | if (cachedLen === copy.length) { 71 | break 72 | } 73 | } 74 | return res 75 | } 76 | 77 | console.log(convert(list)) 78 | 79 | /** 80 | * 大数据量 81 | list = [] 82 | for (let i = 0; i < 100; i++) { 83 | list.push({ name: `层级${i}` }) 84 | } 85 | 86 | for (let i = 0; i < 10000; i++) { 87 | let pl = i % 100 88 | list.push({ parent_ind: `层级${pl}`, name: `层级${pl}-${i}` }) 89 | } 90 | 91 | for (let i = 0; i < 10000; i++) { 92 | let ppl = i % 100 93 | list.push({ parent_ind: `层级${ppl}-${i}`, name: `层级${ppl}-${i}-${i}` }) 94 | } 95 | 96 | list.sort((a, b) => Math.random() - 0.5) 97 | */ 98 | -------------------------------------------------------------------------------- /markdown-title-parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | let list = [ 3 | 'h3', 4 | 'h2','h3', 5 | 'h1','h2','h3','h3', 6 | 'h2','h3', 7 | 'h1','h2','h4','h2','h3', 8 | ] 9 | 10 | 需要输出 11 | let result = [{ 12 | "name": "h3" 13 | }, { 14 | "name": "h2", 15 | "child": [{ 16 | "name": "h3" 17 | }] 18 | }, { 19 | "name": "h1", 20 | "child": [{ 21 | "name": "h2", 22 | "child": [{ 23 | "name": "h3" 24 | }, { 25 | "name": "h3" 26 | }] 27 | }, { 28 | "name": "h2", 29 | "child": [{ 30 | "name": "h3" 31 | }] 32 | }] 33 | }, { 34 | "name": "h1", 35 | "child": [{ 36 | "name": "h2", 37 | "child": [{ 38 | "name": "h4" 39 | }] 40 | }, { 41 | "name": "h2", 42 | "child": [{ 43 | "name": "h3" 44 | }] 45 | }] 46 | }] 47 | */ 48 | 49 | let list = [ 50 | 'h3', 51 | 'h2','h3', 52 | 'h1','h2','h3','h3', 53 | 'h2','h3', 54 | 'h1','h2','h4','h2','h3', 55 | ] 56 | 57 | function makeTree(arr) { 58 | let tree = [] 59 | let max = Infinity 60 | let prev = {} 61 | 62 | arr.forEach(title => { 63 | let level = Number(title[1]) 64 | if (level <= max) { 65 | let node = { 66 | name: title, 67 | } 68 | tree.push(node) 69 | prev.level = level 70 | prev.node = node 71 | max = level 72 | } else if (level === prev.level) { 73 | // 等级相同的话 上级节点的child继续增加子节点 74 | prev = prev.prev 75 | pushNodeAndAdvanceQueue() 76 | } else if (level > prev.level) { 77 | pushNodeAndAdvanceQueue() 78 | } else { 79 | while (level <= prev.level) { 80 | // 向上回溯,找到平级的再上一层node 81 | prev = prev.prev 82 | } 83 | pushNodeAndAdvanceQueue() 84 | } 85 | 86 | function pushNodeAndAdvanceQueue() { 87 | let node = { 88 | name: title, 89 | } 90 | if (!prev.node.child) { 91 | let child = [] 92 | prev.node.child = child 93 | } 94 | prev.node.child.push(node) 95 | let next = { 96 | level, 97 | node, 98 | prev, 99 | } 100 | prev = next 101 | } 102 | }) 103 | 104 | return tree 105 | } 106 | 107 | console.log(makeTree(list)) 108 | 109 | /** 110 | * 思路 111 | * 112 | * 利用链表来向上回溯合适的父节点 113 | * 利用max变量记录当前分组的最大标题,如果比max还大,就需要新开一个分组了。 114 | */ 115 | -------------------------------------------------------------------------------- /useMap.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from 'react-hooks-testing-library'; 2 | import { useMap } from '../useMap'; 3 | 4 | const dataSource = [ 5 | { id: 1, name: 'foo' }, 6 | { id: 2, name: 'bar' }, 7 | ]; 8 | const testSymbol = Symbol('test'); 9 | 10 | describe('test useMap', () => { 11 | test('should useMap works with single argument', () => { 12 | const { result } = renderHook(() => useMap(dataSource)); 13 | const { map, set } = result.current; 14 | 15 | expect(map).toEqual({ 1: undefined, 2: undefined }); 16 | 17 | const testItem = dataSource[0]; 18 | set(testItem.id, testSymbol); 19 | expect(result.current.map[testItem.id]).toBe(testSymbol); 20 | }); 21 | 22 | test('should useMap works with options', () => { 23 | const dataSource = [ 24 | { aid: 1, name: 'foo' }, 25 | { aid: 2, name: 'bar' }, 26 | ]; 27 | const { result } = renderHook(() => 28 | useMap(dataSource, { 29 | key: 'aid', 30 | initial(item) { 31 | return item.aid + 1; 32 | }, 33 | }) 34 | ); 35 | const { map, set } = result.current; 36 | expect(map).toEqual({ 1: 2, 2: 3 }); 37 | 38 | const testItem = dataSource[1]; 39 | set(testItem.aid, testSymbol); 40 | expect(result.current.map[testItem.aid]).toBe(testSymbol); 41 | }); 42 | 43 | test('should new dataSource item be initialized', () => { 44 | let dataSource = [{ id: 1, name: 'foo' }]; 45 | const { result, rerender } = renderHook(() => 46 | useMap(dataSource, { 47 | initial() { 48 | return 'initial'; 49 | }, 50 | }) 51 | ); 52 | dataSource = [{ id: 3, name: 'baz' }]; 53 | rerender(); 54 | expect(result.current.map[3]).toBe('initial'); 55 | }); 56 | 57 | test('should set works', () => { 58 | const { result } = renderHook(() => useMap(dataSource)); 59 | const { set } = result.current; 60 | const testItem = dataSource[0]; 61 | set(testItem.id, testSymbol); 62 | expect(result.current.map[testItem.id]).toBe(testSymbol); 63 | }); 64 | 65 | test('should useMap other dispatch types work', () => { 66 | const { result } = renderHook(() => useMap(dataSource)); 67 | const { dispatch } = result.current; 68 | 69 | dispatch({ 70 | type: 'CHANGE_ALL', 71 | payload: testSymbol, 72 | }); 73 | dataSource.forEach(({ id }) => { 74 | expect(result.current.map[id]).toBe(testSymbol); 75 | }); 76 | 77 | dispatch({ 78 | type: 'SET_MAP', 79 | payload: { 1: 'bar' }, 80 | }); 81 | expect(result.current.map[1]).toBe('bar'); 82 | 83 | dispatch({ 84 | type: 'SOME_UNKNOWN_TYPE' as any, 85 | payload: undefined, 86 | }); 87 | expect(result.current.map[1]).toBe('bar'); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /html-compiler/parse.js: -------------------------------------------------------------------------------- 1 | export const parse = (html) => { 2 | let str = html 3 | 4 | function parseChildren() { 5 | const nodes = [] 6 | 7 | while (str) { 8 | let node 9 | const char = str[0] 10 | if (char === "<") { 11 | // Tag 12 | const nextChar = str[1] 13 | if (/[a-z]/i.test(nextChar)) { 14 | // Start of tag 15 | node = parseElement() 16 | } else if (nextChar === "/") { 17 | // End of tag 18 | const [endTag] = /<\/.*?>/.exec(str) 19 | go(endTag.length) 20 | break 21 | } 22 | } else { 23 | node = parseText() 24 | } 25 | 26 | if (node) { 27 | nodes.push(node) 28 | } 29 | } 30 | 31 | return nodes 32 | } 33 | 34 | function parseElement() { 35 | const match = /<([a-z][^\t\r\n\f />]+)/i.exec(str) 36 | const [raw, tag] = match 37 | go(raw.length) 38 | 39 | const attrs = parseAttributes() 40 | const children = parseChildren() 41 | 42 | return { 43 | type: "tag", 44 | tag, 45 | attrs, 46 | children, 47 | } 48 | } 49 | 50 | function parseText() { 51 | // Content of element 52 | const textMatch = /[^<]*/.exec(str) 53 | if (textMatch) { 54 | const [text] = textMatch 55 | go(text.length) 56 | return { 57 | type: "text", 58 | value: text, 59 | } 60 | } 61 | } 62 | 63 | function parseAttributes() { 64 | const attrs = {} 65 | goSpaces() 66 | 67 | while (!str.startsWith(">")) { 68 | const matchName = /(.*?)=/.exec(str) 69 | const [rawName, name] = matchName 70 | go(rawName.length) 71 | 72 | // Parse value 73 | const quoteMatch = /'|"/.exec(str[0]) 74 | if (quoteMatch) { 75 | // Quoted value. 76 | const [quote] = quoteMatch 77 | go(1) 78 | 79 | const end = str.indexOf(quote) 80 | const value = str.substr(0, end) 81 | // Included quote 82 | go(value.length + 1) 83 | attrs[name] = value 84 | } else { 85 | // Unquoted value 86 | const [value] = /^[^\t\r\n\f >]+/.exec(str) 87 | go(value.length) 88 | attrs[name] = value 89 | } 90 | 91 | goSpaces() 92 | } 93 | 94 | // Skip end '>' of tag 95 | go(1) 96 | 97 | if (Object.keys(attrs).length) { 98 | return { 99 | type: "attrs", 100 | value: attrs, 101 | } 102 | } 103 | } 104 | 105 | function goSpaces() { 106 | const match = /^[\t\r\n\f ]+/.exec(str) 107 | if (match) { 108 | go(match[0].length) 109 | } 110 | } 111 | 112 | function go(len) { 113 | str = str.slice(len) 114 | } 115 | 116 | return { 117 | type: "root", 118 | children: parseChildren(), 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /fetch-dev-cache.ts: -------------------------------------------------------------------------------- 1 | export interface CacheFetchOptions { 2 | /** 3 | * 生成缓存key的策略,默认策略是直接拼接 url + stringify(body) 4 | */ 5 | generateKey?: (url: RequestInfo, body: object) => string; 6 | /** 7 | * 传入 url 和 fetch 选项 判断是否需要缓存 8 | */ 9 | shouldHandleRequest?(url: RequestInfo, requestInit?: RequestInit): boolean; 10 | /** 11 | * 传入 response 响应对象 判断是否需要缓存 12 | */ 13 | shouldCacheResult?(response: Response): Promise; 14 | } 15 | 16 | const STORAGE_PREFIX = 'request-dev-cache:'; 17 | 18 | export const startCache = ({ 19 | generateKey = defaultGenerateKey, 20 | shouldHandleRequest = () => true, 21 | shouldCacheResult = async () => true, 22 | }: CacheFetchOptions) => { 23 | if (process.env.NODE_ENV === 'development') { 24 | const parseBody = (options?: RequestInit) => { 25 | let { body: rawBody } = options ?? {}; 26 | let body: object; 27 | if (typeof rawBody === 'string') { 28 | body = JSON.parse(rawBody as string) ?? {}; 29 | } else { 30 | body = {}; 31 | } 32 | return body; 33 | }; 34 | 35 | const originFetch = window.fetch; 36 | window.fetch = async (...params) => { 37 | const [url, options] = params; 38 | if (!shouldHandleRequest?.(url, options)) { 39 | return originFetch(...params); 40 | } 41 | 42 | const body = parseBody(options); 43 | 44 | const cacheKey = `${STORAGE_PREFIX}${generateKey(url, body)}`; 45 | const cacheResult = localStorage.getItem(cacheKey); 46 | 47 | if (cacheResult) { 48 | const result = JSON.parse(cacheResult); 49 | log(url, body, result); 50 | return new Response(cacheResult); 51 | } else { 52 | const resp = await originFetch(...params); 53 | const clonedResponse = resp.clone(); 54 | const result = await resp.json(); 55 | const stringifyResult = JSON.stringify(result); 56 | if (await shouldCacheResult(clonedResponse)) { 57 | localStorage.setItem(cacheKey, stringifyResult); 58 | } 59 | return new Response(stringifyResult); 60 | } 61 | }; 62 | 63 | /** 64 | * 清除所有缓存 65 | */ 66 | (window as any).cleanAllRequestDevCaches = () => { 67 | const lens = localStorage.length; 68 | const targetKeys: string[] = []; 69 | for (let index = 0; index < lens; index++) { 70 | const key = localStorage.key(index); 71 | if (key?.startsWith(STORAGE_PREFIX)) { 72 | targetKeys.push(key); 73 | } 74 | } 75 | targetKeys.forEach(key => { 76 | localStorage.removeItem(key); 77 | }); 78 | }; 79 | 80 | /** 81 | * 根据 url 清除单个缓存 82 | */ 83 | (window as any).cleanRequestDevCache = (url: string) => { 84 | localStorage.removeItem(`${STORAGE_PREFIX}${url}`); 85 | }; 86 | } 87 | }; 88 | 89 | function log(url, body, result) { 90 | console.groupCollapsed(`接口缓存读取成功 ${url}`); 91 | console.log('%c 接口参数', 'color: #03A9F4; font-weight: bold', body); 92 | console.log('%c 缓存结果', 'color: #4CAF50; font-weight: bold', result); 93 | console.groupEnd(); 94 | } 95 | 96 | function defaultGenerateKey(url, body) { 97 | return `${url}-${JSON.stringify(body as object)}`; 98 | } 99 | -------------------------------------------------------------------------------- /useMap.ts: -------------------------------------------------------------------------------- 1 | import { useReducer, useEffect, useCallback } from 'react'; 2 | 3 | export type MapType = { 4 | [key: string]: any; 5 | }; 6 | 7 | export const CHANGE = 'CHANGE'; 8 | 9 | export const CHANGE_ALL = 'CHANGE_ALL'; 10 | 11 | export const SET_MAP = 'SET_MAP'; 12 | 13 | export type Change = { 14 | type: typeof CHANGE; 15 | payload: { 16 | key: any; 17 | value: any; 18 | }; 19 | }; 20 | 21 | export type ChangeAll = { 22 | type: typeof CHANGE_ALL; 23 | payload: any; 24 | }; 25 | 26 | export type SetCheckedMap = { 27 | type: typeof SET_MAP; 28 | payload: MapType; 29 | }; 30 | 31 | export type Action = Change | ChangeAll | SetCheckedMap; 32 | 33 | export type Initial = (dataItem: T) => any; 34 | 35 | export interface Option { 36 | /** 用来在map中作为key 一般取id */ 37 | key?: string; 38 | initial?: (dataItem: T) => any; 39 | } 40 | 41 | /** 42 | * 根据数组生成对应的map 默认以每一项的id为key 43 | * 44 | * - 在数据异步更新的时候也可以为新增项生成初始值 45 | * - 允许在数据更新的时候自动剔除陈旧项 46 | */ 47 | export function useMap>( 48 | dataSource: T[], 49 | option: Option = {} 50 | ) { 51 | const { key = 'id', initial = () => undefined } = option; 52 | 53 | const getInitialMap = () => 54 | dataSource.reduce((prev, cur) => { 55 | const { [key]: id } = cur; 56 | prev[id] = initial(cur); 57 | return prev; 58 | }, {}); 59 | 60 | const [map, dispatch] = useReducer( 61 | (checkedMapParam: MapType, action: Action) => { 62 | switch (action.type) { 63 | // 单值改变 64 | case CHANGE: { 65 | const { payload } = action; 66 | const { key, value } = payload; 67 | return { 68 | ...checkedMapParam, 69 | [key]: value, 70 | }; 71 | } 72 | // 所有值改变 73 | case CHANGE_ALL: { 74 | const { payload } = action; 75 | const newMap: MapType = {}; 76 | dataSource.forEach(dataItem => { 77 | newMap[dataItem[key]] = payload; 78 | }); 79 | return newMap; 80 | } 81 | // 完全替换map 82 | case SET_MAP: { 83 | return action.payload; 84 | } 85 | default: 86 | return checkedMapParam; 87 | } 88 | }, 89 | getInitialMap() 90 | ); 91 | 92 | /** map某项的值变更 */ 93 | const set = useCallback((key, value) => { 94 | dispatch({ 95 | type: CHANGE, 96 | payload: { 97 | key, 98 | value, 99 | }, 100 | }); 101 | }, []); 102 | 103 | /** map中所有值变更 */ 104 | const setAll = useCallback(value => { 105 | dispatch({ 106 | type: CHANGE_ALL, 107 | payload: value, 108 | }); 109 | }, []); 110 | 111 | // 数据更新的时候 对新加入的数据进行初始化处理 112 | useEffect(() => { 113 | let changed = false; 114 | dataSource.forEach(dataItem => { 115 | const { [key]: id } = dataItem; 116 | if (!(id in map)) { 117 | map[id] = initial(dataItem); 118 | changed = true; 119 | } 120 | }); 121 | 122 | if (changed) { 123 | dispatch({ 124 | type: SET_MAP, 125 | payload: map, 126 | }); 127 | } 128 | // eslint-disable-next-line react-hooks/exhaustive-deps 129 | }, [dataSource]); 130 | 131 | return { 132 | map, 133 | set, 134 | setAll, 135 | dispatch, 136 | }; 137 | } 138 | -------------------------------------------------------------------------------- /parse-json.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://lihautan.com/json-parser-with-javascript 3 | */ 4 | function fakeParseJSON(str) { 5 | let i = 0; 6 | 7 | const value = parseValue(); 8 | expectEndOfInput(); 9 | return value; 10 | 11 | function parseObject() { 12 | if (str[i] === "{") { 13 | i++; 14 | skipWhitespace(); 15 | 16 | const result = {}; 17 | 18 | let initial = true; 19 | // if it is not '}', 20 | // we take the path of string -> whitespace -> ':' -> value -> ... 21 | while (i < str.length && str[i] !== "}") { 22 | if (!initial) { 23 | eatComma(); 24 | skipWhitespace(); 25 | } 26 | const key = parseString(); 27 | if (key === undefined) { 28 | expectObjectKey(); 29 | } 30 | skipWhitespace(); 31 | eatColon(); 32 | const value = parseValue(); 33 | result[key] = value; 34 | initial = false; 35 | } 36 | expectNotEndOfInput("}"); 37 | // move to the next character of '}' 38 | i++; 39 | 40 | return result; 41 | } 42 | } 43 | 44 | function parseArray() { 45 | if (str[i] === "[") { 46 | i++; 47 | skipWhitespace(); 48 | 49 | const result = []; 50 | let initial = true; 51 | while (i < str.length && str[i] !== "]") { 52 | if (!initial) { 53 | eatComma(); 54 | } 55 | const value = parseValue(); 56 | result.push(value); 57 | initial = false; 58 | } 59 | expectNotEndOfInput("]"); 60 | // move to the next character of ']' 61 | i++; 62 | return result; 63 | } 64 | } 65 | 66 | function parseValue() { 67 | skipWhitespace(); 68 | const value = 69 | parseString() ?? 70 | parseNumber() ?? 71 | parseObject() ?? 72 | parseArray() ?? 73 | parseKeyword("true", true) ?? 74 | parseKeyword("false", false) ?? 75 | parseKeyword("null", null); 76 | skipWhitespace(); 77 | return value; 78 | } 79 | 80 | function parseKeyword(name, value) { 81 | if (str.slice(i, i + name.length) === name) { 82 | i += name.length; 83 | return value; 84 | } 85 | } 86 | 87 | function skipWhitespace() { 88 | while ( 89 | str[i] === " " || 90 | str[i] === "\n" || 91 | str[i] === "\t" || 92 | str[i] === "\r" 93 | ) { 94 | i++; 95 | } 96 | } 97 | 98 | function parseString() { 99 | if (str[i] === '"') { 100 | i++; 101 | let result = ""; 102 | while (i < str.length && str[i] !== '"') { 103 | if (str[i] === "\\") { 104 | const char = str[i + 1]; 105 | if ( 106 | char === '"' || 107 | char === "\\" || 108 | char === "/" || 109 | char === "b" || 110 | char === "f" || 111 | char === "n" || 112 | char === "r" || 113 | char === "t" 114 | ) { 115 | result += char; 116 | i++; 117 | } else if (char === "u") { 118 | if ( 119 | isHexadecimal(str[i + 2]) && 120 | isHexadecimal(str[i + 3]) && 121 | isHexadecimal(str[i + 4]) && 122 | isHexadecimal(str[i + 5]) 123 | ) { 124 | result += String.fromCharCode( 125 | parseInt(str.slice(i + 2, i + 6), 16) 126 | ); 127 | i += 5; 128 | } else { 129 | i += 2; 130 | expectEscapeUnicode(result); 131 | } 132 | } else { 133 | expectEscapeCharacter(result); 134 | } 135 | } else { 136 | result += str[i]; 137 | } 138 | i++; 139 | } 140 | expectNotEndOfInput('"'); 141 | i++; 142 | return result; 143 | } 144 | } 145 | 146 | function isHexadecimal(char) { 147 | return ( 148 | (char >= "0" && char <= "9") || 149 | (char.toLowerCase() >= "a" && char.toLowerCase() <= "f") 150 | ); 151 | } 152 | 153 | function parseNumber() { 154 | let start = i; 155 | if (str[i] === "-") { 156 | i++; 157 | expectDigit(str.slice(start, i)); 158 | } 159 | if (str[i] === "0") { 160 | i++; 161 | } else if (str[i] >= "1" && str[i] <= "9") { 162 | i++; 163 | while (str[i] >= "0" && str[i] <= "9") { 164 | i++; 165 | } 166 | } 167 | 168 | if (str[i] === ".") { 169 | i++; 170 | expectDigit(str.slice(start, i)); 171 | while (str[i] >= "0" && str[i] <= "9") { 172 | i++; 173 | } 174 | } 175 | if (str[i] === "e" || str[i] === "E") { 176 | i++; 177 | if (str[i] === "-" || str[i] === "+") { 178 | i++; 179 | } 180 | expectDigit(str.slice(start, i)); 181 | while (str[i] >= "0" && str[i] <= "9") { 182 | i++; 183 | } 184 | } 185 | if (i > start) { 186 | return Number(str.slice(start, i)); 187 | } 188 | } 189 | 190 | function eatComma() { 191 | expectCharacter(","); 192 | i++; 193 | } 194 | 195 | function eatColon() { 196 | expectCharacter(":"); 197 | i++; 198 | } 199 | 200 | // error handling 201 | function expectNotEndOfInput(expected) { 202 | if (i === str.length) { 203 | printCodeSnippet(`Expecting a \`${expected}\` here`); 204 | throw new Error("JSON_ERROR_0001 Unexpected End of Input"); 205 | } 206 | } 207 | 208 | function expectEndOfInput() { 209 | if (i < str.length) { 210 | printCodeSnippet("Expecting to end here"); 211 | throw new Error("JSON_ERROR_0002 Expected End of Input"); 212 | } 213 | } 214 | 215 | function expectObjectKey() { 216 | printCodeSnippet(`Expecting object key here 217 | 218 | For example: 219 | { "foo": "bar" } 220 | ^^^^^`); 221 | throw new Error("JSON_ERROR_0003 Expecting JSON Key"); 222 | } 223 | 224 | function expectCharacter(expected) { 225 | if (str[i] !== expected) { 226 | printCodeSnippet(`Expecting a \`${expected}\` here`); 227 | throw new Error("JSON_ERROR_0004 Unexpected token"); 228 | } 229 | } 230 | 231 | function expectDigit(numSoFar) { 232 | if (!(str[i] >= "0" && str[i] <= "9")) { 233 | printCodeSnippet(`JSON_ERROR_0005 Expecting a digit here 234 | 235 | For example: 236 | ${numSoFar}5 237 | ${" ".repeat(numSoFar.length)}^`); 238 | throw new Error("JSON_ERROR_0006 Expecting a digit"); 239 | } 240 | } 241 | 242 | function expectEscapeCharacter(strSoFar) { 243 | printCodeSnippet(`JSON_ERROR_0007 Expecting escape character 244 | 245 | For example: 246 | "${strSoFar}\\n" 247 | ${" ".repeat(strSoFar.length + 1)}^^ 248 | List of escape characters are: \\", \\\\, \\/, \\b, \\f, \\n, \\r, \\t, \\u`); 249 | throw new Error("JSON_ERROR_0008 Expecting an escape character"); 250 | } 251 | 252 | function expectEscapeUnicode(strSoFar) { 253 | printCodeSnippet(`Expect escape unicode 254 | 255 | For example: 256 | "${strSoFar}\\u0123 257 | ${" ".repeat(strSoFar.length + 1)}^^^^^^`); 258 | throw new Error("JSON_ERROR_0009 Expecting an escape unicode"); 259 | } 260 | 261 | function printCodeSnippet(message) { 262 | const from = Math.max(0, i - 10); 263 | const trimmed = from > 0; 264 | const padding = (trimmed ? 4 : 0) + (i - from); 265 | const snippet = [ 266 | (trimmed ? "... " : "") + str.slice(from, i + 1), 267 | " ".repeat(padding) + "^", 268 | " ".repeat(padding) + message 269 | ].join("\n"); 270 | console.log(snippet); 271 | } 272 | } 273 | 274 | // console.log("Try uncommenting the fail cases and see their error message"); 275 | // console.log("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓"); 276 | 277 | // Fail cases: 278 | printFailCase("-"); 279 | printFailCase("-1."); 280 | printFailCase("1e"); 281 | printFailCase("-1e-2.2"); 282 | printFailCase("{"); 283 | printFailCase("{}{"); 284 | printFailCase('{"a"'); 285 | printFailCase('{"a": "b",'); 286 | printFailCase('{"a":"b""c"'); 287 | printFailCase('{"a":"foo\\}'); 288 | printFailCase('{"a":"foo\\u"}'); 289 | printFailCase("["); 290 | printFailCase("[]["); 291 | printFailCase("[[]"); 292 | printFailCase('["]'); 293 | 294 | function printFailCase(json) { 295 | try { 296 | console.log(`fakeParseJSON('${json}')`); 297 | fakeParseJSON(json); 298 | } catch (error) { 299 | console.error(error); 300 | } 301 | } 302 | --------------------------------------------------------------------------------