# 2023 年 02 月

# 2023-02-24

# vue2 升级 vue3 后,watch 死循环问题

import {watch} from 'vue'
watch(formData, () => {
  console.log('watch formData')
  // 一些操作,不知道为啥,里面的操作在 vue2 中正常,但 vue3 会导致死循环
}, {immediate: true, deep: true} )

vue3 响应式数据改为 proxy 之后,watch formData 出现了循环调用的问题?暂时未找到具体原因。临时解决方法

import {watch} from 'vue'

watch(formData, (val, oldVal) => {
  console.log('watch formData')
  // 发现循环触发时,新旧数据一致。可能是 formData.value = xxx 重复触发,导致死循环
  // 这里加一个等值判断,如果新旧值一样,就不继续执行
  if (JSON.stringify(val) === JSON.stringify(oldVal)) {
    console.log('值相同,不继续执行')
    return 
  }
  // 一些操作,不知道为啥,里面的操作在 vue2 中正常,但 vue3 会导致死循环
}, 
  { immediate: true, deep: true} 
)

# element-plus el-select remote-method 在失去焦点后也会触发的问题

vue2 版本在失去焦点的时候,不会触发 remote-methods,vue3 后,这个算是新增的行为,之前看过一个 issue,

印象中是为了解决某个场景下的值问题,但不是很有必要。

我们业务代码之中做了一个骚操作,就是点击 el-select 后,会隐藏弹窗下拉框(display: none)。显示自己自定义的一个选择框。

但是发现选中自定义选择部分的内容时,第一次会闪一下,排查了很久,发现是失去焦点后触发了 remote-methods,导致列表数据更新,选择框内容刷新了一遍。

解决方法:remote-method 中加一个判断,判断焦点是否在输入框中

<template>
  <div>
    <el-select
      v-model="someVal"
      ref="selectRef"
      remote
      filterable
      :remote-method="remoteMethod"
    >
      <el-option
        v-for="item in options"
        :key="item.value"
        :label="item.label"
        :value="item.value"
      />
    </el-select>
  </div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const someVal = ref("");
const selectRef = ref();

const options = [
  {
    value: "Option1",
    label: "Option1",
  },
  {
    value: "Option2",
    label: "Option2",
  },
];

const remoteMethod = (query: string) => {
  console.log("remoteMethod", query);
  // 焦点判断,如果是失去焦点,不执行,关于焦点,可以搜索 focus, hasFocus
  const hasFocus = selectRef.value.$el.contains(document.activeElement);
  if (!hasFocus) {
    console.log("失去焦点不做处理");
    return;
  }
  // 正常逻辑
  console.log("正常逻辑");
};
</script>

# vue3 data hasOwnProperty 非响应式问题

vue2 代码

<template>
  <div :class="[testOgj.hasOwnProperty('a') ? 'true-class' : 'false-class']"></div>
</template>
<script>
export default {
  data() {
    return {
      testObj: { a: 1 }
    }
  }
}
</script>

升级到 vue3 之后,testObj 数据变成 ref 后,更改 class 的操作 hasOwnProperty 变得不是响应式了。

修改为 testOgj['a'] 这种写法才 ok。

# 2023-02-20

# vue2/vue3/react源码,面试题,面经

https://whf293.github.io/ (opens new window)

# Syntax Error: TypeError: Cannot read property 'content' of null

vue 组件内两个 script 标签导致该错误

<script lang="ts" setup></script>
<!-- 忘记加 lang="ts" 了, 出现上面的错误,加上就好了 -->
<script></script>

# element-plus 糟糕的提示问题?ElOnlyChild 问题

ElmenetPlusError: [ElOnlyClid] no valid child node found,并没有提示具体是哪个组件的问题

导致,对于复杂的项目,代码量大的场景,都不知道具体是哪个组件报的这个异常,需要在大片的代码里面去排查,

比较耗时。提个 PR 改进?这种报错,提示粒度到组件?

<el-tooltip><div v-if="xx">123</div></el-tooltip>

# 2023-02-17

# vue3 bug ?activePreFlushCbs.length 报错

