手写react核心API(二)

202 阅读17分钟

手写实现react系列,实现react的常用api;参照源码简化实现,抽取核心部分,相关函数命名与源码一致。

传送

手写react核心API(一)

手写react核心API(二)

前言

还是那句话:

网上那些八股文,都是别人对某个知识点掌握后整理的描述,背下来没有任何意义!代码是数学题,不是背课文! 自己写一遍,彻底搞清楚它的实现过程原理,这样收获的才是自己的!用自己的理解总结出来的,才是真的掌握了!知其然,知其所以然!

导图:

手写react导图.png

接着上一篇继续

一、实现完整的生命周期

1.复习

先回顾一下生命周期函数有哪些,还是看图比较直接点

详情可以参考React之类组件核心及原理这里不再赘述

旧版:

image.png

新版:

新版生命周期.jpg

上一篇我们实际上已经把旧版第一部分的initialization阶段的setup props and state已经实现了,其他很多钩子函数实际也进行了处理,这里再做一次汇总!所以直接从挂载阶段开始写!

2.实现componentWillMount

上一篇代码其实已经写了,这里回顾一下!

打开react-dom.js,定位到类组件挂载函数mountClassComponent

function mountClassComponent(vdom) {
  // 挂载前……
    
  // -------------------关键代码位置---------------------    
  /* 记录vdom的实例,后面需要从该属性上拿到生命周期钩子,并执行钩子*/
  vdom.classInstance = classInstance;
  // -------------------关键代码位置---------------------    

  // 挂载中……
  
  // -------------------关键代码位置---------------------    
  /* 暂时把didMount方法暂存到dom上,前面的mount方法中会调用该钩子 */
  if (classInstance.componentDidMount) {
    /* 用bind确保其this指向始终是当前实例 */
    dom.componentDidMount = classInstance.componentDidMount.bind(classInstance);
  }
  // -------------------关键代码位置---------------------    

  return dom; // 返回真实dom
}

3.实现componentDidMount

在上面的mountClassComponent中,将componentDidMount暂存到了真实dom的属性上,在插入到容器中后,直接调用该方法即可!

定位到mount方法中

function mount(vdom, container) {
  // 调用createDom方法,传入虚拟dom,根据虚拟dom,创建出真实dom
  let newDOM = createDOM(vdom);
  // 将得到的真实dom, 插入容器中(父元素)
  container.appendChild(newDOM);
  
  // -------------------关键代码位置--------------------- 
  // 此时如果真实dom上,存在componentDidMount,就调用该生命周期函数!
  if (newDOM.componentDidMount) newDOM.componentDidMount();
  // -------------------关键代码位置--------------------- 
}

这样只实现了第一次加载的组件,此后更新时新增的还需要继续处理

再定位到compareTwoVdom方法中,在更新时,如果是新增的组件,渲染完毕后也要触发钩子

export function compareTwoVdom(parentDOM, oldVdom, newVdom, nextDOM) {
  // ……
  } else if (!oldVdom && newVdom) {
    //如果老的没有,新的有,就根据新的组件创建新的DOM并且添加到父DOM容器中
    let newDOM = createDOM(newVdom); // 创建真实dom
    if (nextDOM) {
      // 插入前看一下有没有位置标记,如果有,就放入指定的位置
      parentDOM.insertBefore(newDOM, nextDOM);
    } else {
      parentDOM.appendChild(newDOM); // 没有标记位置时,直接放到最后
    }
    
    // -------------------关键代码位置--------------------- 
    /* 新增的组件,需要触发生命周期钩子函数componentDidMount */
    if (newDOM.componentDidMount) newDOM.componentDidMount();
    // -------------------关键代码位置--------------------- 
    
  } else if (oldVdom && newVdom && oldVdom.type !== newVdom.type) {
    //新老都有,但是type不同(例如新的是div 旧的是p)也不能复用,则需要删除老的,添加新的
    let oldDOM = findDOM(oldVdom); // 先获取 老的真实DOM
    let newDOM = createDOM(newVdom); // 创建新的真实DOM

    /* 在卸载旧的组件前,需要执行生命周期钩子函数componentWillUnmount */
    if (oldVdom.classInstance && oldVdom.classInstance.componentWillUnmount) {
      oldVdom.classInstance.componentWillUnmount(); // 执行组件卸载方法
    }
    /* 通过老的父节点,用新的把旧的替换掉 */
    oldDOM.parentNode.replaceChild(newDOM, oldDOM);
    
    // -------------------关键代码位置--------------------- 
    /* 新的挂载完成后,需要执行生命周期钩子函数componentDidMount */
    if (newDOM.componentDidMount) newDOM.componentDidMount();
    // -------------------关键代码位置--------------------- 
    
  } else {
    // 老的有,新的也有,且类型也一样,需要复用老节点,进行深度的递归dom diff了
    updateElement(oldVdom, newVdom);
  }
}

