react 学习(一) 实现简版虚拟 dom 和挂载

楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试的也都能答出来。但对于 react 只是有一定的了解,没有真实的学习过实现,虽然之前也看过一些文章,但是只停留在表面,因为别人这么写了,也就下意识的认为是这样。本次正好配合工作的契机,我们从零开始学习一下,使用的话呢就简单一过,相信大家也都用过或者看完官网也都了解了。如果您是大佬,欢迎批评指正;如果您是初级选手,希望能够一起学习。

初始化项目

我们借助脚手架实现开发环境,内部使用的库用自己开发的。

  1. npx create-react-app react-dome1 (当然也可以全局安装脚手架)

public 目录只留下 index.htmlsrc 目录下只留下 index.js

  1. 修改 scripts 命令

我们需要使用旧的转换方式,这样我们可以自己实现 createElement 方法

// cross-env 需要自己安装
scripts": {
  "start": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts start",
  "build": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts build",
  "test": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts test",
  "eject": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts eject"
},

react 17 引入了新的 jsx 转换特性,因为之前的开发,即使页面中未直接使用 React,但是也要引入,这是因为 babel 在转译之后会触发 React.createElement,如果不引入会报错,但是引入了可能也会触发 eslint 的报错,引入但是未使用。新特性可以单独使用 JSX 而无需引入 React

新特性一些好处

  1. 使用全新的转换,你可以单独使用 JSX 而无需引入 React
  2. 根据你的配置,JSX 的编译输出可能会略微改善 bundle 的大小。
  3. 它将减少你需要学习 React 概念的数量,以备未来之需

之前的转换方式

import React from 'react';

function App() {
  return <h1>Hello World</h1>;
}
====================================
import React from 'react';

function App() {
  return React.createElement('h1', null, 'Hello world');
}

新特性转换方式

function App() {
  return <h1>Hello World</h1>;
}
==================================
// 由编译器引入(禁止自己引入!)
import {jsx as _jsx} from 'react/jsx-runtime';

function App() {
  return _jsx('h1', { children: 'Hello world' });
}

实现 React.createElement

我们先看下原生 createElement 的返回结果

// src/index.js
import React from 'react'
const jsx = <h1 className='title' style={{color: 'red'}}>hello</h1>
console.log(jsx)

我们看到返回了对象,几个重要属性,$$typeof, props, type。我们实现下自己的

createElement 函数。

定义类型常量

// src/constants.js

// react 内的元素都是这个类型
export const REACT_ELEMENT = Symbol("react.element");
// react 文本类型
export const REACT_TEXT = Symbol("react.text");

实现 createElement

// src/react.js
// 这三个参数是 babel 解析完,调用React.createElement 传入的,从第三个参数开始都是儿子
function createElement(type, config, children) {
  if(config) {
    // 这里可写可不写,就是为了简化下我们自己写的,只把必要的返回,没用的参数越少越清晰嘛
    delete config.__source
    delete config.__self
  }
  const props = {...config}
  if (arguments.length > 3) {
    // 有多个儿子
    props.chidlren = Array.prototype.slice.call(arguments, 2)
  } else if (argument.length === 3) {
    // 只有一个子,直接赋值
    props.children = children
  }
  
  return {
    $$typeof: REACT_ELEMENT,
    type,
    props
  }
}

const React = {
  createElement
}
export default React

这里也可以 ...children 形式,判断只要判断 children 长度就可以了,但是属于 es6 的用法,我们按照源码实现

实现 toVdom 辅助函数

我们这里还要进行一下处理,因为如果是文本类型的话,直接就是字符串了,没有类型这种标识了,所以我们要对 children 进行一下包裹,也为了后面的 diff

// src/utils.js

// 统一规范,方便  diff
export function toVdom(element) {
  return typeof element === "string" || typeof element === "number"
    ? { // 字符串包裹
        $$typeof: REACT_ELEMENT,
        type: REACT_TEXT,
        props: element,
      }
    : element;
}

修改 createElement 函数,包裹儿子节点

...
props.children = Array.prototype.slice.call(arguments, 2).map(toVdom);
...
props.children = toVdom(children);

调用我们自己的实现,我们可以得到如下结果

页面挂载

我们引入 react-dom,看下原生渲染

import React from "react";
import ReactDOM from "react-dom";

let jsx = (
  <h1 className="title" style={{ color: "red", backgroundColor: "pink" }}>
    hello
    <span>111</span>
  </h1>
);
ReactDOM.render(jsx, document.getElementById("root"));

实现 reactDOM.render

大家可以按我写的第几步阅读,基本都做了注视

// 做了两件事
// 1. 虚拟dom变真实dom
// 2. 挂载
function render(vdom, container) {            //。 第二步
    //1 
    const newDOM = createDOM(vdom) // 不同功能写在不同函数里,清晰          // 第三步
    //2
    container.appendChild(newDOM)
}

// 创建真实 dom
function createDOM(vdom) {
  let {type, props} = vdom // 我们知道虚拟dom就是我们生成的那个对象
  let dom // 最后要返回的
  
  if (type === REACT_TEXT) {
    // 如果是个文本
    dom = document.createTextNode(props)
  } else {
    // 标签节点
    dom = document.createElement(type)
  }
    
  // 需要对props 中的 style 和 children 和其他进行处理
  if(props) {
    // 单独处理属性
    updateProps(dom, {}, props)         // 第四步
    // 单独处理 chidlren
    if(props.chidlren && typeof props.children === 'object' && props.chidlren.$$typeof) {
      // 文本
      render(props.chidlren, dom)
    } else if (Array.isArray(props.children)) {
      // 子为数组,把子挂载到当前的父 dom
      reconcileChildren(props.children, dom)            // 第五步
    } 
  }
  
  return dom
}

// 子虚拟节点,父真实节点
function reconcileChildren(chidlrenVdom, parentDom) {
  // 循环递归处理, 算法题里非二叉树的多子树节点,也是 for 循环遍历处理
  for (let i = 0; i < childrenVdom.length; i++) {
    render(childrenVdom[i], parentDOM);
  }
}


// 对 dom 进行新属性赋值,旧属性没有的删除, vue中也是类型的操作,遍历新属性,旧属性
function updateProps(dom, oldProps, newProps) {
  for(let key in newProps) {
    if (key === 'children') {
      continue // 单独处理
    } else if (key === 'style') {
      let styleObj = newProps[key]
      for(let attr in styleObj) {
        dom.style[attr] = styleObj[attr] 
      }
    } else {
      dom[key] = newProps[key]
    }
  }
  // 老的有,新的没有 删除
  for(let const key in oldProps) {
    if (!newProps.hasOwnProperty(key)) {
      delete dom[key]
    }
  }
}

// 根据调用,返回的一定是对象       第一步
const ReactDOM = {
  render
}
export default ReactDOm

在入口文件使用我们自己的方法

// src/index.js
import React from "./react";
import ReactDOM from "./react-dom";

可以看到,也实现了渲染

本篇就介绍到这里,我们了解了虚拟 dom 的对象形式,了解了如果挂载到页面上,下一节我们学习下类组件和函数组件的实现,如果有不对,欢迎指正!

原文地址:https://cloud.tencent.com/developer/article/1972437

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom