筹备工作

须要用到的模板文件的仓库地址

1. JSX

先看看jsx语法,做了什么事件 babel.js

能够看到,这些jsx语法,通过babel转译后,最终调用了React.createElement,其须要三个参数type, props, children。其返回值就是virtual DOM对象。也就是说,咱们能够应用babel将咱们的jsx代码,转换成虚构DOM, 然而咱们须要实现一个本人的createElement办法

2. 我的项目配置

查看仓库地址,能够间接获取到模板文件。这里次要介绍一下咱们的.babelrc中如何配置,帮忙咱们解析jsx代码,并主动的调用咱们本人写的createElement办法
能够看看babel官网 是如何配置react的。

presets中配置@babel/prset-react,咱们将应用他来转换咱们代码中的jsx代码。想想下面的代码,咱们写的函数式组件,或者jsx代码,都被转换成了React.createElement代码,所以咱们借助babel就能够实现咱们自定义的createElement性能

{  "presets": [    "@babel/preset-env",    [      "@babel/preset-react",      {        "pragma": "MyReact.createElement" //默认的pragma就是 React.createElement,也就是说,咱们要实现一个咱们的MyReact.createElement, 那么就须要在这里写成MyReact.createElement (only in classic runtime)      }    ]  ]}

3. Virtual DOM

3.1 什么是Virtual DOM

应用javascript对象来形容实在的dom对象,其构造个别就是这样

vdom = {    type: '',    props: {} // 属性对象    children: [] // 子元素,子组件}

3.2 创立Virtual DOM

3.2.1 实现一个createElement办法

在咱们的模板文件中, 曾经应用webpack配置好了代码的入口文件,装置好依赖,而后将我的项目运行起来,这时候浏览器啥都没有产生。解析jsx的工作也都有babel帮咱们实现了
如果你呈现了这种状况,那么请自行更改webpack中devserver的端口号

这是咱们的我的项目目录构造:


同时,也能够看看咱们我的项目的目录构造, 这里我曾经增加了一个createElement.js的文件,咱们将在这个文件中,实现将jsx代码,转换为virtual DOM对象。

下面咱们提到过,React.createElement会接管三个参数type, props, children,而后会主动的将jsx代码转换成上面这个类型,因而咱们须要做的就是提供这么一个办法,接管这三个参数,而后在将其组装成咱们想要的对象。

vdom = {    type: '',    props: {} // 属性对象    children: [] // 子元素,子组件}
  1. 首先在MyReact文件夹下创立createDOMElement.js,他的构造咱们下面提到过,接管三个参数,并且返回一个vdom的对象

    export default function createElement(type, props, ...children) {  return { type, props, children  }}
  2. 创立好了createElement办法,那么咱们须要往外裸露,因而在MyReact/index.js中,咱们将其裸露进去

    // MyReact/index.jsimport createElement from './createElement'export default {  createElement,}
  3. 而后咱们在入口文件, 引入咱们的MyReact,同时写一段jsx的代码,看看能不能合乎咱们的预期

    // index.jsimport MyReact from "./MyReact"// 依照react的应用办法,这里咱们先引入咱们自定义的MyReact 此处的MyReact会将jsx语法 通过调用MyReact.createElement(),而后返回咱们所须要的VDOM// 这个是在.babelrc配置的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)

    看看打印后果,是不是咱们的预期

    bingo,的确是咱们想要的。这里大家能够看到

  • children中,有些节点是一个boolean,还有就是咱们可能节点就是个null, 不须要转换
  • children中,有些节点间接就是文本,须要转换文文本节点
  • props中,须要能够拜访children节点
    上述两个非凡状况都没有被正确的转换成vDOM,因而咱们接下来须要做的就是,对children节点,在进行一次createElement的操作。

    3.2.2 改良createElement办法

    下面咱们说到,咱们须要递归的调用createElement办法去生成vDOM。依据下面三个问题,咱们能够作如下改良

    export default function createElement(type, props, ...children) {// 1.循环children对象进行对象转换,如果是对象,那么在调用一次createElement办法,将其转换为虚构dom,否则间接返回,因为他就是一般节点. const childrenElements = [].concat(...children).reduce((result, child) => {  // 2.节点中还有表达式, 不能渲染 null节点 boolean类型的节点, 因而咱们这里用的reduce,而不是用map  if (child !== true && child !== false && child !== null) {    // child曾经是对象了,那么间接放进result中    if (child instanceof Object) {      result.push(child)    } else {      // 如果他是文本节点,则间接转换为为本节点      result.push(createElement("text", { textContent: child }))    }  }  return result}, [])// 3. props 能够拜访children节点,return {  type,  props: Object.assign({ children: childrenElements }, props), // hebing Props  children: childrenElements}}

当初再看看咱们的输入, 能够看到,之前咱们children中有false的,以及纯文本的节点,都被正确的解决了,到这里,咱们的createElement就完结了

3.3 实现render办法

3.3.1 render办法

首先在MyReact文件夹下创立render.js
在render中,咱们还要一个diff办法,diff算法就是保障视图只更新变动的局部,须要将新旧dom进行比照(vDOM, oldDOM),而后更新更改局部的dom(container)。咱们先写一个diff办法,理论的算法,咱们留在前面来补充。

// MyReact/render.jsimport diff from "./diff"export default function render(vDOM, container, oldDOM) {  // diff算法  diff(vDOM, container, oldDOM)}

而后,咱们在MyReact/index.js将render办法进行导出。

import createElement from './createElement'import render from './render'export default {  createElement,  render}
3.3.2 diff办法

刚刚的剖析,咱们能够通晓,这个diff算法是须要三个参数的,newDom, container, oldDom, 在这里,咱们须要做的是就是比照新旧dom,这里,咱们须要一个办法,用来创立元素,于是咱们当初又须要一个 mountElement办法,于是创立文件mountElement.js,用于创立元素。

// MyReact/diff.jsimport mountElement from "./mountElement"export default function diff(vDOM, container, oldDOM) {  // 判断oldDOM是否存在  if (!oldDOM) {    // 创立元素    mountElement(vDOM, container)  }}
3.3.3 mountElement办法

咱们的元素,须要辨别原生dom元素还是组件。组件分为class组件,以及函数组件。在这咱们先把原生dom进行渲染。

  • mountElement

    • mountNativeElement
    • mountComponentElement

      • class组件
      • 函数组件
// MyReact/mountElement.js    export default function mountElement(vDOM, container) {  // 此处须要辨别原生dom元素还是组件,如何辨别? 这个逻辑咱们前面再补充  mountNativeElement(vDOM, container)}
3.3.4 mountNativeElement办法

在这个办法中,咱们须要将virtual DOM转成真正的DOM节点,在这里,咱们借助一个办法,来创立实在DOM元素,而后再将其append到容器中。

// MyReact/mountNativeElement.jsimport createDOMElement from "./createDOMElement"/** * 渲染vdom到指定节点 * @param {*} vDOM * @param {*} container */export default function mountNativeElement(vDOM, container) {  let newElement= createDOMElement(vDOM)  container.appendChild(newElement)}

上面咱们来实现这个createDOMElement办法,因为后续咱们也会用到,所以把它作为一个公共的函数,不便其余中央应用。
这个办法,咱们须要做上面的几件事件。

  1. 将传进来的vDOM创立成html元素
  2. 创立html元素 又分为两种状况, 纯文本节点,还是元素节点
  3. 递归创立子节点的html元素

    // MyReact/createDOMElement.jsimport mountElement from "./mountElement"/** * 创立虚构dom * @param {*} vDOM * @returns */export default function createDOMElement(vDOM) {  let newElement = null  // 1. 渲染文本节点, 依据咱们之前解决的,纯文本节点,通过text去标记, 值就是props中的textContent  if (vDOM.type === 'text') { newElement = document.createTextNode(vDOM.props.textContent)  } else { // 2.渲染元素节点 newElement = document.createElement(vDOM.type) // type 就是html元素类型 div input p这些标签等等 // 留神,这里咱们只渲染了节点,并没有将props的属性,放在html标签上,这个咱们前面在进行  }  // 以上步骤仅仅只是创立了根节点,还须要递归创立子节点  vDOM.children.forEach(child => { // 将其搁置在父节点上, 因为不确定以后子节点是组件还是一般vDOM,因而咱们再次调用mountElement办法,以后的节点容器,就是newElement mountElement(child, newElement)  })  return newElement}

    代码曾经就绪,咱们去浏览器看看有没有什么变动, 这个时候,你的浏览器应该长这样了。而后咱们再来剖析下,咱们还缺什么?

3.3.5 更新节点属性的办法(updateNodeElement)

咱们当初曾经实现了将jsx的代码,渲染到了页面上。然而当初看看咱们的虚构DOM的构造。跟咱们的预期还短少了上面的货色

  1. className没有被渲染为 class
  2. data-test type value等这些原生属性没有被增加到对应的标签上
  3. button的响应事件

接下来,咱们就去实现这个updateNodeElement办法。
还是先创立MyReact/updateNodeElement.js这个文件。思考一个问题,咱们什么时候调用这个办法来更新node的属性呢?
在下面 3.3.4中,咱们在进行更新节点的步骤,因而更新node节点的属性,也须要在那里进行
而后能够必定的是,咱们须要两个参数,一个是容器container,一个是咱们的虚构DOM,这样能力确定一个残缺的element.
接下来的工作就是要把props属性,顺次的赋值给html。回顾一下,如何设置html的属性?咱们应用 element.setAttribute('prop', value)来实现.
明确了如何更新html下面的属性,接下来来剖析下,咱们要解决哪些属性,和事件
首先咱们须要遍历以后vDOM的props属性,依据键值确定应用何种设置属性的形式

  1. 事件绑定:咱们绑定事件都是以on结尾,相似这样 onClick;
  2. value checked这样的属性值,就不能应用setAttribute办法了,想想咱们应用原生dom的时候,对于输入框这样的value值,咱们是间接用的input.value设置输入框的值;
  3. children属性,这是咱们之前手动增加的节点属性,因而,咱们要把children给他剔除;
  4. className属性,这个须要咱们将其改为 class,剩下的属性,就能够间接应用键来设置了
    代码实现如下:

    // MyReact/updateNodeElement.jsexport default function updateNodeElement (newElement, vDOM) {  const { props } = vDOM  // 遍历vdom上的key,获取每个prop的值,  Object.keys(props).forEach(key => { const currentProp = props[key] // 如果是以on结尾的,那么就认为是事件属性,因而咱们须要给他注册一个事件 onClick -> click if (key.startsWith('on')) {   // 因为事件都是驼峰命名的,因而,咱们须要将其转换为小写,而后取最初事件名称   const eventName = key.toLowerCase().slice(2)   // 为以后元素增加事件处理函数   newElement.addEventListener(eventName, currentProp) } else if (key === 'value' || key === 'checked') {   // input 中的属性值   newElement[key] = currentProp } else if (key !== 'children') {   // 抛开children属性, 因为这个是他的子节点, 这里须要辨别className和其余属性   newElement.setAttribute(key === 'className' ? 'class' : key, currentProp) }  })}

    接下来,咱们找到createDOMElement.js文件,咱们须要在渲染元素节点后,更新他的属性值

    // MyReact/createDOMElement.jsimport mountElement from "./mountElement"/** * 创立虚构dom * @param {*} vDOM * @returns */export default function createDOMElement(vDOM) {  let newElement = null  // 1. 渲染文本节点, 依据咱们之前解决的,纯文本节点,通过text去标记, 值就是props中的textContent  if (vDOM.type === 'text') { newElement = document.createTextNode(vDOM.props.textContent)  } else { // 2.渲染元素节点 newElement = document.createElement(vDOM.type) // type 就是html元素类型 div input p这些标签等等 // 更新dom元素的属性,事件等等 updateNodeElement(newElement, vDOM)  }  // 以上步骤仅仅只是创立了根节点,还须要递归创立子节点  vDOM.children.forEach(child => { // 将其搁置在父节点上, 因为不确定以后子节点是组件还是一般vDOM,因而咱们再次调用mountElement办法,以后的节点容器,就是newElement mountElement(child, newElement)  })  return newElement}

    至此,咱们曾经实现了属性设置,当初回到浏览器看看,咱们的后果,class属性被正确的加载了元素下面,其余属性,以及事件响应,也都做好了。

阶段一实现!

阶段一小结

  1. jsx语法在babel加持下,能够转换成vDOM,咱们应用 babel-preset-react,并配置.babelrc,能够让咱们实现自定义的createElement办法。而后将jsx转换成虚构DOM.
  2. 咱们通过createElement办法生成的虚构DOM对象,通过diff算法(本文未实现),而后进行dom的更新
  3. 虚构dom对象须要咱们对所有的节点进行实在dom的转换。
  4. 创立节点咱们须要应用 element.createElement(type)创立文本节点element.createElement(type), 设置属性,咱们须要用到 element.setAttribute(key, value)这个办法。
  5. 节点更新须要辨别html节点,组件节点。组件节点又须要辨别 class组件以及函数组件

未完待续
能够看到,咱们仅仅实现了jsx代码可能被正确的渲染到页面中,咱们还有很多工作未做,比方上面的这些,后续的代码更新都放在这里了。源码

  • 组件渲染函数组件、类组件
  • 组件渲染中 props的解决
  • dom元素更新时的vDOM比照,删除节点
  • setState办法
  • 实现ref属性获取dom对象数组
  • key属性的节点标记与比照