在项目中,一个弹窗关闭的场景,vue3 销毁时,修改某个值,activePreFlushCbs.length 报异常,activePreFlushCbs 为 null

没有加 ?. 空值判断,待复现

# 2023-02-15

# 关于设计感

soybean-admin (opens new window) A fresh and elegant admin template, based on Vue3,Vite3,TypeScript,NaiveUI and UnoCSS [一个基于Vue3、Vite3、TypeScript、NaiveUI 和 UnoCSS的清新优雅的中后台模版]

# vue2 升级 vue3,element-plus 的一些破坏性更新

date 格式问题

1、value-format="yyyy-MM-dd" 代码需要改为 value-format="YYYY-MM-DD"

参考:

2、el-popover v-mode 变为 v-model:visible,el-dialog 的 :visible.sync 变更为 v-model

3、tab-click 行为变更,加了 await, 与 vue2 行为不一致,需要改为 tab-change

4、el-form rules 和 el-form-item rules 合并规则变更,原先是item覆盖form规则,现在是合并,都会生效。

5、icon 变更为 svg 后,改动较大 el-icon 几乎全部失效。

# sourcemap 还原位置有偏移?windows 与 linux 环境生成 soucemap 不一致的问题

在巡检线上异常时,发现有错误,本地生成 source map 后,还原,发现还原不了,定位到具体函数,发现位置有偏移。

问了下同事,同事用相同的方法测是正常的,于是让同事将生成的 map 文件发我

使用 git 来比对生成的 js、map 文件发现有 1-2 行区别,windows 是 \r\n,同事 mac 是 \n,但在 vscode

开发时,开发者时无感知的(可以在底部设置 LF、CRLF),注意 map 文件生成,最好在更接近生产的 linux 环境或 mac 环境,不要用 windows 生成。

CRLF, LF 是用来表示文本换行的方式。CR(Carriage Return) 代表回车,对应字符 '\r';LF(Line Feed) 代表换行,对应字符 '\n'。由于历史原因,不同的操作系统文本使用的换行符各不相同。主流的操作系统一般使用CRLF或者LF作为其文本的换行符。其中,Windows 系统使用的是 CRLF, Unix系统(包括Linux, MacOS近些年的版本) 使用的是LF。

理解 CRLF,LF (opens new window)

# 2023-02-14

# vue-router 4.x 相比 3.x watch route 行为变更问题

vue3(对应vue-rotuer 4.x) setup 写法中、watch route 新旧值都是一样的,但在 vue2 版本(对应 3.x)中是好的。

<!-- xxx.vue -->
<script lang="ts" setup>
import { useRoute } from 'vue-router'

const route = useRoute()

watch(route, (newVal, oldVal) => {
  console.log(newVla, oldVal) // 同一个页面,路由变更(hash)时,两个值一样
})

看了下文档,在 vue3 中,vue-router 有对应的 composition API, onBeforeRouteUpdate

import { onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'

export default {
  setup() {

    // 与 beforeRouteUpdate 相同,无法访问 `this`
    onBeforeRouteUpdate(async (to, from) => {
      //仅当 id 更改时才获取用户,例如仅 query 或 hash 值已更改
      if (to.params.id !== from.params.id) {
        userData.value = await fetchUser(to.params.id)
      }
    })
  },
}

# 2023-02-13

# TS video 实例变量不存在 pause() 方法,对应类型怎么查找

搜索 video MDN,可以找到对于的类型 HTMLMediaElement

# 2023-02-18

# 前端 nginx 配置负载均衡,ip黑名单等

(三)Nginx一网打尽:动静分离、压缩、缓存、黑白名单、跨域、高可用、性能优化...想要的这都有! (opens new window)

# 看英文文档方法

掘金高赞

我只是用了个“笨”方法,一个月后不再惧怕英文文档 (opens new window)

# 2023-02-06

# 什么是 Turopack,号称比 vite 快 10 倍,尤雨溪发长文回怼

Turbopack is an incremental bundler optimized for JavaScript and TypeScript, written in Rust by the creators of webpack and Next.js(opens in a new tab) at Vercel(opens in a new tab).

On large applications Turbopack updates 10x faster than Vite and 700x faster than webpack. For the biggest applications the difference grows even more stark with updates up to 20x faster than Vite.

实际快 2 倍不到,Evan You 回复

Is Turbopack really 10x Faster than Vite? · Discussion #8 · yyx990803/vite-vs-next-turbo-hmr (opens new window)

https://twitter.com/youyuxi/status/1587279357885657089 (opens new window)

# Rust Is The Future of JavaScript Infrastructure

Rust Is The Future of JavaScript Infrastructure (opens new window), 作者是 Vercel 的开发者关系主管。

中文翻译:Rust 是 JavaScript 基础设施的未来 (opens new window)

Turbopack

参考

# 一个 dialog 占 1G 内存?vue3 的一个内存泄漏bug

[Component] [dialog] dialog组件导致内存泄漏 (opens new window)

vue 相关问题?

The onUnmounted callback is not triggered when using Teleport

https://github.com/vuejs/core/issues/6347 (opens new window)

# 什么是 swc? parcel 2 使用它替代 js 打包性能提升 10 倍?

SWC (stands for Speedy Web Compiler) is a super-fast TypeScript / JavaScript compiler written in Rust.

Rust-based platform for the Web,目标:Make the web (development) faster.

Parcel 2 beta 3 (opens new window)

10x faster JavaScript compiler written in Rus

# smooth-scrolling 平滑滚动产生的 bug

table 纵向懒加载时候,默认滚动到最下面,需要滚动到之前的位置,直接修改 scrollTop 会触发平滑滚动

但我的电脑有点问题,平滑滚动效果是关的,导致滚动有问题,怎么在滚动的时候关闭平滑滚动效果,以及怎么配置浏览器开启平滑滚动效果

chrome://flags/#enable-smooth-scrolling

滚动时,不触发平滑滚动

element.scrollTo({
  top: 100,
  left: 100,
  behavior: 'smooth' // smooth (平滑滚动),instant (瞬间滚动),默认值 auto,效果等同于 instant
});

# 通过 vue3 的两个历史 bug,来了解正则 s 标志位的用处

正则表达式格式为 /pattern/flags, flags 标志位:g 不仅仅匹配第一个,全局匹配。i 忽略大小写。

根据 vuejs 的一个 bug 来看 s 标志位的应用场景。

s 标志位的使用场景:

  • 正则表达式的模式匹配中,. 默认可以匹配除 \n 外的任意一个字符。
  • 如果加了 /s 标志位,. 就可以匹配全部字符了,包括 \n

参考:正则表达式 - 修饰符(标记) (opens new window)

vue3 的源码中,有一个 . 无法匹配换行符 (\n) 导致的 bug,下面来看看,以下是 PR 链接

fix(shared): parse multi-line inline style by sxzz · Pull Request #6777 · vuejs/core (opens new window)

regex-s-flag.png

如下图,再没有修复这个 bug 前,下面的多行内联样式,在 padding-box, 位置会结束,后面的内容会匹配不到

<div style="
      border: 1px solid transparent;
      background: linear-gradient(white, white) padding-box,
        repeating-linear-gradient(
          -45deg,
          #ccc 0,
          #ccc 0.5em,
          white 0,
          white 0.75em
        );
    ">
</div>

之前使用的正则是,(.+) 匹配不到换行

const propertyDelimiterRE = /:(.+)/

中间 尤雨溪 加了如下改动,链接

const propertyDelimiterRE = /:(.+)/s 
// 删除上面这一行,这里加了 s 标志位,但是有位置的单元测试过不去,
// 最后改为下面这一行,链接 https://github.com/vuejs/core/pull/6777/commits/2a50d03e5c6a737a860de349e0df16743c654582
const propertyDelimiterRE = /:([^]+)/

文件地址:https://github.com/vuejs/core/blob/main/packages/shared/src/normalizeProp.ts (opens new window)

再来看下面一个 bug,vue 3.2.40 版本中 style 内联样式中注释下面的一行样式会不生效。"Inline style regular doesn't work when below comments · Issue #6807 · vuejs/core" (opens new window)

  <div
    style="
      /* something */
      width: 300px;
      height: 300px;
      background-color: pink;
    "
  ></div>

解决方法如下,使用正则匹配 style 中的评论,然后删除,也用到了额 s 标志位

const styleCommentRE = /\/\*.*?\*\//gs
// /* .*? */
export function parseStringStyle(cssText: string): NormalizedStyle {
  const ret: NormalizedStyle = {}
  cssText
    .replace(styleCommentRE, '') // 将 style 内容中的注释内容删除
    .split(listDelimiterRE)
    .forEach(item => {
      if (item) {
        const tmp = item.split(propertyDelimiterRE)
        tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim())
      }
    })
  return ret
}

