MiniReact系列之实现一个简易render(二)

303 阅读5分钟

MiniReact系列之实现一个简易createElement(一)

昨天实现了一个简易版本的createElement,只是虚拟DOM貌似没啥用,今天尝试着将他转化成真实DOM,咋搞呢(⊙_⊙)??????走一步看一步吧。。。毕竟我是个菜逼(菜逼要有菜逼的样子( ̄. ̄))

完整代码git地址:github.com/nocodig/min…

一、开整

文档上 说在提供的container中渲染一个React元素,返回对该组件的一个引用,如果之前已经渲染过React元素,那么会对其进行更新操作。

呃。。好像有点难。。。。和昨天一样我先搞一个乞丐版的,先只考虑渲染新节点的情况吧,并且不考虑元素节点是React组件的情况。

二、乞丐版

render方法

render方法我们传入三个参数

  • virtualDOM:虚拟DOM节点
  • container:挂载的节点容器
  • oldDOM: 上一次渲染的节点数据

oldDOM参数什么时候用,这篇文章中我们先不考虑,先假设不存在。并不会影响理解render方法

render方法。根据官方文档渲染虚拟节点到指定容器上,而且里面会进行虚拟DOM比对

// render.js
import diff from "./diff";

export default function render(virtualDOM, container, oldDOM) {
  // 暴露出外部调用方法,需要考虑两种情况
  // 1. 没有旧的节点,直接调用,挂载新的节点
  // 2. 原来已经挂载节点,需要对新就节点进行比对,然后进行DOM更新
  diff(virtualDOM, container, oldDOM);
}

接下来写下diff方法

diff方法

与render方法一样,接收三个参数

  • virtualDOM:虚拟DOM节点
  • container:挂载的节点容器
  • oldDOM: 上一次渲染的节点数据

由于我是个菜逼,咱们先不考虑存在旧节点的情况,只考虑新节点挂载,所以此时需要在函数判断一下,没有旧节点时,直接调用的mountElement用来挂载新的元素节点

// diff.js
import mountElement from "./mountElement";

export default function diff(virtualDOM, container, oldDOM) {
  if (!oldDOM) {
    // 原来没有旧节点,直接挂载新的节点即可
    mountElement(virtualDOM, container)
  }
  // 存在旧节点,需要对新旧节点进行比对更新,新空着
}

mountElement方法

这个方法主要作用就是解析virtualDOM,但是解析时节点可能是React元素或者是浏览器普通元素,需要做些特殊处理,用来区分是React组件还是普通元素。

mountElement方法接收两个参数

  • virtualDOM:虚拟DOM节点
  • container:挂载的节点容器

React组件我们下次在进行处理,这次先把普通元素解析解决

// mountElement.js
import mountNativeElement from "./mountNativeElement";

export default function mountElement(virtualDOM, container) {
  // 存在两种情况,考虑是nativeDOM,还是ComponentDOM,
  mountNativeElement(virtualDOM, container);
}

mountNativeElement方法

这个方法用来将浏览器普通元素进行解析。该方法接收两个参数:

  • virtualDOM:虚拟DOM节点
  • container:挂载的节点容器

virtualDOM节点类型分为:

  • 文本节点
  • 元素节点

如果是文本节点时,我们要使用createTextNode创建一个文节点,如果是元素节点,使用createElement创建一个元素节点。

virtualDOM上存储了节点所有信息。比如typepropschildren使用virtualDOM.type === 'text'即可判断是否是文本节点,值存储在virtualDOM.props.textContent,元素节点类似,但是元素节点上可能有子节点,需要对virtualDOM.children进行遍历,需要递归调用mountElement,将子节点也生成DOM节点。

为什么需要调用mountElement而不是mountNativeElement,是因为子节点下面可能存在普通的元素节点,也有可能存在React元素

// mountNativeElement.js
import mountElement from './mountElement.js
export default function mountNativeElement(virtualDOM, container) {
  // 考虑此时节点存在文本节点和普通节点
  let newElement = null
  if (virtualDOM.type === "text") {
    newElement = document.createTextNode(virtualDOM.props.textContent);
  } else {
    newElement = document.createElement(virtualDOM.type);
  }

  virtualDOM.children.forEach((child) => {
    // 此时调用mountElement由于不知子几点是否存在reactDOM
    mountElement(child, newElement);
  });

  container.appendChild(newElement);
}

基本解析需要的方法都写完了,看下页面效果

看起来都解析出来了,但是此时点击按钮并不会有任何反应,那是因为我们只是创建了元素,但是没有将事件绑定上去,属性也没有绑定上去,我们还需要给元素节点上把属性加上去

三、乞丐升级版

updateNodeElement方法

这个方法主要是将想DOM节点上增加属性,绑定事件,接收两个参数:

  • newElement:属性、事件绑定的节点
  • virtualDOM:虚拟DOM节点,我们需要的属性都是props属性上

我们这个方法在哪里调用呢??????在mountNativeElement方法中调用就行,我对他改造下:

import mountElement from './mountElement.js

export default function mountNativeElement(virtualDOM, container) {
  // 考虑此时节点存在文本节点和普通节点
  let newElement = null
  if (virtualDOM.type === "text") {
    newElement = document.createTextNode(virtualDOM.props.textContent);
  } else {
    newElement = document.createElement(virtualDOM.type);
 
    // 为元素设置属性🔽就是这里
    updateNodeElement(newElement, virtualDOM)
  }

 ....
}

由于virtualDOM.props属性是对象,我们需要用Object.keys(props)取出属性,并且遍历,遍历要做下面事情:

  • 判断是否是事件,需要使用newElement.addEvenLister进行事件绑定
    • 如何判断是事件,截取前两个字符是否是on
  • 判断属性是否value或者checked,此时不能用setAttribute设置属性,需要用newElement.value = xxx方式赋值
  • children属性不需要设置成属性
  • className需要转化成class

updateNodeElement代码如下:

export default function updateNodeElement(newElement, virtualDOM) {
  const newProps = virtualDOM.props;

  Object.keys(newProps).forEach(propName => {
    const propValue = newProps[propName]

    if (propName.slice(0, 2) === 'on') {
      // 为元素添加事件,事件函数前面都是通on开头
      const eventName = propName.toLowerCase().slice(2)

      newElement.addEventListener(eventName, propValue)
    } else if (propName === 'value' || propValue === 'checked') {
      // 为元素添加value、checked属性,这两个属性不能setAttribute设置
      newElement[propName] = propValue
    } else if (propName !== 'children') {
      // children 不作为属性存在要进行过滤
      if (propName === 'className') {
        // className需要转换成class
        newElement.setAttribute('class', propValue)
      } else {
        newElement.setAttribute(propName, propValue)
      }
    }
  })
}

让我们看下效果,发现样式变了,输入框中也有值了。

点击下按钮,出现了弹窗:

说明乞丐升级版是OK的

结束

简单的实现了一下最简单的render方法,缺失的功能后面博客中还需继续更新这部分的实现,今天实在太困了,写不动了,,,,,身体重要,拒绝内卷。。