博客列表 - 第 6 页

共 92 篇文章

IndexDB游标的正确打开方式

# IndexDB游标的正确打开方式 ## 引言 我最近在维护我的玩具项目`react-door`,当我试图使用IndexDB的游标遍历某个表时,我遇到了一些让人十分困惑的问题,下面我将带大家来复现一下事故的现场。 ## 复现 首先,在我的印象中,游标类似迭代器,所以刚开始我试图拿到游标传递出去然后在dao层调用: ```js // indexDB.js // 获取cursor const getCursor = () => { if (!db) { return Promise.reject("database connect instance can't be null") } const store = db.transaction(storeName, 'readonly').objectStore(storeName) const request = store.openCursor(); return new Promise((resolve, reject) => { request.onsuccess = (e) => { const cursor = e.target["result"] resolve(cursor) } request.onerror = (e) => { reject(e) } }) } // in dao.js async function someFunc() { const cursor = await getCursor(); if (cursor) { // do something cursor.continue() } else { console.log("no more data") } } ``` 一开始我是这么设想的,但是我发现了一个令人特别疑惑的问题:为什么在MDN的文档中,使用`if...else...`来遍历这个cursor呢? ```js if (cursor) { var listItem = document.createElement("li"); listItem.innerHTML = cursor.value.albumTitle + ", " + cursor.value.year; list.appendChild(listItem); cursor.continue(); } else { console.log("Entries all displayed."); } // 来自MDN,可以看到使用 if else 进行遍历 ``` 当时我也是借鉴MDN,使用`if...else...`来遍历,但是我发现我的代码只能运行一次,这肯定不叫遍历啊。**谁家好人用`if...else`做遍历啊???** 年轻人不气盛能叫年轻人吗?这肯定是MDN写错了!根据我的经验,立刻给他改写成了这样: ```js async function someFunc() { const cursor = await getCursor(); // 从 if 语句变为 while 语句 while (cursor) { // 猜猜会输出什么? console.log(cursor.key); // do something cursor.continue() } console.log("no more data") } ``` 看起来没什问题,非常正确! **但是** ,在我实际运行这段代码的时候,并不能正常运行。我们希望它的输出是`1 2 3 4 5 ....`,而实际的输出结果却是`1 1 1 1 1 ...`。为啥会出现这种结果呢?😥本来我以为是`cursor.continue()`方法是异步的,但是结果这是一个完完全全的同步函数。怎么回事呢? 如果说`continue`方法不起作用的话,但是它确实能够帮我们将cursor向前移位,并在cursor结束时结束循环(虽然报了一个info),但是如果说它起作用的,我们的cursor每次都指向第一条数据。这是怎么回事呢??? 难道IndexDB这个方法还有bug?不可能吧?*难道真的是使用`if...else`遍历*?于是我再次查阅MDN,现在我为大家贴上MDN完整的代码段: ```js function displayData() { var transaction = db.transaction(["rushAlbumList"], "readonly"); var objectStore = transaction.objectStore("rushAlbumList"); objectStore.openCursor().onsuccess = function (event) { var cursor = event.target.result; if (cursor) { var listItem = document.createElement("li"); listItem.innerHTML = cursor.value.albumTitle + ", " + cursor.value.year; list.appendChild(listItem); cursor.continue(); } else { console.log("Entries all displayed."); } }; } ``` 经过我的观察,难道...`cursor只能在openCursor的回调中使用??` 带着这个猜想,我将我的代码改为这样: ```js if (!db) { return Promise.reject("database connect instance can't be null") } const store = db.transaction(storeName, 'readonly').objectStore(storeName) const request = store.openCursor(); return new Promise((resolve, reject) => { request.onsuccess = (e) => { const res = [] const cursor = e.target["result"] if (cursor) { res.push(cursor.value) cursor.continue() } else { resolve(res) } } request.onerror = (e) => { reject(e) } }) ``` 嗯,果然不出所料,现在我们已经可以正确的遍历cursor了!但是又有一个问题随之而来,为什么每次我的res只有一个数据😅?经过我的一番思考,一个很恐怖的想法闪现出来,沃日,indexDB你不会....**每次游标移位都要调用onSuccess回调吧**? 于是我们再次改写代码,写成这样: ```js if (!db) { return Promise.reject("database connect instance can't be null") } const store = db.transaction(storeName, 'readonly').objectStore(storeName) const request = store.openCursor(); // 将res放到回调函数之外 const res = [] return new Promise((resolve, reject) => { request.onsuccess = (e) => { const cursor = e.target["result"] if (cursor) { res.push(cursor.value) cursor.continue() } else { resolve(res) } } request.onerror = (e) => { reject(e) } }) ``` 这时,我们终于如愿以偿的拿到了我们需要的数据(真是辛苦啊😭) ## 总结 现在我们来总结一下cursor的坑: - cursor必须配合`oepnCursor`函数的回调函数一起使用 - 每次调用`cursor.continue()`并不是简单的将cursor移位,而是请求IDB的API更新cursor并再次触发`openCursor`的回调函数 所以上面那一版代码就是使用cursor遍历的标准模板,因为这个过程是类似递归的过程,所以我们的cursor是真的使用`if...else...`遍历代码😥,没毛病哦兄弟们

使用node.js创建一个todo列表——node.js服务器搭建、json读写以及使用pm2保持服务运行