# 2023-02-05

# Find X3 火星版官网动效实现

OPPO Find X3 Pro 火星探索版 有色彩的地方 就有让生命感动的力量 | OPPO 官方网站 (opens new window)

效果预览:nice.zuo11.com (opens new window)

find_x3_section1-2.gif

1、首屏是一个 video 动画,没有加 loop,比较简单

2、第二屏也是一个 video 视频,加了鼠标样式,文字动画

修改鼠标样式代码如下

/*
<div class="section-2" style="width: 100%">
  <video src="xxx" muted autoplay loop style="width: 100%" ></video>
  <!-- @鼠标样式_start -->
  <div class="player-btn">
      <div>鼠标样式图片</div>
      <span class="text">观看完整视频</span>
  </div>
</div>
*/
let section2El = document.querySelector(".section-2");
let playBtnEl = document.querySelector(".player-btn");
section2El.addEventListener("click", (e) => {
  // alert("播放视频");
});
window.addEventListener(
  "mousemove",
  _.throttle((e) => {
    // 鼠标相对页面的位置
    let x = e.pageX;
    let y = e.pageY;
    // section-2 容器相对视口位置
    let parentX = section2El.offsetLeft;
    let parentY = section2El.offsetTop;
    let isYOut = y < parentY || y > parentY + section2El.clientHeight;
    let isXOut = x < parentX || x > parentX + section2El.clientWidth;
    if (isXOut || isYOut) {
      // console.log("移出去了");
      playBtnEl.style.opacity = "0";
      return;
    } else {
      playBtnEl.style.opacity = "1";
    }
    // 鼠标不在正中心,需要减去鼠标样式区域的宽高才能达到居中效果
    playBtnEl.style.left = `${x - parentX - playBtnEl.clientWidth / 2}px`;
    playBtnEl.style.top = `${y - parentY - playBtnEl.clientHeight / 2}px`;
  }, 50)
);

文字也有一个过渡动画,通过 clip-path 进行显示隐藏切换,再加上 transition 过渡

/* 初始化时,默认隐藏文字 */
.section-2-text .title {
  transition: all 0.7s; /* 当 css 属性发生变化时,0.7s 内完成变更 */
  /* 裁剪路径,inset 显示这个区域的内容,right、left 50%,会隐藏元素 */
  clip-path: inset(0 50% 0 50%);
  /*滚动到当前区域时,增加 class*/
}

通过 gsap 判断滚动距离,当这个区域滚动到视口中间(center)时,触发 class 添加。有 transition 就形成了动画

// 到视口中间时,父元素添加一个 fade-in 样式,显示该元素
.section-2.fade-in .title {
  clip-path: inset(0 0 0 0); /* 滚到到视口时,显示元素 */
}

// gsap 滚动处理
gsap.registerPlugin(ScrollTrigger);
gsap.to(".section-2-text", {
  opacity: 1,
  scrollTrigger: {
    trigger: ".section-2",
    start: "top center", // 当元素顶部部,滚动到达视口中间时, 开始动画
    // end 默认是 trigger 离开视口
    toggleClass: "fade-in",
    scrub: true, // 表示动画可以重复执行改成false表示只执行一次
    // markers: true, // 绘制开始位置和结束位置的线条
    // pin: false, // 动画执行期间,页面不进行滚动,动画执行结束后
  },
});

3、星空背景动画

find_x3_section-3.gif