4.实现componentWillReceiveProps

组件更新前对props的拦截处理,在updateClassComponent中触发该钩子

定位到updateClassComponent方法中

function updateClassComponent(oldVdom, newVdom) {
  let classInstance = (newVdom.classInstance = oldVdom.classInstance);
  newVdom.oldRenderVdom = oldVdom.oldRenderVdom;
  
  // -------------------关键代码位置--------------------- 
  /* 
    触发组件的生命周期钩子componentWillReceiveProps
    + 此更新可能是由于父组件更新引起的,父组件在重新渲染的时候,给子组件传递新的属性
  */
  if (classInstance.componentWillReceiveProps) {
    classInstance.componentWillReceiveProps();
  }
  // -------------------关键代码位置--------------------- 
  
  // 调用组件实例的updater方法,将新的props传递过去,递归继续更新!
  classInstance.updater.emitUpdate(newVdom.props);
}

5.实现shouldComponentUpdate

是否应该更新组件

这个钩子是shouldUpdate方法中触发的,通过组件实例调用此方法,再判断是否需要更新

定位到Component.js中的shouldUpdate方法

function shouldUpdate(classInstance, nextProps, nextState) {
  let willUpdate = true; // 是否要更新的标杆,默认值是true

  // -------------------关键代码位置--------------------- 
  if (classInstance.shouldComponentUpdate) {
    /* 
      shouldComponentUpdate钩子函数的处理:
      + 如果有此方法,就将此方法的返回值,作为是否更新的标杆
      + 传入nextProps 和 nextState
    */
    willUpdate = classInstance.shouldComponentUpdate(nextProps, nextState);
  }
  // -------------------关键代码位置--------------------- 

  // -------------------关键代码位置--------------------- 
  if (willUpdate && classInstance.componentWillUpdate) {
    /* 
      componentWillUpdate钩子函数的处理
      + 如果上面允许更新,且存在该钩子函数,就执行此函数
    */
    classInstance.componentWillUpdate();
  }
  // -------------------关键代码位置--------------------- 
  
  // ……
  
  if (willUpdate) {
    /* 经过上面的判断,如果依旧可以更新,则触发实例上的forceUpdate */
    classInstance.forceUpdate();
  }
}

6.实现componentWillUpdate

组件将要更新的钩子

细心的你其实已经发现,上面的代码中已经实现了componentWillUpdate

7.实现componentDidUpdate

组件更新完成的钩子

这个方法是在Component构造类的forceUpdate中调用的,也就是组件实例.forceUpdate中

定位到Component构造类 的 forceUpdate中

export class Component {
  // ……

  forceUpdate() {
    // ……更新处理

    /* 拿到新的虚拟dom后,开始走更新逻辑,调用compareTwoVdom进行vdom比对 */
    compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom);

    /* 重置旧的vdom,将新的作为下一次的旧的,用于下一次的更新 */
    this.oldRenderVdom = newRenderVdom;

    // -------------------关键代码位置--------------------- 
    if (this.componentDidUpdate) {
      /* 
        触发componentDidUpdate生命周期钩子函数
        + 将最新的props和state,以及上面的更新快照传递过去
      */
      this.componentDidUpdate(this.props, this.state, extraArgs);
    }
    // -------------------关键代码位置--------------------- 
  }
}

到此,旧版的生命周期已经实现完,接下来开始实现新版生命周期

8.实现getDerivedStateFromProps

componentWillReceiveProps的替代品!作用主要是将新的props更新到state上!

替换原因:因为以有很多人在使用componentWillReceiveProps会调用this.setState经常引起死循环!

所以:这个钩子函数被设计为静态函数,它属于构造函数,避免在这个钩子函数中访问到this!

触发该钩子时,需要通过this.constructor.getDerivedStateFromProps调用

该钩子在shouldUpdate方法中调用,定位到Compondnt.js 中的 shouldUpdate方法

function shouldUpdate(classInstance, nextProps, nextState) {
  // ……更新判断及处理,参考前面代码
  
  // -------------------关键代码位置--------------------- 
  if (classInstance.constructor.getDerivedStateFromProps) {
    /* 
      getDerivedStateFromProps钩子函数的处理
      + 传入最新props和实例state,通过该钩子得到最新的 state
      + 如果state有状态,就作为实例的state
    */
    let nextState = classInstance.constructor.getDerivedStateFromProps(
      nextProps,
      classInstance.state
    );
    if (nextState) {
      classInstance.state = nextState;
    }
  } else {
    /* 默认赋值最新的 永远指向最新的状态 */
    classInstance.state = nextState;
  }
  // -------------------关键代码位置--------------------- 
  
  if (willUpdate) {
    /* 经过上面的判断,如果依旧可以更新,则触发实例上的forceUpdate */
    classInstance.forceUpdate();
  }
}

