跳至内容

Jixun's Blog 填坑还是开坑,这是个好问题。

一个简易的类 React.createElement 实现

今天突然就讨论到如何快速的建立一个元素,于是又糊了一遍简单的类 createElement 函数…

var h = (function () {
  function deref(fn) {
    return Function.call.bind(fn);
  }

  var slice = deref(Array.prototype.slice);

  // Lodash code starts here
  var MAX_SAFE_INTEGER = 9007199254740991;

  function isObject(value) {
    var type = typeof value;
    return value != null && (type == 'object' || type == 'function');
  }

  function isLength(value) {
    return typeof value == 'number' && value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
  }

  function isArrayLike(value) {
    return value != null && isLength(value.length) && !isFunction(value);
  }

  function isObjectLike(value) {
    return value != null && typeof value == 'object';
  }
  // Lodash code ends here

  function isFunction(value) {
    return value instanceof Function;
  }

  function makeArray(v) {
    return isArrayLike(v) && typeof v !== 'string' ? slice(v) : [v];
  }

  function isNode(el) {
    return el instanceof Node;
  }

  function isObjectLikeNotArray(value) {
    return isObjectLike(value) && !isArrayLike(value);
  }

  /**
   * 深度对象合并
   * @param {Object} src 需要扩展的对象。
   * @param {Object} [...ext] 待扩展的属性。
   */
  function merge(src) {
    slice(arguments, 1).forEach(function (ext) {
      if (isObjectLikeNotArray(ext)) {
        Object.keys(ext).forEach(function (key) {
          var value = ext[key];
          if (isObjectLikeNotArray(value)) {
            if (!src[key]) {
              src[key] = {};
            }
            merge(src[key], value);
          } else {
            src[key] = value;
          }
        });
      }
    });

    return src;
  }

  function appendChildren(container, children) {
    children.forEach(function (children) {
      makeArray(children).forEach(function (child) {
        if (child || child === '') {
          container.appendChild(isNode(child) ? child : document.createTextNode(child));
        }
      });
    });
    return container;
  }

  /**
   * 建立一个 HTML 元素
   * @param {String|Function} tag 元素标签,或传入 Stateless 组件函数。
   * @param {Object.<String.String|Bool|Number>} [attrs = {}] 元素属性集合。
   * @param {Array.<String|Node>|String|Node} [...children] 子元素集合。
   */
  function h(tag, attrs) {
    var children = slice(arguments, 2);

    // Stateless 组件建立
    if (isFunction(tag)) {
      return tag(Object.assign({ children }, attrs));
    }

    var el = merge(document.createElement(tag), attrs);
    return appendChildren(el, children);
  }

  h.Fragment = function Fragment({ children }) {
    return appendChildren(document.createDocumentFragment(), children);
  };

  return h;
})();

/**
 * 将建立的元素挂载到一个 HTML 节点 (会先暴力清空节点内容)。
 * @param {HTMLElement} mnt 挂载点。
 * @param {Node} node HTML 元素或节点。
 */
function mount(mnt, node) {
  mnt.innerHTML = '';
  mnt.appendChild(node);
}

简单用例 #

简单的使用例子。部分使用到 JSX 特性写法的需要 Babel + React 插件进行预处理(或等价预处理方案)。

需要预处理 #

/** @jsx h */
var app = (
  <section>
    <h1>Test: <code>test</code></h1>
    <article className="post">
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc quis ornare velit.</p>
      <p>Nullam interdum, lorem non volutpat auctor, quam ante tempus massa, eget dignissim ligula erat non arcu.</p>
    </article>
  </section>
)

mount(document.getElementById("app"), app);

不需要预处理 #

var app = h(
  'section',
  null,
  h('h1', null, 'Test: ', h('code', null, 'test')),
  h(
    'article',
    { className: 'post' },
    h('p', null, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc quis ornare velit.'),
    h(
      'p',
      null,
      'Nullam interdum, lorem non volutpat auctor, quam ante tempus massa, eget dignissim ligula erat non arcu.',
    ),
  ),
);

mount(document.getElementById('app'), app);

Stateless 组件 #

/** @jsx h */
(function (root) {
  function UnsafeHtml({ html }) {
    var div = (<i />);
    div.innerHTML = html;
    return div.childNodes;
  }

  function ExternalLink({ className, href, children }) {
    return (
      <a className={className} href={href} target="_blank">
        {children}
      </a>
    );
  }

  function Author({ author, uri }) {
    var AuthorTag = uri ? ExternalLink : "i";
    return (
      <AuthorTag className="author" href={uri}>
        {author || "无名氏"}
      </AuthorTag>
    );
  }

  function Comment({ website, author, uri, text }) {
    return (
      <div className="comment">
        <p>
          <Author className="author" author={author} uri={website} />
          (来源: <ExternalLink href={`https://jixun.moe` + uri}>{uri}</ExternalLink>)
        </p>
        <div className="body">
          <UnsafeHtml html={text} />
        </div>
      </div>
    );
  }

  function App({ comments }) {
    return (
      <div className="comments">
        {comments.map(comment => (
          <Comment {...comment} />
        ))}
      </div>
    );
  }

  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/latest");
  xhr.onload = xhr.onerror = function () {
    var data;
    try {
      data = JSON.parse(xhr.responseText);
      mount(root, <App {...data} />);
    } catch (error) {
      root.textContent = "获取评论出错。";
    }
  }
  xhr.send();
})(document.getElementById("app"));

URL 高亮渲染 (Fragment + Stateless) #

function Port({ port }) {
  if (port !== '') {
    return h('span', { className: 'port' }, ':' + port);
  }
}

function Domain({ domain }) {
  /** @type {string[]} */
  const ltdParts = domain.split('.');
  let rootDomainSegments = -1;

  // 如果后两个部分不超过 5 字符,则认为这是一个合法的二级域名
  // 如 co.jp、co.uk
  if (ltdParts.length > 2 && (ltdParts.at(-2) + ltdParts.at(-1)).length <= 5) {
    rootDomainSegments = -3;
  } else if (ltdParts.length > 1) {
    rootDomainSegments = -2;
  }

  const subDomain = ltdParts.slice(0, rootDomainSegments).join('.');
  const rootDomain = ltdParts.slice(rootDomainSegments).join('.');
  return h(
    h.Fragment,
    null,
    h('span', null, subDomain),
    subDomain && '.',
    h('span', { style: { color: '#000' } }, rootDomain),
  );
}

function Credential({ username, password }) {
  return h(
    h.Fragment,
    null,
    h('span', { style: { color: 'red' } }, username),
    password && h(h.Fragment, null, password && ':', h('span', { style: { color: 'blue' } }, password)),
    (username || password) && '@',
  );
}

function UrlRender({ url }) {
  return h(
    'span',
    { style: { color: '#777' } },
    h('span', null, url.protocol),
    '//',
    h(Credential, { username: url.username, password: url.password }),
    h(Domain, { domain: url.hostname }),
    h(Port, { port: url.port }),
    h('span', null, url.pathname),
    h('span', null, url.search),
    h('span', null, url.hash),
  );
}

const url = new URL('https://jixun:[email protected]/path?query#hash');
mount(document.querySelector('#root'), h(UrlRender, { url }));

更新日志 #

  • 2018.04.13: 加入了类 React Stateless 组件 支持。
  • 2023.09.02:
    • 修正了组件为 falsy 内容时的渲染逻辑
    • 加入了 Fragment 组件支持,使用 h(h.Fragment, null, ...)

知识共享许可协议 本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。

评论区