十四、「深入React源码」--- 手写实现Hook之useReducer、useContext、useEffect

393 阅读12分钟

一、useReducer

1. 介绍

useState的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 dispatch

2. 思路

创建函数useReducer,入参为:reducer处理函数、initialState状态初始值。主要思想是基于老状态通过处理函数更新为新状态。

2-1. 计算新状态

首先通过hookStates[hookIndex]获取到当前的老状态,并记录下当前的索引(因为hookIndex随着程序执行会变动),如果没有值,说明是初次渲染,那就直接取initialState初始值。

2-2. 实现dispatch

dispatch最重要的是分发处理器。把老状态和action传给reducer开始处理,拿到新的结果赋值给hookStates[hookIndex]完成状态更新。

2-3. 优化

从思路来看,useStateuseReducer的实现思路大体一致。其实useStateuseReducer的语法糖,是useReducer的简易版。唯一区别就是useState没有处理器函数reducer。因此我们可以简化useState的写法,并改造useReducer。

即:useState内部直接返回useReduer的调用,没有reducer我们就传值为null。那么useReducer内部就要进行判断,reducer是否存在,如果存在就触发action对应的reducer,如果不存在,就直接返回action

3. 实现

3-1. src/index.js

import React from "./react";
import ReactDOM from "./react-dom";

/**
 * 状态处理函数
 * @param {*} state 老状态
 * @param {*} action 类型
 */
