React学习第三天---Virtual DOM 及 Diff 算法(实现VirtualDOM)(二)

·  阅读 1601
React学习第三天---Virtual DOM 及 Diff 算法(实现VirtualDOM)(二)

这是我参与更文挑战的第8天,活动详情查看: 更文挑战

tinyReact项目: https://github.com/zelixag/tiny-react-learn/tree/master/tiny-react

今天我们来学习怎么创建一个Virtual DOM对象,和将virtualDOM对象怎么转化成一个realDOM对象

创建 VirtualDOM 对象

通过上篇文章我们已经知道 VirtualDOM 是由 JSX 转化而来,JSX会被转化为 React.createElement 方法的调用,在调用createElement方法时会传入元素的类型,元素的属性,以及元素的子元素,createElement 方法的返回值为构建好的Virtual DOM对象。所以接下来我们回去实现createElement方法。

我们最开始也说了,我们通过学习是想自己实现一个tiny-React,所以JSX转化的时候应该不是将其转化为React.createElement了,而是当编译JSX的时候babel转化成tinyReact.createElement才能达到我们的目的,那如何告诉babel这件事情呢,这怎么解决呢?

  1. 第一种解决方法:在每个含有JSX的页面顶端加一个注释

/** @jsx TinyReact.createElement */这样Babel在编译的时候就会将React对象且换成我们想要的TinyReact的对象。不过虽然可行但是操作过于麻烦,不太方便。

image.png

  1. 第二种方法:就是在我们tiny-React项目的根目录下添加一个.babelrc文件,在文件中指明当react编译时使用TinyReact.createElement进行编译,这样我们就可以再一个地方配置,所有JSX搜这样转化
// .babelrc
{
  "presets": [
    "@babel/preset-env",
    [
      "babel/preset-react",
      {
        "pragma": "TinyReact.createElement"
      }
    ]
  ]
}
复制代码

我们完成第一步,将JSX进行编译的时候将React.createElement替换我们自己的TinyReact.createElement,那如何实现createElement呢?

如何实现createElement

createElement方法要根据传入的参数返回一个virtualDOM对象,对象当中要有type属性表示节点类型,要有props属性表示节点的属性,要有children表示子节点,刚好这些信息都通过参数传递了过来,我们只需要通过传递过来的参数构建virtualDOM对象即可。

在TinyReact文件夹下创建createElement.js文件,我们来创建createElement方法 image.png

// createElement.js
export default function createElement (type, props, ...children) {
  return {
    type,
    props,
    children
  }
}
// TinyReact/index.js文件

import createElement from "./createElement"

export default {
  createElement
}
复制代码

使用我们编写的TinyReact.createElement进行编译JSX

// src/index.js
import TinyReact from "./TinyReact"

const virtualDOM = (
  <div className="container">
    <h1>你好 Tiny React</h1>
    <h2 data-test="test">(编码必杀技)</h2>
    <div>
      嵌套1 <div>嵌套 1.1</div>
    </div>
    <h3>(观察: 这个将会被改变)</h3>
    {2 == 1 && <div>如果2和1相等渲染当前内容</div>}
    {2 == 2 && <div>2</div>}
    <span>这是一段内容</span>
    <button onClick={() => alert("你好")}>点击我</button>
    <h3>这个将会被删除</h3>
    2, 3
    <input type="text" value="13" />
  </div>
)

console.log(virtualDOM)
复制代码

浏览器显示转化的virtualDOM为:

image.png

可以看出在这个对象当中有props属性,type属性,children属性,到目前为止我们已经将一个最基本virtualDOM对象创建好了。

查看我们做的整个过程是,我们提前准备一段JSX代码,然后这段代码会将JSX里面的每个元素转化为Tiny.createElement方法,就会执行我们自己编写的createElement方法,从而输出virtualDOM对象。

目前我们生成的virtualDOM还是有一些问题:

image.png

  1. 问题一: 和文本节点相关,我们可以看出我们的virtualDOM的文本节点是以文本字符串的形式存在VirtualDOM当中的,这明显不符合我们的要求。我们的要求是即使是文本节点,也要以节点对象的形式表示出来。

正确表示:

{type: 'text', props: {textContent: 'Hello'}}

那我们继续优化createElement方法,我们将children属性进行遍历,判断这个子节点如果是对象不做处理,如果不是对象我们就认为是一个文本节点,是文本节点我们就去手动调用createElement方法将文本转化为一个节点对象。

export default function createElement (type, props, ...children) {
  const childElement = [].concat(...children).map(child => {
  // 是对象直接返回,不是就是文本节点
    if(child instanceof Object) {
      return child
    } else {
    // 手动调用createElement
      return createElement("text", {textContent: child});
    }
  })
  return {
    type,
    props,
    children: childElement
  }
}
复制代码

这样我们就成功解决第一个问题将文本节点转化成一个文本节点对象

image.png 2. 问题二: 在JSX中有js表达式他的值呢是true或者false,根据我们之前学习了解到,virtualDOM中不展示true, flase ,null这三个值的,我们应该将这三个值清除

image.png 转的virtualDOM对象

