(扩展 vue 思维)使用 React 构建一个轮子(LoadingBar加载组件)让 useState 挂载 ref 实例,函数外部 js 调用实例 ref.useState 实现子组件更新

11 篇文章 0 订阅
9 篇文章 1 订阅

一、组件概述、使用场景描述(Vue2版本)

1、思维,及实现方式描述

在这里插入图片描述
先上一个图,这边的ui库是我这边之前使用vue2版本构建的一个组件库
tinkerbell-ui(vue2)

对应的说一下功能 总的来说就是一个加载组件,那么这个加载组件的话,首先还是先过一下对应的逻辑思维以及如何去实现

1、首先是一个组件,那如果我想在一个vue实例上面使用this.$loading.start() 触发我编写的 .vue 组件可以参考一下对应组件实例挂载触发文章
2、仔细看下面这两步

const Profile = Vue.extend({xxx})
new Profile().$mount(’#mount-point’)

这是一个在js中挂载实例的方法,那同样的因为本身Profile就是一个类,这个时候我们就可以使用 this 抓到实例,并且可以根据这个this.去操纵里面的data、methods数据,实现数据量的更新,因为本身在vue里面就已经将这些数据方法全部挂载到了其组件实例上,所以在 Profile.prototype 可以大肆旗鼓的使用this去调用实例方法并且可以实现数据的响应式更新

当然还是把代码贴一下 有兴趣的同学也可以根据注释参考自己尝试一下,并且实际的应用在自己的项目当中

2、main.vue


<template>
  <div class="tb__loading-bar">
    <div class="tb__loading-bar--bar" :class="isError ? 'tb__loading-bar--error' : ''" role="bar" :style="{ transform: 'translate3d(-' + (100 - totalProgress) + '%, 0, 0)' }">
      <div class="tb__loading-bar--peg"></div>
    </div>
    <div class="tb__loading-bar--spinner" role="spinner" v-if="showSpinner">
      <div class="spinner-icon" :class="isError ? 'spinner-icon--error' : ''" :style="{ animation: 'tb-spinner 400ms ' + easing + ' infinite' }"></div>
    </div>
  </div>
</template>

<script>
export default {
  name: "tbLoadingBar",
  props: {
    type: {
      type: Number,
      default: 1,
    },
  },
  data() {
    return {
      // 200ms进度 格  越大一次性进度越多0-100
      speed: 1,
      // 小圆圈动画样式  linear   ease   ease-in   ease-out   ease-in-out   cubic-bezier(n,n,n,n)
      easing: "linear",
      // 速率0-1
      percentNum: 0,
      // 进度条长度
      totalProgress: 0,
      // 是否显示圆圈动画
      showSpinner: true,
      // 是否为错误error
      isError: false,
    };
  },
};
</script>
<style lang="less">
.tb__loading-bar {
  &--bar {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 2px;
    z-index: 9999;
    transition: all 200ms ease;
    transform: translate3d(-100%, 0, 0);
    background: #1089ff;
  }
  &--error {
    background: #ff4d4f;
  }
  &--peg {
    display: block;
    position: absolute;
    right: 0;
    width: 100px;
    height: 100%;
    box-shadow: 0 0 10px #fea638, 0 0 5px #fea638;
    opacity: 1;
    transform: rotate(3deg) translate(0px, -4px);
  }
  &--spinner {
    display: block;
    position: fixed;
    z-index: 1031;
    top: 15px;
    right: 15px;
    .spinner-icon {
      width: 18px;
      height: 18px;
      box-sizing: border-box;
      border: solid 2px transparent;
      border-top-color: #1089ff;
      border-left-color: #1089ff;
      border-radius: 50%;
    }
    .spinner-icon--error {
      border-top-color: #ff4d4f;
      border-left-color: #ff4d4f;
    }
  }
}
@keyframes tb-spinner {
  0% {
    transform: rotate(0);
  }
  to {
    // 转一圈
    transform: rotate(1turn);
  }
}
</style>

3、index.js


// 导入组件,组件必须声明 name
import Vue from "vue";
import LoadingBar from "./main.vue";

/**
 * @description: 此步用于获取实例   Vue.extend(LoadingBar) 用于构建模板:但注意此时对应的数据还并没有实例化就相当于  是Vue 还没有new
 * @param {*}
 * @return {*}
 */
let LoadingBarConstructor = Vue.extend(LoadingBar);

let timer = null;
let removeTimer = null;

/**
 * @description: 返还的是一个实例化对象  也就是相当于返还了一个组件  可以这样去理解
 * @param {*}
 * @return {*}
 */

let tbLoadingBar = () => {
  return new LoadingBarConstructor();
};

/**
 * @description:
 * @param {*} options 参数为一个对象,注意 prototype中的this指向的便是实例化后 的this 所以我们就可以在这里使用this 进行变更组件中data的值
 * @return {*}
 */
LoadingBarConstructor.prototype.config = function(options) {
  // 便利对应的
  Object.keys(options).forEach((key) => {
    // 在这里我们不能让其传入对应的isError  和  totalProgress  因为这两个参数分别代表了错误状态以及对应的加载进度条行走进度
    if (key === "isError" || key === "totalProgress") {
      // 抛出错误
      throw new Error("配置传递错误!");
    }
    // 对应data中的配置值  也就等于我们自己定义的key值
    this[key] = options[key];
  });
  // 完成之后调用一次this.start()
  this.start();
};

/**
 * @description: 此处用于初始化对应的vue模板 将对应的挂载的模板真正的追加到document.body文档当中,因为样式我们已经写好所以 不用考虑冲突的问题
 * @param {*}
 * @return {*}
 */
LoadingBarConstructor.prototype.init = function() {
  // 首先初始化我们要做的就是清空对应的计时器 相当于一个节流,主要为了用于防止重复点击
  clearTimeout(timer);
  // 进度初始值变为0
  this.totalProgress = 0;

  // 是否错误状态设置为false初始值
  this.isError = false;

  // 手动挂载将一个模板,new出来成为一个实例  对应的返回值就是这个vue实例  对应的我们挂载到this.vm上面
  this.vm = this.$mount();
  // 其实此处挂与不挂都是可以的,因为两者指向的都是vue实例
  // console.log(this === this.vm);
  // 因为我们已经实例化了,所以我们就可以调用this.vm.$el  这样的话就相当于于将一个creatElement 填充到document上面
  /**
   * Vue2 官网实例  所以我们就可以采用在文档之外渲染并且随后挂载的方式进行挂载
   *  同理注意 我们的this.$mount 其实就相当于实例化之后的new MyComponent()
   *  var MyComponent = Vue.extend({
        template: '<div>Hello!</div>'
      })

      // 创建并挂载到 #app (会替换 #app)
      new MyComponent().$mount('#app')

      // 同上
      new MyComponent({ el: '#app' })

      // 或者,在文档之外渲染并且随后挂载
      var component = new MyComponent().$mount()
      document.getElementById('app').appendChild(component.$el)
   */
  // 追加
  document.body.appendChild(this.vm.$el);
  // debugger
  return this;
};

/**
 * @description: 特定开始api
 * @param {*}
 * @return {*}
 */
LoadingBarConstructor.prototype.start = function() {
  // 每次开始的时候我们调用对应的初始化api 进行初始化
  this.init();
  /**
   * @description: 定时器  可以理解为每次点击按钮的时候每次重置
   * @param {*} setInterval
   * @return {*}
   */
  timer = setInterval(() => {
    // 每次都要判断  最多到90  因为我们要的其实只是一个状态,真正关闭的时候  通常一个ajax关闭是需要在调用完毕之后再去调用对应的end关闭方法
    if (this.totalProgress < 90) {
      // 如果有传递进来的值 就用传递进来的值,反之就用随机值 * 每毫秒加载几个格
      this.totalProgress += (this.percentNum || Math.random()) * this.speed;
    }
  }, 100);
};
/**
 * @description: 特定结束api
 * @param {*}
 * @return {*}
 */
LoadingBarConstructor.prototype.end = function() {
  // 主要用于判断是不是已经开始了  因为只有开始了timer才会有值  可以根据它进行判断 走init
  timer || this.init();
  // 外层使用定时器包裹主要是想要
  setTimeout(() => {
    // 如果是结束的话那么对应的总进度就要立刻变更为100
    this.totalProgress = 100;
    // 每次结束的时候同样也要清除定时器  防止重复点击
    clearTimeout(removeTimer);
    // 200毫秒之后立即清除掉对应的进度条
    removeTimer = setTimeout(() => {
      // 同样的结束之后我们没有必要在使用timer计时器了,可以清除了
      clearTimeout(timer);
      // timer此时可以置空
      timer = null;
      // 同样的将数据在文档中删除
      document.body.removeChild(this.vm.$el);
    }, 200);
  }, 0);
  // clearTimeout(newTimer);
};

LoadingBarConstructor.prototype.error = function() {
  /**
   * @description: 注意此时调用的end其实是内部有异步操作的
   * @param {*}
   * @return {*}
   */

  this.end();
  // 此步其实可有也可无  初始化后立即变为100 之后便在200ms后isError 颜色为红色 对应的操作走完移除
  // this.totalProgress = 100;
  this.isError = true;
};

// 抛出函数运行后的结果
export default tbLoadingBar();

在这里插入图片描述

Vue.prototype.$loading = tbLoadingBar;

实现对应的挂载 就可以使用 this.$loading.xxx实现对应的功能

    <tb-button @click="start">开始</tb-button>
    <tb-button @click="end">结束</tb-button>
    <tb-button @click="error">错误</tb-button>
    <script>
        export default{
            methods: {
            start () {
                this.$loading.start()
            },
            end () {
                this.$loading.end()
            },
            error () {
                this.$loading.error()
            }
            }
        }
    </script>
    

二、组件构思(react版本)

1、思考如何挂载组件问题

我们还是参考 vue 的挂载dom模式 react实现的性质还是一个性质,渲染function函数,使其成为一个html认识的node节点,插入到 createElement 中

那么如何实现这个功能?
在这里插入图片描述

还是参考index加载页,对应如何挂载到root根元素上?那么我们想要挂载到createElement也是一样的性质

在这里插入图片描述
那么滤清这一步之后我们就可以继续向下一步进行

2、函数式编程,其中没有类的概念,没有this,如何在外部调用实例方法?

现在要考虑一个问题,就是我怎么在函数外部调用 函数内部的useState,实现数据的响应式更新,那很简单的一个函数就是一个函数,我能抓到的也就是一个函数,所以现在要考虑的问题就是将抓到的函数,变成一个实例,类似于 vue 这种 new 我就又可以抓到dom节点又可以抓到里面对应的方法变量等等,然后通过实例操控响应式数据,实现 数据->视图->数据 的更新

3、使用 React.createRef() 获取一个ref实例

首要的ref抓取实例dom节点肯定是没问题的,但是我抓到的dom节点也就是仅仅局限于抓到的dom节点

我在调用 ref.current 的时候其对应展示的也就是一个dom节点

在这里插入图片描述
对应展示的 dom 节点
在这里插入图片描述
但是注意当我们展开这个dom节点之后我们要知道,还是记住一句话万物皆对象,实则这个dom他也是一个对象,那对象的话我是不是在子组件赋值的时候,利用栈堆思想,利用对象同指一个内存,是否可以实现一些想要的操作呢?

换个角度思考一下 向 ref 塞入useState对应的 [count,setCount],是不是在外部就可以用 ref.current.setCount(10),也就实现了调用子组件的更新行为了呢?

在这里插入图片描述
对应调用位置
在这里插入图片描述
从而我们也就实现了使用ref从而间接的调用函数内部声明的setState方法

4、index.tsx


import React, { useState, useEffect } from 'react'

import './index.scss'
var ReactDOM = require('react-dom')
interface Iprops {
  type: number
}
const LoadingBar: any = React.forwardRef((props: any, ref: any) => {
  const { type = 1 }: Iprops = props
  // 声明一个名为“count”的新状态变量
  const [speed, setSpeed] = useState(1)
  const [easing, setEasing] = useState('linear')
  const [percentNum, setPercentNum] = useState(0)
  const [totalProgress, setTotalProgress] = useState(0)
  const [showSpinner, setShowSpinner] = useState(true)
  const [isError, setIsError] = useState(false)
  // 类似于 componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    ref.current = {
      ...ref.current,
      methods: {
        type,
        speed,
        setSpeed,
        easing,
        setEasing,
        percentNum,
        setPercentNum,
        totalProgress,
        setTotalProgress,
        showSpinner,
        setShowSpinner,
        isError,
        setIsError
      }
    }
    console.log(showSpinner)
  }, [speed, easing, percentNum, totalProgress, showSpinner, isError])
  return (
    <div ref={ref} className='tb__loading-bar'>
      <div
        className={[
          'tb__loading-bar--bar',
          isError ? 'tb__loading-bar--error' : ''
        ].join(' ')}
        role='bar'
        style={{
          transform: 'translate3d(-' + (100 - totalProgress) + '%, 0, 0)'
        }}
      >
        <div className='tb__loading-bar--peg'></div>
      </div>
      {showSpinner && (
        <div className='tb__loading-bar--spinner' role='spinner'>
          <div
            className={[
              'spinner-icon',
              isError ? 'spinner-icon--error' : ''
            ].join(' ')}
            style={{ animation: 'tb-spinner 400ms ' + easing + ' infinite' }}
          ></div>
        </div>
      )}
    </div>
  )
})

