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

| 2019-05-17

初始化项目
我们借助脚手架实现开发环境,内部使用的库用自己开发的。
 
npx create-react-app react-dome1 (当然也可以全局安装脚手架)
public 目录只留下 index.html,src 目录下只留下 index.js
 
修改 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。
 
新特性一些好处
使用全新的转换,你可以单独使用 JSX 而无需引入 React。
根据你的配置,JSX 的编译输出可能会略微改善 bundle 的大小。
它将减少你需要学习 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";

可以看到,实现了渲染









编辑:航网科技 来源:腾讯云 本文版权归原作者所有 转载请注明出处

在线客服

微信扫一扫咨询客服


全国免费服务热线
0755-36300002

返回顶部