手写 React 系列第一篇之【初始渲染】

毕业生求职中,有坑联系~javascript

【github】 【我的简历】html

最近感悟

刚毕业不久,转行前端,自学半年多,以后去找工做,发现工做机会真的很难找,内心焦急万分。这时候前辈鼓励我,稳定心态,你所须要作的就是投资本身,当工做机会真正来临的时候,可以作到一把抓住就能够了。忽然以为这句话颇有道理,我想只要天天都有进步,哪怕一时找不到工做,我也不吃亏,顶多不能尽早赚钱。。前端

找准方向

我决定找一个比较流行的框架进行深度学习,在 Vue 和 React 之间,我选择了 React,主要是由于对 React 框架比较熟,并且大厂对 React 的使用度比较高。java

明确目标

这个系列的目标有三个:react

  • 首先在学习过程当中,可以加深本身对于 React 的理解,作到手写一个简单的 React 框架。
  • 其次总结 React 框架中的一些优秀的算法思想。
  • 最后但愿这些内容可以帮助到他人。

正文开始

本文以 15.X 版本的 React 框架进行学习,在实现 类 React 框架以前,咱们先看看须要有什么前置知识?webpack

  • 熟练的原生 JS 技能。
  • JSX 语法。
  • Webpack 构建能力。

JSX 和虚拟 DOM

咱们声明以下一个组件实例。git

var component = <div>测试中</div>;
复制代码

webpack 打包的时候会自动将上面的写法转换成 React.createElement 的方式,最终返回虚拟DOM 对象,以下所示。github

var virtualDOM = {
    props:{
        children:['children'],
    },
    type:'div'

    
}
复制代码

实际上 React 的虚拟 DOM 对象还有一些其余属性,好比 key,ref,这里为了简单起见,本节咱们只讲解初始渲染过程,因此咱们只关注和渲染相关的 propstype 两个关键属性,到后面讲解 diff 的时候,再去关注他们。web

JSX 转化成虚拟 DOM

上面的转换须要借助 JSX 语法解析器的能力,解析器可以将 JSX 元素编译为一个虚拟 DOM 对象,即 virtualDOM。算法

借助虚拟 DOM,咱们能够不须要真正操做一个 DOM 对象,而是将实际的 DOM 对象映射给一个 JS 对象中,经过对比虚拟 DOM 来决定是否要操做真实 DOM。

JSX 首先在编译器由 webpack 结合 babel 将 JSX 元素编译为以下格式的代码,以上面代码为例。

var component = <div>测试中</div>
复制代码

这段代码在编译期间转化为以下代码:

var component = React.createElement("div", null, "\u6D4B\u8BD5\u4E2D");
复制代码

当有两个子元素的时候,

var component = <div>测试中<button>知道了</button></div>
复制代码

编译为

var component = React.createElement("div", null, "\u6D4B\u8BD5\u4E2D", React.createElement("button", null, "\u77E5\u9053\u4E86"));
复制代码

那么,当webpack将 JSX 语法编译为 React.createElement 方法调用以后,如何再经过createElement 方法返回虚拟 DOM 对象呢?

这就要涉及到 createElement 的函数实现了,接下来咱们实现它。

首先,该方法接收一系列参数。

createElement(type, props, children1, children2, ...);
复制代码
  • type 表明虚拟 DOM 的节点类型。
  • props 表明虚拟 DOM 的属性。
  • children1,children2,... 表明子节点。

咱们的 createElement 方法须要将这些参数组装成一个 虚拟DOM 对象输出出去,实现比较简单,以下:

function createElement(type, props, ...children){
    props = {...props};
    if(children.length > 0){
        props.children = children;
    }
    
    return {
        type,
        props
    }
    
}
复制代码

render 实现

那么,有了虚拟 DOM 了,下一步要作的事就是将虚拟 DOM 转化为真实 DOM ,而且将真实 DOM 插入到页面中。

