当前位置: 代码迷 >> 综合 >> react源码有关的初探-虚拟Dom / render / createElement / Fiber
  详细解决方案

react源码有关的初探-虚拟Dom / render / createElement / Fiber

热度:91   发布时间:2023-12-22 02:32:34.0

有一次遇到需要动态生成Dom的需求,于是认识到了react使用JS对象描述Dom结构的方式。为了更好地研究开始了此次初探。从非常浅显的开始-render/createElement

先解决以下问题:
1、什么是虚拟Dom?
2、为什么要使用虚拟Dom?
3、虚拟Dom到底比真实Dom快在哪里?

参考:
什么是虚拟Dom:https://github.com/livoras/blog/issues/13
浏览器工作原理:https://segmentfault.com/a/1190000009975744
为什么操作原生Dom性能差:https://www.zhihu.com/question/324992717/answer/690011952
本来还想拆开一个个回答的,但还是合在一起说吧。


自我理解复述:
虚拟Dom是一个JS对象,框架抽象封装后的Dom结构,用于描述真实Dom数据。

1、首先,js直接操作Dom结构代价大。
①Dom对象内部数据笨重,而浏览器的 JavaScript 引擎与 DOM 引擎共享一个主线程,,当JS操作Dom使用渲染引擎时JS引擎会阻塞,如果遇上重绘重排占用更多时间,用户会觉得页面卡顿。
②例如原生或JQuery直接操作Dom数据是不会缓存的,也就是说操作一次,页面就重绘一次,两种引擎间来回切换消耗的性能更大。不像框架会进行批量处理减少操作次数。

2、使用JS对象足够描述Dom结构属性,在这基础之上使用JS操作虚拟Dom,生成Fiber树通过Diff算法找出最小更新方式,能够精准、批量操作真实Dom。就相当于在缓慢的真实Dom操作和快速的JS操作之间多加了一个虚拟Dom缓冲层,JS计算,虚拟Dom体现,最后框架抽象、集合,统一更新到真实Dom上去。

由于Diff算法得出最小代价更新方式+减少了操作Dom次数+JS计算性能优越,虚拟Dom才显得比较快。


查资料看到有关的好文章:
1、https://www.jianshu.com/p/b189b2949b33?utm_campaign=maleskine

1、render

看react官网:render是react内唯一必须实现的函数,render用处是把虚拟Dom树结构转换成真实Dom结构插入容器内,同时创建Diff使用的Fiber树,更新的时候也是靠render函数内部去协调、调用diff的API。
(协调=diff比对得出最小更新操作,再统一更新)

不知道有没人和我一样好奇JSX / 虚拟Dom树 / Fiber树三者的关系。
(不知道我理解的对不对,有问题麻烦指出,如果我以后发现有问题也会回来修改。)

转换过程:JSX=>String praser =>React.createDOM=>vdom(fiber tree)=>DOM

更新过程可以看下面这个文章,讲得很详细。
走进Fiber架构,这个写的特别好:https://juejin.cn/post/6844904019660537869

①JSX转为JS对象。
②初始化调用render:JSX被createElement递归等等创建成虚拟Dom树(JS对象表示)。同时创建Fiber树(基于Dom树结构),在react16中Fiber节点就是虚拟DOM,把树转为类链表结构,最后生成真实Dom插入Container里。
③更新的时候再调render,使用diff协调,Fiber树更新对比新旧两棵树,(新workInProgress树,旧current树)。将根据Diff替换规则把修改后的数据更新到新树上,更新完虚拟Dom之后生成真实Dom插入Container内。

别人写的一个理解,我觉得挺好的,记录下,知乎react的虚拟dom是指的虚拟渲染树吗?这个问题下的回答,作者匿名。


    react jsx是React.createElement的语法糖,一个jsx节点就是一个js对象,对dom的抽象节点(虚拟dom,props属性和真实dom属性对应。可以看作是真实dom的缓存层),包含type, props, children属性。所以,你写出来的jsx嵌套结构就是一个js对象,通过children属性链接成一颗vdom树。每个节点的类型和属性在type props里保存。

    react的调度算法会把vdom tree转换成fiber链表,利用fiber reconciler深度优先遍历每一个fiber节点,利用alternate属性链接到旧的fiber节点,diff两个fiber的属性,并计算expirationTime,把更改的操作存在effects队列里,return归并到父级effects list中。利用requestIdleCallback循环处理fiber queue直到全部fiber处理完毕,同时通过比较expirationTime和deadline.timeRemaining()确定effect fiber优先级,按优先级排列好effect顺序然后一次性执行所有effect,确保充分利用js引擎空闲时间而又不阻塞主线程,保证一次性的dom操作能流畅进行。

    react是单向数据流,改变变量和css是不会触发dom rerender的,你需要在修改变量后调用setState或者ReactDOM.render来触发react diff算法,react会帮你高效地更改dom内容。(主要是虚拟dom缓存技术,和dom操作优先级调度算法。缓存技术在Canvas的离屏图层也是一个性能优化点。)

2、Fiber和Diff

Fiber长话短说版:https://zhuanlan.zhihu.com/p/297971861
浅谈React16框架 - Fiber(只有协调无执行):https://www.cnblogs.com/zhuanzhuanfe/p/9567081.html
走进React Fiber 架构:https://www.jianshu.com/p/cb63554df8c3
知乎上写的很好的一篇:https://zhuanlan.zhihu.com/p/37095662