image.png 实现代码如下:

export default function createElement (type, props, ...children) {
  // 如果说是刨除某些节点,我们就不能使用map了,我们可以使用reduce
  const childElement = [].concat(...children).reduce((result, child) => {
  // 过滤掉 true child null
    if(child !== true && child !== false && child !== null)  {
      if(child instanceof Object) {
        result.push(child)
      } else {
        result.push(createElement("text", {textContent: child}));
      }
    }
    return result;
  }, [])
  return {
    type,
    props,
    children: childElement
  }
}

复制代码

查看结果:true,false, null节点都不存在了

image.png

  1. 问题三: 回顾一下,当我们在React中使用组件的时候,是不是可以通过组件的props.children属性来拿到这个组件的子节点,但是在我们现在创建的virtualDOM当中,props当中是没有children的,所以还需要把props放到props当中。如何放置呢,很简单

使用Object.assign({children: childrenElements}, props)即可,完成children属性的添加


export default function createElement (type, props, ...children) {
  const childElements = [].concat(...children).reduce((result, child) => {
  ···
  return {
    type,
    props: Object.assign({children: childrenElements}, props)// 添加children属性,
    children: childElements
  }
}
复制代码

image.png

将普通virtualDOM对象转化为真实DOM对象

要将普通virtualDOM对象转化为真实DOM对象,我们需要实现一个方法,那就是render方法。我们可以照着createElement做一些准备工作。

  1. 在文件夹TinyReact创建render.js文件,创建同名方法render,并在同级目录下文件index中暴露出去
import createElement from "./createElement"
import render from "./render"

export default {
  createElement,
  render
复制代码
  1. src/index.js中调用render函数,render函数需要一个真实dom挂载点在 src/index.html添加标签<div id="root"></div>

import TinyReact from "./TinyReact"

const root = document.getElementById("root") // 获取挂载节点

const virtualDOM = (
  <div className="container">
  ···
  </div>
)
TinyReact.render(virtualDOM, root) // 调用render方法
console.log(virtualDOM)
复制代码

好!!!准备工作已完成我们来实现render方法,接收两个参数第一个参数virtualDOM,第二个参数container,第三个参数oldDOM。oldDOM在diff中使用,在第一次挂载的时候暂时使用不上。我们可以声明diff文件并创建diff方法。在没有oldDOM对象的时候我们直接将virtualDOm对象进行转化,我们这里还是要注意一个问题这个virtualDOM是个普通的virtualDOM对象还是组件形式的VirtualDOM对象,什么意思呢,就是说我们可以像图中直接是JSX,也可能是函数类型或者对象类型的组件JSX。 image.png

这时候我们需要调用另外一个方法叫mountElement方法来处理这种情况。接收两个参数virtualDOM, container, 我们暂时只处理普通virtualDOM对象,创建一个mountNativeElement.js 声明同名函数mountNativeElement来处理普通虚拟DOM转化为真实DOM对象。

那在这里我们怎么转化呢? - 首先我们查看节点类型是什么,是元素节点呢还是文本节点,如果是元素节点我们就应该创建一个该类型的元素,如果是文本节点我们就去创建文本。

// mountNativeElement.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当中,接下来我们只需要递归遍历children创建子节点元素即可

import mountElement from "./mountElement"
export default function mountNativeElement (virtualDOM, container)  {
  let newElement = null
  ···
  // 递归创建子节点
  virtualDOM.children.forEach((child) => {
    mountElement(child, newElement);
  })

  container.appendChild(newElement);
}
复制代码

这样我们就完成了一个初步的普通VirtualDOM的转化:

image.png 这次新加的文件和方法 image.png

总结:今天我们学习了怎么创建一个virtualDOM对象,将createElement传进来的参数创建一个基本virtualDOM对象,记住要将文本节点也需要转化成一个虚拟DOM对象的格式{type: 'text', props: {textContent: 'Hello'}},根据我们第一天使用react时候注意到我们应该将JSX里面含有false, true, null刨除掉,而且我们在react中props还有一个children属性我们也要加上,创建好一个virtualDOM之后

我们开始将普通的VirtualDOM转化成真实DOM,首先准备工作在src/index.html中添加<div id="root"></div>为挂载点,然后在TinyReact/render.js文件中创建render方法,这个render方法是给框架使用者使用的,我们会在src/index.js中调用render函数, render方法接收三个参数 virtualDOM,container(挂载容器),oldDOM(用于diff算法)。render内部还需要调用其他方法,调用了diff方法,接收三个参数virtualDOM,container,oldDOM显示不表。我们处理了oldDOM不存在的情况,这时候我们分别处理调用mountElement,这个方法会处理两种情况是组件virtualDOM还是普通virtualDOM,在 mountElement当中我们会调用mountNativeElement,这个方法就是用来处理普通virtualDOM,在这个方法里面创建真实DOM的元素节点和文本节点。节点里面的子节点通过递归创建真实DOM,创建完之后添加到容器container里面。

今天学到这里,第三天我们将会学习为DOM对象添加属性,组件渲染,敬请期待!!!

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改