咱们来看一段经典的 React 代码。

import React from 'react';
import ReactDOM from 'react-dom';

var app = <div>首次渲染</div>;

ReactDOM.render(app, document.querySelector('#root'));
复制代码

咱们看下 render 方法如何实现。

元素、组件、组件实例的关系

在继续往下讲解以前,咱们先了解一下 React 中的概念。

  • 元素 元素是指经过 createElement 方法建立出来的 JS 对象,即虚拟 DOM,它会对应 html 文档里的一个真实 DOM 片断。

  • 组件类

    • 组件类是一个构造函数或者一个类,它可以生成组件实例,组件实例经过必定的方法生成元素,具体的方法后面咱们也会讲解。
    • 组件的另外一个好处是方便复用和扩展。
  • 组件实例。 咱们一般说组件的时候,其实有时候是指组件类,有时候是指组件实例,很容易引发歧义。接下来的篇幅,咱们会分清楚这两个概念。

渲染方法的实现

  • 最简单的虚拟 DOM

看下面最简单的元素:

var component = 1;
var component1 = '2';
var component2 = true;
var component3 = null;
var component4 = undefined;
复制代码

咱们的 render 方法须要可以处理它们,处理策略以下:

  • 针对 null、undefined、布尔类型的值,返回一个空字符串的 text 节点。
  • 针对字符串,返回该字符串对应的 text 节点。
  • 针对数字,返回该数字对应的 text 节点。

针对以上这三种状况,render 的实现以下:

function render(vdom, container){
    let dom = renderElement(vdom);
    container.appendChild(dom);
}

function renderElement(vdom){
    if(typeof vdom === 'boolean' ||  vdom === null || vdom === undefined){
        return document.createTextNode('');
    }
    if(typeof vdom === 'number'){
        return document.createTextNode(String(vdom));
    }
    if(typeof vdom === 'string'){
        return document.createTextNode(vdom);
    }
}
复制代码
  • 稍微简单一些的虚拟 DOM

假设这样一个元素:

var component = <div>测试中</div>;
复制代码

对应虚拟 DOM 也就是

{
    props: {
        children: ["测试中"]
    },
    type: "div"
}
复制代码

那咱们的render方法须要要怎么改进,才能够对其进行渲染呢?

很明显咱们能够根据 type 区分,对 type 为 string 类型的虚拟 DOM 单独处理,下面看下具体实现。

//改造 renderElement 方法。
function renderElement(vdom){
    //基础数据类型处理
    //此处略。
    if(typeof vdom.type === 'string'){
        return renderNativeDom(vdom);
    }
}
// 将类型为原生节点的虚拟 DOM 转化为真实 DOM。
function renderNativeDom(vdom){
    let dom = document.createElement(vdom.type);
    const { props: { children }} = vdom;
    for(var i = 0; i < children.length; i++){
        // 渲染子节点
        let childNode = renderElement(children[i]);
        dom.appendChild(childNode);
    }
    return dom;
}
复制代码
  • 函数组件

再来看比较经常使用的函数组件。

function A(props){
    return <div>测试中</div>
}
复制代码

上面这种方式实际上定义了一个函数组件类,注意是组件类,不是组件实例。

那么咱们用的时候,就要经过这样的方式来实例化组件了。

var component = <A />; 复制代码

咱们看下,该如何改造 renderElement 方法。

按照惯例,咱们先看下函数组件实例对应的虚拟 DOM 形式。

{
    props: {},
    type: ƒ B()
}
复制代码

可见,type 是一个函数,这个函数就是组件类的构造函数。因此咱们能够依然能够根据 type进行区分。

  • 策略以下:

    • 针对 type 为 function 的虚拟 DOM,经过执行该 function 来拿到待渲染的虚拟 DOM。

    • 以后的策略就很简单了,递归执行 renderElement 方法就好了。

咱们看下实现

