React中的JSX原理渐析

2,299 阅读18分钟

JSX

相信使用react的大家对于jsx已经游刃有余了,可是你真的了解jsx的原理吗?

让我们由浅入深,来一层一层揭开jsx的真实面目。

文章中涉及的代码地址 戳我👇

React.createElement

react官方中讲到,关于jsx语法最终会被babel编译成为React.createElement()方法。

我们来看看这段jsx

<div className="wang.haoyu">hello</div>

经过babel编译后它变成这样的代码:

React.createElement("div", {
    className:'wang.haoyu'
}, "hello");

jsx中存在多个节点元素时,比如:

<div>hello<span>world</span></div>

它会将多个节点的jsxchildren属性变成多个参数进行传递下去:

React.createElement("div", null, "hello", React.createElement("span", null, "world"));

可以看到,外层的div元素包裹的children元素依次在React.createElement中铺平排列进去,并不是树型结构排列。

需要注意的是,旧的react版本中,只要我们使用jsx就需要引入react这个包。而且引入的变量必须大写React,因为上边我们看到babel编译完jsx之后会寻找React变量。

新版本中,不再需要引入React这个变量了。有兴趣的同学可以去看看打包后的react代码,内部会处理成为Object(s.jsx)("div",{ children: "Hello" }),而老的版本是React.createElement('div',null,'Hello')

这两种方式效果和原理是一模一样的,只是新版额外引入包去处理了引入。所以不需要单独进行引入React

React元素

React之中元素是构建React的最小单位,其实也就是虚拟Dom对象。

本质上jsx执行时就是在执行函数调用,是一种工厂模式通过React.createElement返回一个元素。

const element = <div>Hello</div>
console.log(element,'element')

image.png

先忽略掉一些ref/key之类的属性,这个时候来看我们发现它其实就是一个js对象,记录了type表示元素类型。props表示元素的接受的prop,注意这里会将jsx内部标签内容插入到propschildren属性中。

需要注意的是这里的children属性,如果内部标签元素存在多个子元素时候。children会是一个数组。因为这里仅仅只有文本节点,所以只有一个Hello

在我们平常使用react项目的时候,index.tsx中总是会存在这样一段代码:

ReactDOM.render(<App />, document.getElementById('root'));

结合上边我们所讲的React.createElement,我们不难猜出ReactDOM.render这个方法它的作用其实就是按照React.createElement生成的虚拟DOM节点对象,生成真实DOM插入到对应节点中去,这就是简单的渲染过程。

元素的更新

react中元素本身是不可变的。

比如:

const element = <h1 title="hello" >Hello</h1>
console.log(JSON.stringify(element,null,2))

image.png

当我们想将它的内容改成world时,如果直接通过

element.props.children = 'world'

这样是不可以的,react会提示:

Uncaught TypeError: Cannot assign to read only property 'children' of object '#<Object>'

无法给一个只读属性children进行赋值,修改其他属性比如type之类同理也是不可以的。

当我们通过这种方式给react元素增加属性时,也是增加的。

Cannot add property xxx, object is not extensible

not extensiblereact17之后才进行增加的。通过Object.freeze()将对象进行处理元素。

需要注意Object.freeze()是一层浅冻结,在react内部进行了递归Object.freeze()

所以在react中元素本身是不可变的,当元素被创建后是无法修改的。只能通过重新创建一个新的元素来更新旧的元素。

你可以这样理解,在react中每一个元素类似于动画中的每一帧,都是不可以变得。

当然在react更新中仅仅会更新需要更新的内容,内部会和Vue相同的方式去进行diff算法,高效更新变化的元素而不是更新重新渲染所有元素。

jsx原理分析

需要注意我们这里使用旧的React.createElement方法,如果是^17版本下,需要在环境变量中添加DISABLE_NEW_JSX_TRANSFORM=true

上边我们已经分析过React.createElement这个方法的返回值,接下来我们就尝试自己来实现jsx的渲染。

先来看看原本React中createElement方法的返回值:


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

const element = (
  <div className="header" style={{ color: 'red' }}>
    <span>hello</span>world
  </div>
);

