一些概念
- 宿主树:一棵会随时间变化的树,它是react程序输出的。树具体是什么样子呢,是一棵DOM树,或者JSON对象等等,其他类型比如ios视图层,web不涉及,暂时不做深入探讨。
- 宿主实例:其实是DOM环境中的DOM节点,有自己的属性。
- React元素:是一个javascript对象,用来描述宿主实例
特点
宿主树特点
- 宿主树是稳定的,绝大多数情况的UI更新,不会从根本上改变整个树的结构。
- 通用性。宿主树可被拆分为一些常见的,可复用的UI模式,比如按钮,列表,表单之类。
React元素特点
- 不会一直存在,经常被重建和删除
- 不可变性,比如不能改变React元素中的属性和子节点。如果想要改变,重新创建新的react元素
协调
举个栗子
ReactDom.render(
<button className="blue" />,
document.getElementById("root")
)
上面的程序执行两次,结果是什么?宿主实例button会被重新创建两次吗?
协调:指的是如何操作宿主实例来响应新的信息。
上面的例子中,react会重用宿主实例,因为button是同一个,并且挂载在宿主树的同一位置。
划重点: 如果相同的元素类型在同一个地方先后出现两次,React 会重用已有的宿主实例。只要元素类型相同即可,属性等可以不同
条件
react对包含if条件判断语句的内容,如何渲染?
举个栗子
const Form = ({showMessage}) => {
const message = showMessage? <p>click here</p>: null
return (
<p>
{message}
<input />
</p>
)
}
<Form showMessage={false} />
<Form showMessage={true} />
以上代码中,Form组件被调用两次,无论showMessage是true或者false,input元素都是父级元素p的第二个子元素。位置不变。
两次渲染对比:
p->p 元素类型不变,重用宿主实例
null->p 新建宿主实例
input->input 元素类型不变,重用宿主实例
所以,将在父元素p中新插入一个p元素
列表
划重点:比较树中同一位置的元素类型,决定该重用还是重建相应的宿主实例,只适用于当子元素是静止的并且不会重排序的情况
举个栗子
function ShoppingList({ list }) {
return (
<form>
{list.map(item => (
<p>
You bought {item.name}
<br />
Enter how many do you want: <input />
</p>
))}
</form>
)
}
在react中,当遍历数组,渲染元素时,为什么一定要给元素加key?
如果不给每个元素加key, 以上例子中,当list列表中元素顺序发生改变时,react会进行如下处理:每个子元素的类型,比如p,input的元素类型未发生改变,所以react之后更新每个子元素的内容,重用之前的宿主实例,不会重新创建宿主实例。
但是这样做导致的问题是:当商品List发生改变,每一个元素input框中的记录的商品数量不会发生改变。
通过给每个元素加key,解决以上问题。通过key, React 判断子元素是否真正相同, 即使在渲染前后它在父元素中的位置不是相同的。
Note:
- key与父元素关联,React 并不会去匹配父元素不同但 key 相同的子元素
- key的赋值:找到 一个元素即使它在父元素中的顺序被改变,它的值也不会发生变化。
控制反转
对于调用函数式组件,react写法是<From />而不是Form(),基于哪些考虑?
写成<From />,react可以识别到他是一个组件,而不是函数调用
react本身对封装的组件进行了一些处理:
- 组件不只是函数。React能够用在树中与组件本身紧密相连的局部状态等特性来增强组件功能。如果直接调用组件,react不会自动构建这些特性,这些局部状态指的是?
- 组件类型参与协调。让react来调你的组件,他能了解更多元素树的结构,比如当先后渲染<From />和<Profile />组件,react不会去重用宿主实例。相反,当同一个组件先后渲染两次,react会重用组件的宿主实例。
- React可以推迟协调。让react来调你的组件,react会单独做一些优化工作,比如浏览器对组件调用做一些优化,比如渲染庞大组件树时候,阻塞主线程。
惰性求值
//(2) 再执行
animal(
// (1) 先执行
eat()
)
在javascript中,参数会在函数被调用之前执行。
举个栗子
function Story({ currentUser }) {
return (
<Page user={currentUser}>
<Comments />
</Page>
);
}
function Page({ currentUser, children }) {
if (!currentUser.isLoggedIn) {
return <h1>Please login</h1>;
}
return (
<Layout>
{children}
</Layout>
);
}
如果我们像函数一样调用Comments(), 比如<Page>{Comments()}</Page>
,不论Page组件的条件是什么,该组件都会被渲染。如果传递的是react元素,比如<Page><Comments /></Page>
,在不满足渲染条件时,react不会尝试渲染comments组件。这就是惰性求值,好处是避免不必要的渲染。
缓存
当组件的状态发生改变时,react默认会协调整个子树,因为react并不知父组件的更新是否影响其子组件。当宿主树比较庞大,可以让react缓存子树,重用先前渲染的结果。react提供了useMemo() hook去更精细粒度的缓存控制。
function Row({ item }) {
// ...
}
export default React.memo(Row);
以上代码,当传递给Row组件的item与上次浅比较相同,React会直接跳过协调过程。
默认情况下,react不会故意缓存组件。
综上所述,缓存一般适用于组件的props不频繁发生改变的组件。
批量更新
当遇到组件想要更新状态响应同一事件,举个栗子
function Parent() {
let [count, setCount] = useState(0);
return (
<div onClick={() => setCount(count + 1)}>
Parent clicked {count} times
<Child />
</div>
);
}
function Child() {
let [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Child clicked {count} times
</button>
);
}
当child组件点击事件触发,根据冒泡原理,Parent组件点击事件也会被触发,如果react立即渲染组件响应状态更新,则子组件会本渲染两次。过程如下:
1.child组件点击事件触发
子组件渲染
2.Parent组件点击事件触发
父组件渲染
子组件渲染
所以react会在组件内所有事件触发完成后再进行批量更新。避免重复渲染
总结
- 组件名称比如大写,如果是小写,react的JSX语法会将其识别为字符串,而不是组件标识符。
- 在react中,如果用index作为key会有什么问题呢?
如果用index作为key,如果我们删除了一个节点,那么数组的后一项可能会前移,这个时候移动的节点和删除的节点就是相同的key了,在react中,如果key相同,就会视为相同的组件,但这两个组件并不是相同的,这就会出现一些我们不想看到的问题
本文章非原创,是用来知识总结沉淀的。
参考:https://overreacted.io/zh-hans/react-as-a-ui-runtime/#%E5%89%AF%E4%BD%9C%E7%94%A8
网友评论