美文网首页
听说你想写个 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