美文网首页
听说你想写个 React - Component

听说你想写个 React - Component

作者: 微微笑的蜗牛 | 来源:发表于2021-07-10 12:53 被阅读0次

大家好,我是微微笑的蜗牛,🐌。

上一篇文章介绍了 virtual dom,以及简单的 diff 操作。

这篇文章将介绍 Component、State、组件生命周期的实现。

React.Component

在 React 中,Component 是所有组件的基类。

它内部定义了 render 方法用于返回节点信息,同时包含 state、props 属性用于存储数据。在更新 state 后,组件会自动根据前后数据差异来更新页面。

比如我们可自定义 MyComponent,继承自 React.Component。在 render 方法中返回节点描述。

class MyComponent extends React.Component {
    render() {
        const { name } = this.state;
        return <span>{name}</span>;
    }
}

在使用自定义组件时,像标签一样使用即可。

const element = (
  <MyComponent id="container">
    <a href="/bar">bar</a>
  </MyComponent>
);

上述也是 jsx 的写法,那么它在经过 babel 转义后,结构会与 div 这类内置标签的有所不同吗?

通过 babel 提供的工具,以上代码转义后的结构如下:

image

我们可以发现,组件和普通标签转换后还是有些差异的。

标签传入的节点类型是字符串,而组件却是类 MyComponent。所以,这可作为区分它们的标志。

Component 实现

我们可以仿造 React 定义自己的 Component 基类,包括 props 和 state。

class Component {
  constructor(props) {
    this.props = props;
    this.state = this.state || {};
  }
}

由于组件和标签经 babel 转换后的类型不同,各自的处理也不一样。这样一来,构建 virutal dom 的实现需要做出修改,根据类型做不同处理。

// virtual dom,保存真实的 dom,element,childInstance
function instantitate(element) {
  const { type, props } = element;

  // 如果 type 是字符串,则表明非组件
  const isDomElement = typeof type === "string";

  if (isDomElement) {
    // 省略
  } else {
    // 处理组件
  }
}

组件 vdom 结构

由于组件的 render 方法返回的是节点信息,那么我们必然会主动调用到 render 方法来获取节点描述信息。因此,在 vdom 中关联组件的实例是很有必要的,这样才能方便调用到组件的 render 方法。

所以呢,在原有 vdom 的结构中就需多出一个字段来记录其关联的组件实例,我们称之为 publicInstance。

同时,由于组件的 render 方法只能返回一个节点,所以子节点的 virtual dom 无需用数组来保存,只需一个普通对象即可。

组件 vdom 结构修改如下:

  • 新增 publicInstance,保存关联的组件实例。
  • 子节点 vdom 由 childInstances → childInstance,不再是数组。

组件 vdom 各字段定义如下:

{
  dom,
  element,
  childInstance,
  publicInstance,
}

组件 vdom 构建

组件 vdom 构建方式也有所改变,大体过程如下:

  • 创建组件实例
  • 调用组件 render 方法,返回节点描述信息
  • 创建子节点 dom & vdom
  • 返回 vdom 结构
const instance = {};

// 创建组件实例
const publicInstance = createPublicInstance(element, instance);

// 调用组件的 render 方法,返回节点
const childElement = publicInstance.render();

// 调用 instantiate 创建 dom & virual dom,因为 render 方法只能返回一个节点
const childInstance = instantitate(childElement);

const dom = childInstance.dom;

Object.assign(instance, {
  dom,
  element,
  childInstance,
  publicInstance,
});

return instance;

State

我们知道,Component 是以数据来驱动界面刷新的。当要刷新 UI 时,需主动调用 setState 来更新数据,直接更新 state 是无效的。

😃用头发丝想想,在 setState 方法中,一定会涉及到 dom diff 的处理。

这样一来,在 Component 内部,也就需要知道对应的 vdom 实例,以此来触发 diff 操作。

下面,我们来看看这两个关键点的实现。

1. 在创建组件实例时,关联 vdom 节点。

// 创建组件对象,内部关联 vdom 实例
function createPublicInstance(element, internalInstance) {
  const { type, props } = element;

    // 创建组件实例
  const publicInstance = new type(props);

    // 关联 vdom 节点
  publicInstance.__internalInstance = internalInstance;

  return publicInstance;
}

可注意看,组件实例的创建是使用 new type(props); 的方式,同时传入了 props。

从上图 babel 转义后的 type 数据,应该就能明白为什么可以这样写。

2. 调用 setState 时,触发 diff 操作。

先更新 state 数据,然后做 diff 操作更新组件。

setState(state) {
    this.state = Object.assign({}, this.state, state);
    
    // __interalInstace 为 virutal dom
    updateInstance(this.__internalInstance);
}

function updateInstance(internalInstance) {
  const parentDom = internalInstance.dom.parentNode;
  const element = internalInstance.element;

  reconcile(parentDom, internalInstance, element);
}

注意,由于普通标签和组件的子节点 vdom 结构是不一样的。一个是数组,一个是单独的对象,所以在进行 diff 时也需区分处理。

普通标签处理

普通标签的 reconcile 处理:

  • 更新节点属性
  • 处理子节点 diff
  • 更新 vdom 属性

代码处理如下:

if (typeof element.type === "string") {
    console.log("reuse dom");

    // 重用节点,更新属性
    updateDomProperties(instance.dom, instance.element.props, element.props);

    // 处理子节点 diff
    instance.childInstances = reconcileChildren(instance, element);

    // 更新 element
    instance.element = element;

    return instance;
}