function reducer(state = { number: 0 }, action) {
  switch (action.type) {
    case "ADD":
      return { number: state.number + 1 };
    case "MINUS":
      return { number: state.number - 1 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = React.useReducer(reducer, { number: 0 });

  return (
    <div>
      <p>{state.number}</p>
      <button onClick={() => dispatch({ type: "ADD" })}>+</button>
      <button onClick={() => dispatch({ type: "MINUS" })}>-</button>
    </div>
  );
}

ReactDOM.render(<Counter />, document.getElementById("root"));

3-2. src/react-dom.js


import {
  REACT_TEXT,
  REACT_FORWARD_REF,
  MOVE,
  PLACEMENT,
  REACT_PROVIDER,
  REACT_CONTEXT,
  REACT_MEMO,
} from "./constants";
import { addEvent } from "./event";

// 全局变量,用来记录hook的值
let hookStates = [];
// 当前hook的索引值
let hookIndex = 0;
// 函数组件更新
let scheduleUpdate;

/**
 *把虚拟DOM变成真实DOM插入容器
 * @param {*} vdom 虚拟DOM/React元素
 * @param {*} parentDOM 真实DOM容器
 */
function render(vdom, parentDOM) {
  mount(vdom, parentDOM);
  scheduleUpdate = () => {
    // 索引重置为0
    hookIndex = 0;
    compareToVdom(parentDOM, vdom, vdom);
  };
}

> > > /**
> > >  *
> > >  * @param {*} reducer 处理器
> > >  * @param {*} initialState 初始值
> > >  */
> > > export function useReducer(reducer, initialState) {
> > >   hookStates[hookIndex] = hookStates[hookIndex] || initialState;
> > >   let currentIndex = hookIndex;
> > >   function dispatch(action) {
> > >     hookStates[currentIndex] = reducer
> > >       ? reducer(hookStates[currentIndex], action)
> > >       : action;
> > >     scheduleUpdate();
> > >   }
> > >   return [hookStates[hookIndex], dispatch];
> > > }
> > > 
> > > /**
> > >  * useState其实是useReducer的语法糖
> > >  * @param {*} initialState 初始值
> > >  */
> > > export function useState(initialState) {
> > >   return useReducer(null, initialState);
> > >   // hookStates[hookIndex] = hookStates[hookIndex] || initialState;
> > >   // let currentIndex = hookIndex;
> > >   // // 不管什么时候调用setState
> > >   // function setState(newState) {
> > >   //   // newState如果是函数,就再执行一次
> > >   //   if (typeof newState === "function")
> > >   //     newState = newState(hookStates[currentIndex]);
> > >   //   // currentInde永远指向hookIndex赋值时的值
> > >   //   hookStates[currentIndex] = newState;
> > >   //   // 状态变化后,执行调度更新
> > >   //   scheduleUpdate();
> > >   // }
> > >   // return [hookStates[hookIndex++], setState];
> > > }

/**
 * useCallback 缓存回调函数
 * @param {*} callback 创建对象的工厂函数
 * @param {*} deps 依赖数组
 */
export function useCallback(callback, deps) {
  // 判断是不是初次渲染
  if (hookStates[hookIndex]) {
    // 上一次的memo和依赖数组
    let [lastCallback, lastDeps] = hookStates[hookIndex];
    // 判断新老依赖数组是否变化
    let same = deps.every((item, index) => item === lastDeps[index]);
    if (same) {
      hookIndex++;
      return lastCallback;
    } else {
      hookStates[hookIndex++] = [callback, deps];
      return callback;
    }
  } else {
    // 初次渲染
    hookStates[hookIndex++] = [callback, deps];
    return callback;
  }
}

/**
 * useMemo 缓存对象
 * @param {*} factrouy 创建对象的工厂函数
 * @param {*} deps 依赖数组
 */
export function useMemo(factroy, deps) {
  // 判断是不是初次渲染
  if (hookStates[hookIndex]) {
    // 上一次的memo和依赖数组
    let [lastMemo, lastDeps] = hookStates[hookIndex];
    // 判断新老依赖数组是否变化
    let same = deps.every((item, index) => item === lastDeps[index]);
    if (same) {
      hookIndex++;
      return lastMemo;
    } else {
      let newMemo = factroy();
      hookStates[hookIndex++] = [newMemo, deps];
      return newMemo;
    }
  } else {
    // 初次渲染
    let newMemo = factroy();
    hookStates[hookIndex++] = [newMemo, deps];
    return newMemo;
  }
}

/** 页面挂载真实DOM */
function mount(vdom, parentDOM) { }

/**
 * 把虚拟DOM变成真实DOM
 * @param {*} vdom 虚拟DOM
 * @return 真实DOM
 */
function createDOM(vdom) { }

/** 挂载memo组件 */
function mountMemoComponent(vdom) { }

/** 挂载Provider组件 */
function mountProviderComponent(vdom) { }

/** 挂载Context组件-Consumer */
function mountContextComponent(vdom) { }

/** 挂载类组件 */
function mountClassComponent(vdom) { }

/** 挂载函数组件 */
function mountFunctionComponent(vdom) { }

/** 挂载经过转发的ref的函数组件 */
function mountForwardComponent(vdom) { }

/** 如果子元素为数组,遍历挂载到容器 */
function reconcileChildren(children, parentDOM) { }

/**
 * 把新的属性更新到真实DOM上
 * @param {*} dom 真实DOM
 * @param {*} oldProps 旧的属性对象
 * @param {*} newProps 新的属性对象
 */
function updateProps(dom, oldProps, newProps) { }

/**
 * DOM-DIFF:递归比较老的虚拟DOM和新的虚拟DOM,找出两者的差异,把这些差异最小化的同步到真实DOM上
 * @param {*} parentDOM 父真实DOM
 * @param {*} oldVdom 老的虚拟DOM
 * @param {*} newVdom 新的虚拟DOM
 * @param {*} nextDOM 新的虚拟DOM
 *
 */
export function compareToVdom(parentDOM, oldVdom, newVdom, nextDOM) { }

/**
 * 新老DOM类型一样的更新----DOM-DIFF精髓之处
 * 如果新老DOM的类型一样,那么节点就可以复用
 */
function updateElement(oldVdom, newVdom) { }

/** 更新memo组件 */
function updateMemoComponent(oldVdom, newVdom) { }

/** 更新Proveder组件 */
function updateProviderComponent(oldVdom, newVdom) { }

/** 更新context组件 */
function updateContextComponent(oldVdom, newVdom) { }

/** 更新类组件 */
function updateClassComponent(oldVdom, newVdom) { }

/** 更新函数组件 */
function updateFunctionComponent(oldVdom, newVdom) { }

/** 递归比较子节点 */
function updateChildren(parentDOM, oldVChildren, newVChildren) { }

const ReactDOM = {
  render,
};
export default ReactDOM;

二、useEffect

1. 介绍

Effect Hook 可以让我们在函数组件中执行副作用操作。 可以把 useEffect 看做 componentDidMountcomponentDidUpdate 和 componentWillUnmount 这三个函数的组合。

与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新,这使应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect 可以使用。

useEffect(() => {
    // 页面渲染后执行此处代码块。可理解为componentDidMount
    // 当依赖项数组发生变化,会再次执行此处代码。可理解为componentDidUpdate
    retrun () => {
        // 页面即将卸载前执行此处代码。可理解为componentWillUnmount
    }
}, [])

2. 思路

与useMemo、useCallback思想类似。因为useEffect需要延迟执行,所以说需要包装成一个新的宏任务。

2-1. 判断是否为初次渲染

我们可以通过hookStates[hookIndex]获取当前hook的值,如果有值说明不是第一次渲染,即更新。相反则说明使第一次渲染。那么我们就需要把本次effect的callback返回的销毁函数和deps依赖数组存入hookStates中以便后续进行新老项的比较。

2-2. 判断依赖项数组是否变化

使用数组方法every判断依赖项数组是否变化。

  • 没变化:只需把hookIndex++,确保索引的数据不乱
  • 变化:首先执行上一个销毁函数,然后把本次effect的callback返回的销毁函数和deps依赖数组存入hookStates中以便后续进行新老项的比较。

3. 实现

3-1. src/index.js、

import React from "./react";
import ReactDOM from "./react-dom";

function Counter() {
  const [number, setNumber] = React.useState(0);
  React.useEffect(() => {
    console.log("开启一个新的定时器");
    const timer = setInterval(() => {
      setNumber((number) => number + 1);
    }, 1000);
    return () => {
      console.log("销毁老的定时器");
      clearInterval(timer);
    };
  });
  return <p>{number}</p>;
}

ReactDOM.render(<Counter />, document.getElementById("root"));

3-2. src/react.js

import { wrapToVdom, shallowEqual } from "./utils";
import { Component } from "./component";
import {
  REACT_FORWARD_REF,
  REACT_ELEMENT,
  REACT_PROVIDER,
  REACT_CONTEXT,
  REACT_MEMO,
} from "./constants";
import {
  useState,
  useMemo,
  useCallback,
> > >   useReducer,
> > >   useEffect,
} from "./react-dom";

// ...

const React = {
  createElement,
  Component,
  createRef,
  forwardRef,
  createContext,
  cloneElement,
  PureComponent,
  memo,
  useState,
  useMemo,
  useCallback,
> > >   useReducer,
> > >   useContext,
> > >   useEffect,
};
export default React;

3-3. src/react-dom.js

import {
  REACT_TEXT,
  REACT_FORWARD_REF,
  MOVE,
  PLACEMENT,
  REACT_PROVIDER,
  REACT_CONTEXT,
  REACT_MEMO,
} from "./constants";
import { addEvent } from "./event";

// 全局变量,用来记录hook的值
let hookStates = [];
// 当前hook的索引值
let hookIndex = 0;
// 函数组件更新
let scheduleUpdate;

/**
 *把虚拟DOM变成真实DOM插入容器
 * @param {*} vdom 虚拟DOM/React元素
 * @param {*} parentDOM 真实DOM容器
 */
function render(vdom, parentDOM) {
  mount(vdom, parentDOM);
  scheduleUpdate = () => {
    // 索引重置为0
    hookIndex = 0;
    compareToVdom(parentDOM, vdom, vdom);
  };
}

> > > /**
> > >  *
> > >  * @param {*} reducer 处理器
> > >  * @param {*} initialState 初始值
> > >  */
> > > export function useReducer(reducer, initialState) {
> > >   hookStates[hookIndex] = hookStates[hookIndex] || initialState;
> > >   let currentIndex = hookIndex;
> > >   function dispatch(action) {
> > >     const a = reducer ? reducer(hookStates[currentIndex], action) : action;
> > >     hookStates[currentIndex] = a;
> > >     scheduleUpdate();
> > >   }
> > >   return [hookStates[hookIndex++], dispatch];
> > > }

> > > /**
> > >  * useState其实是useReducer的语法糖
> > >  * @param {*} initialState 初始值
> > >  */
> > > export function useState(initialState) {
> > >   hookStates[hookIndex] = hookStates[hookIndex] || initialState;
> > >   let currentIndex = hookIndex;
> > >   // 不管什么时候调用setState
> > >   function setState(newState) {
> > >     // newState如果是函数,就再执行一次
> > >     if (typeof newState === "function")
> > >       newState = newState(hookStates[currentIndex]);
> > >     // currentInde永远指向hookIndex赋值时的值
> > >     hookStates[currentIndex] = newState;
> > >     // 状态变化后,执行调度更新
> > >     scheduleUpdate();
> > >   }
> > >   return [hookStates[hookIndex++], setState];
> > > }

/**
 *
 * @param {*} effect
 * @param {*} deps
 */
export function useEffect(callback, dependencies) {
  let currentIndex = hookIndex;
  if (hookStates[hookIndex]) {
    let [destroy, lastDeps] = hookStates[hookIndex];
    let same =
      dependencies &&
      dependencies.every((item, index) => item === lastDeps[index]);
    if (same) {
      hookIndex++;
    } else {
      destroy && destroy();
      setTimeout(() => {
        hookStates[currentIndex] = [callback(), dependencies];
      });
      hookIndex++;
    }
  } else {
    setTimeout(() => {
      hookStates[currentIndex] = [callback(), dependencies];
    });
    hookIndex++;
  }
}

/**
 * useCallback 缓存回调函数
 * @param {*} callback 创建对象的工厂函数
 * @param {*} deps 依赖数组
 */
export function useCallback(callback, deps) {
  // 判断是不是初次渲染
  if (hookStates[hookIndex]) {
    // 上一次的memo和依赖数组
    let [lastCallback, lastDeps] = hookStates[hookIndex];
    // 判断新老依赖数组是否变化
    let same = deps.every((item, index) => item === lastDeps[index]);
    if (same) {
      hookIndex++;
      return lastCallback;
    } else {
      hookStates[hookIndex++] = [callback, deps];
      return callback;
    }
  } else {
    // 初次渲染
    hookStates[hookIndex++] = [callback, deps];
    return callback;
  }
}

/**
 * useMemo 缓存对象
 * @param {*} factrouy 创建对象的工厂函数
 * @param {*} deps 依赖数组
 */
export function useMemo(factroy, deps) {
  // 判断是不是初次渲染
  if (hookStates[hookIndex]) {
    // 上一次的memo和依赖数组
    let [lastMemo, lastDeps] = hookStates[hookIndex];
    // 判断新老依赖数组是否变化
    let same = deps.every((item, index) => item === lastDeps[index]);
    if (same) {
      hookIndex++;
      return lastMemo;
    } else {
      let newMemo = factroy();
      hookStates[hookIndex++] = [newMemo, deps];
      return newMemo;
    }
  } else {
    // 初次渲染
    let newMemo = factroy();
    hookStates[hookIndex++] = [newMemo, deps];
    return newMemo;
  }
}

/** 页面挂载真实DOM */
function mount(vdom, parentDOM) {
  //把虚拟DOM变成真实DOM
  let newDOM = createDOM(vdom);
  //把真实DOM追加到容器上
  parentDOM.appendChild(newDOM);
  if (newDOM.componentDidMount) newDOM.componentDidMount();
}

/**
 * 把虚拟DOM变成真实DOM
 * @param {*} vdom 虚拟DOM
 * @return 真实DOM
 */
function createDOM(vdom) {
  if (!vdom) return null; // null/und也是合法的dom

  let { type, props, ref } = vdom;
  let dom; //真实DOM
  if (type && type.$$typeof === REACT_MEMO) {
    return mountMemoComponent(vdom);
  } else if (type && type.$$typeof === REACT_PROVIDER) {
    return mountProviderComponent(vdom);
  } else if (type && type.$$typeof === REACT_CONTEXT) {
    return mountContextComponent(vdom);
  } else if (type && type.$$typeof === REACT_FORWARD_REF) {
    return mountForwardComponent(vdom);
  } else if (type === REACT_TEXT) {
    // 如果元素为文本,创建文本节点
    dom = document.createTextNode(props.content);
  } else if (typeof type === "function") {
    if (type.isReactComponent) {
      // 说明这是一个类组件
      return mountClassComponent(vdom);
    } else {
      // 函数组件
      return mountFunctionComponent(vdom);
    }
  } else if (typeof type === "string") {
    //创建DOM节点 span div p
    dom = document.createElement(type);
  }

  // 处理属性
  if (props) {
    //更新DOM的属性 后面我们会实现组件和页面的更新。
    updateProps(dom, {}, props);
    let children = props.children;
    //如果说children是一个React元素,也就是说也是个虚拟DOM
    if (typeof children === "object" && children.type) {
      //把这个儿子这个虚拟DOM挂载到父节点DOM上
      mount(children, dom);
      props.children.mountIndex = 0;
    } else if (Array.isArray(children)) {
      reconcileChildren(children, dom);
    }
  }
  vdom.dom = dom; // 给虚拟dom添加dom属性指向这个虚拟DOM对应的真实DOM
  if (ref) ref.current = dom;
  return dom;
}

/** 挂载memo组件 */
function mountMemoComponent(vdom) {
  let { type, props } = vdom; // type是memo组件的返回值,type属性是原始的函数组件
  let renderVdom = type.type(props);
  vdom.prevProps = props; // 记录老的属性对象,以后更新时方便对比
  vdom.oldRenderVdom = renderVdom;
  return createDOM(renderVdom);
}

/** 挂载Provider组件 */
function mountProviderComponent(vdom) {
  let { type, props } = vdom; // type = { $$typeof: REACT_PROVIDER, _context: context }
  let context = type._context; // { $$typeof: REACT_CONTEXT, _currentValue: undefined }
  // 1. 赋值为Provider使用时的value值
  context._currentValue = props.value;
  // 2. 渲染的是Provider包裹的子组件
  let renderVdom = props.children;
  // 为后面更新做准备
  vdom.oldRenderVdom = renderVdom;
  return createDOM(renderVdom);
}

/** 挂载Context组件-Consumer */
function mountContextComponent(vdom) {
  let { type, props } = vdom;
  let context = type._context; // type = { $$typeof: REACT_CONTEXT, _context: context }
  let renderVdom = props.children(context._currentValue);
  vdom.oldRenderVdom = renderVdom;
  return createDOM(renderVdom);
}

/** 挂载类组件 */
function mountClassComponent(vdom) {
  let { type: ClassComponent, props, ref } = vdom;
  // 把类组件的属性传递给类组件的构造函数,
  // 创建类组件的实例,返回组件实例对象,以便在组件卸载时可以直接执行实例的方法
  let classInstance = new ClassComponent(props);
  if (classInstance.contextType) {
    // 把value值赋给实例的context属性
    classInstance.context = ClassComponent.contextType._currentValue;
  }

  // 在虚拟DOM上挂载classInstance,指向类的实例
  vdom.classInstance = classInstance;
  // 如果有ref,就把实例赋值给current属性
  if (ref) ref.current = classInstance;
  if (classInstance.componentWillMount) {
    classInstance.componentWillMount();
  }
  //可能是原生组件的虚拟DOM,也可能是类组件的的虚拟DOM,也可能是函数组件的虚拟DOM
  let renderVdom = classInstance.render();
  //在第一次挂载类组件的时候让类实例上添加一个oldRenderVdom=renderVdom
  // 类组件的虚拟dom的oldRenderVdom属性,指向renderVdom
  vdom.oldRenderVdom = classInstance.oldRenderVdom = renderVdom;
  let dom = createDOM(renderVdom);
  if (classInstance.componentDidMount) {
    dom.componentDidMount = classInstance.componentDidMount.bind(classInstance);
  }
  return dom;
}

/** 挂载函数组件 */
function mountFunctionComponent(vdom) {
  let { type: functionComponent, props } = vdom;
  //获取组件将要渲染的虚拟DOM
  let renderVdom = functionComponent(props);
  // 函数组件的oldRenderVdom属性,指向渲染的虚拟DOM--renderVdom
  vdom.oldRenderVdom = renderVdom;
  return createDOM(renderVdom);
}

/** 挂载经过转发的ref的函数组件 */
function mountForwardComponent(vdom) {
  let { type, props, ref } = vdom;
  let renderVdom = type.render(props, ref);
  return createDOM(renderVdom);
}

/** 如果子元素为数组,遍历挂载到容器 */
function reconcileChildren(children, parentDOM) {
  // 给每个虚拟DOM挂载mountIndex属性记录其索引
  children.forEach((childVdom, index) => {
    childVdom.mountIndex = index;
    mount(childVdom, parentDOM);
  });
}

/**
 * 把新的属性更新到真实DOM上
 * @param {*} dom 真实DOM
 * @param {*} oldProps 旧的属性对象
 * @param {*} newProps 新的属性对象
 */
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 if (/^on[A-Z].*/.test(key)) {
      // 绑定事件 ==> dom.onclick = 事件函数
      // dom[key.toLowerCase()] = newProps[key];
      // 之后不再把事件函数绑定在对应的DOM上,而是事件委托到文档对象
      addEvent(dom, key.toLowerCase(), newProps[key]);
    } else {
      dom[key] = newProps[key];
    }
  }

  for (let key in oldProps) {
    //如果说一个属性老的属性对象里有,新的属性没有,就需要删除
    if (!newProps.hasOwnProperty(key)) {
      dom[key] = null;
    }
  }
}