9.实现getSnapshotBeforeUpdate

获取更新前的快照,更新前获取dom元素等信息,一般用于处理滚动条,例如在线聊天的消息位置定位

该钩子在Component的forceUpdate方法中触发,定位到forceUpdate

export class Component {
  // ……
  forceUpdate() {
    // ……

    // -------------------关键代码位置--------------------- 
    let extraArgs; // 快照的返回值
    if (this.getSnapshotBeforeUpdate) {
      /* 
        在更新前,调用getSnapshotBeforeUpdate生命周期钩子函数
        + 如果存在getSnapshotBeforeUpdate,就调用该钩子函数,将返回值赋值给extraArgs
      */
      extraArgs = this.getSnapshotBeforeUpdate();
    }
    // -------------------关键代码位置--------------------- 

    /* 拿到新的虚拟dom后,开始走更新逻辑,调用compareTwoVdom进行vdom比对 */
    compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom);

    /* 重置旧的vdom,将新的作为下一次的旧的,用于下一次的更新 */
    this.oldRenderVdom = newRenderVdom;

    if (this.componentDidUpdate) {
      /* 
        触发componentDidUpdate生命周期钩子函数
        + 将最新的props和state,以及上面的更新快照传递过去
      */
        
      // -------------------关键代码位置--------------------- 
      this.componentDidUpdate(this.props, this.state, extraArgs);
      // -------------------关键代码位置--------------------- 
    }
  }
}

10.实现componentWillMount

组件将要销毁时的钩子,在mountClassComponent中,组件卸载时触发

定位到react-dom.js 中的 mountClassComponent方法中

function mountClassComponent(vdom) {
  // ……

  /* 记录vdom的实例,后面需要从该属性上拿到生命周期钩子,并执行钩子*/
  vdom.classInstance = classInstance;
    
  // -------------------关键代码位置---------------------     
  /* 如果实例上,有componentWillMount,就执行该钩子函数! */
  if (classInstance.componentWillMount) classInstance.componentWillMount();
  // -------------------关键代码位置--------------------- 

  /* 调用实例的render方法,jsx会编译成虚拟dom对象 */
  let renderVdom = classInstance.render();
    
  // ……    
  return dom; // 返回真实dom
}

二、实现ref

1.复习

先回顾一下ref的几种写法:

1) 类组件ref的3种写法

  1. 字符串赋值+refs取值
  2. 回调函数赋值
  3. createRef + ref赋值
class Demo extends React.Component {
  ref1 = React.createRef()

  getRef = () => {
    console.log(this.ref1.current)
    console.log(this.ref2)
    console.log(this.refs.ref3) // 这种在react严格模式下会报错了
  }

  render () {
    return <>
      <div ref={this.ref1}></div>
      <div ref={(ref) => { this.ref2 = ref }}></div>
      <div ref="ref3"></div>
      <button onClick={this.getRef}>查看ref</button>
    </>
  }
}

2) 函数组件ref的3种写法

  1. 自定义变量 + ref赋值
  2. createRef + ref赋值
  3. useRef
let ref2; // 闭包
const Demo = () => {
  // createRef:每次函数更新都会重新创建,性能差
  // 写在类组件中不会:因为更新的是render,不会重复new组件
  const ref1 = React.createRef()
  // useRef:有缓存,更新时不会重新创建
  const ref3 = React.useRef()

  const getRef = () => {
    console.log(ref1.current)
    console.log(ref2)
    console.log(ref3.current)
  }

  return <>
    <div ref={ref1}></div>
    <div ref={(ref) => { ref2 = ref }}></div>
    <div ref={ref3}></div>
    <button onClick={getRef}>查看ref</button>
  </>
}

3) 获取类组件ref

class Child extends React.Component {
  inputRef = React.createRef();
  inputFocus = () => {
    this.inputRef.current.focus();
  }
  render () {
    return <input ref={this.inputRef} />
  }
}

class Parent extends React.Component {
  childRef = React.createRef();
  getChildFocus = () => {
    this.childRef.current.inputFocus();
  }
  render () {
    return (
      <div>
        <Child ref={this.childRef} />
        <button onClick={this.getChildFocus}>获得焦点</button>
      </div>
    )
  }
}

4) forwardRef

类组件能直接获取ref,是因为类组件创建时,会new,会产生实例!