/**
 * @description: 此步用于获取实例     ReactDOM.render(<LoadingBar ref={componentInstance} />, div) 用于构建模板:但注意此时对应的数据还并没有实例化就相当于  是node节点 还没有插入到dom当中
 * @param {*}
 * @return {*}
 */
const div = document.createElement('div')
// 创建一个Ref对象
const componentInstance: any = React.createRef()
function LoadingBarConstructor() {
  ReactDOM.render(<LoadingBar ref={componentInstance} />, div)
  console.log(componentInstance)
// 暴露出 LoadingBar 的对应方法诸如 start、end等,连带 componentInstance ref节点也暴露出去
  return { ...LoadingBar, componentInstance }
}

let timer: any = null
let removeTimer: any = null

/**
 * @description:
 * @param {*} options 参数为一个对象,对象用于指定对应的useState参数值
 * @return {*}
 */
LoadingBar.config = function (options: { [x: string]: any }) {
  // 便利对应的
  Object.keys(options).forEach((key) => {
    // 在这里我们不能让其传入对应的isError  和  totalProgress  因为这两个参数分别代表了错误状态以及对应的加载进度条行走进度
    if (key === 'isError' || key === 'totalProgress') {
      // 抛出错误
      throw new Error('配置传递错误!')
    }

    key == 'speed' && componentInstance.current.methods.setSpeed(options[key])
    key == 'easing' && componentInstance.current.methods.setEasing(options[key])
    key == 'percentNum' &&
      componentInstance.current.methods.setPercentNum(options[key])

    key == 'showSpinner' &&
      componentInstance.current.methods.setShowSpinner(options[key])
    // 对应data中的配置值  也就等于我们自己定义的key值
    // this[key] = options[key]
  })
  // 完成之后调用一次this.start()
  LoadingBar.start()
}

