React解析实现

249 阅读9分钟

前提

React 17版本之前,都是利用createElement函数生成虚拟dom对象

jsx语法由babel解析后,然后通过createElement得到一个虚拟dom对象例如

babel解析是在webpack打包编译的时候解析的,然后将解析的代码在浏览器执行createElement函数的时候生成虚拟dom对象

<div className="test">zzzz<span>dddd</span></div>

---> babel解析  ----------------->

React.createElement("div", {
   className: "test"
}, "zzzz", /*#__PURE__*/React.createElement("span", null, "dddd"));


----> React.createElement转为虚拟dom对象

const vdom = {
   props: {
     className: 'test',
     children: "zzzzs",
     // child可能是字符串也可能是数组
     // children: ["zzzzs",{
     //   type: 'span',
     //   props: {
            // 属性
     //   }
     // }]
   },
   type: 'div'
}

React 17

实现react.CreateElement


function createElement (type, config, children) {
  if(config) {
    delete config.__self
    delete config.__source
  }
  const props = {...config};
  // 有多个子节点的时候
  if (arguments.length > 3) {
    // children 是一个数组
    children = Array.prototype.slice.call(arguments, 2)
  }

  props.children = children;

  return {
    type,
    props,
  }
}


const React = {
  createElement,
}

export default React;


react

react-dom

有一个render方法,将虚拟dom转为真实dom,然后挂载到容器上

render方法实现

根据虚拟dom创建真实dom

把虚拟dom的属性挂在到真实dom上

把虚拟dom的子节点也变成真实dom挂在到自己的dom上

将dom挂载到容器上


// 传入一个虚拟dom对象.和一个容器进行渲染
function render(vdom, container) {
  // 将虚拟dom转为真实dom,进行渲染
  const dom = createDom(vdom)
  container.appendChild(dom)
}

// 将虚拟dom转为真实dom
function createDom(vdom) {
  // 如果是数字或者字符串,直接返回一个真实到文本节点
  if(typeof vdom === 'string' || typeof vdom === 'number') {
    return document.createTextNode(vdom)
  }

  const { type, props } = vdom;

  // 创建真实dom
  const dom = document.createElement(type);

  // 把虚拟dom属性更新到真实dom上,
  updateProps(dom, props)
  // 把虚拟dom的儿子也变成真实dom挂在到自己的dom上。
  if (typeof props.children === 'string' || typeof props.children === 'number') {
    dom.textContent = props.children
  } else if (typeof props.children === 'object' && props.children.type) {
    // 只是有一个子元素,并且儿子是虚拟dom,把儿子变成真实dom插入到dom上
    render(props.children, dom)
  } else if (Array.isArray(props.children)) {
    // 如果children是一个数组的时候
    updateArryChild(props.children, dom)
  } else {
    document.textContent =  props.children ? props.children.toString() : ""
  }

   // 把真实dom作为一个dom属性放到虚拟dom上,为以后更新准备
  //  vdom.dom = dom;

  return dom;

}

// 将虚拟dom的props属性挂在真实dom 上
/**
 * 
 * @param {*} dom 真实dom
 * @param {*} props  虚拟dom的属性
 */
function updateProps(dom, props) {
  for (let key in props) {
    if (key === 'children') continue;
    if (key === 'style') {
      let styleObj = props.style;
      for (let attr in styleObj) {
        dom.style[attr] = styleObj[attr]
      }
    } else {
      dom[key] = props[key]
    }
  }
}

/**
 * 
 * @param {*} childVdom 虚拟节点的儿子 child是一个数组的时候
 * @param {*} parantdom 父节点
 */
function updateArryChild(childVdom, parantdom) {
  for (let i = 0; i < childVdom.length; i++) {
    let childVdoms = childVdom[i]
    // 将虚拟节点挂在父节点上
    render(childVdoms, parantdom)
  }
}






const ReactDOM = {
  render,
}

export default ReactDOM;

实现Component

子元素是组件

所以要在获取到真实dom的时候做一下判断,然后走不同的逻辑去获取真实dom