//改造 renderElement
function renderElement(vdom){
    //函数组件实例的判断
    if(typeof vdom.type === 'function'){
        return return renderFunctionComponent(vdom);
    }
}
// 增长对函数组件实例的渲染。
function renderFunctionComponent(vdom){
    let inst = vdom.type(vdom.props);
    return renderElement(inst);
}
复制代码
  • 类组件

最后,咱们看下类组件,看一个最经常使用的组件类的定义。

class Header extends React.Component{
    render(){
        return <div>类组件实例</div>
    }
}
复制代码

构造一个类组件实例:

var component = <Header />; console.log(component); 复制代码

看下这种类型实例对应的虚拟 DOM 是什么样子的。

{
    props: {}
    type: ƒ Header()
}
复制代码

可见,类组件实例对应的虚拟 DOM 的 type 也是 函数,因此,咱们没法单纯根据虚拟 DOM 的type 来区分函数组件和类组件的实例了,那怎么区分这两种实例呢?

还记得类组件定义的时候,要继承的 Component 抽象类吗?咱们能够根据这个特色进行区分,只须要判断当前组件实例是不是 Component 的实例便可。

看下代码实现。

// 增长组件基类 Component
class Component(){
    constructor(props){
        this.state = {};
        this.props = props || {};
    }
    setState(nextState){
        
    }
}

//改造 renderElement 方法,识别类组件和函数组件。
function renderElement(vdom){
    if(typeof vdom.type === 'function'){
        //区分类组件仍是函数组件。
        if(vdom.type.prototype instanceof React.Component){
            //类组件
            return renderClassComponent(vdom);
        } else {
            //函数组件
            return renderFunctionComponent(vdom);
        }
    }
}

function renderClassComponent(vdom){
    let inst = new vdom.type(vdom.props);
    let vd = inst.render();
    return renderElement(vd);
}
复制代码

总结

以上就是咱们 React 的第一个环节,初始渲染,代码整理以下:

  • React.js
function createElement(type, props, ...children){
    props = {...props};
    if(children.length > 0){
        props.children = children;
    }
    
    return {
        type,
        props
    }
}

class Component(){
    constructor(props){
        this.state = {};
        this.props = props || {};
    }
    setState(nextState){
        
    }
}

export default {
    createElement,
    Component
}
复制代码
  • React-Dom.js
function render(vdom, container){
    let dom = renderElement(vdom);
    container.appendChild(dom);
}

function renderElement(vdom){
    if(typeof vdom === 'boolean' ||  vdom === null || vdom === undefined){
        return document.createTextNode('');
    }
    if(typeof vdom === 'number'){
        return document.createTextNode(String(vdom));
    }
    if(typeof vdom === 'string'){
        return document.createTextNode(vdom);
    } 
    if(typeof vdom.type === 'function'){
        //区分类组件仍是函数组件。
        if(vdom.type.prototype instanceof React.Component){
            //类组件
            return renderClassComponent(vdom);
        } else {
            //函数组件
            return renderFunctionComponent(vdom);
        }
    }
}


function renderClassComponent(vdom){
    let inst = new vdom.type(vdom.props);
    let vd = inst.render();
    return renderElement(vd);
}


function renderFunctionComponent(vdom){
    let inst = vdom.type(vdom.props);
    return renderElement(inst);
}

function renderNativeDom(vdom){
    let dom = document.createElement(vdom.type);
    const { props: { children }} = vdom;
    for(var i = 0; i < children.length; i++){
        // 渲染子节点
        let childNode = renderElement(children[i]);
        dom.appendChild(childNode);
    }
    return dom;
}
复制代码

结语

本节咱们只实现了 React 的初始渲染,你们会发现咱们并无针对属性作处理,也并无针对 state 变化引起界面渲染的逻辑。

不要紧,咱们一步步来,下一节咱们讲解属性、state,以及组件生命周期的处理。

再见~

相关文章
相关标签/搜索