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

有一次遇到需要动态生成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.js

import "./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->node
  const node = createNode(vnode);
  console.log(vnode);

  // node->container
  container.appendChild(node);
};

// 将虚拟Dom转化为真实Dom
export const createNode = (vnode) => {
  const { type } = vnode;
  let node = null;

  // 组件类型:文本、原生节点、function、component
  if (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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值