/**
 * @description: 此处用于初始化对应的 react 模板数据 将对应的挂载的模板真正的追加到document.body文档当中,因为样式我们已经写好所以 不用考虑冲突的问题
 * @param {*}
 * @return {*}
 */
LoadingBar.init = function () {
  // 首先初始化我们要做的就是清空对应的计时器 相当于一个节流,主要为了用于防止重复点击
  clearTimeout(timer)
  // 进度初始值变为0
  componentInstance.current.methods.setTotalProgress(0)

  // 是否错误状态设置为false初始值
  componentInstance.current.methods.setIsError(false)
  // 追加
  document.body.appendChild(div)
  // debugger
  return LoadingBar
}

/**
 * @description: 特定开始api
 * @param {*}
 * @return {*}
 */
LoadingBar.start = function () {
  // 每次开始的时候我们调用对应的初始化api 进行初始化
  LoadingBar.init()
  /**
   * @description: 定时器  可以理解为每次点击按钮的时候每次重置
   * @param {*} setInterval
   * @return {*}
   */
  timer = setInterval(() => {
    // 每次都要判断  最多到90  因为我们要的其实只是一个状态,真正关闭的时候  通常一个ajax关闭是需要在调用完毕之后再去调用对应的end关闭方法
    if (componentInstance.current.methods.totalProgress < 90) {
      // 如果有传递进来的值 就用传递进来的值,反之就用随机值 * 每毫秒加载几个格
      componentInstance.current.methods.setTotalProgress(
        componentInstance.current.methods.totalProgress +
          (componentInstance.current.methods.percentNum || Math.random()) *
            componentInstance.current.methods.speed
      )
    }
  }, 100)
}
/**
 * @description: 特定结束api
 * @param {*}
 * @return {*}
 */
