React mini --Virtual dom和jsx的简易实现

参考:

原文:

2017年基于 老 react:medium.com/hexacta-eng...

2019年基于React16.8 hooks机制pomb.us/build-your-...

中文版参考(基于 老 react,实现了fiber,无hooks):

一、vdom的定义和数据结构

怎么用 js 生成这样的页面? /Users/wuyanbiao/Documents/demos/frontend-framework-exercize/legacy-vdom/index.html

vdom 全称 virtual dom,用来声明式的描述页面,现代前端框架很多都基于 vdom。前端框架负责把 vdom 转为对真实 dom 的增删改,也就是 vdom 的渲染。

tips: React-native也使用vdom,不过是转为原生UI的渲染,而非dom。

那么 vdom 是什么样的数据结构?又是怎么渲染的呢?

dom 主要是元素(文本)、属性,

vdom 也是一样,type表示元素类型,props表示属性。props.children比较特别,表示子元素数组,设置属性的时候需要额外处理。

javascript 复制代码
/**判断是否为文本 */
function isTextVdom(vdom) {
    return typeof vdom == 'string' || typeof vdom == 'number';
}
/**判断是否为元素 */
function isElementVdom(vdom) {
    return typeof vdom == 'object' && typeof vdom.type == 'string';
}
// vue 则使用以下数据结构
const vdom = {
   tag:
   props:
   children: 
 }
const vdom = {
    type: 'ul',
    props: {
        className: 'list',
        children: [
            {
                type: 'li',
                props: {
                    className: 'item',
                    style: {
                        background: 'blue',
                        color: '#fff'
                    },
                    onClick: function() {
                        alert(1);
                    },
                    children: [
                        'aaaa'
                    ]
                },
            },
            {
                type: 'li',
                props: {
                    className: 'item',
                    children: [
                        'bbbbddd'
                    ]
                },
            },
            {
                type: 'li',
                props: {
                    className: 'item',
                    children: [
                         2
                    ]
                },
            }
        ]
    },
};

React官方vdom数据结构:github.com/facebook/re...

其他数据结构定义思路:

(1)将children 打平,在type\props\children一层

css 复制代码
// 其他人可能这么实现https://github.com/QuarkGluonPlasma/frontend-framework-exercize
const vdom = {
    type: 'ul',
    props: {
        className: 'list',
    },
    children : [] // 注意children不在props里面了!!! 
 }

(2)文本使用对象表示

比如carter将被表示为:

css 复制代码
{
    type: "span",
    props: {
      children: [
          {
            type: "TEXT_ELEMENT",
            props: {
              nodeValue: 'carter',
              children: [],
            },
          }
      ],
    },
  }

二、vdom如何渲染到浏览器

javascript 复制代码
// function mountWithParent(el, parent = null) {
//     return parent ? parent.appendChild(el) : el; // 返回值都是el本身(appendChild也是返回el本身)
// }
const render = (vdom, parent = null) => {
    const mount = parent ? (el => parent.appendChild(el)) : (el => el); // 可以换成上面👆🏻注释的函数mountWithParent,节省内存,不用每次都创建函数mount
    if (isTextVdom(vdom)) {
        return mount(document.createTextNode(vdom));
    } else if (isElementVdom(vdom)) {
        /***** 生成节点;挂载子节点和递归子节点;设置属性 */

        for (const child of vdom.children) {
            render(child, dom); // 递归挂载所有的元素和文本
        }
        for (const prop in vdom.props) {
            setAttribute(dom, prop, vdom.props[prop]);
        }
        const dom = mount(document.createElement(vdom.type)); 
        return dom;
    } else {
        throw new Error(`Invalid VDOM: ${vdom}.`);
    }
};

// 入口
render(vdom, document.getElementById('root'));

实现细节(一) 如何设置属性?

stackoverflow.com/questions/6...

(1)使用properties

有效的属性才会校验通过。

无效属性无法正确被设置。

ini 复制代码
function render(element, parentDom) {
  const { type, props } = element;
  const dom = document.createElement(type);

  const isListener = name => name.startsWith("on");
  Object.keys(props).filter(isListener).forEach(name => {
    const eventType = name.toLowerCase().substring(2);
    dom.addEventListener(eventType, props[name]);
  });

  const isAttribute = name => !isListener(name) && name != "children";
  Object.keys(props).filter(isAttribute).forEach(name => {
    dom[name] = props[name]; 
  });

  const childElements = props.children || [];
  childElements.forEach(childElement => render(childElement, dom));

  parentDom.appendChild(dom);
}

