vlambda博客
学习文章列表

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

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

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

初始化项目

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

  1. npx create-react-app react-dome1 (当然也可以全局安装脚手架) public 目录只留下 index.html, src 目录下只留下 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 的对象形式,了解了如果挂载到页面上,下一节我们学习下类组件和函数组件的实现,如果有不对,欢迎指正!