LoadingBar.end = function () {
  // 主要用于判断是不是已经开始了  因为只有开始了timer才会有值  可以根据它进行判断 走init
  timer || LoadingBar.init()
  // 外层使用定时器包裹主要是想要
  setTimeout(() => {
    // 如果是结束的话那么对应的总进度就要立刻变更为100
    componentInstance.current.methods.setTotalProgress(100)

    // 每次结束的时候同样也要清除定时器  防止重复点击
    clearTimeout(removeTimer)
    // 200毫秒之后立即清除掉对应的进度条
    removeTimer = setTimeout(() => {
      // 同样的结束之后我们没有必要在使用timer计时器了,可以清除了
      clearTimeout(timer)
      // timer此时可以置空
      timer = null
      // 同样的将数据在文档中删除
      document.body.removeChild(div)
    }, 200)
  }, 0)
  // clearTimeout(newTimer);
}

LoadingBar.error = function () {
  /**
   * @description: 注意此时调用的end其实是内部有异步操作的
   * @param {*}
   * @return {*}
   */

  LoadingBar.end()
  // 此步其实可有也可无  初始化后立即变为100 之后便在200ms后isError 颜色为红色 对应的操作走完移除
  componentInstance.current.methods.setTotalProgress(100)
  componentInstance.current.methods.setIsError(true)
  //   this.totalProgress = 100
  //   this.isError = true
}
export default LoadingBarConstructor()