/**
 * DOM-DIFF:递归比较老的虚拟DOM和新的虚拟DOM,找出两者的差异,把这些差异最小化的同步到真实DOM上
 * @param {*} parentDOM 父真实DOM
 * @param {*} oldVdom 老的虚拟DOM
 * @param {*} newVdom 新的虚拟DOM
 * @param {*} nextDOM 新的虚拟DOM
 *
 */
export function compareToVdom(parentDOM, oldVdom, newVdom, nextDOM) {
  /**
    // 之前写得:
    // 拿到老的真实DOM,创建新的真实DOM,新的替换掉老的
    // 性能很差,应完善,去做深度的dom-diff 
    // 获取oldRenderVdom对应的真实DOM
    let oldDOM = findDOM(oldVdom);
    // 根据新的虚拟DOM得到新的真实DOM
    let newDOM = createDOM(newVdom);
    // 把老的真实DOM替换为新的真实DOM
    parentDOM.replaceChild(newDOM, oldDOM);
  */

  // 1.老-无 新-无:啥也不干
  if (!oldVdom && !newVdom) return;
  // 2.老-有 新-无:直接删除老节点
  if (oldVdom && !newVdom) {
    unMountVdom(oldVdom);
  }
  // 3.老-无 新-有:插入节点
  if (!oldVdom && newVdom) {
    mountVdom(parentDOM, newVdom, nextDOM);
  }
  // 4-1.老-有 新-有:判断类型不一样,删除老的,添加新的
  if (oldVdom && newVdom && oldVdom.type !== newVdom.type) {
    unMountVdom(oldVdom);
    mountVdom(parentDOM, newVdom, nextDOM);
  }
  // 4-2.老-有 新-有:判断类型一样,进行DOM-DIFF,并且节点可复用
  if (oldVdom && newVdom && oldVdom.type === newVdom.type) {
    updateElement(oldVdom, newVdom);
  }
}

