Giter Site home page Giter Site logo

blog's People

Contributors

wyh-code avatar

Watchers

 avatar

blog's Issues

防抖节流

概念原理

在项目开发中,会有一些事件高频触发的场景,例如:

  • 1、浏览器窗口的缩放,页面的滑动触发的 resizescroll
  • 2、鼠标事件 onmousemoveonmousedownonmouseup
  • 3、输入框录入触发的 keyupkeydown
  • .....

为了优化此类场景,我们通常会使用两种方案:

  • 节流:事件高频触发中,每 n 秒执行一次。
  • 防抖:事件高频触发后,间隔 n 秒执行一次。

关于二者之间的区别,由以上原理可知,是执行回调函数的时机不同:节流函数的回调执行时机是在高频触发中,而防抖函数的回调函数执行时机是在高频触发后。

实现

根据以上原理,我们可以写出第一版简单实现:

html 页面代码

<body>
   <div id="root" style="width: 500px;height: 200px;background: yellow;"></div>
    <script>
      function func(e) {
        console.log('mousemve', this, e);
      }
      // 节流
      function throttle(){}
      //防抖
      function debounce(){}

      root.addEventListener('mousemove', throttle(func, 1000));
    </script>
</body>

防抖

/**
 *  思路:
 *  每次事件触发删除已有定时器,设置新的定时器,等待 n 秒触发事件回调
 **/
function debounce(fn, wait){
  let timer;
  return function(...args){
    let ctx = this;
    clearTimeout(timer);
    timer = setTimeout(function(){
      fn.apply(ctx, args);
      timer = null;
    }, wait)
  }
}

效果图:
debounce

节流

节流函数的实现有两种方式:
 1、时间戳
 2、定时器

时间戳实现

/**
 * 思路:
 * 每次事件触发,对比当前时间和上次回调执行时间,若间隔大于等待时间则执行
 **/
function throttle(fn, wait){
  let previous = 0;
  return function(...args){
    let ctx = this;
    let now = +new Date();
    if(now - previous > wait){
      fn.apply(ctx, args);
      previous = now;
    }
  }
}

效果图:
时间戳节流

定时器实现

/**
 * 思路:
 * 每次事件触发,判断定时器是否存在
 * 若不存在则设置定时器,
 * 若存在则等待定时器执行完毕之后重新设置定时器
 **/
function throttle(fn, wait){
  let timer;
  return function(...args){
    let ctx = this;
    if(!timer){
      timer = setTimeout(function(){
        timer = null;
        fn.apply(ctx, args)
      }, wait)
    }
  }
}

效果图:
定时器节流

改进

通过以上处理,我们已经可以对高频事件的执行频率及时机加以控制,但是,通过观察我们发现上述代码还存在以下问题:
 1、在防抖函数中,鼠标初次进入色块时,回调函数未执行
 2、在时间戳节流中,鼠标离开色块,间隔 1 秒后回调函数不会执行
 3、在定时器节流中,鼠标初次进入色块时,回调函数未执行

针对以上问题,我们对函数做出以下调整:

防抖
接受一个 option 参数对象,控制初次触发是否执行回调函数,option 为对象便于扩展其他控制功能

/**
 *  思路:
 *  !timer 为 true 则认为是首次触发
 *  leading 等于 true 则首次触发执行回调
 **/
function debounce(fn, wait, option={}){
  let timer;
  let { leading } = option;
  return function(...args){
    let ctx = this;
    if(!timer && leading === true){ // 初次触发执行回调
      fn.apply(ctx, args);
    }
    clearTimeout(timer);
    timer = setTimeout(function(){
      fn.apply(ctx, args);
      timer = null;
    }, wait)
  }
}

节流
我们整合时间戳实现和定时器实现,完成初次触发及鼠标离开后的回调执行

/**
 *  思路:
 *  计算出距离下次事件执行的时间:wait - (now - previous)
 *  若距离下次执行的时间 <=0 则执行回调,并清除定时器
 *  若未到执行时间,且没有定时器,则设置定时器,确保鼠标脱离色块后,依旧执行最后一次事件回调
 **/