-  // 创建真实dom
-  const dom = document.createElement(type);


+  // 创建真实dom
+  let dom;
+  if (typeof type === 'function') {
+    if (type.isReactComponent) {
+.      // 如果是类组件
+      return createDomClassComponent(vdom)
+    } else {
+      return createDomFunctionComponent(vdom)   // 函数组件
+    }
+  } else {
+    dom = document.createElement(type);  // 普通的
+  }



函数组件

创建真实dom react-dom.js

/**
* 
* @param {*} vdom 将函数组件虚拟dom转为真实dom
*/
function createDomFunctionComponent(vdom) {
 const {type: functionType, props} = vdom;
 // 函数组件本质上是一个函数,执行拿到虚拟dom
 const renderDom = functionType(props)
 // 将其转换为真实dom
 return createDom(renderDom)
}

类组件

类组件继承Component, 新建一个component.js文件,也可以写到react文件上

import ReactDOM from './React-dom'

class Component {
  static isReactComponent = true; // 用来判断是不是类组件
  constructor(props) {
    this.props = props;
    this.state = {}
  }
}
export default Component

创建真实dom react-dom.js

/**
* 
* @param {*} vdom 将类组件虚拟dom转为真实dom
*/
function createDomClassComponent(vdom) {
  // 解构类的定义和类的属性对象
 const { type, props } = vdom;
 // 创建类的实例
 const classInstance  = new type(props)
 // 调用实例的render方法返回要渲染的虚拟dom
 const renderVdom = classInstance.render()
 // 根据虚拟dom对象创建真实的dom对象
 const dom = createDom(renderVdom)
 // 为了类组件更新,把真实dom挂在实例上
 classInstance.dom = dom;
 return dom
}

更新类组件

将事件先挂到组件上 react-dom 文件

// 将虚拟dom的props属性挂在真实dom 上
/**
 * 
 * @param {*} dom 真实dom
 * @param {*} props  虚拟dom的属性
 */
function updateProps(dom, props) {
  for (let key in props) {
    if (key === 'children') continue;
    if (key === 'style') {
      let styleObj = props.style;
      for (let attr in styleObj) {
        dom.style[attr] = styleObj[attr]
      }
+    } else if (key.startsWith('on')) {
+      // 处理事件
+      dom[key.toLocaleLowerCase()] = props[key];
    } else {
      dom[key] = props[key]
    }
  }
}

在componetn.js文件上新增setState方法

import ReactDOM from './React-dom'

class Component {
  static isReactComponent = true;
  constructor(props) {
    this.props = props;
    this.state = {}
  }
+  setState(prevState) {
+    let state = this.state;
+    this.state = {...state, ...prevState}
+    // 重新生成虚拟dom
+    const newVdom = this.render()
+    // 更新
+    updateClassComponent(this, newVdom);
+  }
}

/**
 * 
 * @param {*} instance 实例
 * @param {*} vdom  虚拟dom
 */
 // 这里是直接渲染了整个dom结构,没有用diff,后面再比较
function updateClassComponent(instance, vdom) {
  // 取出类组件上次渲染的真实dom
  const oldDom = instance.dom;
  const newDom = ReactDOM.createDom(vdom);
  // 用新的指向老的
  oldDom.parentNode.replaceChild(newDom, oldDom);
  instance.dom = newDom;
}

export default Component

我们写一个类组件

class ClassCounter extends Component {
  constructor(props) {
    super(props)
    this.state = { number: 0 }
  }
  render() {

    const handleClick = () => {
      console.log('xxx')
      this.setState({number: this.state.number + 1})
    }
    

    return (
      <div onClick={handleClick}>
        <span>类组件</span>
        {this.state.number}
      </div>
    )
  }
}

最后就可以顺利更新啦

批量更新

在react里,事件的更新可能是异步的,是批量的,不是同步的 setState, 比如调两次相同的setState只会执行一次调用setstate之后并没有呀立刻更新,而是缓存起来,等事件函数处理完之后,再进行批量更新,一次更新并重新渲染,(这样就不用频繁刷新组件)

比如