console.log(JSON.stringify(element, null, 2), 'element');

image.png

接下来我们就根据结果来推写法,实现一个简单的createElement方法

实现React.crateElement方法-原生DOM元素的渲染

  • 实现utils/react.js

// 这里之所以额外书写一个 wrapToDom元素 是为了方便对比 react源码中没有这段方法是特殊处理的
// 我们为了方便 将普通类型 也统一处理成为Object

const React = {
  createElement: function (type, config, children) {
    const props = {
      ...config,
    };
    // 上边讲到babel编译jsx后
    // 如果参数大于3个 那么就有多个children props.children是一个数组
    if (arguments.length > 3) {
      props.children = Array.prototype.slice.call(arguments, 2);
    } else {
      props.children = children;
    }
    return {
      type,
      props,
    };
  },
};

export default React;

这一步我们已经实现了基础的React.createElement方法。

  • index.tsx
import React from './utils/react';
import ReactDOM from 'react-dom';

// babel编译后的代码会引入 React.createElement
// 此时的React指向的是我们自己写的React
const element = (
  <div className="header" style={{ color: 'red' }}>
    <span>hello</span>world
  </div>
);

ReactDOM.render(element, document.getElementById('root'));

实现ReactDOM.render方法-将react中源生DOM元素变成真实元素插入页面

  • 接着咱们先来实现一个对于children类型的判断方法
// utils.js 
// 常亮 判断文本类型
const REACT_TEXT = Symbol('REACT_TEXT')

// 无论以前是什么元素,都转成VDOM的对象形式
export function transformVNode(element) {
    // 额外处理文本节点 将文本节点输出和其他节点一样的Object类型
    if(typeof element === 'string' || typeof element === 'number') {
        return { type: REACT_TEXT, props: { content: element } }
    }
    return element
}
  • 接下来我们改造一下我们之前写好的React.createElement方法
import { transformVNode } from './utils';

const React = {
  createElement: function (type, config, children) {
    const props = {
      ...config,
    };
    if (arguments.length > 3) {
      props.children = Array.prototype.slice
        .call(arguments, 2)
        .map(transformVNode);
    } else {
      props.children = transformVNode(children);
    }
    return {
      type,
      props,
    };
  },
};

export default React;

  • 接下来我们已经拥有了对应的VDom对象,就可以开始实现React.render方法。

React.render核心思想就是将我们的Vdom对象编程浏览器可以识别的标签节点挂载在对应元素上

/**
 * 把虚拟DOM变成真实DOM插入
 * @param {Object} vDom 虚拟DOM
 * @param {HTMLElement} el 元素
 */

import { REACT_TEXT } from './constant';

// 真正渲染方法
function render(vDom, el) {
  const newDom = createDom(vDom);
  el.appendChild(newDom);
}

// 先不考虑自定义组件
function createDom(vDom) {
  const { type, props } = vDom;
  let dom;
  // 文本节点
  if (type === REACT_TEXT) {
    dom = document.createTextNode(props.content);
  } else {
    dom = document.createElement(type);
  }
  // 更新属性
  if (props) {
    // 更新跟节点Dom属性
    updateProps(dom, {}, props);
    // 处理children 考虑undefined/null 不做任何处理
    // 考虑 children是一个数组 那么就表示他拥有多个儿子
    // 考虑children是一个Object 那么他就只有一个儿子节点
    if (typeof props.children === 'object' && props.children.type) {
      // 单个元素
      render(props.children, dom);
    } else if (Array.isArray(props.children)) {
      // 多个元素
      reconcileRender(props.children, dom);
    }
  }
  // 记录挂载节点
  vDom.__dom = dom;
  return dom;
}

// 挂载多个dom元素 React.createElement先不考虑递归
function reconcileRender(vLists, parentDom) {
  for (let node of vLists) {
    render(node, parentDom);
  }
}

/**
 * 把虚拟DOM变成真实DOM插入
 * @param {HTMLElement} dom 元素
 * @param {Object} oldProps 元素本身的props 用于更新这里暂时用不到
 * @param {Object} newProps 元素新的props
 */

