React原理(一)VirtualDom 到渲染UI

991 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第22天,点击查看活动详情 

VirtualDom介绍

virtualDom实际就是个object类型的字典{} ,字典中包含typepropschildrentextContentkey来描述的是dom元素的。

如下所示真是的dom

<div className={'container'}>
    <div>React</div>
</div>

描述dom的VirtualDom,则为


{
    type: "div",
    props: { className: "container" },
    children: [
        {
            type: "div",
            props: null,
            children: [
                {
                    type: "text",
                    props: {
                        textContent: "React"
                    }
                }
            ]
        }
      ]
 }

VirtualDom生成过程

我们可以先看babel在线转换

<div name= '33'>
  <div>{funcDoc}</div>
</div>

function fucDoc() {
  return <div>function component</div>
}

可以看到上述代码经过babel转换后会调用React.createElement api

"use strict";

/*#__PURE__*/
React.createElement("div", {
  name: "33"
}, /*#__PURE__*/React.createElement("div", null, funcDoc));

function fucDoc() {
  return /*#__PURE__*/React.createElement("div", null, "function component");
}

而调用React.createElement 的api后返回的刚好是我们想要的VirtualDOm 如下代码所示返回的刚好是VirtualDOm对象。

/**
 *
 * @param type 标签类型
 * @param props 标签的属性
 * @param children  子元素
 * @returns {{children: *[], type, props}}
 */
export default function createElement (type, props, ...children) {
     const childElements = ...省略处理逻辑...
    return {
        type,
        props,
        childElements
    }
}

实现tinyReact

环境配置

统一配置

我们需要告诉babel使用我们自己写的TinyReact,需要配置babel

在.babelrc文件中将React改为TinyReact

{
  "presets": [
    "@babel/preset-env",
    [
      "@babel/preset-react",
      {
        "pragma": "TinyReact.createElement" //React改为TinyReact
      }
    ]
  ]
}

通过以上配置告诉babel调用TinyReact.createElement来转换jsx文件

单个文件配置

除了统一配置外也可以在单个文件中添加 /** @jsx TinyReact.createElement */注释,来实现单个文件配置

配置后babel转换的代码

生成VirtualDom

上一步操作会让babel调用我们自己写的createElement方法

//index.js文件

const virtualDOM = (
    <div className="container">
        <h1 test = "text">我是标题一</h1>
        <h2 test = "text">我是标题二</h2>
        <div>
            div嵌套一下 
            <div>里面的div</div>
        </div>
        {shouldHide && <div>条件渲染1</div>}
        {!shouldHide && <div>隐藏我</div>}
        <button onClick={() => alert("你好")}>点我</button>
        2, 3
        <input type="text" value="13" />
    </div>
)
console.log(virtualDOM)

如果createElement直接返回打印结果如下

image.png

可以看到文本被放在children中直接返回了,显然不对的,另外条件渲染!shouldHide为false被直接返回了,我们想要的是隐藏标签的效果

文本节点对应virtualDom为

{
  type: 'text',
  props: {
    textContent: '我是文本'
  }
}
/**
 *
 * @param type 标签类型
 * @param props 标签的属性
 * @param children  子元素
 * @returns {{children: *[], type, props}}
 */
export default function createElement (type, props, ...children) {
    const childElements = [].concat(...children).reduce((result,child) => {
        if (child !== false && child !== true && child !== null) {
            if (child instanceof Object) {
                result.push(child)
            } else {
                // 文本节点 手动调用 createElement
                result.push(createElement("text", {textContent: child}))
            }
        }
         return result
        }, [])
    return {
        type,
        props: Object.assign({ children: childElements }, props),
        children: childElements
    }
}

image.png

实现render方法渲染UI

//index.js

......
TinyReact.render(<virtualDOM />, root)

//TinyReact/index.js

import createElement from "./createElement";
import render from "./render";

export default {
    render,
    createElement
}

//TinyReact/render.js

import mountElement from "./mountElement";
export default function render(virtualDOM, container, oldDom) {
    mountElement(virtualDOM, container)
}

//TinyReact/mountElement

import {isFunction} from "./utils";
import mountComponent from "./mountComponent";
import mountNativeElement from "./mountNativeElement";

export default function mountElement(virtualDOM, container) {
    if (isFunction(virtualDOM)) {
        // 判断是否为组件,组件的逻辑稍后实现
       // mountComponent(virtualDOM, container, oldDOM)
    } else {
        mountNativeElement(virtualDOM, container, oldDOM)
    }
}

//TinyReact/mountNativeElement

import createDOMElement from "./createDOMElement"

export default function mountNativeElement(virtualDOM, container) {
    const newElement = createDOMElement(virtualDOM)
    container.appendChild(newElement)
}

//TinyReact/createDOMElement

export default function updateElementNode(element, virtualDOM) {
  const newProps = virtualDOM.props
  Object.keys(newProps).forEach(propName => {
    const newPropsValue = newProps[propName]
    if (propName.slice(0, 2) === "on") {
      const eventName = propName.toLowerCase().slice(2)
      element.addEventListener(eventName, newPropsValue)
    } else if (propName === "value" || propName === "checked") {
      element[propName] = newPropsValue
    } else if (propName !== "children") {
      if (propName === "className") {
        element.setAttribute("class", newPropsValue)
      } else {
        element.setAttribute(propName, newPropsValue)
      }
    }
  })
}