# 使用node.js创建一个todo列表——node.js服务器搭建以及json读写 ## 前言 前些日子学习了 nodejs ,顺理成章的想要找点东西练练手。恰好最近需要一个简介的todo list网页,因为之前用的一些todo list应用都被墙了,访问速度感人,于是就想自己搞一个todo。 而恰好,我要搞的todo是一个非常简单的程序,非常适合用nodejs这样的解释型脚本语言来实现,比起java这样光搭框架就要半天的专注大型项目的语言,nodejs 的优势就是非明显了。 <!-- more --> nodejs搭建服务器只需要去nodejs官网复制这样的一块代码: ```javascript const express = require('express') const app = express() app.get('/', (req, res) => { res.send('Hi!') }) app.listen(3000, () => console.log('Server ready')) ``` 所以我们可以把更多的经历放在我们的业务实现上。 ## 准备工作 ### 需求分析 在开始动工之前,要先列出需求。当时我的需求如下: - 添加需要做的事情 - 展示需要做的事情 - 完成需要做的事情 - 查看曾经完成的事情 ### 解决方案分析 对于上述需求,我打算这样实现: - 使用json文件系统实现持久化操作 - 使用nodejs来获取数据 - 使用nodejs来处理数据并储存数据 这样的话。明确了需求与实现方式,我们就可以动工了。 ## 实现 ### json读写 nodejs提供了一套文件的读写的模块:`fs`。使用fs可以很简单的实现文本文件的读写。但是为了我们在开发的时候更加方便,我又将他封装成了一个专注json读写的方法: ```javascript //in jsonHandler.js const fs = require("fs"); function test(){ console.log("Hello world!") } function readJson(name){ let jsonFile = fs.readFileSync("./"+name); let toDoList = JSON.parse(jsonFile); //解析json,并直接返回json对象 return toDoList; } function writeJson(name,data){ fs.writeFileSync("./"+name,JSON.stringify(data)); } module.exports = { readJson, writeJson } ``` ### 服务器搭建 还记得我们的服务器的格式吗?我们先定义一下我们的服务器接口: ```javascript //in index.js const jh = require("./jsonHandler") const http = require('http'); const url = require('url'); const util = require('util'); let workList = jh.readJson("list.json"); const port = 3000 const server = http.createServer((req, res) => { //设置响应头与请求编码 res.statusCode = 200 //设置字节流编码为utf-8 res.setHeader('Content-Type', 'text/plain;charset=utf8') //设置允许跨域 res.setHeader('Access-Control-Allow-Origin','*') req.setEncoding('utf8'); //write your code here let ret=""; //这是需要返回的数据 res.end(ret) }) server.listen(port, () => { }) ``` 然后根据我们的需求分析,先来确定以下API接口: - `addWork()`:用来添加一个任务并将数据保存到json - `deleteWord(res)`:用来将一个任务删除并将数据保存到json - `writeHistory(work)`:将完成的任务添加到历史记录中 - `getAllWork()`:获取目前正在运行中的任务 首先我们先开始addWork()的编写,要添加的话,首先要接受并解析客户端发送的请求,然后再储存到json中,代码如下: ```javascript //用来增加一个任务 function addWork(){ let work=""; req.on('data', chunk => { //接受客户端发送的请求流 //这里要取子串的原因是请求会带key,我这里是work=xxx,所以要将'work='去掉 work=chunk.toString().substring(5); //设置编码,不然中文会乱码 work = decodeURIComponent(work); //解析worklist let jsonObj=eval(workList) jsonObj.push({"work":work}) //将数据写回 jh.writeJson("list.json",jsonObj) }) } ``` 任务历史与添加任务几乎同理,只不过这个方法获取的数据是deleteWork提供的,我们不需要再去监控客户端的请求。代码如下: ```javascript //任务历史 function writeHistory(work) { let jsonStr = jh.readJson("history.json"); let jsonObj = eval(jsonStr); let event = {} event.work = work; event.finishTime =new Date(); jsonObj.push(event); jh.writeJson("history.json",jsonObj) } ``` 删除任务也是同理,我们要在前端给每个任务隐性的标上index,然后就可以用来删除了: ```javascript //删除任务 function deleteWork(res){ req.on('data', chunk => { console.log(`可用的数据块: ${chunk}`) //获取点击事件的位置 let index=chunk.toString().substring(3); let jsonObj=eval(workList) //添加历史 let work = jsonObj[index].work writeHistory(work) //删除 jsonObj.splice(index,1); // console.log(jsonObj) jh.writeJson("list.json",jsonObj) }) } ``` 获取任务就非常简单了,我们只需要从json中拿到数据并转发给客户端就可以了: ```javascript //获取所有任务 function getAllWork(){ workList=jh.readJson("list.json"); ret=JSON.stringify(workList); } ``` 最后呢,我们需要写一个路由解析,因为js非常小,所以这些东西都写在一个文件里面就可以: ```javascript /解析路由 let pathname = url.parse(req.url).pathname; //路由配置 if (pathname==="/getAllWork"){ getAllWork() } else if(pathname==="/addWork"){ addWork() } else if(pathname==="/deleteWork"){ deleteWork() } ``` 随下贴出前端源码: ```html <-- in index.html --> <!DOCTYPE html> <html lang="ch"> <head> <link rel="stylesheet" type="text/css" href="https://cdn.simplecss.org/simple.css"> <link rel="stylesheet" type="text/css" href="technological.css"> <meta charset="UTF-8"> <title>Title</title> <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script> </head> <body class="tech-background" style="text-align: center"> <p> wish you a substantial day</p> <p>to do list</p> <label> <input name="add-work" id="add-work" class="tech-input" style="width: 70%"> <button onclick='addWork()' class="tech-btn" style="display: inline;">增加</button> </label> <div id="container"></div> <script> 'use strict' $.ajax({ url:"http://localhost:3000/getAllWork", method:"GET", crossDomain: true, success:(res)=>{ let con=document.getElementById("container"); let workList=JSON.parse(res) console.log(workList) workList=eval(workList) for (let i=0;i<workList.length;i++){ let list=document.createElement("label"); list.innerHTML="<input name=\"work-list\" id=\"work"+i+"\" value='"+workList[i].work+"' class='tech-input' style=\"width: 70%\"> " + "<button onclick='deleteWork(this)' id='"+i+"' class='tech-btn' style=\"display: inline;\">完成</button>"; con.appendChild(list); } } }) function addWork(){ let work=document.getElementById("add-work").value; $.ajax({ url: "http://localhost:3000/addWork", method: "POST", crossDomain: true, data:{ "work":work }, success:(res)=>{ // alert("success"); location.reload(); } }) } function deleteWork(res){ let index=res.id; $.ajax({ url: "http://localhost:3000/deleteWork", method: "POST", crossDomain: true, data:{ "id":index }, success:(res)=>{ // alert("success"); location.reload(); } }) } </script> </body> </html> ``` 这个项目的源码我也会放在GitHub(因为项目实在太小我甚至都没有建一个文件夹) [项目地址](https://github.com/charlesix59/todo_list_by_nodeJS) ## 保持代码运行 我们服务器代码当然要有一个服务器去运行,而这个迷你的项目显然不值得让我们去分配他一个云服务器加一个域名,那么我们是否可以把这个服务运行在本地呢? 可是运行在本地就需要频繁的重启服务,每次开机都要启动一次,这未免也太麻烦了。于是我们就可以使用**pm2**来使我们的项目保持运行 关于pm2我在此不多赘述了,大家有兴趣的可以自行百度搜索,我只给出部署的命令: ```shell npm install pm2 -g #全局安装pm2 cd <项目目录> pm2 start index.js --watch pm2 save pm2 list ``` 如果你部署成功,那么当你运行`pm2 list`之后你就可以看到你的项目了