function throttle(fn, wait) {
  let previous = 0;
  let timer;
  return function (...args) {
    let ctx = this;
    let now = +new Date();
    let remaining = wait - (now - previous);
    
    if (remaining <= 0) {
      if(timer){
        clearTimeout(timer);
        timer = null;
      }
      fn.apply(ctx, args);
      previous = now;
    } else if (!timer) {
      timer = setTimeout(function () {
        previous = +new Date();
        timer = null;
        fn.apply(ctx, args);
      }, remaining)
    }
  }
}

优化

在节流函数中,我们还可以通过传递 option 配置参数对初次触发及尾随触发的回调执行加以控制。
我们复用改进后的节流函数,默认执行头尾回调。借鉴前人的宝贵经验,我们来指定两个变量,对头尾回调函数的执行加以控制:
 当 leading === false 时,我们取消初次触发的函数回调执行
 当 trailing === false 时,我们取消尾随回调的执行

/*****
  思路:
    1、trailing === false 时,尾随回调不执行
      可直接添加判断,当 trailing !== false 时,才添加定时器执行尾随回调
    2、leading ==== false 时,初次回调不执行
      默认中,初次触发回调执行的判断条件是当 remaining <= 时执行
      当 previous 等于 0 时,为初次触发,若使回调不执行,我们可以强制将 remaining 值设为大于 0
      (将 previous 设置为 now ,可得 remaining === wait ,使定时器准时触发)

      执行过程:
          当我们强制设置 remaining > 0 后,第一次触发函数时,将设置定时器,
          第二次触发函数时, 因为 timer 为 true 则函数无反应
          直至定时器执行,设置 timer = null , previous = 0 
      注意:
        定时器执行时,若不设置 previous 为 0 ,当鼠标脱离目标区域,等待时间大于设置 awit 值
        时,再次触发节流函数,因为 previous 未被释放, !previous 为 false ,则不能强制设置 
        remaining > 0 ,因此依旧会执行初次回调,产生 BUG

****/
function throttle(fn, wait, option) {
  let previous = 0;
  let timer;
  let { leading, trailing } = option;

  return function (...args) {
    let ctx = this;
    let now = +new Date();
    if(!previous && leading === false) previous = now; // 通过设置 previous 使  remaining > 0
    let remaining = wait - (now - previous);

    if (remaining <= 0) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      fn.apply(ctx, args);
      previous = now;
    } else if (!timer && trailing !== false) { // 当 trailing !== false 时,添加定时器,执行尾随回调
      timer = setTimeout(function () {
        previous = leading === false ? 0 : +new Date(); // 将 previous 设置为 0
        timer = null;
        fn.apply(ctx, args);
      }, remaining)
    }
  }
}

观察以上代码可知,trailing: falseleading: false 不能同时设置。

当同时设置两个属性时,第一次触发回调,由于 leading === falseprevious = 0,所以 remaining > 0,函数不执行。

当第二次触发回调,由于第一次触发时,已经设置 previous = now ,所以 !previoustrueremaining 值无法强制设置,直至接下来某次触发回调的时间间隔大于设置的 wait 时,代码走进 remaining <= 0if 判断分支。

而鼠标移出时,因为 trailing === false , 则无法开启定时器,不能初始化previous = 0 , 当等待时间大于设置的 wait 值时,再次触发回调,会立即执行,因此违反了 leading === false ,产生 BUG。

最后

以上,我们已经对节流和防抖做了简单了解。
若有错误,请务必给予指正。
谢谢!

一份自动部署VuePress到Github Pages的脚本

如题,脚本如下:

#!/bin/bash

# 脚本只要发生错误,就终止执行。
set -e

# 生成静态文件
npm run build

# 判读是否存在 .git目录
if [ -d .git/ ];then
echo '已经是一个git仓库,需要删除已有pages分支后重新拉取新分支'
# 删除已有分支,避免代码合并冲突
git branch -D pages
# 添加工作区所有变化到暂存区
git add -A
# 将暂存区里的改动提交到本地的版本库
git commit -m 'add'

# 如果不存在 .git 证明不是git仓库,需要初始化git仓库
else
echo '初始化git仓库'
# 初始化git仓库
git init
# 添加工作区所有变化到暂存区
git add -A
# 将暂存区里的改动提交到本地的版本库
git commit -m 'add'
# 命名当前分支
git branch -M master
# 关联远端仓库
git remote add origin [[email protected]:xxx] #自己的代码仓库
fi

## 将更新推送到远程主分支
git push --set-upstream -f origin master

# 进入pages分支
git checkout -b pages

# 将dist移动到根目录
cp -rf docs/.vuepress/dist ./dist
# 删除原有的docs文件夹
rm -rf docs/
# 将dist重命名
mv ./dist/ ./docs/

# 提交更新
git add -A
git commit -m 'deploy'
git push --set-upstream -f origin pages

# 回到主分支
git checkout master

新建VuePress文档

参考VuePress快速上手

1、创建并进入一个新目录

mkdir vuepress-starter && cd vuepress-starter

2、使用你喜欢的包管理器进行初始化

yarn init # npm init

3、将 VuePress 安装为本地依赖

yarn add -D vuepress # npm install -D vuepress

4、创建你的第一篇文档

mkdir docs && echo '# Hello VuePress' > docs/README.md

5、在 package.json 中添加一些 scripts

{
  "scripts": {
    "dev": "vuepress dev docs",
    "build": "vuepress build docs"
  }
}

6、在本地启动服务器

yarn dev # npm run dev

以上命令可以新建一个基础VuePress文档,并在会在 http://localhost:8080 (opens new window)启动一个热重载的开发服务器。

新建github仓库

截屏2023-11-13 23.17.04.png

添加项目配置

1、配置部署站点的基础路径 此处应为github 仓库名更多配置

截屏2023-11-14 11.49.30.png

添加脚本

1、在VuePress文档中新建deploy.sh(复制最上边脚本)

截屏2023-11-14 10.55.07.png

2、复制github仓库地址

截屏2023-11-13 23.29.28.png

3、修改脚本远端仓库地址

截屏2023-11-14 10.59.11.png

4、添加忽略文件.gitignore

截屏2023-11-14 00.14.20.png

部署文档

在根目录执行 sh deploy.sh 命令

截屏2023-11-14 11.04.05.png

配置github

1、进入Setting页

截屏2023-11-14 11.09.28.png

2、选择分支及文档目录

截屏2023-11-14 11.19.41.png

3、刷新页面号,获取站点URL

截屏2023-11-14 11.23.19.png

4、复制URL,在浏览器窗口打开,即可看到部署好的博客站点

截屏2023-11-14 11.47.57.png

注意:

生成站点地址会有几分钟的延迟,若刷新页面没有出现站点地址,请耐心等待几分钟

文档只可放在根目录(/)或(/docs)目录下:

截屏2023-11-14 11.28.54.png

参考文档

Bash 脚本 set 命令教程

Shell 教程

老生常谈,JS中数学计算精度问题的解决方案

故事从0.1+0.2说起

0.1+0.2是否等于0.3呢?

这是一个前端人耳熟能详的故事,每一个初入前端世界的人,应该都会被它来一次灵魂拷问。它的出现,似乎打破了人们以往对于代码世界“执行严谨、一丝不苟”的刻板印象。然而,这看起来“不够严谨”的形成原因,却正是因为底层代码执行的足够严谨

在初入前端世界的时候,有那么一瞬,我甚至在想难道是底层对于0.10.2有一种特殊的感情?

然而事实并非如此,能被底层这种庞然大物看上并针对的,当然不会只有0.10.2这两个看起来平平无奇的数字,而是包含了这两个数字在内的一批特殊存在。

如下图所示:

math-1

当然,除上述图片内的数字之外,还有更多的其他数字也在这反常理的队列之内。

然而,这篇文章我们并不是来深入讨论这些特殊的数字在进行数学计算时,与底层究竟产生了什么样的恩怨纠葛。我们只需要简单知道:在计算机世界中,所有信息最后都是以二进制存储的,可是数字中的小数部分在按照一定规则转换为二进制时,有些数字会产生无限循环的现象,但计算机精度位数是有限的,所以对超出位数的部分做了四舍五入的计算,因此造成了精度的丢失。

本文仅仅针对以上现象,结合日常开发的实践,讨论一些解决问题的方法。

初步解决

既然是因为小数部分在转换二进制时做了四舍五入的处理,那么计算时先将小数转为整数再计算是不是就可以了?

依据上面的**,在javascript中进行小数计算,通常会采用放大倍数取整之后再计算,得出结果之后再缩小还原的技术方案。

例:计算0.1+0.2,通常会将0.1和0.2放大10倍,相加之后再缩小10倍

代码如下:

  /**
   * 已知:a为0.1,b为0.2
   * 求:a与b的和
   * */
  const a = 0.1;
  const b = 0.2;
  const result = ((a * 10) + (b * 10)) / 10;
  console.log(result) // 0.3

如上,针对已知小数位数的数字,我们可以直接采用放大相应倍数取整,然后再计算的方式来规避小数计算的精度问题。

可是在实际的业务开发中,对于需要进行计算处理的数字,我们往往无法预先获知数字包含的小数位数。对于此种情况,我们便需要先确定小数位数,然后确定放大倍数,再进行计算。

代码如下:

  /**
   * 已知:a,b为两个精度随机的小数
   * 求:a与b的和
  */

  // 生成精度随机的小数
  const getNumber = () => {
    const len = Math.random() * 10;
    const num = Number((Math.random() * 10).toFixed(len))
    return num
  }

  // 计算放大倍数
  const getPower = (a, b) => {
    // 获取a,b小数位长度,如没有小数位则默认值为0
    const aLen = a.toString().split(".")[1]?.length || 0;
    const bLen = b.toString().split(".")[1]?.length || 0;
    // 获取最大长度
    const len = Math.max(aLen, bLen);
    // 计算返回放大倍数
    return Math.pow(10, len)
  }

  const a = getNumber();
  const b = getNumber();
  const power = getPower(a, b);

  const result = ((a * power) + (b * power)) / power;

因为以上代码中,ab皆由getNumber函数随机生成,为了便于观察,我们添加log后,在浏览器中运行代码。

如下图所示:

math-2

观察可知,计算结果正确。以上,我们通过使用getPower函数确定放大的倍数,然后进行计算。这也是目前大部分同学解决小数计算精度问题的主要方式。

然而,故事到这里就结束了吗?

当然不是!

以上方式虽然解决了一些精度问题,但是并没有解决所有的精度问题。在这个特殊的小数群体中,并不是所有的小数都可以通过放大倍数来取整的!

如下图所示:

math-3

因此先放大再计算也并不是十分可靠,如下图所示:

math-4

大胆取舍

Number.EPSILON

我们已经知道,精度的误差是由于底层在计算时做了一些四舍五入造成的,因此我们分析后可以断定被舍弃的部分一定是小于可以表示的最小浮点数的。

例如:有数字 a1.234,对 a 做保留两位小数的处理后,得到数字 bb的值为1.23。则上述操作中舍弃的部分 0.004,一定小于保留精度 0.01

基于以上分析,我们有理由相信:在 javascript 中当两个数字之间的差值小于可以表示的最小浮点数,那么我们就认为这两个数字相等。

可是,最小的浮点数该如何获取呢?

javascript 为我们提供了这样一个属性:Number.EPSILON 静态数据属性,表示 1 与大于 1 的最小浮点数之间的差值。

详细介绍可查看MDN

我们对之前的代码做一些优化,在放大一定倍数之后,做差值比较,确认最终结果。

代码如下:

const getPower = (a, b, c) => {
  // 获取a,b小数位长度,如没有小数位则默认值为0
  const aLen = a.toString().split(".")[1]?.length || 0;
  const bLen = b.toString().split(".")[1]?.length || 0;
  const cLen = c.toString().split(".")[1]?.length || 0;
  // 获取最大长度
  const len = Math.max(aLen, bLen, cLen);
  // 计算返回放大倍数
  return Math.pow(10, len)
}
// 差值比价
const compare = (n) => {
  const result = Math.round(n);

  // 如差值小于 Number.EPSILON 则认为和取整之后的数字相等
  return n - result < Number.EPSILON ? result : n;
}

var a = 19.9;
var b = 4788.4;
var c = 0.01;
var power = getPower(a, b, c);

const result = (compare((a * power)) + compare((b * power)) + compare((c * power))) / power
console.log(result)

在浏览器运行代码,可知计算正确。

如下图所示:

math-5 ### Math.round

理论上讲,一个两位小数乘 100 后一定会得到一个整数,一个三位小数乘 1000 以后一定也会得到一个整数。同理可知,一个 n 位小数乘(10^n)后,一定可以得到一个整数!

虽然在计算机世界中小数计算有些误差,但通过上述代码我们知道,这个误差小到几乎可以忽略,那么我们是不是可以大胆一点,放大之后无需比较,直接四舍五入!

我们修改以上代码,舍弃compare函数。

代码如下:

const getPower = (a, b, c) => {
  // 获取a,b小数位长度,如没有小数位则默认值为0
  const aLen = a.toString().split(".")[1]?.length || 0;
  const bLen = b.toString().split(".")[1]?.length || 0;
  const cLen = c.toString().split(".")[1]?.length || 0;
  // 获取最大长度
  const len = Math.max(aLen, bLen, cLen);
  // 计算返回放大倍数
  return Math.pow(10, len)
}

var a = 19.9;
var b = 4788.4;
var c = 0.01;
var power = getPower(a, b, c);

const result = (Math.round((a * power)) + Math.round((b * power)) + Math.round((c * power))) / power
console.log(result)

在浏览器中运行后发现,结果依然正确。

如下图所示:

math-6

封装完善

基于以上推导,我们可以封装一个简易的计算函数。

代码如下:

function compute(type, ...args) {
  // 计算放大倍数
  const getPower = (numbers) => {
    const lens = numbers.map(num => num.toString().split(".")[1]?.length || 0);
    // 获取最大长度
    const len = Math.max(...lens);
    // 计算返回放大倍数
    return Math.pow(10, len)
  }

  // 获取放大倍数
  const power = getPower(args);

  // 获取放大后的值
  const newNumbers = args.map(num => Math.round(num * power));

  // 计算结果
  let result = 0;
  switch (type) {
    case "+":
      result = newNumbers.reduce((preNumber, nextNumber) => preNumber + nextNumber, result) / power;
      break;
    case "-":
      result = newNumbers.reduce((preNumber, nextNumber) => preNumber - nextNumber) / power;
      break;
    case "*":
      result = newNumbers.reduce((preNumber, nextNumber) => preNumber * nextNumber) / (power ** newNumbers.length);
      break;
    case "/":
      result = newNumbers.reduce((preNumber, nextNumber) => preNumber / nextNumber);
      break;
  }

  return {
    result,
    next(nextType, ...nextArgs) {
      return compute(nextType, result, ...nextArgs);
    }
  }
}

// 验证
const arr = [0.1, 0.2, 29.6]
const a = compute('+', ...arr);
const b = a.next('-', 4, 2, 4);
const c = b.next('*', 100);
const d = c.next('+', 2798.4);
const e = d.next('*', 100);
const f = e.next('/', 1000);
const r = compute('+', ...arr).next('-', 4, 2, 4).next('*', 100).next('+', 2798.4).next('*', 100).next('/', 1000);

console.log('a: ', a.result) // a:  29.9
console.log('b: ', b.result) // b:  19.9
console.log('c: ', c.result) // c:  1990
console.log('d: ', d.result) // d:  4788.4
console.log('e: ', e.result) // e:  478840
console.log('f: ', f.result) // f:  478.84
console.log('r: ', r.result) // f:  478.84

经简单测试后,可知compute函数已实现基本的四则运算,且可以链式调用。

结语

若有错误,请务必给予指正。
谢谢!

参考文档

Number.EPSILON

js 动画原理

前言

动画是一个创造运动假象的过程。几乎所有的投影运动媒体都采用帧实现运动。—— 《HTML5+JavaScript动画基础》

相关知识

静态动画

我们知道,投影动画由一帧帧的图像切换而得到,而每一帧的内容是事先已经存在的,不可变的。
这种动画不会与用户产生交互效果(你关闭电源,动画暂停不能算作交互),不管用户做了何种操作,如点击鼠标、敲击键盘,缩放浏览器等等,动画只会以我们事先排好的帧顺序切换。

我们用下面代码,做一个简单模拟:

const frames = ['第一帧', '第二帧', '第三帧', '第四帧', '第五帧'];
function movie(frames){
  let n = 0;
  function draw(frame){
    if(frame){
      // 渲染显示该帧图像
      console.log(frame);
      //执行下一帧
      setTimeout(() => {
        n += 1
        draw(frames[n])
      }, 1000)
    }else{
      console.log('放映完毕!')
    }
  }
  draw(frames[n])
}
movie(frames);

效果图如下:

动态动画

动态动画与静态动画的区别在于,在绘制当前帧的时候,并不会知道下一帧要绘制什么内容。

动态动画会在自己的绘制方法中维护一套规则,通过每次绘制前对当前环境的规则校验,来确定即将要绘制的图形内容。

以此,来完成和用户交互的目的。

模拟代码如下:

function movice(){
  let click = 0;
  function draw(){
    if(click === 1){
      // 用户点击一次后的放映规则
      console.log('用户点击了一次屏幕, 以后每帧画两个苹果');
    }else if(click === 2){
      // 用户点击两次后的规则
      return console.log('用户点击了两次屏幕, 我要停止了')
    }else{
      // 正常的放映规则
      console.log('这次画一个苹果,1 秒后画下一个苹果');
    }
    setTimeout(() => {
      draw()
    }, 1000)
  }
  draw()

  // 2秒后,默认用户第一次点击
  setTimeout(() => click += 1, 2000)
  // 5秒后,模拟用户二次点击
  setTimeout(() => click += 1, 5000)
}

movice()

效果图如下:

eventloop

我们知道 js 是单线程,代码自上至下执行时,遇到宏任务则会添加进宏任务队列,遇到微任务则会添加进微任务队列。

当代码执行完毕后,会首先检查自上而下执行代码产生的微任务,清空微任务队列后,会从宏任务队列中取出要执行的宏任务,执行完这个宏任务后,会再次检查有没有产生微任务,如果产生了微任务,则会清空。之后,会再次检查宏任务队列,周而复始,形成事件环。

定时器属于宏任务,Promise 中的 thenprocess.nextTick 属于微任务。

requestAnimationFrname MDN文档

在早期的 html 规范中,并没有考虑到动画的场景,因此也没有相对应的 API 来供我们使用。

我们只能通过 setTimeout 来实现动画效果。但是通过了解 eventloop 我们可以知道 setTimeout 的计时是不准确的。

代码自上而下执行,遇到 setTimeout 则会挂起,等待当前代码执行完毕之后才会执行 setTimeout,若当前代码量较大时,会造成计时不精确。如下:

/**
 *  到达 setTimeout 时间后,当前代码仍在执行时 
 **/
function test(){
  console.log(+new Date(), '------1-----');
  setTimeout(() => console.log(+new Date(), '-----3----'), 10);
  for(let i = 0; i < 100000000; i++){}
  console.log(+new Date(), '-----2-----')
}
test()
// 1578480299388 "------1-----"
// 1578480299442 "-----2-----"
// 1578480299442 "-----3----"


/**
 *  到达 setTimeout 时间后,当前代码已经执行完时
 **/

function test(){
  console.log(+new Date(), '------1-----');
  setTimeout(() => console.log(+new Date(), '-----3----'), 70);
  for(let i = 0; i < 100000000; i++){}
  console.log(+new Date(), '-----2-----')
}
test()

// 1578480314090 "------1-----"
// 1578480314143 "-----2-----"
// 1578480314161 "-----3----"

效果图如下:

通过代码打印,我们可以清楚的看到 setTimeout 的不精确性。

requestAnimationFrname 会在浏览器下次重绘之前调用指定的回调函数更新动画,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。不仅避免了 setTimeout 的时间不精确问题,而且有效的提高了性能。

封装自己的 animation

通过上面知识的铺垫,下面我们来封装自己的动画函数。

我们先新建一个 html 页面,代码如下:

  <!-- 简单的样式 -->
  <style>
    .box {
      margin: 50px;
      width: 1000px;
      height: 700px;
      border: 1px solid orange;
      padding: 20px;
    }

    .target {
      width: 200px;
      height: 200px;
      background: paleturquoise;
    }
  </style>

  <!-- DOM 结构 -->
  <div class="box">
    <div class="target" id="target"></div>
  </div>

  <!-- js 脚本 -->
  <script>
    function animated(target, props, duration) {
      // 执行动画
    }
    let target = document.getElementById('target');
    // 最终样式
    let props = {
      width: '400px',
      height: '400px'
    }
    animated(target, props, 2000)
  </script>

我们知道:动画分为动态动画和静态动画两种。而不管是我们使用 requestAnimationFrname 还是 setTimeout ,都不能做到时间上的精确,所以我们并不能确定要在何时显示下一帧图像。因此,我们无法通过事先的计算,得到所有帧的描述信息。所以在我们的 animated 方法中,我们一定是使用动态动画的方式来实现动画。

确定了实现动画的方式,我们便知道了接下来需要做些什么:我们需要维护一套规则,在每次 animated 函数调用时,来匹配规则内容,然后得到需要显示的图像信息,最后显示出来。

