慕课 前端框架及项目面试 聚焦VueReactWebpack 第八章 React 原理
函数式编程(不可变值)
- 函数式编程是一种编程范式(面向对象也是一种编程范式),概念比较多
- 函数式编程 重点:
- 纯函数:给定固定的输入,就一定会有固定的输出,且不能对传入的参数进行修改。纯函数不能涉及 异步 或者是关于 时间 的操作,一旦函数中涉及 AJAX请求、new Date()、setTimeout() ,他就 不是纯函数。要求reducer是纯函数
- 不可变值:不管是setState还是redux创建action时,都必须使用不可变值。
- 好处:给前端自动化测试带来很大便捷性。只需要给函数一个输入值,看看函数输出的时候符合预期即可。
- 整个Redux都是函数式编程的范式,要求reducer是纯函数也是自然而然的事情,使用纯函数才能保证相同的输入得到相同的输入,保证状态的可预测。
虚拟dom和diff算法
虚拟DOM
vue和react实现vdom细节不同,但核心概念和实现思路是一样的。
出现原因
- DOM操作非常消耗性能,以前使用原生js可以自己控制DOM操作的时机来进行性能优化(缓存DOM操作,合并DOM操作),react是数据驱动视图,不会直接操作DOM,想有效控制DOM操作提升性能就依靠虚拟DOM。
- 虚拟DOM 就是一个JS对象,用它来描述 真实的DOM。把DOM计算更多的转为js(虚拟DOM)计算(js执行速度很快),计算出最小的变更后再操作DOM(计算过程就是diff算法)。React对性能的提升在于减少了 真实DOM对象 的生成与比较,取而代之的是使用 虚拟DOM(JS对象) 来完成数据改变后的生成与比较。而用JS形成一个 JS对象 性能损耗非常小,生成一个 DOM元素 性能损耗大能。(React中的虚拟DOM)
js对象(vnode)模拟DOM结构
- js对象的结构主要包含三部分:标签名tag、属性props、子元素children,具体命名可能有所不同,但结构都这样
- class在ES6是关键词了,所以这里用的className
- 试试看,要自己能从左图写出右图
diff算法
- diff算法是虚拟DOM中最核心的部分,虚拟DOM的比对就是通过diff算法
- diff算法能在日常使用react中体现出来(如key)
- diff是一个广泛的概念,和组件化一样,她不是react独创的,如linux diff命令、git diff等
- 两棵树做diff,两个js对象也可以做diff
- diff算法 优化时间复杂度:
- 只比较同一层级,不跨级比较
逐层比对,如果第一层就不同,react就不会往下比对,而是直接使用 新的虚拟DOM 去生成真实的DOM。虽然可能会造成DOM节点渲染的浪费,但同层比对的算法简单,效率快,大大减少了两个虚拟DOM之间比对的性能消耗。 - tag不相同,则直接删掉重建,不再深度比较
- tag和key两者都相同,则认为是相同节点,不再深度比较在React中我们会根据key值给 虚拟DOM 命名,数据修改后 新的 虚拟DOM 就会根据key值和 原始的 虚拟DOM 进行快速比对,多出来的就是修改的DOM。这会大大提升性能。(所以key值需要是稳定的,可变的key值也就失去了它存在的意义)
- 只比较同一层级,不跨级比较
JSX的本质
- JSX编译出来是什么本质就是什么
- babel官网的“试一试”可以直接看到JSX编译的结果:遇到组件会继续拆分组件的jsx结构:
- 总结:JSX编译出来
- **JSX的本质就是React.creatElement()**:
- 参数1:组件/html标签(大小写区分)
- 参数2:属性
- 参数3:子元素
- React.creatElement()执行生成vnode(虚拟DOM)
- **JSX的本质就是React.creatElement()**:
React的合成事件机制
- react中所有事件(SyntheticEvent)挂载在document上,而原生的事件(event)是绑定到DOM上面的
- react的event不是原生的,是react封装的SyntheticEvent合成事件对象,模拟出了 DOM 事件对象 所有的能力。event.nativeEvent 才是原生事件对象
- react事件 和 DOM 事件不一样,和 Vue 事件也不一样
合成事件机制:
- div上绑定的事件被触发后冒泡到顶层document上,react中所有事件都绑定在document上
- react会实例化成统一的react合成事件(SyntheticEvent合成事件)再进行事件派发(通过target知道谁触发了事件以后就派发给他进行事件处理)将event对象交给对应的处理器执行
为什么需要合成事件机制
- 更好的兼容性和跨平台
- 所有事件都挂载在document上,减少内存消耗,避免频繁解绑
- 单一标签移除时不需要解绑事件,因为事件都挂载在document上
- 原生js中事件绑定,元素移除后需要解绑:
div.onclick=null;
removeEventListener()
- 方便事件的统一管理(如事务机制)
React的batchUpdate机制(setState异/同步更新)
- setState 可能是异步/同步更新,在合成事件和钩子函数中是“异步”的,在原生DOM事件和setTimeout 中都是同步的。
- setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”
- 可以通过setState第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果(同步)。
- setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。
- setState异步更新前合并/不合并:
- setState中传入对象,会被合并,多个setState只执行一次
- setState中传入函数,不会被合并,多个setState执行多次
setState主流程
batchUpdate机制
setState异步或同步 主要看能否命中batchUpdate机制(通过isBatchingUpadates判断)
注意:实际上isBatchingUpadates是在入口定义的,不是在函数里,上图只是演示方便
能命中batchUpdate机制的入口(setState异步更新)
react可以“管理”的入口:
生命周期(和他调用的函数)
react中注册的事件(合成事件)(和他调用的函数)
不能命中batchUpdate机制的入口(setState同步更新)
react“管不到”的入口:
setTimeout setInterval等(和他调用的函数)
自定义的DOM事件(和他调用的函数)
transaction(事务)机制
React组件渲染和更新的过程
组件渲染过程
- state数据+render()中解析JSX结构(creatElement函数)生成虚拟DOM
- 用 虚拟DOM 生成 真实的DOM ,显示在页面上
组件更新过程
- state 数据 发生改变
- 数据 + JSX模版 结合,生成 新的 虚拟的DOM
- 比较 原始虚拟DOM 和 新的虚拟DOM 的区别(diff算法,纯js计算),找到区别
- 直接操作有区别的DOM(将diff结果渲染DOM)
React-fiber如何优化性能
- React-fiber是React内部运行机制(是一种基于浏览器的单线程调度算法),开发者体会不到
- 了解背景和基本概念即可
- 组件更新过程中,diff算法计算了虚拟DOM的区别以后才进行页面渲染,而JS是单线程,且和DOM渲染共用一个线程。当组件足够复杂,组件更新时计算和渲染都压力大,同时再有DOM操作需求(动画,鼠标拖拽等),将卡顿。
- React-fiber就是解决卡顿的方法。它将递归diff算法比对虚拟DOM这个阶段进行任务拆分(DOM渲染阶段无法拆分),DOM需要渲染时暂停,空闲时恢复(window.requestIdleCallback)
前端路由
React-router使用 路由模式(hash、H5 history)、知乎
- hash特点:
- hash变化会触发网页跳转,即浏览器的前进、后退、跳转
- hash变化不会刷新页面,单页面应用必需的特点
- hash永远不会提交到server端(前端自生自灭)
- history特点:
- 页面和浏览器的history保持一致
- 需要后端支持
- react-router-dom是应用程序中路由的库。React库中没有路由功能,需要单独安装react-router-dom。它提供两个路由器 BrowserRouter和HashRouter。
- BrowserRouter,这是对Router接口的实现。使得页面和浏览器的history保持一致。等于:window.location。
http://127.0.0.1:3000/user/type
- HashRouter,和上面的一样,只是使用的是url的hash部分,等于:window.location.hash。
http://127.0.0.1:3000/#/user/type
不一定是这样,但#是少不了的)
- BrowserRouter,这是对Router接口的实现。使得页面和浏览器的history保持一致。等于:window.location。
- 如果你使用的是一个非静态的站点、要处理各种不同的url那么你就需要使用BrowserRouter。
- 相反的如果你的server只处理静态的url,那么就使用HashRouter。