flex-direction为row时主动换行

# flex-direction为row时主动换行(React native主动换行) 我今天在写`react-native`时候,遇到了很不舒服的一件事情。因为RN默认是`flex`布局,所以如果我想要实现`inline`模式就只能设置父元素的`flex-direction`的value为`row`。这样如果我们如果想要换行需要怎么处理呢? 要知道,在RN中是没有`<br>`标签的,所以要想换行需要采取一些非常的手段。 那有没有什么好办法能够做到主动换行呢? 在正式开始之前我先上一个例子: ```jsx const arr = [1, 2, 3, 4, 5, 6]; export default function App() { return ( <SafeAreaView style={styles.container}> <View style={{ flexDirection: 'row' }}> {arr.map((item, index) => ( <View> <Text key={index}>{item}</Text> </View> ))} </View> </SafeAreaView> ); } ``` 很明显,上述的代码会生成一个纵向排列的`123456`,如果我们希望在**每三个断一下**,即将纵向的`123456`改成横向的`123 <br> 456`应该如何做呢? 刚开始的时候我问了ChatGPT,他给出的答案是使用伪类撑开元素: ```html <div class="flex-container"> <div class="flex-item">1</div> <div class="flex-item break-after">2</div> <!-- 想要在这个元素后换行 --> <div class="flex-item">3</div> <div class="flex-item">4</div> </div> ``` ```css .flex-container { display: flex; flex-wrap: wrap; flex-direction: row; } .flex-item { width: 100px; margin: 5px; } .break-after::after { content: ""; flex-basis: 100%; } ``` 经过我的尝试,这种方法很遗憾的不起任何作用,而且在RN中使用CSS class并不是很容易。但是他给出的代码确实给了我一些启发,经过一些摸索,我发现**如果一次渲染两个元素,其中一个是正常元素而另一个负责撑开元素**,那么就可以实现换行,代码如下: ```jsx const arr = [1, 2, 3, 4, 5, 6]; export default function App() { return ( <SafeAreaView style={styles.container}> <View style={{ flexDirection: 'row', flexWrap: 'wrap', width: 200 }}> {arr.map((item, index) => { if (item % 3 !== 0) { return <Text key={index}>{item}</Text>; } else { return ( <> {[ <Text key={index}>{item}</Text>, <View style={{ width: '100%' }}></View>, ]} </> ); } })} </View> </SafeAreaView> ); } ``` 但是很抱歉,这样依旧有缺陷,因为reactNative中`<></>`实际上会影响继承,即Text和View将会被看作一个整体,而不会被解析成两个单独的元素,**并且`return`中的`jsx fragment`必须有一个根节点**,也就说说这种方法与顶上charGPT给出的方法并没有区别,都无法将一个元素完美换行。这只会让我们需要换行的元素变为单独的一行而已。 那么,还有什么方法呢? ## 完美的换行 这时候我们或许需要更新一下思路,如果在渲染过程中无法做到完美的换行,那么我们为什么不能在数据上动一点手脚呢?经过一系列思路的转换,我突然想到我们完全可以**进行一个数组分割+元素嵌套的方式实现换行**,代码如下: ```jsx const arr = [1, 2, 3, 4, 5, 6]; export default function App() { const [arrState, setArrState] = useState([]); useEffect(() => { const result = []; let tempArr = []; arr.forEach((item) => { tempArr.push(item); if (item % 3 === 0) { result.push(tempArr); tempArr = []; } }); setArrState(result); }, []); return ( <SafeAreaView style={styles.container}> {arrState.map((subArr, index) => ( <View key={index} style={{ flexDirection: 'row', flexWrap: 'wrap', width: 200 }}> {subArr.map((item, subIndex) => { return <Text key={subIndex}>{item}</Text>; })} </View> ))} </SafeAreaView> ); } ``` 终于,我们实现了完美的换行! so,如果你也想要在flex布局中尝试主动换行,那么希望这篇文章能够解答你的疑惑。 当然,如果你有更好的方法,非常希望能在下方评论区和我分享你的好主意🤗