我们再来细化一下思路:

  • 获取目标的当前信息,计算出当前信息与最终期望信息的差值
  • 每次 animated 函数触发时,计算出动画运行时间与设定时间的比值
  • 通过时间比值,得到需要渲染的图像信息
  • 设置临界判断,设定时间超超出时,终止动画

根据以上思路,我们来改造 animate 函数:

function animated(target, props, duration) {
  // 记录动画开始时间
  let start = +new Date();
  // 记录最初信息
  let cssInfo = getInfo(target)
  // 执行动画
  function draw(){
    // 校验是否超出临界值
    let percent = getProgress();
    console.log(percent, '===')
    // 超出临界值则终止动画
    if(percent >= 1) return;
    // 未超出临界值则继续更新目标元素信息
    setStyle(percent);
    // 进入下一次循环
    window.requestAnimationFrame(draw)
  }
  window.requestAnimationFrame(draw)

  // 工具方法
  function setStyle(percent){
    console.log('-=-=-=setStyle', percent)
  }
  function getInfo(el){
    let css = el.ownerDocument.defaultView.getComputedStyle(el, null);
    let cssInfo = {};
    for(let key in props){
      cssInfo[key] = css.getPropertyValue(key)
    }
    return cssInfo
  }
  function getProgress(){
    let percent = (+new Date() - start) / duration;
    return percent >= 1 ? 1 : percent;
  }
}

效果图如下:

通过观察效果图,我们发现,当最后一次校验进度时,若超出临界值则直接停止,可是在上一次更新信息时,进度还不到百分百,如果直接停止,则会造成细微的误差。

为了避免这种误差,我们需要维护另一个变量,来保存上次的进度,如果上次进度不到百分百,而当前进度达到百分百时,我们需要添加一次绘制。

如下:

// 新增代码
function animated(target, props, duration) {
  // ... ...略
  // 记录上次进度
  let oldPercent = 0;
      
  // 执行动画
  function draw(){
   // ... ...略

    // 超出临界值则终止动画
    if(percent >= 1 && oldPercent >= 1) return;
    // 未超出临界值则继续更新目标元素信息
    setStyle(percent);
    // 保存进度
    oldPercent = percent;
    // 进入下一次循环
    window.requestAnimationFrame(draw)
  }
  // ... ...略
}

效果图如下:

现在,我们已经完成了基本的逻辑流程,只要用 setStyle 方法更新信息即可完成动画。

下面,我们来完成 setStyle 方法:

function setStyle(percent){
  let style = getStyle(cssInfo, props, percent);
  for(let key in style){
    target.style[key] = style[key]
  }
}

function getStyle(cssInfo, props, percent){
  let style = {};
  for(let key in props){
    let cssVal = parseFloat(cssInfo[key]);
    let propsVal = parseFloat(props[key]);
    let newVal = (propsVal - cssVal) * percent + cssVal;
    style[key] = `${newVal}px`
  }
  return style;
}

效果图如下:

最后

以上,我们已经完成了一个基本的动画函数,我们还可以通过设置 left,top,right,bottom, margin 等属性,来实现位置的移动。

例如:

  // ... ...略
  let props = {
    width: '400px',
    height: '400px',
    'margin-left': '300px'
  }
  // ... ...略

效果图如下:

当然,这个函数还有很多需要优化的地方,例如我们最后传入的 margin-left,并不能用 marginLeft 这种驼峰的形式传入。我们还需要在 getStyle 中做大量的判断,来实现对 background, transform, opacity 等等属性的兼容。还可以在动画中加入三角函数运算,使得动画的渐变更加平滑。等等... ...

最后

以上,我们已经对 js 的动画原理做了简单了解。
若有错误,请务必给予指正。
谢谢!

参考书籍

《HTML5+JavaScript动画基础》

耗时七天,我写完了自己的第一个小程序

一入红尘深似海。

自2016年参加工作时申请了第一张信用卡,至今已有七年矣。在这七年之中,自己信用卡的数量由1张逐渐增加到12张,后又慢慢减少到如今的5张。七年的卡海浮沉让我从初入社会的一无所有,到如今的负债累累。

当然,这篇文章并不是来记录自己七年的负债之旅,而是在经历了多年“钱都去哪儿了?”的内心呼唤后的心灵觉醒:还是要有个账本记账啊!

我的需求并不复杂:

1、可以快速的知道如今自己卡的总额度是多少,还需要还的欠款是多少(清楚负债情况)

2、可以快速知道每张卡的可用额度是多少,账单日是哪天(便于刷卡时明确知道该刷哪张卡,不至于出现今天刚刷了卡,明天就出了账单要还的现象)

3、可以知道每个月刷卡的总手续费是多少(清楚损益,明白每个月的损耗)

4、记录收支(了解每一分钱都去了哪里)

基于以上四个简单的需求,在尝试现在市面上十几款记账软件后,我惊奇的发现:竟然没有一款合适的软件可以满足我的需求!

于是,我做了一个【XXXX】的决定:自己来写一个工具吧!

然后,就诞生了我发布的第一个小程序:了账

小程序简介

了(liao)账是一款简洁的记账小程序。了账中的了字,是明了之意,清楚明白自己的账目,亦是了结之意,祝愿各位卡友早日上岸。

了账的页面相对简洁,有【未登录】【已登录】两种状态展示,如下图所示:

【未登录】

截屏2023-10-23 08.19.09.png

【已登录】

截屏2023-10-23 08.27.30.png

了账只有账户账单两个tab页,分别用来展示当前账户信息和查看收支记录。

账户页面展示了用户(目前只有作者本人😄)较为关心的几个数据:【当前额度】、【可用额度】、【现金额度】、【信用卡总览】、【当前账户】。

账单页面除了查看每一笔收支记录外,在顶部也展示了当月总出账、总入账信息。

截屏2023-10-29 14.53.05.png

新增账户

用户可通过【新增】按键创建账户,在账户页面,顶部账户信息会随之动态改变。如下图所示:

截屏2023-10-29 15.05.44.png

在新增页面,用户可点击账户类型修改新增账户的类型,目前【了账】共包含【信用卡】、【储蓄卡】、【支付宝】、【微信】、【其他】共五类账户。除信用卡外,其余四类账户额度统一归类为【现金额度】。

信用卡除了【固定额度】之外有时会给一部分【临时额度】,因此,在新增账户页面,除了【固定额度】之外,添加了【当前额度】字段。【当前额度】是包含【固定额度】和【临时额度】的账户总额度。

新增收支

当用户创建过账户后,就可以点击【账户】页面右下角【记一笔】浮块创建收支记录,并在【账单】页面查看。相应的,账户页面所展示的【账户信息】也会随之动态改变。如下图所示:

截屏2023-10-29 15.24.00.png

在记录收支时,不同的账户类型可选的账单类型也不相同。如:信用卡账户下可选择的账单类型为【日常支出】、【个人刷卡】、【账单还款】,储蓄卡账户下可选择的账单类型为【日常支出】、【日常收入】、【转账支出】、【转账收入】,支付宝账户微信账户其他账户则多出【提现】类型可供选择。如下图所示:

截屏2023-10-29 15.41.54.png

当账单类型为【日常支出】时,则须选择支出类型。目前共有【食】、【行】、【衣】、【住】、【娱乐】、【其他支出】六类支出可供选择。如下图所示:

截屏2023-10-29 15.35.20.png

信用卡账户账单类型为【个人刷卡】,以及支付宝账户微信账户其他账户账单类型为【提现】时,则需填写【收款金额】。收款账户为除【信用卡账户】外的其他账户,收款金额为除去手续费之外的实际到账金额。如下图所示:

截屏2023-10-29 15.53.45.png

账单的编辑、删除和账户的编辑、删除

用户可通过左滑对当前账户及当前收支进行编辑、删除。当收支被删除后,账户信息将会回退该笔收支。当账户被删除后,该账户下的所有收支将不可被编辑、删除。如下图所示:

截屏2023-11-11 12.27.15.png

账户详情和账单详情

点击每个账户和账单,可进入详情页,查看详情信息。如下图所示:

截屏2023-11-11 14.25.39.png

写在最后

账本只是工具,最主要的还是要诸位卡友调整好心态,量入为出。祝愿各位早日上岸!!!

欢迎大家体验:

扫码_搜索联合传播样式-标准色版.png

写代码用了7天,备案发布将近一个月!!!最后上线认证居然还收了30块巨款!!!至今仍未明白:经历了实名注册小程序号,实名IPC备案后,最后上线认证的意义在哪里?难道只为承袭小马哥一贯的氪金传统?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.