组件处理

组件的 reconcile 处理:

  • 组件属性的更新
  • 调用组件 render 方法
  • 进行 diff 操作
  • 更新 vdom 各个属性

代码处理如下:

{
    console.log("update component");

    // component
    // 更新 props
    instance.publicInstance.props = element.props;

    // 重新构建节点信息
    const childElement = instance.publicInstance.render();
    const oldChildInstance = instance.childInstance;

    // 更新 dom 节点
    const childInstance = reconcile(parentDom, oldChildInstance, childElement);

    // 更新 vdom 各个属性
    instance.childInstance = childInstance;
    instance.dom = childInstance.dom;
    instance.element = element;

    return instance;
}

组件生命周期

React 中的组件有一系列的生命周期。在此,我们打算实现如下生命周期:

// 组件即将挂载
componentWillMount();

// 组件挂载完毕
componentDidMount();

// 组件即将移除
componentWillUnmount();

// 将收到新的 props
componentWillReceiveProps(nextProps);

// 组件即将更新
componentWillUpdate(nextProps, nextState);

// 组件更新完毕
componentDidUpdate(prevProps, prevState);

componentWillMount

  • 含义:组件即将挂载。
  • 调用时机:在组件即将添加到 dom 树上调用。
function reconcile(parentDom, instance, element) {
  if (instance == null) {
        const isComponent = typeof newInstance.element.type !== "string";

    const newInstance = instantitate(element);

        // 组件生命周期 componentWillMount
    if (isComponent) {
      newInstance.publicInstance.componentWillMount();
    }

    parentDom.appendChild(newInstance.dom);

        // 组件生命周期 componentDidMount
    if (isComponent) {
      newInstance.publicInstance.componentDidMount();
    }

    return newInstance;
  }

    // ...
}

componentDidMount

  • 含义:组件挂载完毕。
  • 调用时机:在组件添加到 dom 树后调用。代码可参照 componentWillMount 一节。

componentWillUnmount

  • 含义:组件即将移除。
  • 调用时机:准备从 dom 树移除时调用。
function reconcile(parentDom, instance, element) {
    if (element == null) {
    console.log("remove dom");
    // remove,若新子节点数 < 原节点数,需移除
    parentDom.removeChild(instance.dom);

    // 组件生命周期 componentWillUnmount
    if (typeof instance.element.type !== "string") {
      instance.publicInstance.componentWillUnmount();
    }

    return null;
  }
}

componentWillReceiveProps

  • 含义:组件即将更新属性。
  • 调用时机:在 reconcile 的组件 diff 中处理。
function reconcile(parentDom, instance, element) {
    // ....
    else {
    // component
    // 更新 props
    // 组件生命周期 componentWillReceiveProps
    instance.publicInstance.componentWillReceiveProps(element.props);

    instance.publicInstance.props = element.props;

    // ...
    return instance;
  }
}

componentWillUpdate

  • 含义:组件即将更新。
  • 调用时机:在更新 dom 树前,需调用 shouldComponentUpdate 判断是否需要真正的更新。

shouldComponentUpdate 的默认实现是:判断当前属性和传入属性是否不等,或者当前 state 和传入 state 是否不等。任一不等,即认为组件应该进行更新。

shouldComponentUpdate(nextProps, nextState) {
    return nextProps != this.props || nextState != this.state;
 }

componentWillUpdate 调用时机,在更新组件前。

setState(nextState) {
    if (this.__internalInstance && this.shouldComponentUpdate(this.props, nextState)) {
      // 组件生命周期 componentWillUpdate
      this.componentWillUpdate(this.props, nextState);

      this.state = Object.assign({}, this.state, nextState);

      updateInstance(this.__internalInstance);

      // 组件生命周期 componentDidUpdate
      this.componentDidUpdate(this.props, nextState);
    } else {
      this.state = Object.assign({}, this.state, nextState);
    }
  }

componentDidUpdate

  • 含义:组件更新完毕。
  • 调用时机:在调用 updateInstance 后,即可触发。代码可参照 componentWillUpdate 一节。

Demo

这样,我们就可以继承于自己实现的 SLReact.Component,来自定义组件了。

class Story extends SLReact.Component {
  constructor(props) {
    super(props);

    this.state = {
      likes: ramdomLikes(),
    };
  }

  componentDidMount() {
    super.componentDidMount();
    console.log("Story componentDidMount");
  }

  handleClick() {
    this.setState({
      likes: this.state.likes + 1,
    });
  }

  render() {
    const { name, url } = this.props;
    const { likes } = this.state;

    return (
      <li>
        <button onClick={(e) => this.handleClick()}>{likes}🐶</button>
        <a href={url}>{name}</a>
      </li>
    );
  }
}

跟 React 中的写法一毛一样。

以上只是组件的定义。有兴趣的童鞋可下载完整 Demo 代码,自行运行试试。

Demo 地址:https://github.com/silan-liu/slreact/tree/master/part4

总结

这篇文章主要介绍了 Component 和 State 的实现,组件实例和 virutal dom 实例相互关联。

至此,「听说你想写个 React」已全部完结,整体内容比较简单,感谢阅读~

该系列全部文章如下:

相关文章

网友评论

      本文标题:听说你想写个 React - Component

      本文链接:https://www.haomeiwen.com/subject/lfeojltx.html