/**
 * 新老DOM类型一样的更新----DOM-DIFF精髓之处
 * 如果新老DOM的类型一样,那么节点就可以复用
 */
function updateElement(oldVdom, newVdom) {
  // memo组件
  if (oldVdom.type.$$typeof === REACT_MEMO) {
    updateMemoComponent(oldVdom, newVdom);
    // Consumer组件
  } else if (oldVdom.type.$$typeof === REACT_PROVIDER) {
    updateProviderComponent(oldVdom, newVdom);
    // Provider组件
  } else if (oldVdom.type.$$typeof === REACT_CONTEXT) {
    updateContextComponent(oldVdom, newVdom);
    // 新老节点都是文本节点:复用老的节点,替换内容
  } else if (oldVdom.type === REACT_TEXT) {
    // 老的真实DOM给新的DOM的dom属性,把内容改掉
    let currentDOM = (newVdom.dom = findDOM(oldVdom));
    if (newVdom.props) currentDOM.textContent = newVdom.props.content;
    // 原生节点
  } else if (typeof oldVdom.type === "string") {
    let currentDOM = (newVdom.dom = findDOM(oldVdom));
    // 更新属性
    updateProps(currentDOM, oldVdom.props, newVdom.props);
    // 递归比较儿子
    updateChildren(currentDOM, oldVdom.props.children, newVdom.props.children);
    // 类组件或函数组件
  } else if (typeof oldVdom.type === "function") {
    // 类组件
    if (oldVdom.type.isReactComponent) {
      // 先同步实例
      newVdom.classInstance = oldVdom.classInstance;
      updateClassComponent(oldVdom, newVdom);
      // 函数组件
    } else {
      updateFunctionComponent(oldVdom, newVdom);
    }
  }
}