如何在 react 表达式中传入 react node[] (节点数组)

# 如何在 react 表达式中传入 react node[] (节点数组) 我们知道 jsx 表达式允许插入`reactNode`以及`ReactNode[]`,当但我们传入一个类似的东西的时候他却会报错: ```jsx <div className='App'> { <p>aaa</p> <p>bbb</p> // ❌ JSX expressions must have one parent element } </div> ``` 但是我们明明可以使用 map 函数传入数组呀! ```jsx <div className="App"> {arr.map((num) => ( <p key={num}>{num}</p> ))} </div> ``` 如果我们希望手动在表达式中传入一个节点数组我们应该怎么做呢?当我遇到这种奇葩问题的时候我也是愣了一会的,经过我对`Array.map()`方法的观察,我突然顿悟,如果想要手动传入一个节点数组应该这么写: ```jsx <div className="App">{[<p>aaa</p>, <p>bbb</p>]}</div> ``` 但是回头想想这个问题奇怪的很,我们明明可以写成: ```jsx <div className="App"> <p>aaa</p> <p>bbb</p> </div> ``` 但是其实有时候我们真的会需要手动的传入一个`node[]`,比如有些组件可能只接受一个节点数组而你又只有有限个节点不希望使用`map()`,或者是调试时候使用,再或者是希望在渲染列表中加入几个与之前的节点结构不同的节点,比如以下这个实例: ```jsx <div className="App"> {arr.map((num) => <p key={num}>{num}</p>) .concat([<p>aaa</p>, <p>bbb</p>])} </div> ``` 以上就是如何使用`reactNode[]`的一点个人心得咯

全面理解useEffect——useEffect用法解析与使用技巧

