react源码解析(一) 手写render函数

545 阅读6分钟

很久以来感觉只要提到react源码就觉得高深莫测,一直安静的住在一个叫node_modules的国度里,她就像你朝思暮想但又不敢开口的女孩,明眸皓齿,楚楚动人,你很想了解她,喜欢会让人胆怯,这种感觉很微妙,但是想要收获爱情,第一步是走到跟前,跟她说:我可以认识你吗?

女孩

勇气是第一步,我们来认识一下吧😏

  • 首先看下react源码是如何将dom渲染到页面中的
  • 根据源码我们自己来手动实现一个render函数

react源码中的render函数

文件地址:https://github.com/facebook/react/blob/master/packages/react-dom/src/client/ReactDOMLegacy.js

我们在这个文件中可以看到我们熟悉的render函数了

export function render(
  element: React$Element<any>,
  container: Container,
  callback: ?Function,
) {
  invariant(
    isValidContainer(container),
    'Target container is not a DOM element.',
  );
  // 源码中出现 __DEV__是对开发模式的判断,这里我们不需要管他
  // if (__DEV__) {
  //   const isModernRoot =
  //     isContainerMarkedAsRoot(container) &&
  //     container._reactRootContainer === undefined;
  //   if (isModernRoot) {
  //     console.error(
  //       'You are calling ReactDOM.render() on a container that was previously ' +
  //         'passed to ReactDOM.createRoot(). This is not supported. ' +
  //         'Did you mean to call root.render(element)?',
  //     );
  //   }
  // }
  return legacyRenderSubtreeIntoContainer(
    null,
    element,
    container,
    false,
    callback,
  );
}

去掉注释之后,render函数就只剩下了短短几行,开头是flow规则校验,对flow不了解的同学,可以类比ts,这里不展开讨论,最后只是使用legacyRenderSubtreeIntoContainer处理我们的参数,那我们来看下这个函数到底有什么神通。

