/react-like

🚀 A compact version of React that implements many castration features

Primary LanguageJavaScriptMIT LicenseMIT

背景

之前自学了一阵子React源码(文章),感觉自己对ComponentsetState,所以这里决定写一个React-Like项目加深一下对React的理解

开始

项目使用了transform-react-jsx来进行JSXJS的转换

组件

既然是写React,那就先定义一下React基本结构

// src/react/index.js
import Component from './Component.js'
import createElement from './CreateReactElement.js'

const React = {
    Component,
    createElement
}

export default React;

其中Component为基本组件作为父类,createElement来创建组件

// src/react/Component.js
import { enqueueSetState } from './StateQueue'

class Component {
  constructor(props){
    this.isComponent = true // 是否为组件
    this.isReplace = false // 是否是更新的组件
    this.props = props
    this.state = {}
  }

  setState(partialState){
    enqueueSetState(partialState, this)
  }
}

export default Component;

这里进行了一些基本的初始化, 还定义了setState方法,其中调用了enqueueSetState(后话)进行组件更新

// src/react/CreateReactElement.js

function createElement(tag, attrs, children){

  var props = {}
  var attrs = attrs || {}

  const childrenLength = arguments.length - 2;
  
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    var childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }

  return {
    tag,
    attrs,
    props,
    key: attrs.key || null
  }
}

export default createElement;

这里同样进行一些初始化操作,但是对传进来的children进行了特殊的处理,利用arguments获得children长度,之后决定是转化成数组还是直接写到porps上去,最后将所有属性作为对象返回,当用户创建React对象时会自动调用这个函数

渲染

同样我们先定义一个ReactDom对象

// src/react-dom/index.js

import render from './Render'

const ReactDOM = {
  render: ( nextElement, container ) => {
      return render( nextElement, container );
  }
}

export default ReactDOM;

在这里定义了一个大名鼎鼎的render函数,传入两个参数分别为当前的元素和要插入的容器,然后调用Render文件中的render方法

// src/react-dom/Render.js

import {
  createComponent,
  setComponentProps
} from './Diff'
import setAttribute from './Dom'

/**入口render方法
 * @param {ReactElement} nextElement 要插入到DOM中的组件
 * @param {DOMElement} container 要插入到的容器
 */
export function render(nextElement, container){

  if(nextElement == null || container == null) return;

  if(nextElement.isComponent){
    const component = nextElement;

    if (component._container) {
      if (component.componentWillUpdate){
        component.componentWillUpdate();
      } else if (component.componentWillMount) {
        component.componentWillMount();
      }
    }

    component._container = container;

    nextElement = component.render()
  }

  const type = typeof nextElement

  if(type === 'string' || type === 'number'){
    let textNode = document.createTextNode(nextElement);
    return container.appendChild(textNode);
  }

  if(typeof nextElement.tag === 'function'){
    let component = createComponent(nextElement.tag, nextElement.attrs)
    setComponentProps(component,nextElement.attrs, container)
    return render(component.base, container);
  }

  const dom = document.createElement(nextElement.tag)

  if(nextElement.attrs){
    Object.keys(nextElement.attrs).map(key => {
      setAttribute(key, nextElement.attrs[key], dom)
    })
  }

  if(nextElement.props){
    if(typeof nextElement.props.children == 'object'){
      nextElement.props.children.forEach(item => {
        render(item, dom)
      })
    }else{
      render(nextElement.props.children, dom)
    }
  }

  if(nextElement._component){
    if(nextElement._component.isReplace){
      var arr = Array.from(nextElement._component.parentNode.childNodes)
      arr.map((item,index) => {
        if(isSameDom(item,dom)){
          return container.replaceChild(dom, nextElement._component.parentNode.children[index])
        }
      })
    }
  }
  return container.appendChild(dom)
}

function isSameDom(item, dom){
  return (item.nodeName == dom.nodeName && item.nodeType == dom.nodeType && item.nextSibling == dom.nextSibling)
}

export default render;

代码比较长,我们这里分段分析一下

const type = typeof nextElement

if(type === 'string' || type === 'number'){
  let textNode = document.createTextNode(nextElement);
  return container.appendChild(textNode);
}

如果元素类型为stringnumber则直接创建TextNode并直接appendcontainer中里