# 全面理解useEffect `useEffect`可以说是react的hook中最强大的一个,他能够以一种极为优雅的方式模拟我们的生命周期。但是优雅的代价就是这个hook极其的晦涩,不容易被理解与掌握。下面我们就来全方位的探讨一下,`useEffect`到底怎么用 ## useEffect的结构 在上一篇关于react的文章我提到过,函数式组件每次`reRender`都会重新执行一遍,如果我们需要生命周期的效果,那我们就需要`useEffect`函数。我们先来看一下useEffect的结构: ```js useEffect(setup, dependencies?) ``` - 参数 - `setup`:一个实现了你需要的副作用逻辑的函数,这个函数将会在组件首次被绑定以及每次**重新渲染导致`dependencies`更新时**调用。同时他还可以`return`一个清除函数,这个函数将会在函数卸载以及每次**重新渲染导致`dependencies`更新之前**调用 - `dependencies`:在`setup`中引用的所有`reactive values`(state、参数以及在组件内部定义的变量和函数)或更新凭据组成的数组。这个数组必须是有限的并且偏平的,每次更新react将使用`Object.is()`比较每个依赖项是否与之前相同,如果不同则更新。 - return - `undefinded` (无返回值) 中文博客中经常会漏掉`setup`函数的返回值,但是它的返回值是这个函数的半壁江山,我们决不能将其忽略不提,如果忽略掉的话我们就有一半的效果无法实现了。 ❗:`useEffect`也是一个hook,请在组件或者自定义hook的最上层使用,不要在循环或分支结构中定义hook ## 模拟生命周期 react生命周期大概分为三种,`mount`、`update`、`unmount`,当然在类式组件,我们已经可以规定是在mount之前,mount时还是mount之后调用,在函数式组件我们就不分的这么清楚了,因为在函数式组件我们的生命周期其实是独立的,是面向状态的。 下面我们就使用useEffect简单模拟一下类式组件的生命周期: **只在初始化时执行(`DidMounted`)** : ```js // 不依赖任何变量,则传入一个空数组 useEffect(() => { console.log("init"); return () => {}; }, []); ``` **每次更新时执行(`render`)** : ```js // 依赖所有变量,则不传入 useEffect(() => { console.log("rerender"); }); ``` **每次卸载时执行(`beforeUnmount`)** : ```js // 不依赖任何变量,return的函数将会在卸载之前被调用 useEffect(() => { return () => {console.log("unmount");}; }, []); ``` 实际上,我们经常说`useEffect`的作用是模拟声明周期,而这似乎违背了函数式组件的初衷,也违背了`useEffect`的初衷。我们应该多从**状态**的角度去思考,`useEffect`并非是什么生命周期函数,而是**状态更新**函数。 ## 控制更新 如果某个变量在每次组件更新的时候都需要更新,那我们完全没有必要使用`useEffect`,我们使用副作用钩子一定是因为我们需要控制某个变量的更新。 我们可以使用`useEffect`将**组件内部的状态与组件外部系统同步**: ```jsx // 比如说,我们可以在父组件更新props的时候调用useEffect const Child = ({ count }) => { const [childContext, setChildContext] = useState(0); useEffect(() => { setChildContext(count); }, [count]); return <p>this is a child, count = {childContext}</p>; }; ``` 或者我们可以使用`useEffect`将**组件组件外部系统与内部的状态同步**: ```jsx import "./styles.css"; import { useEffect, useState } from "react"; const fetch = async (count) => { // 假设这是一个网络请求 // 当然,这个外部状态也可以是一个外部组件,外部变量,jsAPI等等 await new Promise(() => { setTimeout(() => { console.log("fetch date"); }, count); }); }; export default function App() { const [count, setCount] = useState(0); const onClick = () => { setCount(count + 1); }; // 每次修改count都会将更改同步外部系统 useEffect(() => { fetch(count); }, [count]); return ( <div className="App"> <button onClick={onClick}>Change state in father</button> <p>count is {count}</p> </div> ); } ``` 其实生命周期也不过是对状态的统一更改,react现在只是将生命周期将颗粒度从组件降低到了状态。 ## 一些tips ### 在useEffect中使用state 有时候我们可能会需要在`useEffect`中使用和修改`state`,但是这是极端危险的行为,因为当我们修改`state`时会触发页面的`rerender`,然后因为`useEffect`依赖了`state`,这将会导致`state`被再次更改继而引起死循环…… ```jsx const Child = ({ count }) => { const [childContext, setChildContext] = useState(0); useEffect(() => { setChildConte(childContext); }, [count, childContext]); // !! 千万不要这么做,会引发死循环 return <p>this is a child, count = {childContext}</p>; }; ``` 如果我们在`useEffect`中根据之前的state更新state,我们可以这么写: ```jsx useEffect(() => { setChildContext(c => c + 1); }, [count]); // 不需要依赖childContext,安全 ``` 如果你真的既要在Effect中读取state,又要修改state,那么我给出的建议是,:**使用两个依赖同一个更新凭证`useEffect`** 千万别这么做: ```jsx useEffect(() => { setChildContext(Math.random()) console.log(childContext) }, [count, childContext]) // 死循环!!😭 ``` 正确的做法如下: ```jsx useEffect(() => { setChildContext(Math.random()) }, [count]); useEffect(() => { console.log(childContext) }, [count, childContext]) // 安全😊 ``` ### 消除不必要的对象与函数依赖 如果我们在组件内定义了一个函数或对象,我们在上一篇文章详细讲过,它每次都会被初始化。就像这样: ```jsx const childContext = { count: 1 } useEffect(() => { childContext.count = count; console.log("run effect") // 不要这样,每次Render都会触发effect😔 }, [count, childContext]); ``` 取而代之的是,如果你的对象或者函数只在`useEffect`中使用,则写在`setup`内部 ```jsx const Child = ({count}) => { useEffect(() => { const childContext = { count: 1 } childContext.count = count; console.log("run effect") }, [count]); // 推荐,只有在依赖更新的时候才会触发effect 😊 return <p>this is a child</p>; }; ``` 如果你是希望有一个在整个组件都可以被访问的静态变量,那么可以将对象写在组件外部: ```jsx const childContext = { count: 1 } const Child = ({count}) => { useEffect(() => { childContext.count = count; console.log("run effect") }, [count]); // 推荐,只有在依赖更新的时候才会触发effect 😊 return <p>this is a child</p>; }; ``` ## useLayoutEffect 我们在使用`useEffect`的时候有时会出现一种问题:我们的组件会发生闪烁或突然的变化。 这时因为`useEffect`是在`mount`之后执行的,因此第一次渲染出的值是我们定义的初始值,如果更新的性能比较差或者更新的范围比较大,可能会导致我们的`view`发生闪烁的现象。 这个时候我们就可以使用`useLayoutEffect`,他和`useEffect`的最大区别就是他是在`mount`之前执行的,所以不会导致`view`的闪烁。 但是如果我们没有很明显的ui闪烁问题,我们应该尽可能的使用`useEffect`,因为它的运行效率更好。

应该在哪里定义react函数——关于react开发规范的一些理解