而函数组件没有实例!每次函数执行完就销毁了!所以需要搭配辅助函数获取!

forwardRef

const Child = React.forwardRef((props, ref) => {
  return <input ref={ref} />
})

class Parent extends React.Component {
  childRef = React.createRef();
  getChildFocus = () => {
    this.childRef.current.focus();
  }
  render () {
    return (
      <div>
        <Child ref={this.childRef} />
        <button onClick={this.getChildFocus}>获得焦点</button>
      </div>
    )
  }
}

5) forwardRef搭配useImperativeHandle

const Child = React.forwardRef((props, ref) => {
  const childRef = React.useRef()
  const inputFocus = () => {
    childRef.current.focus()
  }

  React.useImperativeHandle(ref, () => ({
    inputFocus
  }))

  return <input ref={childRef} />
})

class Parent extends React.Component {
  childRef = React.createRef();
  getChildFocus = () => {
    this.childRef.current.inputFocus()
  }
  render () {
    return (
      <div>
        <Child ref={this.childRef} />
        <button onClick={this.getChildFocus}>获得焦点</button>
      </div>
    )
  }
}

2.实现类组件ref

这里只实现两种ref:createRef 和 回调函数ref,不去实现淘汰掉的字符串赋值写法

1) 实现createRef

打开react.js文件,新建createRef函数

/**
 * @description: 创建一个ref对象
 * @return {*} 啥也没做,就是创建一个对象而已。。。
 */
function createRef () {
  return { current: null }
}

// ……

// 记得导出!
const React = {
  createElement,
  Component,
  createRef,
  forwardRef
};

2) ref赋值处理

创建好的ref会被编译到props中,随后我们通过createElement方法将其放到了vdom上(和props平级)

上面通过createRef创建好了ref对象,接下来只需要对其赋值处理即可!

在createDOM挂载后赋值、在mountClassComponent更新组件后赋值

定位到createDOM方法中

function createDOM(vdom) {
  let { type, props, ref } = vdom;
  let dom; // 真实DOM元素
  // ……
  vdom.dom = dom; // 让虚拟DOM的dom属生指向它的真实DOM
  
  // -------------------关键代码位置--------------------- 
  if (ref) ref.current = dom; // 让ref.current属性指向真实DOM的实例
  // -------------------关键代码位置--------------------- 
  
  return dom; // 最后返回创建好的dom
}

定位到mountClassComponent方法中

function mountClassComponent(vdom) {
  /* 取出关键属性,此时type是构造函数 */
  let { type, props, ref } = vdom;

  // 初始化defaultProps,类组件中的默认props
  let defaultProps = type.defaultProps || {};

  // new 类组件构造函数,传入props,得到类组件实例
  let classInstance = new type({ ...defaultProps, ...props });
  
  // ……
    
  // -------------------关键代码位置---------------------     
  /* ref.current指向类组件的实例 */
  if (ref) ref.current = classInstance;
  // -------------------关键代码位置--------------------- 

  // ……

  return dom; // 返回真实dom
}

三、实现forwardRef

定位到react.js中,创建forwardRef函数

1.创建forwardRef组件

/**
 * 函数组件的ref转发函数
 * @param {*} render 函数组件本身
 */
function forwardRef(render) {
  return {
    // 一个标记为Symbol("react.forward_ref")的组件
    $$typeof: REACT_FORWARD_REF_TYPE,
    render, // 原来那个函数组件
  };
}

2.mountForwardComponent

现在需要对forwardRef组件进行挂载处理,上一篇已经创建好了相关函数,并且做了调用判断!

这里直接实现mountForwardComponent即可!

/**
 * 挂载forward_ref组件
 * @param {*} vdom
 */
function mountForwardComponent(vdom) {
  /* 同样先取出关键属性 */
  let { type, props, ref } = vdom;
  /* 
    type上的render函数,就是函数组件自身!调用该函数,jsx就能编译出虚拟dom!
    + 将ref属性,作为第二个参数,传递给函数组件!
    + 基于对象的堆内存原理,函数组件中对ref的修改,就能同步到父组件!
  */
  let renderVdom = type.render(props, ref);
  /* 这一步和之前一样,需要记录旧的虚拟dom */
  vdom.oldRenderVdom = renderVdom;
  /* 最后调用createDOM渲染真实dom即可! */
  return createDOM(renderVdom);
}

四、高阶组件

高阶组件的概念来自高阶函数

高阶函数:函数的参数是函数,或者函数的返回值是函数,就可以说它是一个高阶函数!(其他很多语言,例如java是不能把函数作为参数的)

高阶组件的两大用途:属性代理、反向继承

1.属性代理

核心:抽离公共状态、复用逻辑、通过props传递给下级组件!