function updateProps(dom, oldProps, newProps) {
  // 合并props 暂时没有老的 仅处理新的
  Object.keys(newProps).forEach((key) => {
    if (key === 'children') {
      // 单独处理children
      return;
    }
    if (key === 'style') {
      addStyleToElement(dom, newProps[key]);
    } else if (key === 'content') {
      // 文本不做任何操作
    } else {
      dom[key] = newProps[key];
    }
  });
}

function addStyleToElement(dom, styleObject) {
  Object.keys(styleObject).forEach((key) => {
    const value = styleObject[key];
    dom.style[key] = value;
  });
}

const ReactDOM = {
  render,
};

export default ReactDOM;

其实这里的的核心思想就是通过render方法将虚拟DOM根据对应的属性转化成为真实DOM节点进行递归挂载,最终通过appendChild渲染到页面上。

写在最后

目前来说,我们已经基本实现了React.createElementReactDom.render这两个方法。 只不过目前来说仅仅针对于源生DOM节点进行了处理。

React中我们知道会有各种各样我们自己定义的组件,接下来我们会一步一步去看看这些组件的渲染流程。

Function Component

上边我们讲到了React中关于源生DOM节点的渲染和挂载。现在我们来看看关于Function Component的渲染。

当然我们先来看看关于Function Component渲染的结果。

import React from 'react';
import ReactDOM from 'react-dom';
interface IProps {
  name: string;
}

const MyComponent: React.FC<IProps> = (props) => {
  return (
    <div>
      你好,
      <p>{props.name}</p>
    </div>
  );
};

const element = <MyComponent name="wang.haoyu" />;
console.log(element);

上边这段代码我们创建了一个函数组件,并且使用了这个函数组件赋值给了element。我们来看看它打印出了什么:

image.png

这个时候我们可以看到,相对于普通dom节点。纯函数组件的不同点:

  1. $$typeofSymbol(react.element)表示这个元素节点的类型是一个纯函数组件。
  2. 在普通dom节点中,type类型为对应的标签类型。而当为纯函数组件时。type类型为函数自身

组件的type类型,就是函数自身,这点很重要。

相信看到这里你已经明白element纯函数组件元素应该如何转化成为上边的VDOM对象了。我们再来看看babel的编译:

image.png

可以看到,针对纯函数组件的jsx最终就是编译成为

const element = /*#__PURE__*/React.createElement(MyComponent, {
  name: "wang.haoyu"
});

修改React.createElement方法

来看看我们之前写的React.createElement方法,第一个参数为type,第二个为组件的props,第三个为children...

我们发现我们之前写的React.createElement方法在纯函数组件是不需要任何修改的:

import { transformVNode } from './utils';

const React = {
  createElement: function (type, config, children) {
    const props = {
      ...config,
    };
    if (arguments.length > 3) {
      props.children = Array.prototype.slice
        .call(arguments, 2)
        .map(transformVNode);
    } else {
      props.children = transformVNode(children);
    }
    return {
      type,
      props,
    };
  },
};

export default React;

传入一个纯函数组件,仍然能返回正确的结果(这里先不考虑$$typeof的结果)。返回虚拟DOM的type属性指向它自身,config为传入的props。剩余children作为属性挂载在props.children上。

其实我们平常使用的displayName,defaultProps都是挂载在这个函数自身的属性。看到这里你应该也能明白为什么我们平常需要获取这些属性的时候,需要使用xxx.type.displayName等。

修改ReactDOM.render方法

既然React.createElement方法不需要做任何修改。那么我们就来看看对应的ReactDOM.render方法。

此时我们render方法希望我们传入一个自定义函数组件,ReactDOM也会将我们的自定义组件转化成为真实DOM进行挂载。

我们先来分析分析,经过React.createElement(FunctionCompoent,props,children)。传入的type是一个自身的函数,这个函数返回的是一个JSX对象。

如果这么说你还是不能理解的话,你可以这样理解这段代码:

const MyComponent: React.FC<IProps> = (props) => {
  return (
    <div>
      你好,
      <p>{props.name}</p>
    </div>
  );
};