<canvas id="zn-starry-star" width="1440" height="969"></canvas>

代码为 canvas,通过 F12 查看 source,搜索 js 文件里面的 zn-starry-star 关键字,找到核心代码

find_x3_section_star_source.png

查找前后代码,如下 https://www.oppo.com/content/dam/oppo/product-asset-library/find-x3-series/fussi-mars/v1/main-v3.js (opens new window)

关键字: vec4 星空动画 vec4 canvas 星空动画 vec4 canvas 星空动画

关键字: three.js 星空,搜索到一个比较接近的星空效果,地址:three.js 制作星空 (opens new window)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>3Dstar</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }
    </style>
    <script src="./threejs/three.min.js"></script
    <script src="./threejs/OrbitControls.js"></script>
    <!-- 
      建议保存到本地, 文件链接:
      https://cdn.bootcdn.net/ajax/libs/three.js/0.149.0/three.min.js
      http://nice.zuo11.com/3-find-x3-mars/threejs/OrbitControls.js 
     -->
  </head>
  <body>
    <script>
      /**
       * 创建场景对象Scene
       */
      var scene = new THREE.Scene();
      var intersectsArr = [];

      //星空背景
      var cloud = cloudFun();
      scene.add(cloud);

      function cloudFun() {
        var geom = new THREE.Geometry();
        var material = new THREE.ParticleBasicMaterial({
          size: 2,
          vertexColors: true,
        });
        var n = 1200;
        for (var i = 0; i < 3000; i++) {
          var particle = new THREE.Vector3(
            (Math.random() - 0.5) * n,
            (Math.random() - 0.5) * n,
            (Math.random() - 0.5) * n
          );
          geom.vertices.push(particle);
          let color_k = Math.random();
          // 蓝白色
          // geom.colors.push(new THREE.Color(color_k, color_k, color_k * 2.0));
          // 橙色为RGB为255,165,0,代码#FFA500
          geom.colors.push(new THREE.Color(color_k * 10, color_k * 3, color_k));
        }
        var cloud = new THREE.ParticleSystem(geom, material);
        return cloud;
      }

      /**
       * 透视投影相机设置
       */
      var width = window.innerWidth; //窗口宽度
      var height = window.innerHeight; //窗口高度
      /**透视投影相机对象*/
      var camera = new THREE.PerspectiveCamera(60, width / height, 1, 1000);
      camera.position.set(651, 613, 525); //设置相机位置
      camera.lookAt(scene.position); //设置相机方向(指向的场景对象)

      /**
       * 光源设置
       */
      //点光源
      var point = new THREE.PointLight(0xffffff);
      point.position.set(800, 200, 300);
      scene.add(point);
      // 点光源2  位置和point关于原点对称
      var point2 = new THREE.PointLight(0xffffff);
      point2.position.set(0, -500, 0); //点光源位置
      scene.add(point2); //点光源添加到场景中

      //环境光
      var ambient = new THREE.AmbientLight(0x000000);
      scene.add(ambient);

      /**
       * 创建渲染器对象
       */
      var renderer = new THREE.WebGLRenderer({
        antialias: true,
      });
      renderer.setSize(width, height); //设置渲染区域尺寸
      renderer.setClearColor(0x101010, 1); //设置背景颜色
      document.body.appendChild(renderer.domElement); //body元素中插入canvas对象

      let clock = new THREE.Clock();
      var FPS = 30;
      var refreshTime = 1 / FPS;
      var timeS = 0;
      function render() {
        var renderInterval = clock.getDelta();
        timeS = timeS + renderInterval;
        if (timeS > refreshTime) {
          //执行渲染操作
          renderer.render(scene, camera);
          timeS = 0;
        }
        //每次渲染位置变化,动态效果
        cloud.rotation.x += 0.0002;
        cloud.rotation.y += 0.0002;
        cloud.rotation.z += 0.0002;
        //周期性渲染
        requestAnimationFrame(render);
      }

      render();
      var controls = new THREE.OrbitControls(camera); //创建控件对象
    </script>
  </body>
</html>

three.js 星空在线预览 (opens new window)

相关链接:

gsap 逻辑与 clip-path 逻辑

  • 内容分为 5 个部分,每个部分都是重叠的,初始都是隐藏的,opacity为 0, clip-path 左边裁切 100%(不显示)
  • 当滚动到对应的区域时,增加 active 属性,opacity 过渡到 1, clip-path 右裁切过渡到0 (不裁剪,完全显示)
  • 第一个动画结束后,onLeave 钩子里,开启下一个场景 active 添加。onEnter 钩子里面切换当前进度百分比显示
  • 整体的 pin 使用了父元素 pin,end 为滚动到 3000px 后,才结束固定。(屏幕适配可能会有问题,待优化)
<style>
.section-3-info .part img,
.left-desc .text-title,
.left-desc .text-detail {
  clip-path: inset(0 0 0 100%); /* 左侧裁剪 100% => 左侧裁剪 0%, 从右到左*/
  opacity: 0;
}
.section-3-info .part img {
  transition: clip-path 0.8s, opacity 0.8s;
}
.left-desc .text-title {
  transition: clip-path 0.4s, opacity 0.4s;
}
.left-desc .text-detail {
  transition: clip-path 0.4s 0.2s, opacity 0.6s;
}
.part.active .text-title, 
.part.active .text-detail, 
.part.active img {
  opacity: 1;
  clip-path: inset(0 0 0 0);
}
</style>
<script>
const changeProgress = (val) => {
  document.querySelector("#progress").innerHTML = `${val}%`;
};
let tl = gsap.timeline();
tl.to(".part-1", {
  opacity: 1,
  scrollTrigger: {
    trigger: ".part-1",
    start: "top top",
    end: "+1000",
    toggleClass: "active",
    scrub: true, // 表示动画可以重复执行改成false表示只执行一次
    //   markers: true, // 绘制开始位置和结束位置的线条
    onEnter: function () {
      changeProgress(33);
    },
    onEnterBack: function () {
      changeProgress(33);
    },
    onLeave: function () {
      gsap.to(".part-2", {
        opacity: 1,
        scrollTrigger: {
          trigger: ".part-2",
          start: "top top",
          end: "+1000",
          toggleClass: "active",
          scrub: true, // 表示动画可以重复执行改成false表示只执行一次
          // markers: true, // 绘制开始位置和结束位置的线条
          onEnter: function () {
            changeProgress(66);
          },
          onEnterBack: function () {
            changeProgress(66);
          },
          onLeave: function () {
            gsap.to(".part-3", {
              opacity: 1,
              scrollTrigger: {
                trigger: ".part-3",
                start: "top top",
                end: "+1000",
                toggleClass: "active",
                scrub: true, // 表示动画可以重复执行改成false表示只执行一次
                //   markers: true, // 绘制开始位置和结束位置的线条
                onEnter: function () {
                  changeProgress(100);
                },
                onEnterBack: function () {
                  changeProgress(100);
                },
                onLeave: () => {
                  // gsap.set('.part-2', {autoAlpha: 0});
                },
              },
            });
          },
        },
      });
    },
  },
});

// 固定容器
gsap.to(".section-3", {
  opacity: 1,
  scrollTrigger: {
    trigger: ".section-3",
    start: "top top", // 当元素顶部部,滚动到达视口中间时, 开始动画
    // end 默认是 trigger 离开视口
    end: "+3000", // 当section-2底部,到达可视区域bottom 500px时,结束动画
    // scrub: true, // 表示动画可以重复执行改成false表示只执行一次
    // markers: true, // 绘制开始位置和结束位置的线条
    pin: true, // 动画执行期间,页面不进行滚动,动画执行结束后
    onEnterBack() {
      document.querySelector('.abs.s-4-2').classList.remove('active')
      document.querySelector('.comp-inner').classList.remove('active')
    }
  },
});
</script>

小黄点定位 css animation

find-x3-mars-orange-dot-animation.gif