/** 更新memo组件 */
function updateMemoComponent(oldVdom, newVdom) {
  let { type, prevProps } = oldVdom; // 类型 老属性
  // 新老属性不相等,更新
  if (!type.compare(prevProps, newVdom.props)) {
    // 获取老的真实DOM
    let oldDOM = findDOM(oldVdom);
    // 获取真实父节点
    let parentDOM = oldDOM.parentNode;
    let { type, props } = newVdom;
    let renderVdom = type.type(props);
    compareToVdom(parentDOM, oldVdom.oldRenderVdom, renderVdom);
    newVdom.prevProps = props;
    newVdom.oldRenderVdom = renderVdom;
    // 新老属性相等,跳过更新,直接赋值
  } else {
    newVdom.prevProps = prevProps;
    newVdom.oldRenderVdom = oldVdom.oldRenderVdom;
  }
}

/** 更新Proveder组件 */
function updateProviderComponent(oldVdom, newVdom) {
  // 获取老的真实DOM
  let oldDOM = findDOM(oldVdom);
  // 获取真实父节点
  let parentDOM = oldDOM.parentNode;
  let { type, props } = newVdom;
  let context = type._context;
  // 新的属性赋值给_currentValue
  context._currentValue = props.value;
  let renderVdom = props.children;
  compareToVdom(parentDOM, oldVdom.oldRenderVdom, renderVdom);
  newVdom.oldRenderVdom = renderVdom;
}