使组件拥有强大的逻辑复用能力!

示例1

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

/* 
  + 一个显示loading状态的复用组件
  + 提供 show 和 hide 两个方法
*/
const withLoading = (OldComponent) => {
  return class extends React.Component {
    show = () => {
      let loading = document.createElement("div");
      loading.innerHTML = `<p id="loading" 
      style="position:absolute;top:100px;left:50%;z-index:10;background-color:gray">loading</p>`;
      document.body.appendChild(loading);
    };
    hide = () => {
      document.getElementById("loading").remove();
    };
    render() {
      return <OldComponent {...this.props} show={this.show} hide={this.hide} />;
    }
  };
};

@withLoading // 可通过类的装饰器实现
class Panel extends React.Component {
  render() {
    return (
      <div>
        {this.props.title}
        {/* 这里用到的show  和hide  都是从withLoading传递过来的 */}
        <button onClick={this.props.show}>显示</button>
        <button onClick={this.props.hide}>隐藏</button>
      </div>
    );
  }
}
//let LoadingPanel = withLoading(Panel); // 也可直接调用函数

ReactDOM.render(<Panel title="这是标题" />, document.getElementById("root"));

实例2

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

function withTracker(OldComponent) {
  return class extends React.Component {
    state = {
      x: 0,
      y: 0,
    };
    handleMouseMove = (event) => {
      this.setState({
        x: event.clientX,
        y: event.clientY,
      });
    };
    render() {
      return (
        <div onMouseMove={this.handleMouseMove}>
          <OldComponent {...this.state} />
        </div>
      );
    }
  };
}

function Welcome(props) {
  return (
    <div>
      <h1>移动鼠标</h1>
      <p>
        当前的鼠标位置是x={props.x},y={props.y}
      </p>
    </div>
  );
}

let Tracker = withTracker(Welcome);

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

2.反向继承

在不修改组件代码的前提下,对组件进行改写!

需求:渲染某个别人的组件时,我想在不修改其代码的基础上,修改其内部的属性和children,请问该怎么做?

// 假如这是一个antd的按钮组件,理论上无法对其进行修改
class AntdButton extends React.Component {
  state = { name: "default" };
  render() {
    return <button className={this.state.name}>{this.props.title}</button>;
  }
}

const wapper = (component) => {
  return class extends component {
    state = { number: 0 };
    handlerClick = () => {
      this.setState({ number: this.state.number + 1 });
    };
    render() {
      // 通过super,调用被继承组件的render,被反向继承组件的虚拟dom
      const renderElement = super.render();
      /* 此时element是被冻结的!你无法对其进行任何修改!需要搭配cloneElement使用! */
      const newProps = {
        ...renderElement.props,
        onClick: this.handlerClick,
      };
      // 借助cloneElement,将新的props和children(第三个及之后的参数)传过去,创建一个新的虚拟dom
      const cloneElement = React.cloneElement(
        renderElement,
        newProps,
        this.state.number
      );
      return cloneElement;
    }
  };
};

const WapperAntdBtn = wapper(AntdButton);

ReactDOM.render(
  <WapperAntdBtn title="这是标题" />,
  document.getElementById("root")
);

3.实现cloneElement

在上面的高阶组件实例中有说到,基于安全考虑,jsx编译出来的虚拟dom是无法被修改的!

所以我们在实现反向继承时,需要用到cloneElement,现在开始实现这个api(其实很简单)

/**
 * 根据一个老的元素,克隆出一个新的元素
 * @param {*} oldElement 老元素
 * @param {*} newProps 新属性
 * @param {*} children 新的儿子们
 */
function cloneElement(oldElement, newProps, children) {
  if (arguments.length > 3) {
    children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
  } else {
    children = wrapToVdom(children);
  }
  let props = { ...oldElement.props, ...newProps, children };
  return { ...oldElement, props };
}

五、实现context

前面我们已经预留了context组件的挂载函数,以及调用判断,现在开始实现context

1.实现createContext

定位到react.js中

/**
 * 创建上下文组件
 * @param {*} render 函数组件本身
 */
function createContext() {
  /* 标记他是一个上下文组件 */
  let context = { $$typeof: REACT_CONTEXT };
  /* 标记Provider组件 */
  context.Provider = { $$typeof: REACT_PROVIDER, _context: context };
  /* 标记Consumer属性, Consumer的type 就是 REACT_CONTEXT!  */
  context.Consumer = { $$typeof: REACT_CONTEXT, _context: context };
  return context;
}

2.完善mountProviderComponent

上下文组件创建好后,需要完善其挂载方法

/**
 * 挂载上下文的provider组件
 * @param {*} vdom
 */