(2) 使用attributes

vbnet 复制代码
const setAttribute = (dom, key, value) => {
    if (isEventListenerAttr(key, value)) {
        const eventType = key.slice(2).toLowerCase(); // 这里不能直接使用el[key.toLowerCase()],类似onClick,因为onClick只能存一个事件,后来的事件会覆盖之前的https://stackoverflow.com/questions/6348494/addeventlistener-vs-onclick
        dom.addEventListener(eventType, value);
    } else if (isStyleAttr(key, value)) {
        Object.assign(dom.style, value);
    } else if (isPlainAttr(key, value)) {
        dom. setAttribute (key, value); 
    }
}

实现细节(二)如何提升性能,减少dom 绘制带来的layout 消耗?

将创建好的dom树,最后append到root节点上

内存中的dom node节点,比如const dom = document.createElement('span'); 不会触发渲染layout。

三、jsx 编译成 vdom

上面的 vdom 改为 jsx 来写就是这样的:

ini 复制代码
const jsx = 
<ul className="list">
    <li className="item" style={{ background: 'blue', color: 'pink' }} onClick={() => alert(2)}>aaa</li>
    <li className="item">bbb</li>
</ul>

明显比直接写 vdom 紧凑了不少,但是需要做一次babel编译。

编译产物:

php 复制代码
const jsx = createElement("ul", {
  className: "list"
}, createElement("li", {
  className: "item",
  style: {
    background: 'blue',
    color: 'pink'
  },
  onClick: () => alert(2)
}, "aaa"), createElement("li", {
  className: "item"
}, "bbb"));

render(jsx, document.getElementById('root'));

为啥不直接是 vdom对象,而是函数的执行返回对象呢?

因为函数是可以动态变化的,这样会有一次执行的过程,可以放入一些动态逻辑。createElement函数里可以自定义增强一些功能表现。

ini 复制代码
/*** 在这里自定义createElement,而不使用React.createElement */
const createElement = (type, props, ...children) => {
  // 可以额外执行一些逻辑,增加新的vdom属性,兼容地递增 增加新的功能。
  if (props === null) props = {};
    return {
        type,
        props: {
            ...props,
            children,
        }
    };
};

四、声明式 vs 命令式

先说结论:

  • 声明式代码,开发体验更好,更容易维护。

  • 命令式代码比声明式代码运行得更快。

(1)JQuery和原生dom操作是命令式的。

(2)React和Vue都是声明式框架。

由于声明式代码中,只需要找出需要更新的vdom,进行dom的更新。找出差异的时间消耗B,如果比A数量级小,那么基本就可以认为A =~ A + B

ops/s表示每秒的操作数Operation per second。

相关推荐
奔跑的蜗牛ing7 小时前
CentOS Nginx 安装与静态文件服务器配置指南
前端·面试·架构
KaifuZeng10 小时前
通信与接口协议面试五、RS422
嵌入式硬件·面试·通信与接口
SamDeepThinking11 小时前
组合还是继承?我在项目里主要看这两个判断
java·后端·面试
wear工程师11 小时前
setState 到底是不是异步的?这题从 React 16 问到 18,标准答案早就变了
react.js·面试
SamDeepThinking14 小时前
为什么很多人学了Java,还是没真正理解类和对象?
java·后端·面试
阿新聊ai14 小时前
Claude Code 为什么总不懂你的项目?缺一张「项目地图」
前端·后端·面试
用户8524950718415 小时前
Promise 全解析:从三状态到 Promise.all
面试
嘟嘟071715 小时前
Node.js spawn 实现迷你 Cursor 自动化终端|子进程完整解析
前端·后端·面试
weedsfly15 小时前
纯函数与副作用——前端开发中最容易被忽视的质量保证
前端·javascript·面试
whaledown16 小时前
互联网大厂Java求职面试三轮提问详解(涵盖Spring Boot、微服务、Kafka等核心技术)
java·jvm·数据库·spring boot·微服务·面试·kafka