const handleClick = () => {
      this.setState({number: this.state.number + 1})
      console.log(this.state.number) // 0 异步更新
      this.setState({number: this.state.number + 1})
      console.log(this.state.number) // 0 异步更新
      this.setState({number: this.state.number + 1})
      console.log(this.state.number) // 0 异步更新,当函数执行玩,state变为1
      // 调用三次setState ,其实没有变化,相当于只调用一次
      setTimeout(() => {
        console.log(this.state.number, '---?') // 1 
        this.setState({number: this.state.number + 1})
        console.log(this.state.number) //2 同步更新
        this.setState({number: this.state.number + 1})
        console.log(this.state.number) // 3 同步更新
        this.setState({number: this.state.number + 1})
        console.log(this.state.number) //4 同步更新
      } ,0)
     // 所以不管是宏任务还是微任务都是一样的,
     queueMicrotask(() => {
        console.log(this.state.number, '---????') // 1
        this.setState({number: this.state.number + 1})
        console.log(this.state.number) //2
        this.setState({number: this.state.number + 1})
        console.log(this.state.number) // 3
        this.setState({number: this.state.number + 1})
        console.log(this.state.number) //4
      })
    }

setState返回一个函数也是一样的

const handleClick = () => {
      this.setState((lastState) => ({number: lastState.number + 1}))
      console.log(this.state.number) // 0
      this.setState((lastState) => ({number: lastState.number + 1}))
      console.log(this.state.number) // 0
      this.setState((lastState) => ({number: lastState.number + 1}))
      console.log(this.state.number) // 0
      setTimeout(() => {
        console.log(this.state.number) // 3
        this.setState((lastState) => ({number: lastState.number + 1}))
      console.log(this.state.number) // 4
      this.setState((lastState) => ({number: lastState.number + 1}))
      console.log(this.state.number) // 5
      this.setState((lastState) => ({number: lastState.number + 1}))
      console.log(this.state.number) // 6
      },0)
    }

setState也可以传第二个参数为一个回调函数,这个回调函数会在等函数执行完的时候才执行

结论: 事件处理函数只要归react管就是异步,比如在handlCkick里面,(事件处理函数,生命周期函数), 只要不归react管就是同步

设置一个批量更新的标志,以及存储每一个组件的updaters,当组件是批量更新的时候,将state存储起来,当标志变成为false的时候,直接更新组件


import ReactDOM from './React-dom'

export const updateQueue = {
  isBatchUpdate: false, // 当前是否处于批量更新模式,默认false
  updaters: new Set()
}

class Updater {
  constructor(classInstance) {
    this.instance = classInstance; // 类组件的实例
    this.pendingState = []; // 等待生效的状态,可能是一个对象,也可能是一个函数
    this.callBacks = [];
  }
  addState(prevState, callback) {
    // 等待更新或者生效的状态
    this.pendingState.push(prevState)
    // 状态更新后的回调
    if (typeof callback === 'function') {
       this.callBacks.push(callback)
    }
    if (updateQueue.isBatchUpdate) {
      // this 是updater的实例
      updateQueue.updaters.add(this) // 如果是批量更新,缓存updater
    } else {
      // 否则就直接更新组件
      this.updateComponent()
    }
  }

  // isBatchUpdate 状态为false 下 直接更新组件
  updateComponent() {
    const { instance, pendingState, callBacks } = this;
    // 如果有等待更新的状态的话,先计算出新状态
    if (pendingState.length > 0) {
      // 计算新状态
      instance.state = this.computedState(); 
      instance.updateStateComponet()
      callBacks.forEach(cb => cb())
    }
  }

  // 计算新state
  computedState() {
    const { instance, pendingState, callBacks } = this;
    let { state } = instance;
    pendingState.forEach((nextState) => {
      // 判断等待更新的状态里面是对象还是函数
      if (typeof nextState === 'function') {
        nextState = nextState.call(instance, state);
      }
      state = {...state, ...nextState}
    })
    pendingState.length = 0;
    callBakcs.length = 0;
    return state;
  } 


}