function mountProviderComponent(vdom) {
  /* 取出关键属性 */
  let { type, props } = vdom;
  // 在渲染Provider组件的时候,拿到属性中的value,赋给context._currentValue
  type._context._currentValue = props.value;
  /* props.children就是renderDom! */
  let renderVdom = props.children;
  /* 同样需要记住旧的,后面比对更新会用 */
  vdom.oldRenderVdom = renderVdom;
  /* 调用createDOM创建真实dom */
  return createDOM(renderVdom);
}

3.完善mountClassComponent

前面实际已经把代码写好了,但还是看一眼

function mountClassComponent(vdom) {
  /* 取出关键属性,此时type是构造函数 */
  let { type, props, ref } = vdom;

  // ……

  if (type.contextType) {
    /* 
      对类组件context的处理:
      + 如果构造函数中有contextType属性,就将其_currentValue赋值给实例的context
      + contextType必须加static的原因就在这里,它是类组件自身的,而不是实例的!
    */
    classInstance.context = type.contextType._currentValue;
  }

 // ……

  return dom; // 返回真实dom
}

4.完善mountContextComponent

Consumer组件的挂载逻辑

/**
 * 挂载上下文组件
 * @param {*} vdom
 */
function mountContextComponent(vdom) {
  /* 取出关键属性 */
  let { type, props } = vdom;
  /* 
    props返回值中的children属性,就是写在Consumer组件内的子组件
    + Consumer内的组件是一个函数!
    + 例如  <Context.Consumer value={}> {
                (props)=> <Children ...props/>
            }</Context.Consumer>
  */
  let renderVdom = props.children(type._context._currentValue);
  // 同样保留旧的虚拟dom用于下一次更新比对
  vdom.oldRenderVdom = renderVdom;
  /* 调用createDOM创建真实dom */
  return createDOM(renderVdom);
}

5.完善updateProviderComponent

provider组件的更新逻辑

function updateProviderComponent(oldVdom, newVdom) {
  /* 找到父节点的真实dom */
  let parentDOM = findDOM(oldVdom).parentNode;
  let { type, props } = newVdom;
  /* 重新对_context._currentValue赋值 */
  type._context._currentValue = props.value;
  let renderVdom = props.children;
  /* 比对更新 */
  compareTwoVdom(parentDOM, oldVdom.oldRenderVdom, renderVdom);
  /* 重置oldRenderVdom */
  newVdom.oldRenderVdom = renderVdom;
}

6.完善updateContextComponent

consumer组件的更新逻辑

function updateContextComponent(oldVdom, newVdom) {
  /* 找到父节点的真实dom */
  let parentDOM = findDOM(oldVdom).parentNode;
  let { type, props } = newVdom;
  /* 和挂载时一样,调用children传入新的上下文数据 */
  let renderVdom = props.children(type._context._currentValue);
  /* 比对更新渲染真实dom */
  compareTwoVdom(parentDOM, oldVdom.oldRenderVdom, renderVdom);
  /* 重置oldRenderVdom */
  newVdom.oldRenderVdom = renderVdom;
}

六、实现PureComponent

PureComponent就是帮我们加了一个shouldComponentUpdate钩子函数,并且在里面做了浅比较

为什么是浅比较?为了性能!如果有很深的数据,做深比较,太浪费性能!再加上react没有依赖收集!每次都是从root更新!就是慢上加慢!

export class PureComponent extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    // utils中的浅比较方法排上用场了
    return (
      !shallowEqual(this.props, nextProps) ||
      !shallowEqual(this.state, nextState)
    );
  }
}

七、实现memo

memo和PureComponent的作用差不多

1.创建memo组件

/**
 * memo组件
 * @param {*} type 原组件
 * @param {*} compare compare 
 * 第二个参数是函数 是自定义的比对逻辑,根据该返回值决定是否更新
 * 如果不传,默认是浅比较!
 */
function memo(type, compare = shallowEqual) {
  return {
    $$typeof: REACT_MEMO,
    type, //原来那个真正的函数组件
    compare,
  };
}

2.挂载memo组件

完善前面创建好的mountMemoComponent函数

/**
 * 挂载memo组件
 * @param {*} vdom
 */
function mountMemoComponent(vdom) {
  /* 同样取出关键参数 */
  let { type, props } = vdom;
  /* type就是memo接收的函数组件,调用函数并传入props */
  let renderVdom = type.type(props);
  /* 记录一下老的属性对象,在更新的时候会用到 */
  vdom.prevProps = props;
  vdom.oldRenderVdom = renderVdom;
  /* 创建真实dom */
  return createDOM(renderVdom);
}

3.更新memo组件

/**
 * 更新memo组件
 * @param {*} oldVdom 旧的虚拟dom
 * @param {*} newVdom 新的虚拟dom
 */