if(typeof nextElement.tag === 'function'){
  let component = createComponent(nextElement.tag, nextElement.attrs)
  setComponentProps(component,nextElement.attrs, container)
  return render(component.base, container);
}

如果元素的tag类型为function即为React组件,则调用Diff中的方法来创建组件(后话)

const dom = document.createElement(nextElement.tag)

if(nextElement.attrs){
  Object.keys(nextElement.attrs).map(key => {
    setAttribute(key, nextElement.attrs[key], dom)
  })
}

如果都不是的话即为普通元素,则直接调用document.createElement创建Dom,之后遍历attrs调用setAttribute来设置属性,Object.keys将对象转化成数组方便遍历,接下来我们看一下setAttribute方法

function setAttribute(key, value, dom){
  if(key === 'className'){
    key = 'class'
  }

  if(typeof value === 'function'){
    dom[key.toLowerCase()] = value || '';
  }else if(key === 'style'){
    if(typeof value === 'string'){
      dom.style.cssText = value || '';
    }else if(typeof value === 'object'){
      for (let name in value) {
        dom.style[name] = typeof value[name] === 'number' ? value[name] + 'px' : value[name];
      }
    }
  }else{
    if(value){
      dom.setAttribute(key, value);
    }else{
      dom.removeAttribute(key, value);
    }
  }
}

export default setAttribute;
  • 先将className转化为class
  • 若绑定的类型为function则转化成小写后写入dom属性
  • keystyle,则分类讨论,若属性为string则写入cssText,若为object则判断其是否为number,若是则自动在后面添加px,然后写入style
  • 若为其他则直接调用原生setAttribute方法
  • 若属性值为空则在dom上删除该属性
if(nextElement.props){
  if(typeof nextElement.props.children == 'object'){
    nextElement.props.children.forEach(item => {
      render(item, dom)
    })
  }else{
    render(nextElement.props.children, dom)
  }
}

顺着render往下看,这里遍历元素的子元素递归渲染

if(nextElement._component){
  if(nextElement._component.isReplace){
    var arr = Array.from(nextElement._component.parentNode.childNodes)
    arr.map((item,index) => {
      if(isSameDom(item,dom)){
        return container.replaceChild(dom, nextElement._component.parentNode.children[index])
      }
    })
  }
}
return container.appendChild(dom)

最后判断两次render的组件是否为同一个,若为同一个则调用replaceChild方法进行替换,否则appendChild到容器中

回到上面nextElement.tag === 'function'中,其中有两个函数createComponentsetComponentProps

// src/react-dom/Diff.js

export function createComponent(component, props){
  let instance;
  if(component.prototype && component.prototype.render){
    instance = new component(props)
  }else{
    instance = new component(props)
    instance.constructor = component
    instance.render = function() {
      return this.constructor(props)
    }
  }

  return instance;
}

第一个if判断是不是class创建的组件,若是则直接new一个,若不是则为函数返回组件,调整一下constructor以及render方法,然后将新组件返回

// src/react-dom/Diff.js

export function setComponentProps(component, props, container){
  if (!component.base){
    if (component.componentWillMount) 
      component.componentWillMount();
	}else if(component.componentWillReceiveProps){
		component.componentWillReceiveProps(props);
  }
  
  component.props = props;
  component.parentNode = container

  renderComponent(component, container)
}

首先判断组件的base是否存在,若存在则判断是否为初次挂载,否则判断是否为接受新的props,然后将propsrender中的attrscontainer作为成员添加到component上,parentNode用来定位父元素方便更新,然后调用renderComponent进行组件挂载或者更新

// src/react-dom/Diff.js

export function renderComponent(component, container){
  let base;

  if ( component.base && component.componentWillUpdate ) {
    component.componentWillUpdate();
  }

  base = component.render()

  if (component.base) {
    if (component.componentDidUpdate){
      component.componentDidUpdate();
    }
  }else if(component.componentDidMount) {
    component.componentDidMount();
  }

  component.base = base;
  base._component = component;

  if(!container){
    component.isReplace = true
    render(base, component.parentNode)
  }
}

basecreateComponent中的component渲染后结果,然后进行一下简单的生命周期判断,最后判断container是否为空,若为空则为更新组件,把component.parentNode作为container传回render

State更新