class Component {
  static isReactComponent = true;
  constructor(props) {
    this.props = props;
    this.state = {};
    // 每一个组件都有一个,一个组件对应一个updater的实例
    this.updater = new Updater(this)
  }
  setState(prevState, callback) {
    // 将state的状态和callback保存起来
    this.updater.addState(prevState, callback);
  }

  updateStateComponet() {
    // 重新生成虚拟dom
    const newVdom = this.render();
    // 更新
    updateClassComponent(this, newVdom);
  }

}

/**
 * 
 * @param {*} instance 实例
 * @param {*} vdom  虚拟dom
 */
function updateClassComponent(instance, vdom) {
  // 取出类组件上次渲染的真实dom
  const oldDom = instance.dom;
  const newDom = ReactDOM.createDom(vdom);
  // 用新的指向老的
  oldDom.parentNode.replaceChild(newDom, oldDom);
  instance.dom = newDom;
}

export default Component

合成事件

使用事件代理,将event事件进行包装,可以做兼容处理,兼容不同的浏览器,也可以在事件处理函数之前和之后做一些事情,比如react事件的批量更新

我们在将虚拟dom上的事件挂载到真实dom上的时候,进行事件代理,将在要挂载的事件进行处理

// React-dom.js
import { CompositeEvent } from './CompositeEvent'

function updateProps(dom, props) {
  for (let key in props) {
    if (key === 'children') continue;
    if (key === 'style') {
      let styleObj = props.style;
      for (let attr in styleObj) {
        dom.style[attr] = styleObj[attr]
      }
    } else if (key.startsWith('on')) {
    -  // 处理事件
    -  // dom[key.toLocaleLowerCase()] = props[key];
    +  // 合成事件
    +  CompositeEvent(dom, key.toLocaleLowerCase(), props[key])
    } else {
      dom[key] = props[key]
    }
  }
}

给updateQueue中添加一个方法,用来绑定到事件里面去

// component.js
export const updateQueue = {
  isBatchUpdate: false, // 当前是否处于批量更新模式,默认false
  updaters: new Set(),
+  batchUpdate() {
+   // 批量更新,在react事件里面刚开始的时候将isBatchUpate设置为true,执行完设置为false
+    for (let updater of this.updaters) {
+      updater.updateComponent()
+    }
+    this.isBatchUpdate = false;
+  }

}


新建一个 CompositeEvent.js 文件用来实现事件代理,不管给哪一个dom元素绑定事件,最后都要统一代理到document事件上面去,重写event事件


import { updateQueue } from "./Component";

/**
 * 
 * @param {*} dom  真实dom
 * @param {*} eventType 事件类型
 * @param {*} handleEvent 事件处理函数
 */
export function CompositeEvent(dom, eventType, handleEvent) {
   // 给dom绑定一个onClick事件回调函数,,handleEvent 就是于handlclick
  //  let store;
  // if(dom.store) {
  //   store = dom.store
  // } else {
  //   dom.store = {}
  //   store = dom.store
  // }
   let store = dom.store || (dom.store = {})
   store[eventType] = handleEvent;  // store.onclick = handleClick
   if (!document[eventType]) { //事件委托 不管给哪个dom元素上绑定事件,最后统一代理到document事件上去
      document[eventType] = dispatchEvent; // document.onclick = dispatchEvent
   } 
}
// 存储创建出来的合成事件
let eventStore = {
  bubbling: false,
  stopBubbling() {
    this.bubbling = true;
    console.log('阻止冒泡')
  }
}

// event 是原生事件
function dispatchEvent(event) {
  let { target, type } = event; // 事件源 button哪个dom元素,类型 type= click
  const eventType = `on${type}`;
  //React控制事件里面,批量更新
  updateQueue.isBatchUpdate = true;
  // 根据原生事件对象创建出一个合成事件对象
  eventStore = createCompositeEvent(event);
  // 事件冒泡
  while(target) {
    const { store } = target;
    const handleEvent = store && store[eventType];
    // 执行事件处理函数
    handleEvent && handleEvent.call(target, eventStore);
    if (eventStore.bubbling)  
    break;
    target = target.parentNode;
  }
 
  // 让每一次都用eventStore这个对象,形成单例模式
  for(let key in eventStore) {
    eventStore[key] = null;
  }
  updateQueue.batchUpdate()
}