function updateMemoComponent(oldVdom, newVdom) {
  let { type, prevProps } = oldVdom;
  //比较老的属性对象和新的属性对象是否相等

  // 就算未渲染该组件也必须执行render 保证hook的hookIndex能下移
  let renderVdom = newVdom.type.type(newVdom.props);

  /* 取出useReducer时,缓存在当前挂载实例mountingComponent(vdom) 上面的hooks */
  let hookKeys = Object.keys(oldVdom.hooks);

  if (
    type.compare(prevProps, newVdom.props) &&
    hookKeys.every((key) => hookState[key] === oldVdom.hooks[key])
  ) {
    /* 
        调用compare方法,传入新旧props和state进行浅比较,如果相同,就不能更新!
        同时需要通知基于useReducer绑定的所有hooks执行!!
    */
    /* oldRenderVdom传递 不做更改 */
    newVdom.oldRenderVdom = oldVdom.oldRenderVdom;
    /* props传递 不做更改 */
    newVdom.prevProps = oldVdom.props;
    /* 执行全部关联的hook */
    hookKeys.forEach((key) => {
      oldVdom.hooks[key] = hookState[key];
    });
  } else {
    /* 需要更新 找到父节点的真实dom */
    let parentDOM = findDOM(oldVdom).parentNode;
    /* 比对更新 */
    compareTwoVdom(parentDOM, oldVdom.oldRenderVdom, renderVdom);
    /* 重置props和oldRenderVdom */
    newVdom.prevProps = newVdom.props;
    newVdom.oldRenderVdom = renderVdom;
  }
  newVdom.hooks = oldVdom.hooks; // 将新hooks 缓存下来,下一次使用
}

接下来开始实现react中的几个常用hooks

八、实现useReducer

先看上一篇创建的几个状态

// 缓存当前挂载的实例,reducer需要用到这个属性
let mountingComponent = null;
// 这里存放着所有的状态,源码时fiber链表,这里用数组简单实现
let hookState = [];
// 当前的执行的hook的索引
let hookIndex = 0;
// 调度更新方法,数据变化后 能找到组件对应的此方法更新视图

关键点 都在注释上

/**
 * 实现useReducer
 * @param {*} reducer 需要执行的操作
 * @param {*} initialState 初始状态
 */
export function useReducer(reducer, initialState) {
  /* 
    从hookState中取出对应的数据,源码时fiber链表,这里用数组替代
    + 如果hookState中没有,就新增,有救取值
  */
  if (!hookState[hookIndex]) {
    /* 新增hook,将初始状态存进去 */
    hookState[hookIndex] = initialState;
    // 将对应的hooks 下标和值缓存到当前挂载的实例
    if (mountingComponent && !mountingComponent.hooks) {
      mountingComponent.hooks = {};
      mountingComponent.hooks[hookIndex] = hookState[hookIndex];
    }
  }
  /* 闭包缓存当前hook对应的索引! */
  let currentIndex = hookIndex;

  /* 改变状态的方法 */
  function dispatch(action) {
    /* 
        如果初始时传了reducer,就用reducer的返回值作为新的state
        如果没传,就用传进来的直接覆盖原状态!
        + useState是useReducer的语法糖!
        + 这玩意就是redux的作者 被谷歌挖过去后写的!
    */
    hookState[currentIndex] = reducer
      ? reducer(hookState[currentIndex], action)
      : action;

    /* 
        触发前面保存的更新函数,再补充:
        + react的每一次更新都是从根元素开始!没有做依赖收集!
        + 所以它的性能真的不如vue!别杠!
    */
    scheduleUpdate();
  }
  
  /* 
    注意这里!!!hookIndex++ !有新的hook进来时,保证其能往数组里面加!
    每次触发scheduleUpdate更新完后,hookIndex会重置!
  */
  return [hookState[hookIndex++], dispatch];
}

九、实现useState

useState是useReducer的语法糖!!上面useReducer代码中的dispatch中做的处理,就是给useState的!

export function useState(initialState) {
  // 调用useReducer,reducer传null,disPatch时就直接用新的state覆盖旧的
  return useReducer(null, initialState);
}

十、实现useMemo