<style>
@keyframes circleScale {
  from {
    transform: scale(2);
  }
  to {
    transform: scale(0.5);
  }
}
@keyframes circleScaleSlow {
  from {
    transform: scale(1);
  }
  to {
    transform: scale(0.5);
  }
}
.circle-wrapper .fast-flow,
.circle-wrapper .slow-flow {
  width: 100%;
  height: 100%;
  position: absolute;
  opacity: 1;
  border: 1px solid #ff995e;
  background: transparent;
  border-radius: 50%;
}
.circle-wrapper .fast-flow {
  animation: circleScale 1.5s ease infinite;
}
.circle-wrapper .slow-flow {
  animation: circleScaleSlow 1.5s ease infinite;
}
/* 中心小黄点 */
.circle-wrapper .circle {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #ff995e;
    margin-top: 50%;
    margin-left: 50%;
    transform: translate(calc(-50% + 1px), calc(-50% + 1px));
}
</style

<body>
  <!-- 所在位置 点 动画-->
  <div class="circle-wrapper abs circle1">
    <div class="fast-flow"></div>
    <div class="slow-flow"></div>
    <div class="circle"></div>
  </div>
</body>

其他场景代码参见: https://github.com/zuoxiaobai/nice-func/tree/main/src/3-find-x3-mars (opens new window)

# 2023-02-02

# 关于 vue3 composition 与 utils 功能的区分界定

一般 vue3 composition 里面也可以写纯 js 逻辑,但建议仅当包含 vue 相关代码时,才使用 composition use-xx 来定义。

对于纯 js 逻辑或者 dom 操作,使用 utils 就行。

比如下面这种包含 vue 方法的,可以使用 composition 格式定义

import { ref } from "vue";
//
export const useXxx = () => {
  const foo = ref("test");
  return { foo };
};

针对下面的纯 js 逻辑,建议放到 utils 分类中

const xxx = () => {
  document.querySelector("xxx").xx; // 一些 dom 操作
  // 其他 js 逻辑等
};

# 关于算法系统化学系、刷题

hello 算法教程

宫水三叶

其他

# 怎么通过前端 js 代码计算页面的 fps

在看 揭秘 Vue.js 九个性能优化技巧 (opens new window) 这边文章时,发现 演示项目 (opens new window) 右上角有一个实时显示页面 fps 的功能,如下图

fps.gif

这个功能时怎么实现的呢?查看源码后发现,是引入了一个 fps-indicator js 库,使用方法如下

// src/main.js
import fps from "fps-indicator";

fps({
  position: "top-right",
  style: `
    font-size: 24px;
  `,
});

核心实现代码,通过触发 requestAnimationFrame 函数来计算 fps,基本上执行一次该函数就是一帧

  • raf (opens new window),可以直接理解为 requestAnimationFrame,封了一层,做了一些兼容性处理
//enable update routine
var that = this;
raf(function measure() {
  count++;
  var t = now();

  if (t - lastTime > period) {
    lastTime = t;
    values.push(count / (max * period * 0.001));
    values = values.slice(-w);
    count = 0;

    ctx.clearRect(0, 0, w, h);
    ctx.fillStyle = getComputedStyle(that.canvas).color;
    for (var i = w; i--; ) {
      var value = values[i];
      if (value == null) break;
      ctx.fillRect(i, h - h * value, 1, h * value);
    }

    that.valueEl.innerHTML = (values[values.length - 1] * max).toFixed(1);
  }

  raf(measure);
});

完整代码

/**
 * @module fps-indicator
 */
var raf = require("raf");
var now = require("right-now");
var css = require("to-css");
module.exports = fps;

