协调
确定该对宿主实例(真实dom节点的树)做什么来响应新的信息有时候叫做协调
简单讲就是将虚拟dom映射到真实dom的过程
闭包
如果外层函数定义了内层函数,则内层函数的作用域和作用域链都和外层做了绑定,如果返回了内层函数或异步使用了外层函数的变量(异步引用了原本的变量,只要引用了就不会被销毁),只要内存函数还在使用,这些作用域链上的变量就不会被销毁
setStatu(s=>s+1)函数式赋值之所以同步,因为里面的s未使用外层变量s,是useStatus自己的变量,所以不是闭包。
react元素
<dialog className="box">
{message}
<input />
</dialog>
这样的jsx格式在react眼里是这样的:
{
type:"dialog",
props:{
className:"box",
children:[
message, // 此处的message的jsx内容已经有占位,所以改变也不会影响到input,不会导致
{ // input重建,可以重用react实例
type:"input",
props:{}
}
]
}
}
react元素只有重建和删除
组件的纯净性和幂等性
只要调用组件多次是安全的,不会影响其他组件的渲染,react不会关心代码是否想严格模式下的函数式编程一样百分百纯净。在react中,幂等性比纯净性更重要。
简单来说,多次调用函数组件在屏幕上不能看到任何变化
纯函数(pure funtions)
只依赖输入参数的函数,并且返回值也依赖参数。不引用和影响函数外的变量,不产生console结果,写入文档,传递网络信息,任何异步操作等副作用。
优点:
1对于这种函数参数不变时,chrome v8 引擎会返回缓存的结果,而不是重新运行结果
2函数的结果更易预测,不会影响其他函数和组件
Pure Component
纯组件,是react component的一种,在props和state不改变时不会重新渲染,但注意只会进行浅比较,复杂数据类型只会比较引用从而阻断重新渲染
组件嵌套和递归
当元素类型是个组件函数时,react会调用这个函数组件,然后询问组件想要渲染什么函数。例如
● 你: ReactDOM.render(, domContainer)
● React: App ,你想要渲染什么?
○ App :我要渲染包含 的 。
● React: ,你要渲染什么?
○ Layout :我要在
● React: ,你要渲染什么?
○ :我要在
// 最终的 DOM 结构
<div>
<article>
Some text
<footer>some more text</footer>
</article>
</div>
为什么不直接调用函数组件而是<组件 />
因为直接调用 组件()react不知道这个组件,而<组件 />这种方式react就知道这是一个组件并且会自己掉用它,这就是控制反转。还有好几个好处。
- react能用在树中与组件本身紧密相连的局部状态来增强组件特性。
- react能更了解元素树的结构
- 推迟协调,让浏览器在组件调用之间做一些工作,这样重渲染大量组件时就不会阻塞主线程
- 惰性求值fn1( fn2() ) fn2先执行,fn1后执行,如
<Page>
{Form()} // 函数组件Form直接调用
</Page>
react元素树变成
{
type:Page,
props:{
children:[{
type:Form() // 此时不论Page是否想渲染它,它都会立即执行
}]
}
}
react将所有工作分为渲染阶段和提交阶段
渲染阶段是react调用你的组件然后进行协调的过程,在此阶段 进行干涉是安全的且在未来这个阶段会变成异步的
提交阶段就是react操作宿主树的时候,这个阶段永远是同步的
批量更新
react会在所有事件排队触发完后再进行批量更新,否则同时三次设置状态则会渲染三次组件。
为什么hook总在顶层调用
因为react的状态和在树中与其相关的组件紧密连接在一起,即状态都是定义为组件的属性的。
就像import都在顶层调用是一样的。如果在条件和子函数中调用则会改变状态的作用域。
effect状态过期
如果effect中直接或间接(比如函数包含)引用了props或status,并且没有添加进依赖,则因为闭包的缘故,这些引用是不会因为外界的更新而更新的。
react的视图更新 <div>{a}</div>
react的状态和props更新,使得视图发生更新,不是因为双向绑定啥的,状态和props也只是普通值,就像const a=1一样,只是它们会触发组件重新渲染(相当于组件被重新调用),就像const a=2被重新赋值,视图上的a自然也更新成2了。
ps:在组件每次被调用并重复渲染时,每次包含的状态或者props或者其他数据或者effect都独立于其他的渲染,并且每一次渲染的事件处理函数也是独立于其他渲染的,就好像多次调用普通函数一样
function sayHi(person) {
const name = person.name;
setTimeout(() => {
alert('Hello, ' + name);
}, 3000);
}
let someone = {name: 'Dan'};
sayHi(someone);
someone = {name: 'Yuzhi'};
sayHi(someone);
someone = {name: 'Dominic'};
sayHi(someone);
useEffect的清除函数执行的时机
是在重新渲染ui后,执行上次渲染的effect的清除函数,然后再执行这次的effect函数
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
};
});
● React 渲染{id: 10}的UI。
● 浏览器绘制。我们在屏幕上看到{id: 10}的UI。
● React 运行{id: 10}的effect。
● React 渲染{id: 20}的UI。
● 浏览器绘制。我们在屏幕上看到{id: 20}的UI。
● React 清除{id: 10}的effect。
● React 运行{id: 20}的effect。
ps:执行上次effect的清理函数时,其中的参数和状态还是上一个函数保留的
useEffect设置状态时,此状态依赖另一个状态时
此时或者写类似setSomething(something => …)的代码时用useReducer去替换它们非常合适。
reducer可以让你把组件内发生了什么(actions)和状态如何响应并更新分开表述
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' }); // Instead of setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
react会保证dispatch在组件声明周期内保持不变,所以上面的例子中不用再重新订阅定时器
1.useReducer里,每次重新渲染组件时react记住的是action,即{type:‘tick’},他会调用下一次渲染的reducer,这样就能保证每次reducer里的props和status是最新的,因为不是在effect里调用reducer
,所以可以去除不必要的依赖
2.如果某个函数不依赖组件里的任何值,那么可以将它提取到外部,减少依赖
3.class生命力周期组件发现不了函数改变,而useCallback和effect可以
jsx的原理
jsx每个html标签都是调用react.createElement方法创建,返回一个ReactElement方法,这个方法返回一个虚拟dom ,即
{
type:Page,
props:{
children:[{
type:Form() // 此时不论Page是否想渲染它,它都会立即执行
}]
}
}
jsx代码>babel编译>React.createElement调用>ReactElement调用>虚拟dom>作为参数传入ReactDOM.render()>渲染处理>真实DOM
请求竞态
在生命周期update或者函数组件状态改变时会重新请求,如果此时发生多次更新,但是请求速度不一,可能前一次请求结果覆盖了最新的请求结果,此时effect中可以返回一个清楚函数清除上一个请求,或者不让上一个请求赋值。如下:
useEffect(() => {
let didCancel = false;
async function fetchData() {
const article = await API.fetchArticle(id);
if (!didCancel) {
setArticle(article);
}
}
fetchData();
return () => {
didCancel = true;
};
}, [id]);
虚拟dom的价值
● 研发体验/研发效率的提升,数据驱动视图,声明式编程,不用过多关注dom操作而专注于数据处理,在数据更新量不大的时候,性能也不错
● 跨平台开发
● 批量更新,多次差量更新用户只能看到最后一次更新效果,但是每次更新都会重复渲染页面,这时batch函数缓冲每次生成的补丁集最后批量生成更新
diff算法
1.分层对比,尽量不要做跨层级的操作
2.相同类型的组件才有对比的必要性,不同类型组件直接替换
3.key属性帮助react记住同层级的节点,当重新渲染时相同key的节点就不会删除直接复用
● diff是深度优先遍历节点进行对比,调和器(协调)也是,重复父组件调用子组件的过程,直到最深一层节点更新完毕,再向上回朔,react15的 stack Reconciler 是同步进行渲染的,不能打断,所以可能会出现卡顿/卡死等问题
Fiber架构
FIber是比线程还要纤细的一个过程,所谓“纤程”,意在对渲染过程实现更精细的控制
● 架构角度:Fiber是对react核心算法的重写,实现增量渲染,目的是任务的可中断,可恢复,并赋予不同的优先级
○ Fiber把渲染更新过程拆分成多个子任务根据优先级执行。当时间不足时挂起当前任务
○ 增加了异步任务,调用requestIdleCallback api,浏览器空闲的时候执行之前未能完成的任务
render阶段:纯净没有副作用,可能暂停终止或重新启动,在内存中做计算,diff明确dom树的更新点,完成Fiber树的构建
commit:可以读取使用dom,运行副作用,安排更新
● 编码角度:FIber是React内部所定义的一种数据结构
○ dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执行
● 工作流:Fiber节点保存了组件需要更新的状态和副作用
FIber架构的重要特征就是可以被打断的异步渲染模式。
render阶段执行过程中允许暂停、终止、重启,是异步的,commit是同步的
对比React 15 的 Stack Reconciler:
当 React在渲染组件时,从开始到渲染完成整个过程是一气呵成的,无法中断
如果组件较大,那么js线程会一直执行,然后等到整棵VDOM树计算完成后,才会交给渲染的线程
这就会导致一些用户交互、动画等任务无法立即得到处理,导致卡顿的情况
react合成事件
js事件委托
利用事件的捕获和冒泡,将多个子元素的同一类型监听逻辑,合并到父元素上,通过一个监听函数来管理的行为(如ul下的每个li都输入其中的文字,则可以在ul上绑定监听)
react的合成事件,事件在具体dom上触发时,冒泡到document,在document上绑定统一的事件处理程序,将事件分发到具体的组件实例。
优点:底层抹平浏览器差异,上层暴露统一、稳定、与dom原生事件相同的事件接口
(若要通过react事件访问原生dom事件,e.nativeEvent)