vlambda博客
学习文章列表

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

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

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

初始化项目

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

  1. npx create-react-app react-dome1 (当然也可以全局安装脚手架) public 目录只留下 index.htmlsrc 目录下只留下 index.js
  2. 修改 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)

react 学习(一) 实现简版虚拟 dom 和挂载我们看到返回了对象,几个重要属性,$$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 和挂载

页面挂载

我们引入 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 的对象形式,了解了如果挂载到页面上,下一节我们学习下类组件和函数组件的实现,如果有不对,欢迎指正!