React实现-节点创建和渲染

logo-og

这一篇主要来实现React的节点渲染部分。该篇需要用到介绍和准备中的内容。

Element概念

首先引出我们的第一个概念element,它是如下形式:

{
  type: 'div',
  props: {
    className: 'container',
    children: ['hello world']
  }
}
复制代码

暂时可以先把这个当做React中虚拟节点的概念,后面会有一些变化,这里先看做是虚拟节点。上节也提到过babel-plugin-transform-react-jsx 会帮助我帮将JSX转换成以下形式:

// 转换前的jsx
<div className="container">hello world</div>

// 转换后
React.createElement("div",  { className: "container" }, "hello world");
复制代码

函数createElement的第一个参数字符串标签名(此处先忽略组件的情况),第二个参数是它的所接受到的属性,从第三个参数开始,后面都是它的子节点。

createElement

理解了这些下面我们来实现我们自己的createElement,函数参数与上面一致,返回值是一个“虚拟节点”。

function createElement(type, initProps, ...args) {
  const props = Object.assign({}, initProps);
  const hasChildren = args.length > 0;
	
	// 这里不直接使用args,而使用`[].concat(...args)`是为了扁平化数组
	// 将[1, [2], [3]]转为[1, 2, 3]
  const rawChildren = hasChildren ? [].concat(...args) : [];
	
  // 将children属性赋值给props
  props.children = children;
  return { type, props };
}
复制代码

在这个函数中,还有一种子节点是文本节点的情况,像我们上面写的那样。它跟其他节点不同,其他节点都可以通过React.createElement函数执行后,返回一个element。文本节点是一个字符串,为了后面能够减少if判断,这里我们需要把字符串的形式统一为虚拟节点的形式,下面来写一个createTextElement函数:

function createTextElement(text) {
  return {
    type: 'TEXT ELEMENT',
    props: { nodeValue: text }
  }
}
复制代码

将其类型规定为TEXT ELEMENT的形式,同时将值放在nodeValue里面,方便后面直接给文本节点赋值。有了createTextElement函数,我们需要改造下我们的createElement函数:

function createElement(type, initProps, ...args) {
  const props = Object.assign({}, initProps);
  const hasChildren = args.length > 0;
  const rawChildren = hasChildren ? args : [];
	
  // 过滤null和false,不做任何渲染,同时将文本节点进行转换
  const children = rawChildren
    .filter(child => child !== null && child !== false)
    .map(child => child instanceof Object ? child : createTextElement(child));
  props.children = children;
  return { type, props };
}
复制代码

createElement函数到这里完成了。

render

下一步我们要开始渲染我们的节点,就像我们使用ReactDOM.render那样,我们的渲染函数render接收两个参数,第一个是我们要渲染的element,第二个是我们要挂载的节点。注意后面所有的命名element都会使用xxxElement的形式,dom节点都会使用xxxDom的形式。

function render(element, parentDom) {
  const { type, props } = element;
  // 是文本节点则创建文本节点,这里创建一个空的文本节点,后面利用nodeValue直接给该节点赋值
  const isTextNode = type === 'TEXT ELEMENT';
  const childElements = props.children || [];
  const childDom = isTextNode
    ? document.createTextNode('')
    : document.createElement(type);

  const isEvent = name => name.startsWith('on');
  const isAttribute = name => !isEvent(name) && name !== 'children';

  // 绑定事件
  Object.keys(props).filter(isEvent).forEach(name => {
    const eventName = name.toLowerCase.substring(2);
    childDom.addEventListener(eventName, props[name]);
  });

  // 添加属性
  Object.keys(props).filter(isAttribute).forEach(name => {
    childDom[name] = props[name];
  });

  // 递归渲染
  childElements.forEach(childElement => {
    render(childElement, childDom);
  });

  // 挂载到父节点
  parentDom.appendChild(childDom);
}
复制代码

我们首先创建了要渲染的节点。当为文本节点时,我们使用创建了一个空节点,随后在添加属性的步骤,通过childDom['nodeValue'] = name的形式给文本节点赋值。所有onXxx的属性都绑定为事件,其他有效属性都直接赋值给childDom节点。然后对子节点进行递归渲染,最后挂载到父节点。整个渲染过程到这里就完成了。

我们可以使用我们实现的函数来渲染一个界面了,这是Codepen上的演示地址。我们成功渲染了界面,但是我们忽略了组件的情况,下一篇我们来实现组件和setState。

这是github原文地址。接下来我会持续更新,这是github原文地址,欢迎star,欢迎watch。

实现React系列列表:

猜你喜欢

转载自juejin.im/post/5b57d642e51d455d6825cf5b
今日推荐