export function useMemo(factory, deps) {
  /* 这一块的处理和前面的useReducer差不多 */
  if (hookState[hookIndex]) {
    // 如果有值,说明不是第一次是更新
    // 取出上一次的memo函数,以及上一次收集的依赖
    let [lastMemo, lastDeps] = hookState[hookIndex];
    // 遍历比对新的依赖和旧的依赖是否相等!
    let everySame = deps.every((item, index) => item === lastDeps[index]);
    if (everySame) {
      // 如果相等,不做任何处理,同时让hookIndex递增,确保下一个hook能正常执行
      hookIndex++;
      return lastMemo; // 用上一个memo作为返回值
    } else {
      /* 依赖发生变化,重新执行factory函数,获取最新数据 */
      let newMemo = factory();
      // 赋值的同时,让hookIndex++ 确保下一个hook正常实行
      hookState[hookIndex++] = [newMemo, deps];
      return newMemo; // 返回最新的数据
    }
  } else {
    /* 新增hook 调用 factory的返回值作为数据*/
    let newMemo = factory();
    // 赋值的同时,让hookIndex++ 确保下一个hook正常实行
    hookState[hookIndex++] = [newMemo, deps];
    return newMemo; // 返回最新的数据
  }
}

十一、实现useRef

这个可能是最简单的hook,看完上面的,这里应该不用写注释了吧。。。

这个和createRef的区别就在于,useRef有缓存,所以哪怕你能在函数组件中使用createRef,也不建议你用!

export function useRef() {
  if (hookState[hookIndex]) {
    return hookState[hookIndex++];
  } else {
    hookState[hookIndex] = { current: null };
    return hookState[hookIndex++];
  }
}

十二、实现useEffect

这个应该是最难的一个hook,它的特性、原理、实现、都需要掌握大量js知识!

/**
 * @param {*} callback 当前渲染完成之后下一个宏任务
 * @param {*} deps 依赖数组,
 */
export function useEffect(callback, deps) {
  if (hookState[hookIndex]) {
    /* 如果hook存在 则取出关键属性 */
    let [destroy, lastDeps] = hookState[hookIndex];
    /* 比对收集的依赖是否发生变化,和useMemo大致差不多 */
    let everySame = deps.every((item, index) => item === lastDeps[index]);
    if (everySame) {
      hookIndex++; // 如果一样,就不需要做任何处理
    } else {
      // 销毁函数每次都是在下一次执行的时候才会触发执行
      destroy && destroy(); //先执行销毁函数
      setTimeout(() => {
        // 在下一个宏任务中,重新调用callback将返回值作为destroy
        let destroy = callback();
        // 重新给hook赋值
        hookState[hookIndex++] = [destroy, deps];
      });
    }
  } else {
    //初次渲染的时候,开启一个宏任务,在宏任务里执行callback,保存销毁函数和依赖数组
    setTimeout(() => {
      // 调用callback 如果传入的函数有返回值,就作为销毁函数存起来
      let destroy = callback();
      hookState[hookIndex++] = [destroy, deps];
    });
  }
}

十三、实现useLayoutEffect

这个钩子和useEffect的区别:

  • useEffect是在宏任务中执行,会在dom渲染完成后执行!
  • useLayoutEffect是在微任务中执行,会在在dom渲染时同步执行
  • 其实现原理和useEffect几乎一样,只是setTimeout变成了queueMicrotask
export function useLayoutEffect(callback, deps) {
  if (hookState[hookIndex]) {
    let [destroy, lastDeps] = hookState[hookIndex];
    let everySame = deps.every((item, index) => item === lastDeps[index]);
    if (everySame) {
      hookIndex++;
    } else {
      destroy && destroy(); //先执行销毁函数
      queueMicrotask(() => {
        let destroy = callback();
        hookState[hookIndex++] = [destroy, deps];
      });
    }
  } else {
    //初次渲染的时候,开启一个宏任务,在宏任务里执行callback,保存销毁函数和依赖数组
    queueMicrotask(() => {
      let destroy = callback();
      hookState[hookIndex++] = [destroy, deps];
    });
  }
}

十四、实现useCallback

看过上面的实现原理,相信这里已经可以不加注释了吧~

export function useCallback(callback, deps) {
  if (hookState[hookIndex]) {
    let [lastCallback, lastDeps] = hookState[hookIndex];
    let everySame = deps.every((item, index) => item === lastDeps[index]);
    if (everySame) {
      hookIndex++;
      return lastCallback;
    } else {
      hookState[hookIndex++] = [callback, deps];
      return callback;
    }
  } else {
    hookState[hookIndex++] = [callback, deps];
    return callback;
  }
}

十五、实现useContext

这个超级简单将传入的context上的_currentValue属性取出来就行了

function useContext(context) {
  return context._currentValue;
}

十六、实现useImperativeHandle

这个也超级简单,将接收的第二个参数(函数)的返回值,赋值给第一个参数ref就行了。

function useImperativeHandle(ref, factory) {
  ref.current = factory();
}

结语

到此,基本api已经全部实现,都是超级精简的代码!注重其实现原理!

如果需要代码笔记的,可以私信我~