# 应该在哪里定义react函数——关于react开发规范的一些理解 ## 前言 我之前有一个同事,从vue技术栈转入react技术栈,对于函数式编程还有一些不太熟悉。 最近他问了我一个问题,**react函数应该定义在什么地方**?他发现有些函数写在react组件之内,有些却写在react函数之外。他想知道哪种做法是最合适的。这是一个很好的问题,因为这体现了对于代码规范的追求。现在我希望在此妥善的梳理一下这个问题。 ## 看法 先开门见山的说一下我的看法,我的看法就是<mark>应外尽外</mark>,意思就是**能够**放到组件外部的函数都应该放到外部。但是需要注意的是,只有满足一定条件的函数才能够放到组件外部。在具体的讨论这些规则之前,我们应该先来看一下函数写在组件内与组件外部的<mark>区别</mark>。 ## 区别 其实两者的区别十分显著,我们在编写`jsx`文件的时候一般每个文件会暴露一个文件入口,也就是我们的组件函数。当我们在父组件使用我们的子组件的时候,实际上就相当于`new`出了一个新的组件实例。而在组件外的函数或者变量,则与组件形成了闭包。(不太了解闭包的可以理解为组件内的东西是原型模式,而组件外的东西是单例模式) 我们来看一个例子: 首先我们有一个Test组件: ```jsx let a = "123"; const Test = (props) => { return( <> <button onClick={()=>{console.log(a);a = props.value;}}>change</button> </> ) } export default Test; ``` 然后我们在App.js中调用这个组件: ```js function App() { return ( <div className="App"> <Test value={666}></Test> <Test value={777}></Test> </div> ); } ``` 可以看到我们在这里调用了两次Test组件,页面上应当有两个按钮。 这时我们应该想一下,当我们依次点击这两个按钮的时候输出的是 > 123 123 还是 > 123 666 呢? 答案显而易见,是`123 666`,各位可以自己去尝试一下。导致这种现象出现的原因其实不是react的组件,而是我们的`esm`。我们应该都对闭包有一定的概念,其实我们的模块化正是使用了闭包这一概念。当我们像上文那样编写Test组件的时候,其实闭包就已经形成了。 下面我们来梳理一下哪些函数是需要放到组件内的以及这其中的原因。 ## 需要在组件内的函数 ### Hook函数 一个hook函数只能在以下两个位置定义: 1. 自定义hook函数内部 2. React组件内部 但是其实我们的自定义hook最终也是在React组件内部调用,所以这就相当于每一个Hook函数最终都必须在React组件内部环境中执行。 ### 依赖Hook函数的函数 上文说过,hook函数只能在React组件内部调用,那么调用hook函数的函数也理所应当的需要写在组件内部。 ### 依赖State的函数 如果我们的函数需要直接使用State的值,那么我们需要将这个函数放置在组件内部,因为state是每一个组件实例私有的,在外部的函数无法读取到这个值。但是对于可能会复用的函数我们还是更推荐大家使用参数传递的方式传递state的值然后再使用setState的方式设置返回值。 不推荐: ```jsx // :( not recommend // a.jsx const [count,setCount] = useState(1); function a(){ setCount(count+2); } // b.jsx const [count,setCount] = useState(2); function b(){ setCount(count+2); } ``` 推荐 ```jsx // :) recommend //calcCount.js function calcCount(count){ return count+2; } //a.jsx const [count,setCount] = useState(1); setCount(calcCount(count)); // b.jsx const [count,setCount] = useState(2); setCount(calcCount(count)); ``` ### 依赖props的函数 依赖props的函数当然也不能将其写在组件外面。我们很大程度上是通过props去区别某一个组件函数的不同实例的,所以props也是组件实例私有的,需要写在外面。 ### 事件处理函数 虽然事件处理函数从理论上来说完全可以放置到组件外部(只要不依赖上面所说的),但是我们会认为事件处理函数是比较私人的东西,从习惯上来说我们还是更习惯将其放到组件的内部而不是抽出到一个util文件中,而且对于不同组件的事件处理函数,我们能抽象的部分也优先。 而且比起将事件处理函数抽象出来,我们更推荐将事件处理函数中处理逻辑的部分代码抽取出来,类似我们在MVC框架中所作的那样,model与controller的分离是值得推崇的设计思想。 ### 其他依赖组件实例的方法、变量的函数 其实我们可以看到,凡是我们的函数依赖了我们组件实例的私有变量、方法,我们都需要放在组件内部。 ## 为啥应外尽外? 以上我们整理出很多需要放到组件内部的情况,那为啥还推荐放到函数外部呢?直接全部放到函数内部不是会简单很多吗? 尽量将函数放到外面其实是一种函数式与模块化的设计思想,他驱使我们尽可能去复用我们的函数以及使用纯函数,综合来说,将函数与变量放到外部有如下几个好处: - 减少内存占用:减少组件内部的函数与变量可以减少组件实例中的内容,减少对内存的占用 - 重用代码:我们完全可以将一些负责逻辑、可以重用的函数归类并且放置到方法类(utils.js之类)中,减少我们的代码量 - 模块化:我们将逻辑与视图抽离完全可以减少我们组件的耦合度,并且可以降低模块体积,提高我们模块化的“效率”。而且能够使我们尽量去使用纯函数编写更优雅、更容易维护的代码 - 共享信息:我们可以刻意的通过将数据放置到组件外让这个组件的不同实例去共享信息。不过需要注意的是,我们放到组件外的数据肯定不是`React State`,这就意味着它们不是响应式的(一般不会有人这么做吧?) 综上所述,对于能够放置到组件函数外的应该尽量放置到函数外,对于能够复用的代码片,最好是能够抽取成全局的函数,这样才更符合模块化的要求。

软考复习之KMP算法

# KMP算法 KMP算法我曾今写过一篇[博客](https://blog.csdn.net/Charlesix59/article/details/119875803)讲过,但是我当时觉得这个算法好烂,就没细讲,后来发现他们还挺喜欢考这个算法的,于是我再开帖讲一下 ## 自然语言描述 自然语言描述是我根据《algorithm》的内容以及我自己的理解写出来的,,一般来说是正确的计算next[]与解释KMP的算法,这个方法比较考验悟性,但是原理确实是这样😂 ![](kmp/1.png) ## 考研教材解释 我们还是拿`pat=abcac`举例子,我们从a开始取字串,然后找前缀与后缀的最长相等长度 - ”a“最长相等长度为0 - "ab"最长相等长度0 - "abc"最长相等长度0 - "abca"最长相等长度1 - "abcac"最长相等长度0 所以我们可以得到PM={0,0,0,1,0} 但是我们有公式`右移位数=已匹配的字符数-对应的部分匹配值` 然后 <mark>对应的部分匹配值 是 j 前一个值</mark>,于是为了方便我们把PM整体右移一位,并用-1作为next[0]的值,得到`next[]={-1,0,0,0,1}` 所以我们有公式: `Move=(j-1)-next[j]` 这就相当于我们将J回退到: `j回退到的位置=j-Move=j-((j-1)-next[j])=next[j]+1` 所以我们就可以都得到next[]的新的形式: `next[]={0,1,1,1,2}` <mark>这里next[]的含义是在j处匹配失败后,j回退到的位置</mark> ## 拓展:更底层的匹配方法 实际上,KMP算法应该生成一个有限状态自动机(DFA),然后通过DFA去匹配字串 继续按照`pat=abcac`为例,画出的DFA如下: ![](kmp/2.jpg) 生成DFA的算法如下 ```cpp /*KMP字符串查找*/ class KMP { private: std::string pat; const static int r = 256; int *dfa[r]; //建立一个二维数组 public: KMP(std::string pat) { this->pat = pat; int m = pat.length(); for (int i = 0; i < r; i++) { dfa[i] = new int[m]; std::fill(dfa[i], dfa[i] + m, 0); } dfa[pat[0]][0] = 1; for (int x = 0, j = 1; j < m; j++) { for (int c = 0; c < r; c++) { dfa[c][j] = dfa[c][x]; //复制匹配失败情况下的值 } dfa[pat[j]][j] = j + 1; //设置匹配成功情况下的值 x = dfa[pat[j]][x]; //更新重启状态 } } int seatch(std::string txt) { int i, j, n = txt.length(), m = pat.length(); for (i = 0, j = 0; i < n&&j < m; i++) { j = dfa[txt[i]][j]; } if (j == m) return i - m; //找到匹配 else return n; //未找到匹配 } }; ```