上边这段JSX代码会babel在编译阶段转化成为这样的代码:

const MyComponent = props => {
  return /*#__PURE__*/React.createElement("div", null, "\u4F60\u597D,", /*#__PURE__*/React.createElement("p", null, props.name));
};

其实你完全可以将MyComponent调用后返回的结果理解成为一个对象(本质上也就是对象),这样的话你会好理解很多:

image.png

注意:返回的是一个React.createElement函数的调用,这个函数调用返回的就是一个虚拟DOM对象。

搞清楚这些,我们来尝试改写ReactDOM.render方法:

/**
 * 把虚拟DOM变成真实DOM插入
 * @param {Object} vDom 虚拟DOM
 * @param {HTMLElement} el 元素
 */

import { REACT_TEXT } from './constant';

// 真正渲染方法
function render(vDom, el) {
  const newDom = createDom(vDom);
  el.appendChild(newDom);
}

// 先不考虑自定义组件
function createDom(vDom) {
  const { type, props } = vDom;
  let dom;
  // 文本节点
  if (type === REACT_TEXT) {
    dom = document.createTextNode(props.content);
  } else if (typeof type === 'function') {
    // 如果是纯函数组件 type是一个函数 并且运行函数会返回一个虚拟DOM对象
    // 通过createDom方法递归将虚拟DOM转化成真实DOM返回
    return mountFunctionComponent(vDom);
  } else {
    dom = document.createElement(type);
  }
  // 更新属性
  if (props) {
    // 更新跟节点Dom属性
    updateProps(dom, {}, props);
    // 处理children 考虑undefined/null 不做任何处理
    // 考虑 children是一个数组 那么就表示他拥有多个儿子
    // 考虑children是一个Object 那么他就只有一个儿子节点
    if (typeof props.children === 'object' && props.children.type) {
      // 单个元素
      render(props.children, dom);
    } else if (Array.isArray(props.children)) {
      // 多个元素
      reconcileRender(props.children, dom);
    }
  }
  // 记录挂载节点
  vDom.__dom = dom;
  return dom;
}

// 挂载函数式组件
function mountFunctionComponent(vDom) {
  const { type, props } = vDom;
  // type(props)执行FunctionComponent函数,返回对应的虚拟DOM对象
  // 通过createDom方法将虚拟DOM转化成真实DOM返回
  return createDom(type(props));
}

// 挂载多个dom元素 React.createElement先不考虑递归
function reconcileRender(vLists, parentDom) {
  for (let node of vLists) {
    render(node, parentDom);
  }
}

/**
 * 把虚拟DOM变成真实DOM插入
 * @param {HTMLElement} dom 元素
 * @param {Object} oldProps 元素本身的props 用于更新这里暂时用不到
 * @param {Object} newProps 元素新的props
 */

function updateProps(dom, oldProps, newProps) {
  // 合并props 暂时没有老的 仅处理新的
  Object.keys(newProps).forEach((key) => {
    if (key === 'children') {
      // 单独处理children
      return;
    }
    if (key === 'style') {
      addStyleToElement(dom, newProps[key]);
    } else if (key === 'content') {
      // 文本不做任何操作
    } else {
      dom[key] = newProps[key];
    }
  });
}

function addStyleToElement(dom, styleObject) {
  Object.keys(styleObject).forEach((key) => {
    const value = styleObject[key];
    dom.style[key] = value;
  });
}

const ReactDOM = {
  render,
};

export default ReactDOM;

可以看到我们在createDOM方法上做了小小的修改,判断如果传入的vDomtype是一个函数的话:

  1. 传入vDomprops,运行这个函数。得到返回的vDom对象。
  2. 拿到vDom对象后,通过之前的createDom方法将vDom转化成真实节点返回。
  3. 此时render方法就可以拿到对应生成的真实DOM对象,从而挂载在DOM元素上。

本质上还是通过递归进行判断,如果是函数那么就运行函数的到返回的vDOM,然后在通过createDomvDom转化为对应的真实DOM挂载。

