通过实现 createElement、Component 和 render 深入理解React 原理

文章目录

  • createElement
  • render(vdom, container)
  • Component

文章主要是通过官网和源码学习之后,对 React 核心 API 加入了自己的理解和总结。注意代码中将react 版本锁定在17.0.0 之前。

createElement

简单粗暴,我们先来看下面这段代码:

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

function HelloMessage(props) {
    return (
        
hello, {props.name} 尝试一起来写个简易的react core api}
) } ReactDOM.render( , document.getElementById('root'));

我们知道,JSX其实是语法糖,17.0.0 版本前通过Babel将jsx转换成React.createElement(type,config,children)的函数调用,React.createElement方法主要是对一系列参数「type,key,ref,props,childrend」进行处理,最终创建并返回虚拟dom,这也是我们为何在头部引入React 的缘故;17.x版本之后,不需要显示导入import React from 'react',babel对jsx进行了新的转换不需要依React.createElement,babel编译后会自动导入

import { jsx as _jsx } from 'react/jsx-runtime',
...
return _jsx(type, config, children)

我们可以通过Babel REPL来看下,这段代码实际被转换成什么了

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

function HelloMessage(props) {
  return React.createElement(
  "div", 
  null, 
  "hello,
   ", 
   props.name, 
  React.createElement("span", null, "尝试一起来写个简易的react core api"));
}

ReactDOM.render( React.createElement(HelloMessage, {
  name: "lxcy"
}), document.getElementById('root'));

OK! 我们再来捋一下createElement到底做了什么?结合源码,我简单总结了几点

  • 通过编译器babel,jsx被转换成React.createElement(type, config, …children)的函数调用
  • React.createElement 接受组件类型,属性,子组件,返回一个js对象(v-dom)
  • createElement主要做了对组件参数的处理,以及添加一些必要的标识;最后返回一个js对象

到此,我们可以尝试实现createElement

function createElement(type, config, ...children) {
    delete config.__self
    delete config.__source

    // 处理一些特殊的props, 如:key, ref, 源码中还有对default Props的处理
    const { key, ref, ...extraProps } = config

    return {
        $$typeof: Symbol('react.element'), // 标识 React组件类型
        type,
        ref,
        key,
        props: {
            ...extraProps,
            children: children.map(c => typeof c === 'object' ? c : createTextNode(c))
        }
    }
}
function createTextNode(value) {
    return {
        type: undefined,
        props: {
            nodeValue: value,
            children: []
        }
    }
}

render(vdom, container)

render接受createElement返回的js对象和一个容器,将 vdom 转换成真实的 dom,然后挂载到容器中;
render 函数返回的是 type 的实例,比如是原生 dom 则返回元素的实例,class 组件返回类的实例对象,函数组件则返回 null。

function render(vnode, container) {
	// vdom 是一棵 js 对象树,通过递归 diff 来更新 真实 dom 树
    // 为了方便递归,我们将抽取为一个函数, 作为虚拟 dom 的 diff 块
    const node = initVDOM(vnode);
    container.appendChild(node);
    return node;
}

根据 type 类型,分为 class 组件、function 组件、原生标签和文本节点;源码中 Component类会在其原型上增加类组件标识Component.prototype.isReactComponent = {};因此,我们可以通过此属性来区分类组件和函数组件。

function shouldConstruct(Component) {
      const  prototype = Component.prototype;
      return !!(prototype && prototype.isReactComponent);
 }
function initVDOM(vnode) {
    const { type } = vnode;
    if (typeof type === 'function') {
        if (shouldConstruct(type)) {
            return createClassElement(vnode);
        } else {
            return createFunElement(vnode);
        }
    } else if (typeof type === 'string') {
        return createDOMElement(vnode);
    } else {
        return document.createTextNode(vnode.props.nodeValue);
    }
}

先来实现原生标签
注意⚠️:这里以及后面的实现都未考虑状态变更,只实现了基本的 dom 挂载,在后续 fiber 架构实现 文章中会补齐状态变更和 dom 更新的实现;

function createDOMElement(vnode) {
    // 通过 type 可以拿到元素标签
    const { type, props } = vnode;
    // 1. 创建元素
    const node = document.createElement(type);
    // 分离出一些特殊的属性,比如className、htmlFor等
    const { className, htmlFor, children, ...res } = props; 
    // 2. 设置属性
    if (className) {
        node.setAttribute('class', className);
    }
    if (htmlFor) {
        node.setAttribute('for', htmlFor);
    }

    Object.keys(res).forEach(k => node.setAttribute(k, props[k]));
    // 3. 遍历解析子元素,并插入父节点
    children.forEach(c => {
        // 处理 数组遍历出多个子节点
        if (Array.isArray(c)) {
            c.forEach(l => {
                node.appendChild(initVDOM(l))
            })
        } else {
            node.appendChild(initVDOM(c))
        }

    })
    // 4. 最后将元素返回
    return node;
}

vnode 对象映射了元素,属性和子元素,因此可以通过type创建元素,插入 props 中的属性,react 中有一些特殊的属性名,如className, htmlfor ...需要进行处理,包括事件处理名,如onClick; 这里需要特别说明的是,在处理 children时,我们需要考虑数组类型,当子元素是数组时,需要逐一递归处理;常见场景如:

    { list.map(c => {c.name}) }

同理,在 class 组件和 function 组件 中,我们只需要拿到 vdom ,然后递归调用 initVDOM(vdom), 函数组件非常简单,type 是一个函数,可以直接通过 type(props)调用来返回 vnode树,class 组件则可以通过实例化并调用其render函数 来返回;

// 如果是类组件,获取实例,并调用 render() 来返回 vnode
function createClassElement(vnode) {
    const { type, props } = vnode
    // 通过 class 组件实例化,调用render()函数,返回渲染元素
    const instance = new type(props);
    const rendererElement = instance.render();
    return initVDOM(rendererElement);
}

// 如果是函数组件,它的实例为null, 直接调用函数即可
function createFunElement(vnode) {
    const { type, props } = vnode
    const rendererElement = type(props);
    return initVDOM(rendererElement);
}

至此,100行代码左右,就可以初步实现一个react, 当然还有非常多的细节我们没有去考虑,包括最重要的状态更新,React16版本引入了fiber架构,我将在下一篇简易实现fiber 架构 中来实现状态的更新;

Component

这里简单补齐一下Component的代码

class Component {
    constructor(props) {
        this.props = props
        this.state = {}
        this.updater = {}
    }
    setState(ins) { }
    forceUpdate() {}
}
Component.prototype.isReactComponent = {};

关于setState 和 forceUpdate 的具体实现,将会放在单独的文章中讲解,敬请关注!

你可能感兴趣的:(React高级,react)