function createCompositeEvent(event) {
  for (let key in event) {
    eventStore[key] = event[key]
  }
  return eventStore
}

生命周期 15版本

  1. 实现 componentWillMount
  2. 实现 componentDidMount (要在组件渲染到页面上之后才执行,所以先保存到真实dom上,当真实dom挂载完成执行)
// react-dom


// 传入一个虚拟dom对象.和一个容器进行渲染
function render(vdom, container) {
  // 将虚拟dom转为真实dom,进行渲染
  const dom = createDom(vdom)
  container.appendChild(dom)
+  // 组件渲染之后执行
+  dom.componentDidMount&& dom.componentDidMount()
}

/**
 * 
 * @param {*} vdom 将类组件虚拟dom转为真实dom
 */
function createDomClassComponent(vdom) {
   // 解构类的定义和类的属性对象
  const { type, props } = vdom;
  // 创建类的实例
  const classInstance  = new type(props)
  // 生命周期
+  if (classInstance.componentWillMount) {
+    classInstance.componentWillMount();
+  }
  // 调用实例的render方法返回要渲染的虚拟dom
  const renderVdom = classInstance.render()
  // 根据虚拟dom对象创建真实的dom对象
  const dom = createDom(renderVdom)
  // 为了类组件更新,把真实dom挂在实例上

+  if (classInstance.componentDidMount) {
+    // 组件渲染之后执行,所以在将组件挂载到root节点上的时候才执行
+    dom.componentDidMount = classInstance.componentDidMount.bind(classInstance)
+  }

  classInstance.dom = dom;
  return dom
}

shouldComponentUpdate

// component.js

  addState(prevState, callback) {
    // 等待更新或者生效的状态
    this.pendingState.push(prevState)
    // 状态更新后的回调
    this.callBacks.push(callback)

  -  // 状态改变, state改变
  -  // if (updateQueue.isBatchUpdate) {
  -  //   // this 是updater的实例
  -  //   updateQueue.updaters.add(this) // 如果是批量更新,缓存updater
  -  // } else {
  -  //   // 否则就直接更新组件
  -  //   this.updateComponent()
  -  // }
  +  this.emitUpdate()
  }

 // 不管组件属性变了还是状态变,都会更新 发射更新
   emitUpdate (newprops) { // 传一个参数,属性改变,也就是props改变
      if (updateQueue.isBatchUpdate) {
      // this 是Updater的实例
      updateQueue.updaters.add(this) // 如果当前的批量更新模式,先缓存updater
    } else {
      this.updateComponent(); // 直接更新组件
    }
  }

  // isBatchUpdate 状态为false 下 直接更新组件
  updateComponent() {
    const { instance, pendingState, callBacks } = this;
    // 如果有等待更新的状态的话,先计算出新状态
    if (pendingState.length > 0) {
      // 计算新状态
  -    // instance.state = this.computedState(); 
  -    // instance.updateStateComponet()
       callBacks.forEach(cb => cb())
  +    shouldUpdate(instance, this.computedState())
    }
  }


function shouldUpdate(instance, nextState) {
    // 不管组件要不要刷新,其实组件的state属性一定会改变
    instance.state = nextState;
    // 如果有这个方法,并且方法返回值为false,则不需要哦继续更新
    if (instance.shouldComponentUpdate&&!instance.shouldComponentUpdate(instance.props,instance.state)) {
      return;
    }
    instance.updateStateComponet();
}


componentWillUpdate

componentDidUpdate


 updateStateComponet() {
  +  if (this.componentWillUpdate) {
  +    this.componentWillUpdate()
  +  }
    // 重新生成虚拟dom
    const newVdom = this.render();
    // 更新
    updateClassComponent(this, newVdom);

  + if (this.componentDidUpdate) {
  +    this.componentDidUpdate()
  +  }
  }

生命周期 16版本

diff

image.png

------------------------------------>