关于javascript:记实现一个minireact

42次阅读

共计 7909 个字符,预计需要花费 20 分钟才能阅读完成。

筹备工作

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

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.js
    import createElement from './createElement'
    export default {createElement,}
  3. 而后咱们在入口文件, 引入咱们的 MyReact, 同时写一段jsx 的代码,看看能不能合乎咱们的预期

    // index.js
    
    import 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.js

import 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.js
import 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.js
import 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.js
    import 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.js
    export 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.js
    import 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 属性的节点标记与比照

正文完
 0