/** 更新context组件 */
function updateContextComponent(oldVdom, newVdom) {
  // 获取老的真实DOM
  let oldDOM = findDOM(oldVdom);
  // 获取真实父节点
  let parentDOM = oldDOM.parentNode;
  let { type, props } = newVdom;
  let context = type._context;
  // 从 取值
  let renderVdom = props.children(context._currentValue);
  compareToVdom(parentDOM, oldVdom.oldRenderVdom, renderVdom);
  newVdom.oldRenderVdom = renderVdom;
}

/**
 * 更新类组件
 * @param {*} oldVdom
 * @param {*} newVdom
 */
function updateClassComponent(oldVdom, newVdom) {
  // 复用老的类组件实例
  let classInstance = (newVdom.classInstance = oldVdom.classInstance);
  if (classInstance.componentWillReceiveProps) {
    classInstance.componentWillReceiveProps(newVdom.props);
  }
  classInstance.updater.emitUpdate(newVdom.props);
}

/**
 * 更新函数组件
 * @param {*} oldVdom
 * @param {*} newVdom
 */
function updateFunctionComponent(oldVdom, newVdom) {
  // 获取老的真实DOM的父节点
  let parentDOM = findDOM(oldVdom).parentNode;
  let { type, props } = newVdom;
  let newRenderVdom = type(props);
  // 函数组件更新每次都要重新执行函数,拿到新的虚拟DOM
  compareToVdom(parentDOM, oldVdom.oldRenderVdom, newRenderVdom);
  newVdom.newRenderVdom = newRenderVdom;
}

