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。

相关推荐
_一条咸鱼_22 分钟前
深入解析 Vue API 模块原理:从基础到源码的全方位探究(八)
前端·javascript·面试
_一条咸鱼_2 小时前
深入剖析 Vue 状态管理模块原理(七)
前端·javascript·面试
uhakadotcom2 小时前
一文读懂DSP(需求方平台):程序化广告投放的核心基础与实战案例
后端·面试·github
uhakadotcom2 小时前
拟牛顿算法入门:用简单方法快速找到函数最优解
算法·面试·github
小黑屋的黑小子3 小时前
【数据结构】反射、枚举以及lambda表达式
数据结构·面试·枚举·lambda表达式·反射机制
JiangJiang3 小时前
🚀 Vue人看React useRef:它不只是替代 ref
javascript·react.js·面试
卫崽3 小时前
JavaScript 中的 ?? 与 || 运算符详解
javascript·面试
逆袭的小黄鸭4 小时前
JavaScript 开发必备规范:命名、语法与代码结构指南
前端·javascript·面试
独立开阀者_FwtCoder4 小时前
2025年,真心佩服的十大开源工具
前端·后端·面试
雷渊4 小时前
深度分析Elasticsearch的倒排索引
后端·面试