不要停留太久,无需弄懂源码的每一行是什么意思,只要知道他在做什么即可

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: Container,
  forceHydrate: boolean,
  callback: ?Function,
) {
  let root: RootType = (container._reactRootContainer: any);
  let fiberRoot;
  if (!root) {
    // Initial mount
    // 初始化渲染,只是单纯的渲染阶段
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    fiberRoot = root._internalRoot;
    // 这里是判断接受的是组件类型,将虚拟dom转成真实dom
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Initial mount should not be batched.
    // 初始化函数不需要进行batchedUpdates批处理
    unbatchedUpdates(() => {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Update
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  // 将dom挂在到页面的父节点上,不再具体展开
  return getPublicRootInstance(fiberRoot);
}

我们可以看到,在这个函数中进行了几步处理

  1. 判断组件类型,处理不同逻辑
  2. 将虚拟dom处理成真实dom
  3. 将生成的dom挂载到传入的父元素上

那我们来实现一个ReactDom.render函数吧~

jsx是如何挂载到页面上的呢?

有一道经常被问的面试题:请问jsx是什么?为什么使用jsx?

:我们知道render函数会返回jsx,而jsx只是react.createElement的语法糖。 至于为什么使用,官方大大已经给出了回答:

我们看下使用jsx被编译成原生js代码的效果

很神奇但也仅此而已

终于啰嗦完可以安静写代码了,我们借助create-react-app创建一个叫learn-react的项目(启动)create-react-app中文文档

我们再src下新建一个文件夹Mreact,里面新建两个文件,react-dom.js、Component.js(后面传class组件时要用到) 新建目录结构如下(只是新增一个Mreact文件夹,不需要改动其他项目文件)

我们修改index.js 文件

代码里默认vnode为虚拟dom,node为真实dom

// import React, { cloneElement } from 'react';
// import ReactDom from 'react-dom';

import ReactDOM from './Mreact/react-dom'
import Component from './Mreact/Component'
import './index.css'

const jsx = (
   <div className="content">
     <h1>手写react render component组件</h1>
     <a href="www.baidu.com">明非</a>
   </div>
 )
 ReactDOM.render(jsx, document.getElementById("root"))

再react-dom.js文件中我们打印下,我们打印下,我们传入的jsx,会被解析成什么

function render(vnode, container) {
  console.log('vnode', vnode)
 
}
export default {render}; //不要忘记export

控制台打印

这便是传说中的虚拟domprops中保存了子节点,type展示父元素的标签类型,到这里我们就已经拿到了要操作的所有素材,还记得我们源码中的思路吗,将虚拟dom转成真实dom,挂载到父元素之上。

我们来创建一个生成node函数,生成一个标签函数,并完善render函数

function render(vnode, container) {
  console.log('vnode', vnode)
  // vnode => node
  const node = createNode(vnode)
  
  // node => container
  container.appendChild(node)
}

// 生成node节点
function createNode(vnode) {
  let node;
  const {type} = vnode
  node = updateHostComponent(vnode);
  return node;
}

// 原生标签节点
function updateHostComponent(vnode) {
  const {type, props} = vnode;
  const node  = document.createElement(type);
  return node
}

这个时候屏幕还是雪白一片,但是我们审查元素惊喜的发现,div已经挂载到root下了!

我们接下来将文本添加到标签里,在createNode时,我们使用type来判断标签是否为文本标签,修改react-dom.js中这几个函数

// 生成node节点,判断节点类型,拼装dom树
function createNode(vnode) {
  let node;
  const {type} = vnode
  node = updateHostComponent(vnode);
  if (typeof type === 'string') {
    node = updateHostComponent(vnode);
  } else {
    node = updateTextComponent(vnode)
  }
  return node;
}

// 原生标签节点
function updateHostComponent(vnode) {
  const {type, props} = vnode;
  const node  = document.createElement(type);

  updateNode(node, props)
  return node
}

// 文本标签
function updateTextComponent(vnode) {
  const {type} = vnode
  const node = document.createTextNode(vnode);
  return node
}

function reconcileChildren(parentNode, children) {
  // 这里如果由多个子元素使用数组包裹(react16版本之后可以使用数组,不必用唯一的父元素来包裹)
  const newChildren = Array.isArray(children) ? children : [children]
  for(let i = 0; i < newChildren.length; i++) {
    let child = newChildren[i]
    // node 插入到parentNode
    render(child, parentNode)
  }
}

我们发现,页面已经有数据了,荒芜的页面上已经有了活力,但是我们属性还没有加载出来,我们的className和a标签的href属性没有了,还记得上面我们打印出来的虚拟dom吗,我们要的属性,就保存在props中,只要过滤掉children就好了(一定要打印出虚拟dom展开看一下

// 原生标签节点
function updateHostComponent(vnode) {
  const {type, props} = vnode;
  const node  = document.createElement(type);
  updateNode(node, props)
  reconcileChildren(node, props.children)
  return node
}

// 更新属性
function updateNode(node, nextValue) {
  Object.keys(nextValue)
  .filter(k=> k!== 'children')
  .forEach((k) => (node[k] = nextValue[k]))
}

// 我们在index.css中写点样式
.content {
  margin: 0 auto;
  width: 500px;
  height: 300px;
  border: 1px solid #666;
  text-align: center;
  padding: 10px;
}
a {
  text-decoration: none;
  padding-bottom: 10px;
}

.fuc-component {
  color: blueviolet;
}

.class-component {
  color: red;
}

刷新一下,页面出来了,而且属性也挂载到dom上了

这个时候,我们已经实现了一个render函数,将jsx类型的文件渲染到页面上。 到这里还没有结束哦,我们还要实现函数式组件和class组件的渲染。

渲染函数式组件

我们在index.js中实现一个函数组件

function FunctionComponent(props) {
  return <div className='fuc-component'>函数组件={props.name}</div>
}

const jsx = (
   <div className="content">
     <h1>手写react render component组件</h1>
     <a href="www.baidu.com">明非</a>
     <FunctionComponent name="func" />
   </div>
 )

函数组件,type是一个方法,我们需要修改createNode,执行type,返回jsx,就可以复用上面的逻辑了

// 生成node节点
function createNode(vnode) {
  let node;
  const {type} = vnode
  if (typeof type === 'string') {
    node = updateHostComponent(vnode);
  } else if(typeof type === 'function') {
   node = updateFunctionCompoment(vnode)
  }
  else {
    node = updateTextComponent(vnode)
  }
  return node;
}

// 函数组件
function updateFunctionCompoment(vnode) {
  const {type, props} = vnode;
  const vvnode = type(props) // 执行之后才是真正的jsx
  const node = createNode(vvnode) // 复用createNode,走原生标签逻辑
  return node
}

渲染class组件

我们这个时候,再去源码中看下,我们使用函数组件之前最常用的Component是怎样实现的 git地址:https://github.com/facebook/react/blob/master/packages/react/src/ReactBaseClasses.js

在这个文件中,我们看到了熟悉的Component,还有setState(在之后的文章里会解析setState)

function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};

是的,他不仅(致),而且 这里也有面试官会问,如何判断一个组件是class组件还是函数组件(真的很无聊) 我们就可以根据isReactComponent来回答

那我们继续我们的手写代码

//组件名一定要大写
class ClassComponent extends Component {
  render() {
    return <div className='class-component'>class组件={this.props.name}</div>
  }
}

const jsx = (
   <div className="content">
     <h1>手写react render component组件</h1>
     <a href="www.baidu.com">明非</a>
     <FunctionComponent name="func" />
     <ClassComponent name='class'/>
   </div>
 )

class组件的type也是一个function,我们区别函数组件,可以借用源码的思想,给prototype上新增一个isReactComponent来判断

我们在Component文件中这样写

function Component(props) {
  // class组件要用this
  this.props = props
}

Component.prototype.isReactComponent = {}

export default Component

我们在生成节点时,判断type为function有两种情况

// 生成node节点
function createNode(vnode) {
  let node;
  const {type} = vnode
  if (typeof type === 'string') {
    node = updateHostComponent(vnode);
  } else if(typeof type === 'function') {
  // 判断时类组件还是函数组件
    node = type.prototype.isReactComponent ? updateClassComponent(vnode) : updateFunctionCompoment(vnode)
  }
  else {
    node = updateTextComponent(vnode)
  }
  return node;
}

// class 组件
function updateClassComponent(vnode) {
  const {type, props} = vnode;
  // 类组件需要 new一个实例
  const instance = new type(props)
  // render之后才是jsx对象,复用之前的逻辑
  const vvnode = instance.render()
  const node = createNode(vvnode)
  return node
}

刷新页面

啊,我终于写完了!页面出来了,前端真有意思!

我们来做个总结

我们实现了一个render方法,将jsx,函数组件,类组件,挂载到页面上,初步实现了react的渲染流程,当然,react源码中对类型判断很严格,我们只是粗糙的判断了几个类型,但是如果之前你没有阅读过源码的话,这是我们迈出的一大步,他不再那么神秘,好像也不是那么遥不可及,就像你勇敢的跟你喜欢的女孩子破冰的第一句话,你们已经认识了,接下来,就要趁热打铁,升温感情了!

下一篇文章 我们来了解下,fiber是个啥,react diff算法做了哪些优化。

参考链接

react中文文档

git源码

create-react-app中文文档

代码已上传git