/**
 * 递归比较子节点
 * @param {*} parentDOM
 * @param {*} oldVChildren
 * @param {*} newVChildren
 */
function updateChildren(parentDOM, oldVChildren, newVChildren) {
  // 为方便后续进行DOM-DIFF,以数组形式保存
  oldVChildren = (
    Array.isArray(oldVChildren) ? oldVChildren : [oldVChildren]
  ).filter((item) => item);
  newVChildren = (
    Array.isArray(newVChildren) ? newVChildren : [newVChildren]
  ).filter((item) => item);
  // DOM-DIFF 1.构建老map {虚拟DOM的key: 虚拟DOM}
  let keyedOldMap = {};
  oldVChildren.forEach((oldVChild, index) => {
    let oldKey = oldVChild.key ? oldVChild.key : index;
    keyedOldMap[oldKey] = oldVChild;
  });
  // 补丁包:存放要进行的操作
  let patch = [];
  // 上一个放置好的、不需要移动的索引
  let lastPlaceIndex = 0;
  // DOM-DIFF 2.遍历新数组查找老虚拟DOM
  newVChildren.forEach((newVChild, index) => {
    newVChild.mountIndex = index;
    let newKey = newVChild.key ? newVChild.key : index;
    // 查找老的虚拟DOM中是否存在这个key的节点
    let oldVChild = keyedOldMap[newKey];
    // 如果找到,复用老节点
    if (oldVChild) {
      // 先更新
      updateElement(oldVChild, newVChild);
      // 判断是否移动 把oldVChild移动到mountIndex当前索引处
      if (oldVChild.mountIndex < lastPlaceIndex) {
        patch.push({
          type: MOVE,
          oldVChild,
          newVChild,
          mountIndex: index,
        });
      }
      // 删除已经被复用的节点
      delete keyedOldMap[newKey];
      lastPlaceIndex = Math.max(oldVChild.mountIndex, lastPlaceIndex);
    } else {
      // 如果没找到,插入新节点
      patch.push({
        type: PLACEMENT,
        newVChild,
        mountIndex: index,
      });
    }
  });
  // DOM-DIFF 3.获取需要移动的元素
  let moveChildren = patch
    .filter((action) => action.type === MOVE)
    .map((action) => action.oldVChild);
  // 遍历map留下的元素(其实就是没有被复用的)
  Object.values(keyedOldMap)
    .concat(moveChildren)
    .forEach((oldVChild) => {
      // 获取老DOM
      let currentDOM = findDOM(oldVChild);
      parentDOM.removeChild(currentDOM);
    });
  // DOM-DIFF 4.插入或移动节点
  patch.forEach((action) => {
    let { type, oldVChild, newVChild, mountIndex } = action;
    // 真实DOM节点集合
    let childNodes = parentDOM.childNodes;
    if (type === PLACEMENT) {
      // 根据新的虚拟DOM创建新真实DOM
      let newDOM = createDOM(newVChild);
      // 获取老DOM中对应的索引处的真实DOM
      let childNode = childNodes[mountIndex];
      if (childNode) {
        parentDOM.insertBefore(newDOM, childNode);
      } else {
        parentDOM.appendChild(newDOM);
      }
    } else if (type === MOVE) {
      let oldDOM = findDOM(oldVChild);
      let childNode = childNodes[mountIndex];
      if (childNode) {
        parentDOM.insertBefore(oldDOM, childNode);
      } else {
        parentDOM.appendChild(oldDOM);
      }
    }
  });

  // // 最大长度
  // let maxLength = Math.max(oldVChildren.length, newVChildren.length);
  // // 每一个都进行深度对比
  // for (let i = 0; i < maxLength; i++) {
  //   // 在老的虚拟DOM查找,有老节点并且老节点真的对应一个真实DOM节点,并且这个索引要比我大(目的是找到本身的下一个节点)
  //   let nextVdom = oldVChildren.find(
  //     (item, index) => index > i && item && findDOM(item)
  //   );
  //   compareToVdom(
  //     parentDOM,
  //     oldVChildren[i],
  //     newVChildren[i],
  //     nextVdom && findDOM(nextVdom)
  //   );
  // }
}