关于jsx转化成React.crateElement(...)babel在做这一层的转译。后续涉及编译原理的知识在展开去看。

其实从这里也可以看出为什么React中返回的jsx必须要求最外层元素需要一个包裹元素。

ReactDom.render方法接受传入的Element。内层只有一个根节点时,比如

const element = <div>This is <p>me</p>.<p>My name is wang.haoyu</p></div>

编译后:

React.createElement(
  'div',
  null,
  'This is ',
  React.createElement('p', null, 'me'),
  '.',
  React.createElement('p', null, 'My name is wang.haoyu')
);

ReactDOM.render的处理方式为传入一个React虚拟节点。通过该虚拟节点生成真实DOM,然后在一层一层递归它的children,将children的虚拟节点通过createDOM方法生成对应的真实DOM然后在挂载在对应的父节点DOM上。

明确一个思想: ReactDOM.render()方法仅仅支持传入一个VDOM对象和el。他的作用就是将VDOM生成真实DOM挂载在el上。此时如果VDOM存在一些children,那么ReactDOM.render会递归他的children,将children生成的DOM节点挂载在parentDom上。一层一层去挂载。

针对FunctionComponetchildren的渲染

为了加深了解,我们再来走一遍我们自己手写的ReactReactDOM的过程。 当我传入这样一段代码:


function MyComponent(props) {
  return (
    <div>
      <p>hello</p>
      <p>{props.children}</p>
    </div>
  );
}

const element = (
  <MyComponent name="wang.haoyu">
    <div>你好</div>
    <p>hello</p>
  </MyComponent>
);
ReactDOM.render(element)

首先针对于我们刚才的代码,经过Babel处理后会变成这样一段代码

const element1 = React.createElement(
  MyComponent,
  {
    name: 'wang.haoyu',
  },
  React.createElement('div', null, '\u4F60\u597D'),
  React.createElement('p', null, 'hello')
);

接下来会进入我们已经实现的React.createElement会将MyComponent这个函数组件进行转化。会转化成这样样子的对象:

const element = {
  props: {
    name: 'wang.haoyu',
    children: [{
        type: 'div',
        props: {
          children: {
            type: Symbol(text)
            content: '你好'
          }
        }
      },
      {
        type: 'p',
        props: {
          children: {
            type: Symbol(text)
            content: 'hello'
          }
        }
      }
    ]
  },
  type: MyComponent
}

之后我们调用了RenderDOM.render(element,el)进行了渲染。 首先进入createDOM函数中发现他的类型是一个函数组件。那么按照我们的逻辑就会运行这个函数组件,同时传入它的props。此时我们可以清晰的看到type(props)的结构。

调用这个函数组件并且传入对应的props

当我们调用这个函数组件的时候,会返回一个jsx,这一步我们已经轻车熟路了。最终的jsx会被转译成为VDOM对象:

function MyComponent(props) {
  return (
    <div>
      <p>hello</p>
      <p>{props.children}</p>
    </div>
  );
}

转化后的VDOM对象(省略React.crateElement,直接输出VDOM结果):

  1. 第一次进入createDOM方法,发现他是FC类型。传入自身props调用自身FC函数。
  2. 运行完毕函数后,会递归调用createDOM,此时VDOM已经变成了这样的对象。
{
  type: 'div',
  props: {
    children: [
      {
        type: 'p',
        props: {
          children: {
            type: Symbol('text'),
            content: 'hello',
          },
        },
      },
      {
        type: 'p',
        props: {
         // 注意这里的props.children
         // 为了理解 我这里先这样写 props其实就是函数组件调用时传入的那个props
          children: props.children,
        },
      },
    ],
  },
};

真实应该是这样:

{
  type: 'div',
  props: {
    children: [
      {
        type: 'p',
        props: {
          children: {
            type: Symbol('text'),
            content: 'hello',
          },
        },
      },
      {
        type: 'p',
        props: {
          // 本质上这里的内容就相当于 props.children
          // children: props.children
          children: [
            {
              type: 'div',
              props: {
                children: {
                  type: 'Symbol(text)',
                  content: '你好',
                },
              },
            },
            {
              type: 'p',
              props: {
                children: {
                  type: 'props.children',
                  content: 'hello',
                },
              },
            },
          ],
        },
      },
    ],
  },
};