function fps(opts) {
  if (!(this instanceof fps)) return new fps(opts);

  if (typeof opts === "string") {
    if (positions[opts]) opts = { position: opts };
    else opts = { container: opts };
  }
  opts = opts || {};

  if (opts.container) {
    if (typeof opts.container === "string") {
      this.container = document.querySelector(opts.container);
    } else {
      this.container = opts.container;
    }
  } else {
    this.container = document.body || document.documentElement;
  }

  //init fps
  this.element = document.createElement("div");
  this.element.className = "fps";
  this.element.innerHTML = [
    '<div class="fps-bg"></div>',
    '<canvas class="fps-canvas"></canvas>',
    '<span class="fps-text">fps <span class="fps-value">60.0</span></span>',
  ].join("");
  this.container.appendChild(this.element);

  this.canvas = this.element.querySelector(".fps-canvas");
  this.textEl = this.element.querySelector(".fps-text");
  this.valueEl = this.element.querySelector(".fps-value");
  this.bgEl = this.element.querySelector(".fps-bg");

  var style = opts.css || opts.style || "";
  if (typeof style === "object") style = css(style);

  var posCss = "";
  posCss = positions[opts.position] || positions["top-left"];

  this.element.style.cssText = [
    "line-height: 1;",
    "position: fixed;",
    "font-family: Roboto, sans-serif;",
    "z-index: 1;",
    "font-weight: 300;",
    "font-size: small;",
    "padding: 1rem;",
    posCss,
    opts.color ? "color:" + opts.color : "",
    style,
  ].join("");

  this.canvas.style.cssText = [
    "position: relative;",
    "width: 2em;",
    "height: 1em;",
    "display: block;",
    "float: left;",
    "margin-right: .333em;",
  ].join("");

  this.bgEl.style.cssText = [
    "position: absolute;",
    "height: 1em;",
    "width: 2em;",
    "background: currentcolor;",
    "opacity: .1;",
  ].join("");

  this.canvas.width = parseInt(getComputedStyle(this.canvas).width) || 1;
  this.canvas.height = parseInt(getComputedStyle(this.canvas).height) || 1;

  this.context = this.canvas.getContext("2d");

  var ctx = this.context;
  var w = this.canvas.width;
  var h = this.canvas.height;
  var count = 0;
  var lastTime = 0;
  var values = opts.values || Array(this.canvas.width);
  var period = opts.period || 1000;
  var max = opts.max || 100;

  //enable update routine
  var that = this;
  raf(function measure() {
    count++;
    var t = now();

    if (t - lastTime > period) {
      lastTime = t;
      values.push(count / (max * period * 0.001));
      values = values.slice(-w);
      count = 0;

      ctx.clearRect(0, 0, w, h);
      ctx.fillStyle = getComputedStyle(that.canvas).color;
      for (var i = w; i--; ) {
        var value = values[i];
        if (value == null) break;
        ctx.fillRect(i, h - h * value, 1, h * value);
      }

      that.valueEl.innerHTML = (values[values.length - 1] * max).toFixed(1);
    }

    raf(measure);
  });
}

var positions = {
  "top-left": "left: 0; top: 0;",
  "top-right": "right: 0; top: 0;",
  "bottom-right": "right: 0; bottom: 0;",
  "bottom-left": "left: 0; bottom: 0;",
};

# git switch 和 git checkout 都可以切换分支,有什么区别?

一般 git checkout 和 git switch 都可以切换分支,他们有什么区别?

Git 2.23 引入了 git switch 命令

The first rule of Doug McIlroy’s UNIX Philosophy says:

Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new “features”.

"新增一个功能,最好创建新的功能模块,而不是通过在旧功能上新增一个补丁特性来实现"

git checkout does too many things,has not followed this philosophy very closely.

大致意思是,git checkout 功能太复杂了,有 6 种用法。破坏的 git 用户体验

git switch 更加语义化,功能更简洁一点,对用户更友好。

参考:

# eslint+prettier 自动修复 template 时,元素换行异常问题解决

下面这行代码在保存后自动 fix 时, el-button 的结束标签换行异常

prettier自动fix后元素结束标签换行异常问题.png

<el-button type="primary" text @click="deleteShortLink(scope.$index)"
  >删除</el-button
>
<!--
Replace `>删除</el-button` with `⏎············>删除</el-button⏎··········`eslintprettier/prettier
-->

这种情况需要修改 prettier 配置

// .prettierrc.json 或 .prettierrc.js
{
    "htmlWhitespaceSensitivity": "ignore"
}

修改后,再保存就正常了,注意:一般情况下,这里修改 prettier 配置后,需要关闭 vscode 项目,再重新打开,配置才会生效

prettier配置修改后效果.png

上次更新: 2023/2/25 00:53:24