/**
 * 插入新的真实DOM
 * @param {}} parentDOM
 * @param {*} vdom
 * @param {*} nextDOM
 */
function mountVdom(parentDOM, newVdom, nextDOM) {
  let newDOM = createDOM(newVdom);
  if (nextDOM) {
    parentDOM.insertBefore(newDOM, nextDOM);
  } else {
    parentDOM.appendChild(newDOM);
  }
  if (newDOM.componentDidMount) {
    newDOM.componentDidMount();
  }
}

/**
 * 删除老的真实DOM
 * @param {*} vdom 老的虚拟DOM
 */
function unMountVdom(vdom) {
  let { props, ref } = vdom;
  // 获取老的真实DOM
  let currentDOM = findDOM(vdom);
  // 如果这个子节点是类组件,还要执行它的卸载的生命周期函数
  if (vdom.classInstance && vdom.classInstance.componentWillUnmount) {
    vdom.classInstance.componentWillUnmount();
  }
  // 如果有ref,删除ref对应的真实DOM
  if (ref) ref.current = null;
  // 取消监听函数
  Object.keys(props).forEach((propName) => {
    if (propName.slice(0, 2) === "on") {
      // 事件在真实dom就这样做
      //   const eventName = propName.slice(2).toLowerCase()
      //   currentDOM.removeEventListener(eventName, props[propName])
      //但是我们先处理了合成事件,事件注册再store上
      delete currentDOM.store;
    }
  });
  // 如果有子节点,递归删除所有子节点
  if (props.children) {
    let children = Array.isArray(props.children)
      ? props.children
      : [props.children];
    children.forEach(unMountVdom);
  }
  // 从父节点中把自己删除
  if (currentDOM) currentDOM.parentNode.removeChild(currentDOM);
}

/** 虚拟DOM返回的真实DOM */
export function findDOM(vdom) {
  if (!vdom) return null;
  // 如果有dom属性,说明这个vdom是原生组件的虚拟DOM,会有dom属性指向真实dom
  if (vdom.dom) {
    return vdom.dom;
  } else {
    return findDOM(vdom.oldRenderVdom);
  }
}

const ReactDOM = {
  render,
};
export default ReactDOM;