手写React(一):React与JSX

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

什么是React

在React的官网首页,居中写着这么一句话:React 用于构建用户界面的 JavaScript 库
由此,我们可以知道以下两个信息:

  • react是一个Js库而不是框架(有关库与框架的区别,不在本文讨论范围)。
  • 它的核心专注于构建用户界面。

而与我们熟知的lodashjQuery等其他Js工具库不同,React专注于构建用户视图。

什么是JSX

在React的核心概念中,这样介绍:JSX是JavaScript的语法扩展
来看如下变量声明:

const element = <h1 style={{ color: 'red' }}>Hello, world!</h1>;
复制代码

观察以上代码可知:jsx是一种将jshtml混合的语法,是一种将组件的结构、数据甚至样式都聚合在一起的写法。

React与JSX有什么关系

我们常说:JSX 是 React.createElement 的语法糖。为了让大家对这句话有一个更清晰的认识,我们使用 Babel 来转译以上JSX的声明,如下图所示:

截屏2021-09-29 下午5.27.37.png

删除注释冗余后,得到如下结果:

const element = React.createElement("h1", {
  style: {
    color: 'red'
  }
}, "Hello, world!");
复制代码

由上可知:Babel 会把 JSX 转译成一个名为 React.createElement 的函数调用。
在执行React.createElement后,会返回一个React元素,所谓的 React元素 其本质就是一个描述页面视图的js对象。
为了使这句话更加直观,我们在控制台中打印element元素,如下图所示:

截屏2021-09-29 下午6.18.40.png

截屏2021-09-29 下午6.19.44.png

在删除冗余之后,我们得到以下结果:

// 注意:这是简化过的结构

{
  type: 'h1',
  props: {
    children: 'Hello, world!',
    style: {
        color: 'red'
    }
  }
};
复制代码

在拿到上述React元素后,使用ReactDOM.render函数,将React元素插入页面:

ReactDOM.render(element,document.getElementById('root'));
复制代码

代码实现

经过上述分析,我们可以梳理出以下渲染流程:

  • JSX经过Babel编译后,转化为React.createElement的函数调用。
  • 调用React.createElement函数,生成一个React元素(描述视图结构的js对象)
  • 调用ReactDOM.renderReact.createElement返回的React元素转化为真实的DOM插入容器。

因此我们需要写两个方法:

  • React.createElement:调用后返回一个React元素(描述页面的js对象)
  • ReactDOM.render:将返回的React元素转化为真实DOM,并插入页面

我们使用脚手架新建一个React项目,删除冗余代码后,新建reactreact-dom文件夹,如下图所示:

截屏2021-09-29 下午9.58.23.png

需在启动命令处禁用最新的jsx转换,因为在React17Babel编译后的结果为jsx函数,而不是React.createElement函数

在package.json的启动命令处,添加 cross-env DISABLE_NEW_JSX_TRANSFORM=true 参数

截屏2021-09-29 下午10.04.01.png

完成项目基本搭建后,在index.js里写入如下代码:

// 引入自己的 react
import React from './react';

// 引入自己的 react-dom
import ReactDOM from './react-dom';

// 声明JSX变量
const element = <h1 style={{ color: 'red' }}>Hello, world!</h1>;

// 将元素插入页面
ReactDOM.render(
  element,
  document.getElementById('root')
);
复制代码

React.createElement代码实现如下:

function createElement(type, config, children){
  let key;
  let ref;
  if(config){
    key = config.key;
    ref = config.ref;
  }
  const props = { ...config }
  if(arguments.length > 3){
    props.children = Array.prototype.slice.call(arguments, 2).map(warpToVdom)
  }else{
    props.children = warpToVdom(children)
  }
  return { type, key, ref, props }
}
复制代码

为了后续便于比较子节点,我们使用warpToVdom函数对纯文本子节点进行包装,添加了REACT_TEXT属性:

function warpToVdom(element){
  return ['number', 'string'].includes(typeof element) ? { type: 'REACT_TEXT', props: { content: element } } : element;
}
复制代码

ReactDOM.render代码实现如下:

function render(vdom, container){
  // 抽离挂载逻辑,便于后续复用
  mount(vdom, container)
}

function mount(vdom, parentDom){
  const newDom = createDom(vdom)
  if(newDom){
    parentDom.appendChild(newDom); // 插入dom
  }
}

function createDom(vdom){
  if(!vdom) return null;
  const { type, props, ref } = vdom;
  
  let dom;
  if(type === 'REACT_TEXT'){ // 处理文本节点
    dom = document.createTextNode(props.content)
  }else{
    dom = document.createElement(type)
  }

  if(props){ // 更新属性
    updateProps(dom, {}, props)
    if(props.children){ // 处理子节点
      const children = props.children;
      if(typeof children === 'object' && children.type){ // 说明是一个react元素
        mount(children, dom)
      }else if(Array.isArray(children)){ // 有多个子节点
        reconcileChildren(children, dom)
      }
    }
  }

  // 将真实dom挂载vdom便于dom-diff时查找 
  vdom.dom = dom;
  return dom;
}

// 多个子节点循环挂载
function reconcileChildren(childrenVdom, parentDom){
  childrenVdom.forEach(child => mount(child, parentDom))
}

// 属性更新方法
 function updateProps(dom, oldProps, newProps){
  for(let attr in newProps){
    if(attr === 'children'){ // 子节点单独处理
      continue
    }else if(attr.startsWith('on')){ // 处理事件
      dom[attr.toLocaleLowerCase()] = newProps[attr];
    }else if(attr === 'style'){ // 处理样式
      const styleObj = newProps[attr]
      for(let styleName in styleObj){
        dom.style[styleName] = styleObj[styleName]
      }
    }else{ // 处理其它属性 class id...
      dom[attr] = newProps[attr]
    }
  }
}
复制代码

页面展示效果如下:

截屏2021-10-13 上午11.37.22.png

结语

我们从概念用法入手,梳理了react的工作流程,并简单做了代码实现。当然,以上代码并不能展现react的优势,在下一篇文章中,我们再继续了解它的更多概念及优化。

猜你喜欢

转载自juejin.im/post/7018452769279328264