其实简单来说,组件标签内的元素。直接将它看作转译后的对象作为入参传入FunctionComponent中进行调用自身函数,得到返回的jsx从而得到返回的新的VDOM对象。

这里其实并不难,只是有部分绕。如果不是很明白可以尝试自己手写几遍跟着debugger看看。搞懂之后过一段时间再来看看加深记忆就会很清晰了。

写在最后

  1. 之前对于React中关于<MyComponet>你好</Mycomponent>,以现在的逻辑去渲染这段代码还存在疑惑。之后会梳理总结下debugger的详细流程。
  2. ~~梳理完上述流程后,会展开谈一谈React中关于class组件的渲染。

class组件的渲染

class组件初步分析

虽然react17之后强烈推荐使用hooks代替class component,但是短期内react并没有移除类组件的计划。并且一些情况下类组件是必不可少的,而且类组件中涉及react中很多知识。所以接下来我们来看看react中关于class组件的实现。

React内部组件分为源生组件和自定义组件

  • 源生组件经过babel编译后的VDOMtype属性类型是一个字符串,表示当前元素的节点标签。
  • 自定义组件经过编译后type指向自身的函数。

javascript的世界中其实并没有class的概念,针对class也不过是function的语法糖。

我们来看看类组件的编译结果:

image.png

可以清楚的看到,类组件编译后React.createElement(ClassComponent,{ name:"wang.haoyu"})。传入的type(第一个参数),也为类组件自身。(函数)

当然说到这里一些同学会存在疑问了,既然类组件和函数组件type属性都是一个Function。那么如何区分类组件和函数组件呢。

Reactclass组件因为继承自React.component,所以class组件的原型上会存在一个isReactComponent属性。这个属性仅有类组件独有,函数组件是没有的,这就可以区分类组件和函数式组件。

image.png

我们尝试访问这个属性来看看:

image.png

其实ts的类型提示已经告诉我们结果了,区分类组件和函数组件的区别就是类组件的原型上存在属性isReactComponent属性。

class类组件上的isReactComponent值是一个空对象{},仅仅作为标示。

实现class组件的渲染

接下来我们来实现classComponet的渲染流程。

首先,我们先来实现React.Component这个这个父类。

改造React.js文件

我们给React对象下新增一个Component属性:

import { transformVNode } from './utils';
import { Component } from './component';

const React = {
  Component,
  createElement: function (type, config, children) {
    const props = {
      ...config,
    };
    if (arguments.length > 3) {
      props.children = Array.prototype.slice
        .call(arguments, 2)
        .map(transformVNode);
    } else {
      props.children = transformVNode(children);
    }
    return {
      type,
      props,
    };
  },
};

export default React;

接下来我们新建一个Component文件来实现这个Component类:

// component.js
class Component {
  constructor(props) {
    this.props = props;
  }
}

// 实现类组件独有属性
Component.prototype.isReactComponent = {};

export { Component }

接下来我们来改造React.render方法,让他支持class组件的渲染:

/**
 * 把虚拟DOM变成真实DOM插入
 * @param {Object} vDom 虚拟DOM
 * @param {HTMLElement} el 元素
 */

import { REACT_TEXT } from './constant';

// 真正渲染方法
function render(vDom, el) {
  const newDom = createDom(vDom);
  el.appendChild(newDom);
}