5、index.scss

.tb__loading-bar {
  &--bar {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 2px;
    z-index: 9999;
    transition: all 200ms ease;
    transform: translate3d(-100%, 0, 0);
    background: #1089ff;
  }

  &--error {
    background: #ff4d4f;
  }

  &--peg {
    display: block;
    position: absolute;
    right: 0;
    width: 100px;
    height: 100%;
    box-shadow: 0 0 10px #fea638, 0 0 5px #fea638;
    opacity: 1;
    transform: rotate(3deg) translate(0px, -4px);
  }

  &--spinner {
    display: block;
    position: fixed;
    z-index: 1031;
    top: 15px;
    right: 15px;

    .spinner-icon {
      width: 18px;
      height: 18px;
      box-sizing: border-box;
      border: solid 2px transparent;
      border-top-color: #1089ff;
      border-left-color: #1089ff;
      border-radius: 50%;
    }

    .spinner-icon--error {
      border-top-color: #ff4d4f;
      border-left-color: #ff4d4f;
    }
  }
}

@keyframes tb-spinner {
  0% {
    transform: rotate(0);
  }

  to {
    // 转一圈
    transform: rotate(1turn);
  }
}

6、引入调用及功能实现

在这里插入图片描述
在这里插入图片描述
请添加图片描述
效果动图 有兴趣的同学可以自己尝试下,同样的如果有好的解决方案,或者方法的不足之处等也可以留在评论区,共同探讨。

评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

归来巨星

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值