Skip to content

Latest commit

 

History

History
186 lines (167 loc) · 4.92 KB

File metadata and controls

186 lines (167 loc) · 4.92 KB

JSX 和 ReactDOM

JSX 转为 Virtual DOM

JSX 是 React 的语法糖,结合了 JavaScript 和 XML,在 babel 中,一段 JSX 被转为如下代码:

// JSX
const container = <div className="container">hello, world</div>;
// Babel 编译
var container = /*#__PURE__*/ React.createElement(
  "div",
  {
    className: "container"
  },
  "hello, world"
);

所以一段 JSX 会被转换为对象,也就是虚拟 DOM,下面来实现简单的React.createElement方法,传入的参数有tag, attributes, children, Babel 将 JSX 转换为这些参数,React.createElement是需要将这些参数返回出来即可。

function createElement(tag, attrs, ...children) {
  return {
    tag,
    attrs,
    children
  };
}
const React = {
  createElement
};

render

ReactDOM.render在 Babel 中的编译:

// JSX
ReactDOM.render(
  <div className="container">hello, world</div>,
  document.getElementById("root")
);
// Babel
ReactDOM.render(
  /*#__PURE__*/ React.createElement(
    "div",
    {
      className: "container"
    },
    "hello, world"
  ),
  document.getElementById("root")
);

render有两个参数,第一个参数是个 JSX,在 Babel 中会被转为虚拟 DOM 对象,第二个参数是要插入的 HTMLElement 对象。

/**
 *
 * @param {*} vnode
 * @param {HTMLElement} container
 */
export function render(vnode, container) {
  return container.appendChild(_render(vnode));
}
/**
 *
 * @param {*} vnode
 */
function _render(vnode) {
  if (
    typeof vnode === "undefined" ||
    typeof vnode === "boolean" ||
    vnode === null
  )
    vnode = "";
  if (typeof vnode === "number") vnode = String(vnode);
  // 当vnode不再是虚拟DOM对象,而是String类型时,说明已经递归到了最里层,构造TextNode当作前节点的内容
  if (typeof vnode === "string") {
    const textNode = document.createTextNode(vnode);
    return textNode;
  }
  const dom = document.createElement(vnode.tag);
  if (vnode.attrs) {
    // Notes: 为什么不用for..in而用Object.keys()?
    // 因为前者会有继承链上的属性,后者只有对象本身的属性
    Object.keys(vnode.attrs).forEach(key => {
      const value = vnode.attrs[key];
      // 需要特殊处理,像className->class,事件,style
      setAttribute(dom, key, value);
    });
  }
  // 对虚拟DOM对象的children递归render
  vnode.children.forEach(child => render(child, dom));
  return dom;
}

由于 JSX 语法里,DOM 元素属性与规范不同,class改为className,组件元素的属性是完全自定义的属性,自事件属性名在 React 里有大写,转为 HTML DOM 时也需要转换。。下面是定制的简化setAttribute方法。

/**
 *
 * @param {HTMLElement} dom
 * @param {String} key
 * @param {String|Object} value
 */
function setAttribute(dom, key, value) {
  if (key === "className") {
    key = "class";
  }
  // 事件属性处理
  if (/on\w+/.test(key)) {
    key = key.toLowerCase();
    dom[key] = value || "";
  } else if (key === "style") {
    // 可以接受字符串和对象
    if (!value || typeof value === "string") {
      dom.style.cssText = value || "";
    }
    // 避免是 value 是 null
    else if (value && typeof value === "object") {
      Object.keys(value).forEach(styleAttr => {
        // style={{width: 20}}
        dom.style[styleAttr] =
          typeof value[styleAttr] === "number"
            ? value[styleAttr] + "px"
            : value[styleAttr];
      });
    }
  } else {
    // Notes: 为什么dom[key] = value赋值后还需要 DOM.setAttribute()?
    // HTMLElement元素有attributes和properties两个不同的属性
    // attributes是标签上定义的属性,properties是DOM对象的属性
    // dom[key]指向的是properties,而不是attributes,dom[key] = value的作用:
    // 例如inputEle[checked]只有true和false
    // 如果value是"false",inputEle.getAttribute("checked") = "false"
    // 但inputEle.checked = true,因为赋值时value是有值的,DOM会将value转为Boolean
    if (key !== "class" && key in dom) {
      dom[key] = value || "";
    }
    if (value) {
      dom.setAttribute(key, value);
    } else {
      dom.removeAttribute(key);
    }
  }
}

最后,在每次 render 前要把旧元素内容清空。

const ReactDOM = {
  render: (vnode, container) => {
    container.innerHTML = "";
    return render(vnode, container);
  }
};

调用如下:

import React from "./react";
import ReactDOM from "./reactdom";

// Notes: 为什么看上去没用到React还需要调用呢?
// JSX 语法糖会把“DOM标签”转化成虚拟DOM对象,需要React.createElement返回虚拟DOM对象

function tick() {
  const element = (
    <div>
      <input type="checkbox" checked={false}></input>
      <h1>Hello World</h1>
      <h2>It is {new Date().toLocaleTimeString()}</h2>
    </div>
  );
  ReactDOM.render(element, document.getElementById("root"));
}

setInterval(tick, 1000);