为何不推荐在react组件中直接定义变量?探秘useRef的使用以及userRef与useState的区别

# 为何不推荐在react组件中直接定义变量?探秘useRef的使用以及userRef与useState的区别 ## 前言 今天在写代码的时候遇到一个问题,我的一个组件需要维护一个不需要在页面中渲染的变量。那么我想,既然它不需要渲染,我直接用`let`变量新建一个不就可以了吗? 但是当我真正用这个`let`创建的变量时,才意识到大事不好,原来这么使用变量有一个大问题! ## 为什么要有hook 要说明问题是什么,我们需要先弄明白一件事情,**我们使用函数式组件时我们怎么去`render`** ?在我们使用类式组件时,`rerender`可能是简单的调用一次`render`方法,那对于函数式组件呢?函数式组件的`render`写在`return`语句中,很明显我们需要重新执行整个函数。 那么我在前言部分提到的问题就呼之欲出了:组件每次渲染更新都会使得变量变为初始值。也就是说我在组件的一个函数中修改变量并更新组件,想在另一个函数中使用时我们获取到的就只会是初始值了。 可以看到,函数式组件是没有状态的!而react引入`hook`的原因就是补全函数式组件在这方面的缺陷。比如useState,当函数重新渲染时,state能够从状态池中获取到上次render的状态,像useEffect能根据变量来确定是否需要渲染,从而起到生命周期函数的作用。 也就是说,如果我们希望有一个能在每次渲染的时候不被重新初始化的变量,那么我们就需要借助hook了。 那么问题又来了,我们应该用哪一个`hook`呢?这里我推荐一个:`useRef`。 ## useRef 我们使用`useRef()`创建的变量不会每次都被重新渲染 ```js let count = 0 // 每次render都会init let count = useRef(0) // rerender时不会init ``` `useRef`的特性如下: - ref的值通过`current`属性获取 - ref是可以更改的 - 更改ref的值**不会**引起`rerender` - 如果ref的值被用于渲染,那么则不可更改 - 不建议在组件渲染中**读**写ref的`current`,推荐在`useEffect`与`event handler`中使用使用`ref.current` 可以看到,ref就是为我们保存**不用每次初始化**而且**不必渲染**的变量而生的。 ### useRef与useState 那么为什么我们要用ref而不去使用state呢?这两个hook有两个非常大的区别: - **ref不会引起rerender,而state会**。当我们调用setState时,会触发rerender,而在使用ref就不必担心这个,你可以随意的使用 - **ref是同步的,state是异步的**。当我们同步的使用useState更新数据之后,我们会获取不到最新的值,因为他是异步更新的。而ref则不会有这样的问题,你可以在更新之后立刻获取到最新的值。 所以对我现在的需求来说,`useRef`要比`useState`合适的多。 ### 使用useRef操作dom useRef的另一个重要的作用就是操作dom,我们在这边也顺带提一下吧。 我们可以这样使用ref: ```jsx const domRef = useRef(null) function focusInput(){ domRef.current.focus(); } return ( <> <input ref={domRef}/> <button onClick={focusInput}>click to focus<button> </> ) ``` 我们通过jsx中的`VNODE`的ref属性属性传递我们的ref,这样react会在渲染的时候自动将`current`的值设置为node。然后我们就可以访问到这个node了。 当这个node被从页面中移除时,`ref.current`也会被设置为`null` ## 总结 如果你希望在react函数式组件中保存一个不会每次渲染时更新并且无须被渲染的值,那么`useRef`是你的不二之选

解决插件使用GitHub calendar 插件无响应问题

