[问题] 1.动画不流畅,有问题,需要优化 2.组件可以进一步抽离 3.hook的特性和使用
1.vite构建框架#
使用vite创建框架,选择React+Typescript
npm create vite@latest
使用npm创建Vite项目
2.零散的知识点细节#
#HTML-标签 是网页的基础语法,用来告诉浏览器“这部分内容是什么”
<div>也是一种HTML标签,可以理解为一个普通的盒子/容器,
例如
<div>这是一个盒子</div>
浏览器会把它当成一个块级区域,可以往里面放:
- 文字
- 图片
- 按钮
- 其他标签 其中有一些HTML标签是原生的,由HTML语言定好的规范,也有可以自己定义的组件,但最终还会渲染成原生HTML标签
#属性
<div className="text-slate-500 text-4xl">Hello</div>
这里className是React特有的写法,普通html里面是class
eg: .html 文件:class .jsx / .tsx 里的 React 标签:className
小写开头:原生 DOM 标签,比如 div、p、button、input 大写开头:React 组件,比如 App、GenerateWords、UserCard
#DOM 真实DOM标签是指浏览器可以解析的标签,任何自定义的组件最终都会被拆为真实DOM组件然后再给浏览器解析
#自闭合标签
本身就自带元素或者内容的东西,不需要额外再在中间添加内容来输出,可以自己结束自己
普通的成对标签长这样:
<div>Hello</div>
开始标签<div> 结束标签</div> 中间内容Hello
自闭合标签:
<GenerateWords words = {words} />
//这里{words}是JSX 表达式语法,意思是:不要把 words 当普通文字显示,而是把 JavaScript 变量 words 的值塞进页面里
#组件 这里GenerateWords是一个包含有JSX内容的组件,words是组件内的变量名 提前用
const words = faker.word.words(10)tsx对他进行了定义,然后
const GenerateWords =({words}:{words:string})=>{
return <div className="text-slate-500 text-4xl">{words}</div>
}tsx这里定义GenerateWords,然后等号右边说明了要传入什么变量,这个变量是什么属性,这里是Ts语法
这样在App()里调用GenerateWords的时候,就会从环境里取出words的变量,传给这个JSX组件,然后JSX组件知道他收到的是数据类型为string的words变量,要传给return里面的{words}
拓展: 如果这里有多个变量,就写多个props,像HTML属性一样一个个写上去 例如
const words = faker.word.words(10)
const title = "Typing Speed"
const count = 10
<GenerateWords words={words} title={title} count={count} />tsx子组件接受的时候
const GenerateWords = ({
words,
title,
count,
}: {
words: string
title: string
count: number
})tsx如果props过多的时候,也可以整理类型
type GenerateWordsProps = {
words: string
title: string
count: number
}typescript然后子组件这样写
const GenerateWords = ({ words, title, count }: GenerateWordsProps)
3.组件的定义和导入#
#导出和导入 一般写好的组件会放在src/components目录下 方便管理和阅读
const Button = () =>{
// 你写的函数
}
export default Buttontsx其中export default表示你默认导出的是Button这个变量保存的组件函数
在使用默认导入的时候,名字可以自己取
例如
import Button from './components/RestartButton'
可以改为
import Duck from './components/RestartButton'
然后使用\<Duck />
#interface 用来定义props的形状
interface RestartButtonProps {
onRestart: () => void //函数
className?: string //字符串,可选
}typescript他不是运行时检查,而是在编译阶段检查 对于<RestartButton /> ,如果没传onRestart,编译器和Ts会报错,而不是因为浏览器运行到这里才检查出来 ‘type’功能与interface相似
告诉 TypeScript:RestartButton 这个组件应该接收哪些 props,以及这些 props 分别是什么类型。它帮你在写代码时提前发现传参错误。
const buttonRef = useRef<HTMLButtonElement>(null)
其中HTMLButtonElement是一个类型参数,告诉ts,我的buttonRef.current以后要么是null,要么是一个HTML button元素
#钩子 也叫Hook
useRef是一个钩子函数
const buttonRef = useRef<HTMLButtonElement>(null)
const handleClick = () => {
buttonRef.current?.blur()
}tsxuseRef<HTMLButtonElement>(null)
会创建一个ref对象,这个对象大概为
{ current: null }tsx这里current是一个属性,绕了我好久才弄明白 属性意思是当前保存的值,是ref固定返回的对象
容器里元素类型为HTMLButtonElement,当这个容器有真实DOM元素的时候,React 在 <button ref={buttonRef}> 渲染完成后,把真实 DOM 节点赋值给 buttonRef.current
#组件组合
const RestartButton = (...)=> {
return (
\<button ...>
\<MdRefresh className="w-6 h-6" />
\</button>
)
}tsx这里React会执行RestartButton组件函数,然后得到return的内容,<button>和/
App
└── RestartButton
└── button
└── MdRefreshplaintext当调用父组件的时候,内部return所有的子组件都会一起渲染出来 可以通过在组件 return 里面嵌套其他组件,来组合出更复杂的组件。 调用 RestartButton 时,React 会渲染 RestartButton 内部 return 的全部内容,包括它里面嵌套的 MdRefresh 图标组件。
4.游戏功能的实现 (1)#
输入UI#
第一个小坑
const Character = ({ char }:{char:string}) => {
return <span className ="text-primary-500">{char}</span>
}tsx这里return <…> {char}之间插入字符的话,会在每个字之间都插入
原因:这里的 span 是用来包住单个字符的行内标签
含义是:每传进来一个字符 char,就渲染成一个黄色字符userInput = “abc”,在 UserTypings 里会被拆成:
["a", "b", "c"]
遍历逻辑是怎么实现的?#
typedCharacters.map((char, index) => {
return <Character char={char} key={index} />
})tsx这里的含义是: typedCharacters 是字符数组,map 遍历这个数组。每遍历到一个元素,就把当前字符作为 char,把当前下标作为 index,然后返回一个 Character 组件。所有返回的 Character 会组成一个新的 React 元素数组。
为什么Caret没有被渲染在每个字符后面? 因为这里的逻辑是: 所有字符先通过 map 渲染出来,然后最后额外追加一个 Caret
return <Character char={char} key={index} />
返回的是类似这样的 React 元素:
<Character char="a" key={0} />
然后map把这些返回值形成数组:
[
<Character char="a" key={0} />,
<Character char="b" key={1} />,
<Character char="c" key={2} />
]tsx这个数组最后会被React展开,变成页面上多个Character组件实例,然后这些组件再渲染成多个 span
例如
<span className="text-primary-500">a</span>
const Charactars({char}:{char:string}) =>{
return
<span className:"text-primary-400"><{char}</span>
}tsx这里的Character 是一个自定义组件。它接收 char 这个 prop,然后返回一个 span
map的核心含义是,遍历数组,但是他不知道遍历之后要干嘛,所以要传给他一个函数/组件 Charactars,告诉他遍历之后要对遍历得到的元素做什么
5.游戏功能的实现 (2)#
#钩子 #hook
单词数量组件#
设计这个组件有俩个思想 第一个思想:设计成组件达到可复用,方便后续维护 让单词数量可以通过count控制,不写死在hook里面
第二个思想:封装,保护setWords核心业务逻辑,防止外界修改setWords从而破坏功能完整性,
第一步: 使用useState,绑定words和setWords,提供一个可以修改words的接口供内部使用,然后用UpdateWords包装 这样,无论useWords 内部管理 words 怎么生成、怎么更新,外部组件不直接碰 setWords,外部组件只拿到 words 和 updateWords,防止外界对setWords修改
useWords 把“随机单词状态”的细节封装起来,只向外暴露安全、明确的接口:words 用来读,updateWords() 用来刷新
这个逻辑中,外界能做的事情只有:
读取 words
调用 updateWords 刷新 words
不知道setWords存在,也不关心怎么生成随机词plaintext对于内部而言:
setWords(faker.word.words(count))
控制 words 必须来自 faker 生成plaintext这俩个参数的含义分别是:
useState 创建 words/setWords 这一对
↓
words 保存当前单词
setWords 负责更新 words
↓
updateWords 调用 setWords
↓
words 变化
↓
页面重新渲染plaintextuseState 让 words 成为状态,也让 setWords 成为更新这个状态的方法;后面的 updateWords 只是封装了一次对 setWords 的调用
const updateWords = useCallback(() => {
setWords(faker.word.words(count));
}, [count]);tsx这里语法是: useCallback(要缓存的函数, 依赖数组) 也就是说,当 count 发生变化的时候,就 创建一个新的 updateWords 函数,被替换的是整个updateWords函数
这里setWords是用来更新words状态的函数,当updateWords被调用,就会执行setWords(faker.word.words(count))
也就是说,这里有俩个功能,一个是count变化的时候,重新生成整个函数,另一个是当updateWords被调用的时候,执行里面的setWords刷新words
因为useCallback 管函数本身,updateWords 管实际更新 words
普通函数:
组件每次重新渲染,函数都会重新创建一次
useCallback:
组件重新渲染时,先看依赖数组
依赖没变:不给你新函数,继续用旧函数
依赖变了:给你新函数plaintextcount 变化的时候,重新创建 updateWords,让它内部拿到最新的 count,之后调用 updateWords() 时就能按新的数量生成 words。
问题1:React本身不是可以实时渲染words{}里的变量吗,为什么还要多次一举来增加一个刷新机制
答:这里是把“刷新单词”这件事单独封装成一个函数 也就是说,这里是把点击重启按钮刷新单词的函数单独拆出来,方便复用 即: 按钮组件:只负责点击 useWords:只负责生成和更新单词 App:负责把它们连接起来
原因: React 会实时渲染变化后的 words,但 React 不会自己决定什么时候生成新的 words。 React只负责 : words 变了 -> 页面重新渲染
黑暗模式切换组件#
const [isDark, setIsDark] = useState(
() => window.matchMedia && window.matchMedia(matchDark).matches
)tsx创建一个状态isDark,它的初始值是:当前系统是不是深色模式,同时创建一个修改它的函数setIsDark,以后可以通过setIsDark(true)或者(false)修改isDark
() => window.matchMedia && window.matchMedia(matchDark).matches
懒初始化函数
先看window.matchedia是否存在,如果不存在,返回window,matchMedia本身,通常是undefined,如果存在,则执行window.matchMedia(matchDark).matches
这里返回window.matchMedia本身没有业务意义,只是为了防止报错 如果 matchMedia 存在,就检测深色模式; 如果不存在,就别报错。
#useState
useState 里的初始值,只在第一次渲染时生效。
后续渲染,状态来自 React 上一次保存的值。
例如:
const [count, setCount] = useState(() => {
console.log("初始化 count")
return 0
})tsx第一次组件显示的时候,会打印初始化count 之后点setCount(count+1) 组件会重新渲染,但是不会再打印初始化count
tips: JavaScript 有一个机制叫 自动分号插入 英文是 ASI 即,return右边如果不接东西,会被理解为return;,无法正确读取jsx结构 需要马上接括号或者与jsx第一行紧贴
#对象解构
const { isDark, setIsDark } = useDarkMode()
这里意思是调用useDarkMode(),然后从useDarkMode返回的对象里拿出两个属性,分别是isDark和setIsDark
等价于:
const result = useDarkMode()
const isDark = result.isDark
const setIsDark = result.setIsDarktsx#props 我将这个props和这个对象经常搞混 props一般是组件参数,例如
const UserCard = ({name,age}) =>{
return <div>{name} - {age}</div>
}tsx这里的{name,age}才是在接受props
当外部使用的时候,这个组件被调用的形式可能是
<UserCard name="Joshua" age={18} />
核心功能:用户输入组件#
依赖数组#
useEffect(() => {
console.log("执行了")
}, [])tsx这里的[]就是依赖数组,当这里留空的时候,说明不依赖任何外部变量,只在组件第一次挂载的时候执行一次
如果这里填timeLeft,则第一次会执行,然后每当timeLeft变化,就重新执行
Hook深入理解#
作为React的核心功能,第一次理解仍然比较抽象