在文章开始提到过,Component中的setState方法调用了enqueueSetState

// src/react/StateQueue.js

const batchingUpdates = [] // 需要更新的状态
const dirtyComponent = [] // 需要更新的组件
var isbatchingUpdates = false // 是否处于更新状态

function callbackQueue(fn){
  return Promise.resolve().then(fn);
}

export function enqueueSetState(partialState, component){
  if(!isbatchingUpdates){
    callbackQueue(flushBatchedUpdates)
  }

  isbatchingUpdates = true

  batchingUpdates.push({
    partialState,
    component
  })

  if(!dirtyComponent.some(item => item === component)){
    dirtyComponent.push(component)
  }
}

isbatchingUpdates判断事务是否处于更新状态(初始值为false),若不为更新则调用callbackQueue来执行flushBatchedUpdates函数来更新组件,然后设置更新状态为true,将当前状态和组件添加到batchingUpdates中,最后判断dirtyComponent中是否有当前组件,若无则添加进去

callbackQueue使用了Promise来达到延时模拟setState的功能

// src/react/StateQueue.js

function flushBatchedUpdates(){
  let queueItem, componentItem;
  while(queueItem = batchingUpdates.shift()){
    const { partialState, component } = queueItem;

    if(!component.prevState){
      component.prevState = Object.assign({}, partialState)
    }

    if(typeof partialState == 'function'){
      Object.assign(component.state, partialState(component.prevState, component.props))
    }else{
      Object.assign(component.state, partialState)
    }

    component.prevState = component.state
  }

  while(componentItem = dirtyComponent.shift()){
    renderComponent(componentItem)
  }

  isbatchingUpdates = false
}

遍历batchingUpdates数组排头(shift自查),获取其中组件和状态,判断组件的前一个状态,若无之前的状态,则将空对象和当前状态合并设为该组件的初始状态,若前一状态为function,则调用该函数并将返回值和之前状态合并,若不为函数则直接合并,然后设置组件的上一状态为其之前的状态,最后遍历dirtyComponent更新组件,完成后设置isbatchingUpdatesfalse

事件委托

入口在为元素设置属性的setAttribute中

// 若绑定的类型为function则挂载到事件委托上
if(typeof value === 'function'){
    setFuncBus(key, value, dom);
}

我们看一下setFuncBus这个函数

/**
 * @msg: 事件代理函数
 * @param {string} key 属性的key
 * @param {any} value 属性的值
 * @param {dom} dom 被设置属性的元素 
 * @return: null
 */
function setFuncBus(key, value, dom) {
  let funcKey = key.toLowerCase();
  let domKey = dom.key;
  
  if(document.eventBus[funcKey]) {
    document.eventBus[funcKey][domKey] = value || '';
  } else {
    document.eventBus[funcKey] = {};
    document.eventBus[funcKey][domKey] = value || '';
    addWindowEventListener(funcKey);
  }
}

对于key, 我简单的在在render函数中生成dom时为其赋值了key,其为一个8位随机数(随便啦

eventBus是一个普通对象。先将函数名转为小写,然后判断eventBus中是否已经委托了该函数,若没有则初始化后赋值,这里简单的以key作为触发元素的唯一标识,最后将funcKey添加到全局的事件监听中。

/**
 * @msg: 添加全局事件委托
 * @param {string} funcKey 委托事件名  
 * @return: null
 */
export function addWindowEventListener(funcKey) {
  let listenName = funcKey.replace('on', '');
  funcKey = funcKey.toLowerCase();

  // 根据eventbus避免全局事件重复注册
  (!document.eventBus[funcKey] || Object.keys(document.eventBus[funcKey]).length < 2 ) ? 
  window.addEventListener(listenName, function(e){
    // 判断当前元素是否为被委托事件
    let func = document.eventBus[funcKey][e.target.key];
  
    // 如果当前元素被委托则执行
    if(func) {
      func();
    } else {
      // 向上冒泡寻找是否有适合条件的委托函数
      // e.path为层级数组,索引从低到高为 子---->父
      e.path.forEach(item => {
        document.eventBus[funcKey][item.key] ? 
          document.eventBus[funcKey][item.key]() : '';
      });
    }
  }) : null;
}

基本的事件委托到这里结束

代码请移步GitHub仓库