// 先不考虑自定义组件
function createDom(vDom) {
  const { type, props } = vDom;
  let dom;
  // 文本节点
  if (type === REACT_TEXT) {
    dom = document.createTextNode(props.content);
  } else if (typeof type === 'function') {
    if (type.prototype.isReactComponent) {
      // 原型上存在isReactComponent属性
      return mountClassComponent(vDom);
    } else {
      // 如果是纯函数组件 type是一个函数 并且运行函数会返回一个虚拟DOM对象
      // createDOM方法本质上就是将虚拟DOM对象转化成为真实DOM返回
      return mountFunctionComponent(vDom);
    }
  } else {
    dom = document.createElement(type);
  }
  // 更新属性
  if (props) {
    // 更新跟节点Dom属性
    updateProps(dom, {}, props);
    // 处理children 考虑undefined/null 不做任何处理
    // 考虑 children是一个数组 那么就表示他拥有多个儿子
    // 考虑children是一个Object 那么他就只有一个儿子节点
    if (typeof props.children === 'object' && props.children.type) {
      // 单个元素
      render(props.children, dom);
    } else if (Array.isArray(props.children)) {
      // 多个元素
      reconcileRender(props.children, dom);
    }
  }
  // 记录挂载节点
  vDom.__dom = dom;
  return dom;
}

// 挂载class组件
function mountClassComponent(vDom) {
  const { type, props } = vDom;
  return createDom(new type(props).render());
}

// 挂载函数式组件
function mountFunctionComponent(vDom) {
  const { type, props } = vDom;
  // type(props)执行FunctionComponent函数,返回对应的虚拟DOM对象
  // 通过createDom方法递归将虚拟DOM转化成真实DOM返回
  return createDom(type(props));
}

// 挂载多个dom元素 React.createElement先不考虑递归
function reconcileRender(vLists, parentDom) {
  for (let node of vLists) {
    render(node, parentDom);
  }
}

/**
 * 把虚拟DOM变成真实DOM插入
 * @param {HTMLElement} dom 元素
 * @param {Object} oldProps 元素本身的props 用于更新这里暂时用不到
 * @param {Object} newProps 元素新的props
 */

function updateProps(dom, oldProps, newProps) {
  // 合并props 暂时没有老的 仅处理新的
  Object.keys(newProps).forEach((key) => {
    if (key === 'children') {
      // 单独处理children
      return;
    }
    if (key === 'style') {
      addStyleToElement(dom, newProps[key]);
    } else if (key === 'content') {
      // 文本不做任何操作
    } else {
      dom[key] = newProps[key];
    }
  });
}

function addStyleToElement(dom, styleObject) {
  Object.keys(styleObject).forEach((key) => {
    const value = styleObject[key];
    dom.style[key] = value;
  });
}

const ReactDOM = {
  render,
};

export default ReactDOM;

其实本质上关于class组件的挂载实现和FunctionComponet组件的实现是类似的。

  1. 首先判断传入的type是否是函数,如果是函数那么无非两种类型。
  2. 接下来判断是否是class组件,因为我们之前已经给父类的prototype上挂载了isReactComponent方法。所以通过子类.prototype.isReactComponent去查找是否是class组件。
  3. 如果是class组件,那么我们需要做的同样是将他的render方法返回的Vdom对象通过createDom方法转化为真实Dom节点来进行挂载。
  4. 把握了核心需要做的事情,接下来就很简单,无非就是createDom(new type(props).render())的到render方法返回的vDom对象,通过createDom去将虚拟DOM转化为真实Dom

这里我们就已经实现了简单的class组件的挂载了。其实万遍不离其宗。本质上就是我们需要通过createDom将传入的vDom对象转化成真实DOM。

核心思想

  1. createDom如果传入的是一个普通节点,那么就直接根据对应type创建标签。
  2. createDom如果传入的是一个函数组件,那么就调用这个函数组件得到它返回的vDom节点,然后在通过createDomvDom渲染成为真实节点。
  3. createDom如果传入的是一个class组件,那么就new Class(props).render()得到返回的vDom对象,然后在将返回的vDom渲染成为真实Dom

无论是FC还是CC这两种组件,内部本质上还是基于普通DOM节点的封装,所以我们只需要递归调用他们直接返回基本的DOM节点之后进行挂载就OK啦~万变不离其宗嘛

写在最后

至此我们已经完成了基础的FunctionComponent,classComponent的渲染。

到这里我们已经了解基础的渲染流程,通过createDom方法将vDom对象递归转变为真实DOM

之后我们会更加深入去了解ClassComponetFunctionCompont,去深入体会React的设计哲学。