最近开始发掘好用的hexo插件,先瞄准的是很久之前就看见过的小冰的GitHub Calendar [教程:hexo-githubcalendar 插件 1.0 | 小冰博客 (zfe.space)](https://zfe.space/post/hexo-githubcalendar.html) 但是按照他描述的方法配置完成之后,发现并没有生成Calendar,于是我就开始了漫长的排查。 <!--more--> ## 问题与解决方案 我这里先说我的问题,因为我是用了fluid主题,所以我先找到了小冰给出的fluid配置参数,实际上只需要将配置中的最后一行改为: ```yaml plus_style: "#github_container > .position-relative > .border{border:0!important}#github-calendar{position: relative;margin-top: -2rem;background-color: var(--board-bg-color);transition: background-color 0.2s ease-in-out;border-radius: 0.5rem;z-index: 3;-webkit-box-shadow: 0 12px 15px 0 rgb(0 0 0 / 24%), 0 17px 50px 0 rgb(0 0 0 / 19%);box-shadow: 0 12px 15px 0 rgb(0 0 0 / 24%), 0 17px 50px 0 rgb(0 0 0 / 19%);}" ``` 但是随着我按照教程完整的配置完全,发现并没有用,在对应插件的没有出现在它应该出现的位置。后来,觉得fluid有很多配置都是直接配置在`_config.fluid.yaml`中,我就尝试着将配置文件复制到这个文件一份,结果阴差阳错的成功了。 后来我又删除了hexo自带的config中的配置,结果又刷不出来了。 **所以,如果你是使用了某个主题,并且刷不出这个插件,<font color=red>你可以将主题和hexo的配置都加入插件配置</font>** ## 其他可能的问题 **这个插件想要使用应该不是只配置一个`_config.yaml`就可以了** ,你还需要在对应的界面(enable_page属性中对应的)加入相应的元素,如果是layout.type是id,那么就要插入: ```html <div id="这里替换为layout.name"> </div> ``` 如果你的layout.type是class,那么你要保证你那个界面又n-1个name为`layout.name`的代码块

React Native 移动光标,都什么年代还在用传统setNativeProps

# React Native 移动光标,都什么年代还在用传统setNativeProps? 对我的故事不感兴趣的直接跳转正文部分。 ## 念叨 今天遇到一个bug,要解决这个bug呢就需要手动移动input的光标。然后我发现之前我在~~抄~~借人家的代码的时候已经借过移动光标的代码了: ```typescript inputRef.current.setNativeProps({selection: {start: 1, end: 1}}); ``` 于是我先去搜索了一下`setNativeProps`的用法,在最新版本的RN文档上明确的给出了这个方法的使用示例,但是却没有API文档。。。😓 也就是说,理论上,这个方法是可以使用的,于是我又搜索了一下如何使用这个方法实现移动光标,结果我发现**上述写法是完全正确的,但是完全没用!** 我还发现这些文章还说有一个`_lastNativeSelection`属性可供读取,但是我只读到了`undefined`。于是乎,我大胆的怀疑,这个方法是不是已经寄了😰 然后我打印了一下`inputRef.current`发现有这么一个属性:`"setSelection": [Function setSelection],`,于是乎我大胆尝试,请看下文…… ## 正文 不要再使用`setNativeProps`方法了,这个方法疑似不再受到支持了。如果你需要操作input的光标,请直接使用`setSelection`。示例如下: ```tsx inputRef.current.setSelection(1, 1); ``` 函数签名(<u>我猜的</u>): `setSelection(selectionStart,selectionEnd)` - `selectionStart` - 被选中的第一个字符的位置索引,从 0 开始。如果这个值比元素的 `value` 长度还大,则会被看作 `value` 最后一个位置的索引。 - `selectionEnd` - : 被选中的最后一个字符的 *下一个* 位置索引。如果这个值比元素的 value 长度还大,则会被看作 value 最后一个位置的索引。

react native 如何正确使用realm

# react native 如何正确使用realm Realm是一个支持云同步的快速的新型的数据库,是SQLite的替代品。Realm的官网实际上对Realm的使用有相当详细的讲解,但是对于我们实际开发中可能出现的一些问题官方文档可能会有没有提到的地方,这篇博客旨在记录笔者在使用realm时踩到的一些坑 ## 安装 安装过程官方说的很明确,但是要注意我们需要安装两个依赖: ```shell # 安装realm库 npm install realm # 安装一些操作realm的hooks等 npm install @realm/react ``` 在使用过程中也需要注意,我们引入依赖的位置。 ## 使用 目前的realm支持两种写法,一种是直接引入的方法: ```tsx import React from 'react'; import {RealmProvider} from '@realm/react'; // Import your models import {Profile} from '../../../models'; export const AppWrapper = () => { return ( <RealmProvider schema={[Profile]}> <RestOfApp /> </RealmProvider> ); }; ``` 在你需要的时候仅需要: ```ts import {useQuery} from '@realm/react'; const profiles = useQuery(Profile); ``` 如果你有多个schema,或者说多个表,直接在`RealmProvider`中的`schema`属性中添加即可,因为这里它接受一个**数组**嘛。 ## 更好的使用 但是我认为上述方法并不是最好的使用方法,我这里还是推荐直接使用`createRealmContext`的方式,这样能够让我们更灵活的使用`realm config`。 ```tsx // 配置realm const config: Realm.Configuration = { schema: [Settings, DarftSchema], schemaVersion: 1, onMigration: (oldRealm, newRealm) => { newRealm.deleteAll(); }, }; // 创建Provider const {RealmProvider, useRealm, useObject, useQuery} = createRealmContext(config); // 包裹App剩余部分 function App(): React.JSX.Element { return ( <RealmProvider> <RealmContext.Provider value={{useRealm, useObject, useQuery}}> <LayoutWarp /> </RealmContext.Provider> </RealmProvider> ); } ``` 但是由于我们使用了这种方式之后,就不能使用直接引用的全局api了,所以我们必须想办法把api传递给后代组件。我的建议是:`useContext` ```ts export const RealmContext = createContext({ useRealm, useObject, useQuery, }); ``` 在需要使用这些api的子组件调用: ```ts const {useRealm, useObject} = useContext(RealmContext); const realm = useRealm(); ``` 这样就不会报错了。