10.说说React Jsx转换成真实DOM过程?
什么是JSX?
JSX即JavaScript XML。一种在React组件内部构建标签的类XML语法。JSX为react.js开发的一套语法糖,也是react.js的使用基础。React在不使用JSX的情况下一样可以工作,然而使用JSX可以提高组件的可读性,因此推荐使用JSX。
class MyComponent extends React.Component { render() { let props = this.props; return ( <div className="my-component"> <a href={props.url}>{props.name}</a> </div> ); } }
jsx的优点
允许使用熟悉的语法来定义 HTML 元素树;
提供更加语义化且移动的标签;
程序结构更容易被直观化;
抽象了 React Element 的创建过程;
可以随时掌控 HTML 标签以及生成这些标签的代码;
是原生的 JavaScript。
react 中jsx语法糖的本质
Jsx是语法糖,实质是js函数,需要babel来解析,核心函数是React.createElement(tag,{attrbuties},children),参数tag是标签名可以是html标签和组件名,attrbuties参数是标签的属性,children参数是tag的子元素。用来创建一个vnode,最后渲染到页面上。
React Jsx转换成真实DOM是什么
react通过将组件编写的JSX映射到屏幕,以及组件中的状态发生了变化之后 React会将这些「变化」更新到屏幕上
在前面文章了解中,JSX通过babel最终转化成React.createElement这种形式,例如:
<div> <img src="avatar.png" className="profile" /> <Hello /> </div>
会被bebel转化成如下:
React.createElement( "div", null, React.createElement("img", { src: "avatar.png", className: "profile" }), React.createElement(Hello, null) );
在转化过程中,babel在编译时会判断 JSX 中组件的首字母:
当首字母为小写时,其被认定为原生 DOM 标签,createElement 的第一个变量被编译为字符串
当首字母为大写时,其被认定为自定义组件,createElement 的第一个变量被编译为对象
最终都会通过RenderDOM.render(...)方法进行挂载,如下:
ReactDOM.render(<App />, document.getElementById("root"));
过程
在react中,节点大致可以分成四个类别:
原生标签节点
文本节点
函数组件
类组件
如下所示:
class ClassComponent extends Component { static defaultProps = { color: "pink" }; render() { return ( <div className="border"> <h3>ClassComponent</h3> <p className={this.props.color}>{this.props.name}</p > </div> ); } } function FunctionComponent(props) { return ( <div className="border"> FunctionComponent <p>{props.name}</p > </div> ); } const jsx = ( <div className="border"> <p>xx</p > < a href=" ">xxx</ a> <FunctionComponent name="函数组件" /> <ClassComponent name="类组件" color="red" /> </div> );
这些类别最终都会被转化成React.createElement这种形式
React.createElement其被调用时会传⼊标签类型type,标签属性props及若干子元素children,作用是生成一个虚拟Dom对象,如下所示:
function createElement(type, config, ...children) { if (config) { delete config.__self; delete config.__source; } // ! 源码中做了详细处理,⽐如过滤掉key、ref等 const props = { ...config, children: children.map(child => typeof child === "object" ? child : createTextNode(child) ) }; return { type, props }; } function createTextNode(text) { return { type: TEXT, props: { children: [], nodeValue: text } }; } export default { createElement };
createElement会根据传入的节点信息进行一个判断:
如果是原生标签节点, type 是字符串,如div、span
如果是文本节点, type就没有,这里是 TEXT
如果是函数组件,type 是函数名
如果是类组件,type 是类名
虚拟DOM会通过ReactDOM.render进行渲染成真实DOM,使用方法如下:
ReactDOM.render(element, container[, callback])
当首次调用时,容器节点里的所有 DOM 元素都会被替换,后续的调用则会使用 React 的 diff算法进行高效的更新
如果提供了可选的回调函数callback,该回调将在组件被渲染或更新之后被执行
function render(vnode, container) { console.log("vnode", vnode); // 虚拟DOM对象 // vnode _> node const node = createNode(vnode, container); container.appendChild(node); } // 创建真实DOM节点 function createNode(vnode, parentNode) { let node = null; const {type, props} = vnode; if (type === TEXT) { node = document.createTextNode(""); } else if (typeof type === "string") { node = document.createElement(type); } else if (typeof type === "function") { node = type.isReactComponent ? updateClassComponent(vnode, parentNode) : updateFunctionComponent(vnode, parentNode); } else { node = document.createDocumentFragment(); } reconcileChildren(props.children, node); updateNode(node, props); return node; } // 遍历下子vnode,然后把子vnode->真实DOM节点,再插入父node中 function reconcileChildren(children, node) { for (let i = 0; i < children.length; i++) { let child = children[i]; if (Array.isArray(child)) { for (let j = 0; j < child.length; j++) { render(child[j], node); } } else { render(child, node); } } } function updateNode(node, nextVal) { Object.keys(nextVal) .filter(k => k !== "children") .forEach(k => { if (k.slice(0, 2) === "on") { let eventName = k.slice(2).toLocaleLowerCase(); node.addEventListener(eventName, nextVal[k]); } else { node[k] = nextVal[k]; } }); } // 返回真实dom节点 // 执行函数 function updateFunctionComponent(vnode, parentNode) { const {type, props} = vnode; let vvnode = type(props); const node = createNode(vvnode, parentNode); return node; } // 返回真实dom节点 // 先实例化,再执行render函数 function updateClassComponent(vnode, parentNode) { const {type, props} = vnode; let cmp = new type(props); const vvnode = cmp.render(); const node = createNode(vvnode, parentNode); return node; } export default { render };
总结
在react源码中,虚拟Dom转化成真实Dom整体流程如下图所示:
其渲染流程如下所示:
使用React.createElement或JSX编写React组件,实际上所有的 JSX 代码最后都会转换成React.createElement(...) ,Babel帮助我们完成了这个转换的过程。
createElement函数对key和ref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个虚拟DOM对象
ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM
11.React 组件间怎么进行通信?
通信是什么
我们将组件间通信可以拆分为两个词:
组件
通信
React的组件灵活多样,按照不同的方式可以分成很多类型的组件
而通信指的是发送者通过某种媒体以某种格式来传递信息到收信者以达到某个目的,广义上,任何信息的交通都是通信
组件间通信即指:组件通过某种方式来传递信息以达到某个目的
如何通信
组件传递的方式有很多种,根据传送者和接收者可以分为如下:
父组件向子组件传递
子组件向父组件传递
兄弟组件之间的通信
父组件向后代组件传递
非关系组件传递
父组件向子组件传递
由于React的数据流动为单向的,父组件向子组件传递是最常见的方式
父组件在调用子组件的时候,只需要在子组件标签内传递参数,子组件通过props属性就能接收父组件传递过来的参数
function EmailInput(props) { return ( <label> Email: <input value={props.email} /> </label> ); } const element = <EmailInput email="123124132@163.com" />;
子组件向父组件传递
子组件向父组件通信的基本思路是,父组件向子组件传一个函数,然后通过这个函数的回调,拿到子组件传过来的值
父组件对应代码如下:
class Parents extends Component { constructor() { super(); this.state = { price: 0 }; } getItemPrice(e) { this.setState({ price: e }); } render() { return ( <div> <div>price: {this.state.price}</div> {/* 向子组件中传入一个函数 */} <Child getPrice={this.getItemPrice.bind(this)} /> </div> ); } }
子组件对应代码如下:
class Child extends Component { clickGoods(e) { // 在此函数中传入值 this.props.getPrice(e); } render() { return ( <div> <button onClick={this.clickGoods.bind(this, 100)}>goods1</button> <button onClick={this.clickGoods.bind(this, 1000)}>goods2</button> </div> ); } }
兄弟组件之间的通信
如果是兄弟组件之间的传递,则父组件作为中间层来实现数据的互通,通过使用父组件传递
class Parent extends React.Component { constructor(props) { super(props) this.state = {count: 0} } setCount = () => { this.setState({count: this.state.count + 1}) } render() { return ( <div> <SiblingA count={this.state.count} /> <SiblingB onClick={this.setCount} /> </div> ); } }
父组件向后代组件传递
父组件向后代组件传递数据是一件最普通的事情,就像全局数据一样
使用context提供了组件之间通讯的一种方式,可以共享数据,其他数据都能读取对应的数据
通过使用React.createContext创建一个context
const PriceContext = React.createContext('price')
context创建成功后,其下存在Provider组件用于创建数据源,Consumer组件用于接收数据,使用实例如下:
Provider组件通过value属性用于给后代组件传递数据:
<PriceContext.Provider value={100}> </PriceContext.Provider>
如果想要获取Provider传递的数据,可以通过Consumer组件或者或者使用contextType属性接收,对应分别如下:
class MyClass extends React.Component { static contextType = PriceContext; render() { let price = this.context; /* 基于这个值进行渲染工作 */ } }
Consumer组件:
<PriceContext.Consumer> { /*这里是一个函数*/ } { price => <div>price:{price}</div> } </PriceContext.Consumer>
非关系组件传递
如果组件之间关系类型比较复杂的情况,建议将数据进行一个全局资源管理,从而实现通信,例如redux。关于redux的使用后续再详细介绍
总结
由于React是单向数据流,主要思想是组件不会改变接收的数据,只会监听数据的变化,当数据发生变化时它们会使用接收到的新值,而不是去修改已有的值
因此,可以看到通信过程中,数据的存储位置都是存放在上级位置中
12.说说你对fiber架构的理解?解决了什么问题?
React 的核心流程可以分为两个部分:
reconciliation (调度算法,也可称为 render)
更新 state 与 props;
调用生命周期钩子;
生成 virtual dom
这里应该称为 Fiber Tree 更为符合;
通过新旧 vdom 进行 diff 算法,获取 vdom change
确定是否需要重新渲染
commit
如需要,则操作 dom 节点更新
要了解 Fiber,我们首先来看为什么需要它
问题:
随着应用变得越来越庞大,整个更新渲染的过程开始变得吃力,大量的组件渲染会导致主进程长时间被占用,导致一些动画或高频操作出现卡顿和掉帧的情况。而关键点,便是 同步阻塞。在之前的调度算法中,React 需要实例化每个类组件,生成一颗组件树,使用 同步递归 的方式进行遍历渲染,而这个过程最大的问题就是无法 暂停和恢复。
解决方案:
解决同步阻塞的方法,通常有两种: 异步 与 任务分割。而 React Fiber 便是为了实现任务分割而诞生的
简述
在 React V16 将调度算法进行了重构, 将之前的 stack reconciler 重构成新版的 fiber reconciler,变成了具有链表和指针的 单链表树遍历算法。通过指针映射,每个单元都记录着遍历当下的上一步与下一步,从而使遍历变得可以被暂停和重启
这里我理解为是一种 任务分割调度算法,主要是 将原先同步更新渲染的任务分割成一个个独立的 小任务单位,根据不同的优先级,将小任务分散到浏览器的空闲时间执行,充分利用主进程的事件循环机制
核心
Fiber 这里可以具象为一个 数据结构
class Fiber { constructor(instance) { this.instance = instance // 指向第一个 child 节点 this.child = child // 指向父节点 this.return = parent // 指向第一个兄弟节点 this.sibling = previous } }
链表树遍历算法: 通过 节点保存与映射,便能够随时地进行 停止和重启, 这样便能达到实现任务分割的基本前提
首先通过不断遍历子节点,到树末尾;
开始通过 sibling 遍历兄弟节点;
return 返回父节点,继续执行2;
直到 root 节点后,跳出遍历;
任务分割,React 中的渲染更新可以分成两个阶段
reconciliation 阶段: vdom 的数据对比,是个适合拆分的阶段,比如对比一部分树后,先暂停执行个动画调用,待完成后再回来继续比对
Commit 阶段: 将 change list 更新到 dom 上,并不适合拆分,才能保持数据与 UI 的同步。否则可能由于阻塞 UI 更新,而导致数据更新和 UI 不一致的情况
分散执行: 任务分割后,就可以把小任务单元分散到浏览器的空闲期间去排队执行,而实现的关键是两个新API: requestIdleCallback 与 requestAnimationFrame
低优先级的任务交给requestIdleCallback处理,这是个浏览器提供的事件循环空闲期的回调函数,需要 pollyfill,而且拥有 deadline 参数,限制执行事件,以继续切分任务;
高优先级的任务交给requestAnimationFrame处理;
// 类似于这样的方式 requestIdleCallback((deadline) => { // 当有空闲时间时,我们执行一个组件渲染; // 把任务塞到一个个碎片时间中去; while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextComponent) { nextComponent = performWork(nextComponent); } });
优先级策略:
文本框输入 > 本次调度结束需完成的任务 > 动画过渡 > 交互反馈 > 数据更新 > 不会显示但以防将来会显示的任务
React的Fiber工作原理,解决了什么问题
React Fiber 是一种基于浏览器的单线程调度算法。
React Fiber 用类似 requestIdleCallback 的机制来做异步 diff。但是之前数据结构不支持这样的实现异步 diff,于是 React 实现了一个类似链表的数据结构,将原来的 递归diff 变成了现在的 遍历diff,这样就能做到异步可更新了
Fiber 其实可以算是一种编程思想,在其它语言中也有许多应用(Ruby Fiber)。
核心思想是 任务拆分和协同,主动把执行权交给主线程,使主线程有时间空挡处理其他高优先级任务。
当遇到进程阻塞的问题时,任务分割、异步调用 和 缓存策略 是三个显著的解决思路。
Fiber 是什么
Fiber 的中文翻译叫纤程,与进程、线程同为程序执行过程,Fiber 就是比线程还要纤细的一个过程。纤程意在对渲染过程实现进行更加精细的控制。
在react中,主要做了以下的操作:
为每个增加了优先级,优先级高的任务可以中断低优先级的任务。然后再重新,注意是重新执行优先级低的任务
增加了异步任务,调用requestIdleCallback api,浏览器空闲的时候执行
dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执行
从架构角度来看,Fiber 是对 React 核心算法(即调和过程)的重写。
从编码角度来看,Fiber 是 React 内部所定义的一种数据结构,它是 Fiber 树结构的节点单位,也就是 React 16 新架构下的"虚拟 DOM"。
一个 fiber 就是一个 JavaScript 对象,Fiber 的数据结构如下:
type Fiber = { // 用于标记fiber的WorkTag类型,主要表示当前fiber代表的组件类型如FunctionComponent、ClassComponent等 tag: WorkTag, // ReactElement里面的key key: null | string, // ReactElement.type,调用`createElement`的第一个参数 elementType: any, // The resolved function/class/ associated with this fiber. // 表示当前代表的节点类型 type: any, // 表示当前FiberNode对应的element组件实例 stateNode: any, // 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回 return: Fiber | null, // 指向自己的第一个子节点 child: Fiber | null, // 指向自己的兄弟结构,兄弟节点的return指向同一个父节点 sibling: Fiber | null, index: number, ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject, // 当前处理过程中的组件props对象 pendingProps: any, // 上一次渲染完成之后的props memoizedProps: any, // 该Fiber对应的组件产生的Update会存放在这个队列里面 updateQueue: UpdateQueue<any> | null, // 上一次渲染的时候的state memoizedState: any, // 一个列表,存放这个Fiber依赖的context firstContextDependency: ContextDependency<mixed> | null, mode: TypeOfMode, // Effect // 用来记录Side Effect effectTag: SideEffectTag, // 单链表用来快速查找下一个side effect nextEffect: Fiber | null, // 子树中第一个side effect firstEffect: Fiber | null, // 子树中最后一个side effect lastEffect: Fiber | null, // 代表任务在未来的哪个时间点应该被完成,之后版本改名为 lanes expirationTime: ExpirationTime, // 快速确定子树中是否有不在等待的变化 childExpirationTime: ExpirationTime, // fiber的版本池,即记录fiber更新过程,便于恢复 alternate: Fiber | null, }
Fiber 如何解决问题的
Fiber 把一个渲染任务分解为多个渲染任务,而不是一次性完成,把每一个分割得很细的任务视作一个"执行单元",React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去,故任务会被分散到多个帧里面,中间可以返回至主进程控制执行其他任务,最终实现更流畅的用户体验。
即是实现了"增量渲染",实现了可中断与恢复,恢复后也可以复用之前的中间状态,并给不同的任务赋予不同的优先级,其中每个任务更新单元为 React Element 对应的 Fiber 节点。
Fiber 实现原理
实现的方式是requestIdleCallback这一 API,但 React 团队 polyfill 了这个 API,使其对比原生的浏览器兼容性更好且拓展了特性。
window.requestIdleCallback()方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间 timeout,则有可能为了在超时前执行函数而打乱执行顺序。
requestIdleCallback回调的执行的前提条件是当前浏览器处于空闲状态。
即requestIdleCallback的作用是在浏览器一帧的剩余空闲时间内执行优先度相对较低的任务。首先 React 中任务切割为多个步骤,分批完成。在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间再进行页面的渲染。等浏览器忙完之后有剩余时间,再继续之前 React 未完成的任务,是一种合作式调度。
简而言之,由浏览器给我们分配执行时间片,我们要按照约定在这个时间内执行完毕,并将控制权还给浏览器。
React 16 的Reconciler基于 Fiber 节点实现,被称为 Fiber Reconciler。
作为静态的数据结构来说,每个 Fiber 节点对应一个 React element,保存了该组件的类型(函数组件/类组件/原生组件等等)、对应的 DOM 节点等信息。
作为动态的工作单元来说,每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作。
每个 Fiber 节点有个对应的 React element,多个 Fiber 节点是如何连接形成树呢?靠如下三个属性:
复制 // 指向父级Fiber节点 this.return = null // 指向子Fiber节点 this.child = null // 指向右边第一个兄弟Fiber节点 this.sibling = null
13.React 中的 setState 同步异步的问题?
什么时候同步什么时候异步
在 React 中,如果是由 React 引发的事件处理(比如通过 onClick 引发的事件处理),调用 setState 不会同步更新 this.state,除此之外的 setState 调用会同步更新 this.state
所谓的除此之外,指的是绕过 React,通过 addEventListener 直接添加的事件处理函数,还有通过 setTimeout/setInterval 产生的同步任务
原因
在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdate 判断是直接更新 this.state 还是放到队列中回头再说,而且 isBatchingUpdate 默认是 false,也就是表示 setState 会同步更新 this.state,但是,有一个函数 batchedUpdate,这个函数会把 isBatchingUpdate 修改为 true,而当 React 在调用事件处理函数之前就会调用这个 batchedUpdates,造成的后果,就是由 React 控制的事件处理过程 setState 不会同步更新 this.state
注意
setState 的 异步 并不是说内部由异步代码实现,其实本身执行的过程和代码是同步的,只是合成事件和钩子函数的调用在更新之前,导致在合成事件和钩子函数中没法里吗拿到更新后的值,形成了所谓的"异步"
但是可以通过 setState(partialState, callback) 中的 callback 拿到更新后的结果