Fiber是react16之后更新的一种调度算法。在16以前,react的Diff比较是同步进行的,这就意味着当页面上有大量 DOM 节点时,diff 的时间可能过长,从而导致交互卡顿。react使用了React Fiber 来处理这样的问题。

react更新阶段分为协调(reconciliation)(=diff) 和执行(commit),协调阶段是可以打断的,执行阶段不可打断。React Fiber主要应用在协调阶段,使用异步可中断的方式Diff。以前是使用递归调用比对虚拟DOM,因此比较层级会越来越深,递归中途打断和恢复很麻烦,因此Fiber采用了类似链表的数据结构,方便遍历和恢复。

React Fiber核心是将任务拆分成一个个Fiber节点(最小工作单元),赋予每个任务优先级,根据优先级利用浏览器空闲时段进行操作,主要使用到了浏览器的requestIdleCallback API。(浏览器空闲的时候回调XXX)一旦浏览器有空闲时间,就唤醒协调操作,协调完了就commit执行。

interface Fiber {
    // 指向父节点return: Fiber | null,// 指向子节点child: Fiber | null,// 指向兄弟节点sibling: Fiber | null,[props: string]: any
};
vnode结构

在这里插入图片描述
$$typeof:symbol类型,证明你是react元素,用于安全性检测,防止XSS攻击。
props:包括children元素,和加入Dom元素内的标签。
type:元素标签。
key:元素标识,diff有关,可作为优化。

其他的懒得多解释了。
开始写。
跟的是一个网课教程,不知道B站有没。

先用create-react-app创建一个react项目,接下来自己用一个test-react代替react。
我们先写render。
一个简单的渲染文本标签和原生标签的demo。下一步是渲染函数和component组件。

index.jsimport "./index.css";
import * as ReactDOM from "./test-react/react-dom";
import Component from "./test-react/Component";function Fun(props) {
    return <div>函数组件-{
    props.name}</div>;
}class ClassComponent extends Component {
    render() {
    return (<div className="border"><p>class组件-{
    this.props.name}</p></div>);}
}const JSX = (<div className="border">文本标签<p style={
    {
     color: "red" }}>这是一段文本</p><Fun name="xxxxx"/><ClassComponent name="xxxxx" /></div>
);ReactDOM.render(JSX, document.getElementById("root"));
test-react/Component.js/** 一个Component的定义* 其实Component也只是一个函数,对函数做了各种处理,导出一个工厂函数*/export default function Component(props){
    this.props = props;
}/** 函数组件和类组件的分别在于原型链上有标识属性 */
Component.prototype.isReactComponent = {
    }
test-react/react-dom.js
// 渲染分为初次渲染和更新渲染// render的第一个参数实际上接受的是createElement返回的对象,也就是虚拟DOM
export const render = (vnode, container) => {
    // vnode->nodeconst node = createNode(vnode);console.log(vnode);// node->containercontainer.appendChild(node);
};// 将虚拟Dom转化为真实Dom
export const createNode = (vnode) => {
    const {
     type } = vnode;let node = null;// 组件类型:文本、原生节点、function、componentif (typeof type === "string") {
    // 是字符串说明是原生标签,无type 文本节点,组件节点另算node = updateHostComponent(vnode);} else if (typeof type === "function") {
    if(type.prototype.isReactComponent){
    // 类组件node = updateClassComponent(vnode);}else{
    // 函数组件node = updateFuntionComponent(vnode);}}else {
    node = updateTextComponent(vnode);}return node;
};// HostComponent是原生标签节点的意思,这个函数用于更新\创建原生标签
const updateHostComponent = (vnode) => {
    const {
     type, props } = vnode;const node = document.createElement(type);// 把虚拟Dom中的props内的属性更新到真实node节点中updateNode(node, props);// 渲染node中的子节点,并且把children插入到node节点中reconcileChildren(node, props.children);return node;
};// HostComponent是原生标签节点的意思,这个函数用于更新\创建原生标签
const updateTextComponent = (vnode) => {
    const node = document.createTextNode(vnode);return node;
};// 更新,创建函数组件
const updateFuntionComponent = (vnode) => {
    const {
     type, props } = vnode;// 如何获得到funtion 返回回来的JSX?当然是直接调用函数,把prop当参数传进去。const vvNode = type(props);// 把返回回来的虚拟DOM转换为真实Dom返回const node = createNode(vvNode)return node;
};// 更新,创建类组件
const updateClassComponent = (vnode) => {
    const {
     type, props } = vnode;// 如何获得JSX,只能先实例化类组件,调用类组件里的render函数返回const instance = new type(props);const vvnode = instance.render();// 把返回回来的虚拟DOM转换为真实Dom返回const node = createNode(vvnode)return node;
};// 把虚拟Dom中的props内的属性更新到真实node节点中
const updateNode = (node, nextVal) => {
    Object.keys(nextVal).forEach((key) => {
    if (key !== "children") {
    node[key] = nextVal[key];}});
};// 在diff算法中,此处是用于对比新旧fibei节点,进行优化更新,要递归。
const reconcileChildren = (parentNode, children) => {
    // 源码是会判断children类型的,此处懒得写类型判断,// 源码中如果非数组就返回非数组,这里偷懒全部弄成数组const newChildren = Array.isArray(children) ? children : [children];newChildren.forEach((element) => {
    render(element, parentNode);});
};// eslint-disable-next-line import/no-anonymous-default-export
export default {
     render };

Fiber和Diff

1、https://zhuanlan.zhihu.com/p/26027085
2、https://www.infoq.cn/article/react-dom-diff/
3、https://segmentfault.com/a/1190000018250127

  相关解决方案