手写React的Fiber架构,深入理解其原理

熟悉React的朋友都知道,React支持jsx语法,我们可以直接将HTML代码写到JS中间,然后渲染到页面上,我们写的HTML如果有更新的话,React还有虚拟DOM的对比,只更新变化的部分,而不重新渲染整个页面,大大提高渲染效率。到了16.x,React更是使用了一个被称为Fiber的架构,提升了用户体验,同时还引入了hooks等特性。那隐藏在React背后的原理是怎样的呢,Fiber和hooks又是怎么实现的呢?本文会从jsx入手,手写一个简易版的React,从而深入理解React的原理。

本文主要实现了这些功能:

简易版Fiber架构

简易版DIFF算法

简易版函数组件

简易版Hook: useState

娱乐版Class组件

本文代码地址:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/fiber-and-hooks

本文程序跑起来效果如下:

Jun-19-2020 17-01-28

JSX和creatElement

以前我们写React要支持JSX还需要一个库叫JSXTransformer.js,后来JSX的转换工作都集成到了babel里面了,babel还提供了在线预览的功能,可以看到转换后的效果,比如下面这段简单的代码:

const App = ( <div> <h1>Title</h1> <a href="http://www.likecs.com/xxx">Jump</a> <section> <p> Article </p> </section> </div> );

经过babel转换后就变成了这样:

image-20200608175937104

上面的截图可以看出我们写的HTML被转换成了React.createElement,我们将上面代码稍微格式化来看下:

var App = React.createElement( 'div', null, React.createElement( 'h1', { id: 'title', }, 'Title', ), React.createElement( 'a', { href: 'http://www.likecs.com/xxx', }, 'Jump', ), React.createElement( 'section', null, React.createElement('p', null, 'Article'), ), );

从转换后的代码我们可以看出React.createElement支持多个参数:

type,也就是节点类型

config, 这是节点上的属性,比如id和href

children, 从第三个参数开始就全部是children也就是子元素了,子元素可以有多个,类型可以是简单的文本,也可以还是React.createElement,如果是React.createElement,其实就是子节点了,子节点下面还可以有子节点。这样就用React.createElement的嵌套关系实现了HTML节点的树形结构。

让我们来完整看下这个简单的React页面代码:

image-20200608180112829

渲染在页面上是这样:

image-20200608180139663

这里面用到了React的地方其实就两个,一个是JSX,也就是React.createElement,另一个就是ReactDOM.render,所以我们手写的第一个目标就有了,就是createElement和render这两个方法。

手写createElement

对于<h1>Title</h1>这样一个简单的节点,原生DOM也会附加一大堆属性和方法在上面,所以我们在createElement的时候最好能将它转换为一种比较简单的数据结构,只包含我们需要的元素,比如这样:

{ type: 'h1', props: { id: 'title', children: 'Title' } }

有了这个数据结构后,我们对于DOM的操作其实可以转化为对这个数据结构的操作,新老DOM的对比其实也可以转化为这个数据结构的对比,这样我们就不需要每次操作都去渲染页面,而是等到需要渲染的时候才将这个数据结构渲染到页面上。这其实就是虚拟DOM!而我们createElement就是负责来构建这个虚拟DOM的方法,下面我们来实现下:

function createElement(type, props, ...children) { // 核心逻辑不复杂,将参数都塞到一个对象上返回就行 // children也要放到props里面去,这样我们在组件里面就能通过this.props.children拿到子元素 return { type, props: { ...props, children } } }

上述代码是React的createElement简化版,对源码感兴趣的朋友可以看这里:

手写render

上述代码我们用createElement将JSX代码转换成了虚拟DOM,那真正将它渲染到页面的函数是render,所以我们还需要实现下这个方法,通过我们一般的用法ReactDOM.render( <App />,document.getElementById('root'));可以知道他接收两个参数:

根组件,其实是一个JSX组件,也就是一个createElement返回的虚拟DOM

父节点,也就是我们要将这个虚拟DOM渲染的位置

有了这两个参数,我们来实现下render方法:

function render(vDom, container) { let dom; // 检查当前节点是文本还是对象 if(typeof vDom !== 'object') { dom = document.createTextNode(vDom) } else { dom = document.createElement(vDom.type); } // 将vDom上除了children外的属性都挂载到真正的DOM上去 if(vDom.props) { Object.keys(vDom.props) .filter(key => key != 'children') .forEach(item => { dom[item] = vDom.props[item]; }) } // 如果还有子元素,递归调用 if(vDom.props && vDom.props.children && vDom.props.children.length) { vDom.props.children.forEach(child => render(child, dom)); } container.appendChild(dom); }

上述代码是简化版的render方法,对源码感兴趣的朋友可以看这里:

现在我们可以用自己写的createElement和render来替换原生的方法了:

image-20200608180301596

可以得到一样的渲染结果:

image-20200608180139663

为什么需要Fiber

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsfxpf.html