2024年10月

本文介绍批量下载大量多时相的遥感影像文件后,基于
Python
语言与每一景遥感影像文件的文件名,对这些
已下载的影像文件
加以缺失情况的核对,并自动统计、列出
未下载影像
所对应的时相的方法。

批量下载
大量遥感影像文件
对于
RS
学生与从业人员可谓十分常见。在我们之前的文章中,就介绍过同样基于文件名称,对未成功下载的遥感影像加以统计,并自动筛选出未下载成功的遥感影像的下载链接的方法;在本文中,我们同样基于
Python
与栅格文件的
文件名称
,对类似的需求加以实现。

首先,本文的需求和前述提及的文章略有不同。在这里,我们已经下载好了大量的、
以遥感数据成像时间为文件名
的栅格文件,如下图所示。

image

其中,不难发现我们这里的遥感影像数据是从每一年的
001
天开始,每隔
8
天生成一景影像,每一景影像的名称后
3
位数字就是
001

009

017
这样表示天数的格式;此外,前
4
位数字表示年份,我们这里有从
2020
开始到
2022
结束、一共
3
年的遥感影像数据。

现在,我们希望对于上述文件加以核对,看看在这
3
年中,是否有未下载成功的遥感影像文件;如果有的话,还希望输出下载失败的文件个数和对应的文件名称(也就是对应文件的成像时间)。

明确了需求后,我们就可以开始具体的操作。首先,本文所需用到的代码如下。

# -*- coding: utf-8 -*-
"""
Created on Sat Dec 30 23:32:54 2023

@author: fkxxgis
"""

import os

def check_missing_dates(folder_path):
    start_year = 2020
    end_year = 2022
    days_per_file = 8

    missing_dates = []

    for year in range(start_year, end_year + 1):
        for day in range(1, 366, days_per_file):
            file_name = str(year) + "{:03d}".format(day) + ".tif"
            file_path = os.path.join(folder_path, file_name)
            
            if not os.path.exists(file_path):
                missing_dates.append(file_name[:-4])

    return missing_dates

folder_path = "F:/Data_Reflectance_Rec/NDVI"
missing_dates = check_missing_dates(folder_path)

print("Total missing dates:", len(missing_dates))
print("Missing dates:")
for date in missing_dates:
    print(date)

这段代码整体思路也很明确。

首先,我们导入所需的模块。在这里,
os
模块用于文件路径操作。

接下来,我们定义一个名为
check_missing_dates
的函数,其接收一个文件夹路径作为参数;这个函数用于检查遗漏的日期。在这个函数中,我们定义了起始年份
start_year
和结束年份
end_year
,以及每个文件之间的日期间隔
days_per_file
;随后,创建一个空列表
missing_dates
,用于存储遗漏的日期。

随后,我们使用嵌套的循环遍历每一年和每一天。在每一天的循环中,构建文件名,如
"2020017.tif"
,并构建文件的完整路径。接下来,使用
os.path.exists()
函数检查文件路径是否存在——如果文件不存在,则将日期添加到遗漏日期列表
missing_dates
中。在循环结束后,返回遗漏日期列表
missing_dates

在函数外部,我们定义要检查的文件夹路径
folder_path
,然后就可以调用
check_missing_dates
函数,传入文件夹路径参数,执行日期检查,将返回的遗漏日期列表赋值给
missing_dates

最后,我们打印遗漏日期的总数
len(missing_dates)
,并打印每个具体的遗漏日期。

执行上述代码,即可出现如下图所示的结果。即在我这里,目前有
8
个日期的遥感影像文件没有下载成功,我们再对照这
8
个遥感影像的日期,重新到相关网站中下载即可。

至此,大功告成。

前言

在Vue3.5版本中最大的改动就是
响应式重构
,重构后性能竟然炸裂的提升了
56%
。之所以重构后的响应式性能提升幅度有这么大,主要还是归功于:
双向链表

版本计数
。这篇文章我们来讲讲使用
双向链表
后,Vue内部是如何实现
依赖收集

依赖触发
的。搞懂了这个之后你就能掌握Vue3.5重构后的响应式原理,至于
版本计数
如果大家感兴趣可以在评论区留言,关注的人多了欧阳后面会再写一篇
版本计数
的文章。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

3.5版本以前的响应式

在Vue3.5以前的响应式中主要有两个角色:
Sub
(订阅者)、
Dep
(依赖)。其中的订阅者有watchEffect、watch、render函数、computed等。依赖有ref、reactive等响应式变量。

举个例子:

<script setup lang="ts">
import { ref, watchEffect } from "vue";
let dummy1, dummy2;
//Dep1
const counter1 = ref(1);
//Dep2
const counter2 = ref(2);
//Sub1
watchEffect(() => {
  dummy1 = counter1.value + counter2.value;
  console.log("dummy1", dummy1);
});
//Sub2
watchEffect(() => {
  dummy2 = counter1.value + counter2.value + 1;
  console.log("dummy2", dummy2);
});

counter1.value++;
counter2.value++;
</script>

在上面的两个watchEffect中都会去监听ref响应式变量:
counter1

counter2

初始化时会分别执行这两个watchEffect中的回调函数,所以就会对里面的响应式变量
counter1

counter2
进行
读操作
,所以就会走到响应式变量的get拦截中。

在get拦截中会进行依赖收集(此时的Dep依赖分别是变量
counter1

counter2
)。

因为在依赖收集期间是在执行
watchEffect
中的回调函数,所以依赖对应的
Sub订阅者
就是watchEffect。

由于这里有两个watchEffect,所以这里有两个
Sub订阅者
,分别对应这两个watchEffect。

在上面的例子中,watchEffect监听了多个ref变量。也就是说,一个
Sub订阅者
(也就是一个watchEffect)可以订阅多个依赖。

ref响应式变量
counter1
被多个watchEffect给监听。也就是说,一个
Dep依赖
(也就是
counter1
变量)可以被多个订阅者给订阅。

Sub订阅者

Dep依赖
他们两的关系是
多对多
的关系!!!
old
上面这个就是以前的响应式模型。

新的响应式模型

在Vue3.5版本新的响应式中,Sub订阅者和Dep依赖之间不再有直接的联系,而是新增了一个Link作为桥梁。Sub订阅者通过Link访问到Dep依赖,同理Dep依赖也是通过Link访问到Sub订阅者。如下图:
new

把上面这个图看懂了,你就能理解Vue新的响应式系统啦。现在你直接看这个图有可能看不懂,没关系,等我讲完后你就能看懂了。

首先从上图中可以看到Sub订阅者和Dep依赖之间没有任何直接的连接关系了,也就是说Sub订阅者不能直接访问到Dep依赖,Dep依赖也不能直接访问Sub订阅者。

Dep依赖我们可以看作是X轴,Sub订阅者可以看作是Y轴,这些Link就是坐标轴上面的坐标。

Vue响应式系统的核心还是没有变,只是多了一个Link,依然还是以前的那一套
依赖收集

依赖触发
的流程。


依赖收集
的过程中就会画出上面这个图,这个不要急,我接下来会仔细去讲图是如何画出来的。

那么依赖触发的时候又是如何利用上面这种图从而实现触发依赖的呢?我们来看个例子。

上面的这张图其实对应的是我之前举的例子:

<script setup lang="ts">
import { ref, watchEffect } from "vue";
let dummy1, dummy2;
//Dep1
const counter1 = ref(1);
//Dep2
const counter2 = ref(2);
//Sub1
watchEffect(() => {
  dummy1 = counter1.value + counter2.value;
  console.log("dummy1", dummy1);
});
//Sub2
watchEffect(() => {
  dummy2 = counter1.value + counter2.value + 1;
  console.log("dummy2", dummy2);
});

counter1.value++;
counter2.value++;
</script>

图中的
Dep1依赖
对应的就是变量
counter1

Dep2依赖
对应的就是变量
counter2

Sub1订阅者
对应的就是第一个
watchEffect
函数,
Sub2订阅者
对应的就是第二个
watchEffect
函数。

当执行
counter1.value++
时,就会被变量
counter1
(也就是
Dep1依赖
)的set函数拦截。从上图中可以看到
Dep1依赖
有个箭头(对照表中的
sub
属性)指向
Link3
,并且
Link3
也有一个箭头(对照表中的
sub
属性)指向
Sub2

前面我们讲过了这个
Sub2
就是对应的第二个
watchEffect
函数,指向
Sub2
后我们就可以执行
Sub2
中的依赖,也就是执行第二个
watchEffect
函数。这就实现了
counter1.value++
变量改变后,重新执行第二个
watchEffect
函数。

执行了第二个
watchEffect
函数后我们发现
Link3
在Y轴上面还有一个箭头(对照表中的
preSub
属性)指向了
Link1
。同理
Link1
也有一个箭头(对照表中的
sub
属性)指向了
Sub1

前面我们讲过了这个
Sub1
就是对应的第一个
watchEffect
函数,指向
Sub1
后我们就可以执行
Sub1
中的依赖,也就是执行第一个
watchEffect
函数。这就实现了
counter1.value++
变量改变后,重新执行第一个
watchEffect
函数。

至此我们就实现了
counter1.value++
变量改变后,重新去执行依赖他的两个
watchEffect
函数。

我们此时再来回顾一下我们前面画的新的响应式模型图,如下图:
new
我们从这张图来总结一下依赖触发的的规则:

响应式变量
Dep1
改变后,首先会指向Y轴(
Sub订阅者
)的
队尾
的Link节点。然后从Link节点可以直接访问到Sub订阅者,访问到订阅者后就可以触发其依赖,这里就是重新执行对应的
watchEffect
函数。

接着就是顺着Y轴的
队尾

队头
移动,每移动到一个新的Link节点都可以指向一个新的Dep依赖,在这里触发其依赖就会重新指向对应的
watchEffect
函数。

看到这里有的同学有疑问如果是
Dep2
对应的响应式变量改变后指向
Link4
,那这个
Link4
又是怎么指向
Sub2
的呢?他们中间不是还隔了一个
Link3
吗?

每一个Link节点上面都有一个
sub
属性直接指向Y轴上面的Sub依赖,所以这里的
Link4
有个箭头(对照表中的
sub
属性)可以直接指向
Sub2
,然后进行依赖触发。

这就是Vue3.5版本使用
双向链表
改进后的依赖触发原理,接下来我们会去讲依赖收集过程中是如何将上面的模型图画出来的。

Dep、Sub和Link

在讲Vue3.5版本依赖收集之前,我们先来了解一下新的响应式系统中主要的三个角色:
Dep依赖

Sub订阅者

Link节点

这三个角色其实都是class类,依赖收集和依赖触发的过程中实际就是在操作这些类new出来的的对象。

我们接下来看看这些类中有哪些属性和方法,其实在前面的响应式模型图中我们已经使用箭头标明了这些类上面的属性。

Dep依赖

简化后的
Dep
类定义如下:

class Dep {
  // 指向Link链表的尾部节点
  subs: Link
  // 收集依赖
  track: Function
  // 触发依赖
  trigger: Function
}

Dep依赖上面的
subs
属性就是指向队列的
尾部
,也就是队列中最后一个Sub订阅者对应的Link节点。

new

比如这里的
Dep1
,竖向的
Link1

Link3
就组成了一个队列。其中
Link3
是队列的队尾,
Dep1

subs
属性就是指向
Link3

其次就是
track
函数,对响应式变量进行读操作时会触发。触发这个函数后会进行依赖收集,后面我会讲。

同样
trigger
函数用于依赖触发,对响应式变量进行写操作时会触发,后面我也会讲。

Sub订阅者

简化后的
Sub
订阅者定义如下:

interface Subscriber {
  // 指向Link链表的头部节点
  deps: Link
  // 指向Link链表的尾部节点
  depsTail: Link
  // 执行依赖
  notify: Function
}

想必细心的你发现了这里的
Subscriber
是一个
interface
接口,而不是一个class类。因为实现了这个
Subscriber
接口的class类都是订阅者,比如watchEffect、watch、render函数、computed等。

new

比如这里的
Sub1
,横向的
Link1

Link2
就组成一个队列。其中的队尾就是
Link2

depsTail
属性),队头就是
Link1

deps
属性)。

还有就是
notify
函数,执行这个函数就是在执行依赖。比如对于watchEffect来说,执行
notify
函数后就会执行watchEffect的回调函数。

Link节点

简化后的
Link
节点类定义如下:

class Link {
  // 指向Subscriber订阅者
  sub: Subscriber
  // 指向Dep依赖
  dep: Dep
  // 指向Link链表的后一个节点(X轴)
  nextDep: Link
  // 指向Link链表的前一个节点(X轴)
  prevDep: Link
  // 指向Link链表的下一个节点(Y轴)
  nextSub: Link
  // 指向Link链表的上一个节点(Y轴)
  prevSub: Link
}

前面我们讲过了新的响应式模型中
Dep依赖

Sub订阅者
之间不会再有直接的关联,而是通过Link作为桥梁。

那么作为桥梁的Link节点肯定需要有两个属性能够让他直接访问到
Dep依赖

Sub订阅者
,也就是
sub

dep
属性。

其中的
sub
属性是指向
Sub订阅者

dep
属性是指向
Dep依赖

new

我们知道Link是坐标轴的点,那这个点肯定就会有上、下、左、右四个方向。

比如对于
Link1
可以使用
nextDep
属性来访问后面这个节点
Link2

Link2
可以使用
prevDep
属性来访问前面这个节点
Link1

请注意,这里名字虽然叫
nextDep

prevDep
,但是他们指向的却是Link节点。然后通过这个Link节点的
dep
属性,就可以访问到后一个
Dep依赖
或者前一个
Dep依赖

同理对于
Link1
可以使用
nextSub
访问后面这个节点
Link3

Link3
可以使用
prevSub
访问前面这个节点
Link1

同样的这里名字虽然叫
nextSub

prevSub
,但是他们指向的却是Link节点。然后通过这个Link节点的
sub
属性,就可以访问到下一个
Sub订阅者
或者上一个
Sub订阅者

如何收集依赖

搞清楚了新的响应式模型中的三个角色:
Dep依赖

Sub订阅者

Link节点
,我们现在就可以开始搞清楚新的响应式模型是如何收集依赖的。

接下来我将会带你如何一步步的画出前面讲的那张新的响应式模型图。

还是我们前面的那个例子,代码如下:

<script setup lang="ts">
import { ref, watchEffect } from "vue";
let dummy1, dummy2;
//Dep1
const counter1 = ref(1);
//Dep2
const counter2 = ref(2);
//Sub1
watchEffect(() => {
  dummy1 = counter1.value + counter2.value;
  console.log("dummy1", dummy1);
});
//Sub2
watchEffect(() => {
  dummy2 = counter1.value + counter2.value + 1;
  console.log("dummy2", dummy2);
});

counter1.value++;
counter2.value++;
</script>

大家都知道响应式变量有
get

set
拦截,当对变量进行读操作时会走到
get
拦截中,进行写操作时会走到
set
拦截中。

上面的例子第一个
watchEffect
我们叫做
Sub1
订阅者,第二个
watchEffect
叫做
Sub2
订阅者.

初始化时
watchEffect
中的回调会执行一次,这里有两个
watchEffect
,会依次去执行。

在Vue内部有个全局变量叫
activeSub
,里面存的是当前active的Sub订阅者。

执行第一个
watchEffect
回调时,当前的
activeSub
就是
Sub1


Sub1
中使用到了响应式变量
counter1

counter2
,所以会对这两个变量依次进行读操作。

第一个
watchEffect

counter1
进行读操作

先对
counter1
进行读操作时,会走到
get
拦截中。核心代码如下:

class RefImpl {
get value() {
  this.dep.track();
  return this._value;
}
}

从上面可以看到在get拦截中直接调用了dep依赖的
track
方法进行依赖收集。

在执行
track
方法之前我们思考一下当前响应式系统中有哪些角色,分别是
Sub1

Sub2
这两个
watchEffect
回调函数订阅者,以及
counter1

counter2
这两个Dep依赖。此时的响应式模型如下图:
step1

从上图可以看到此时只有X坐标轴的Dep依赖,以及Y坐标轴的Sub订阅者,没有一个Link节点。

我们接着来看看dep依赖的
track
方法,核心代码如下:

class Dep {
// 指向Link链表的尾部节点
subs: Link;
track() {
  let link = new Link(activeSub, this);
  if (!activeSub.deps) {
    activeSub.deps = activeSub.depsTail = link;
  } else {
    link.prevDep = activeSub.depsTail;
    activeSub.depsTail!.nextDep = link;
    activeSub.depsTail = link;
  }
  addSub(link);
}
}

从上面的代码可以看到,每执行一次
track
方法,也就是说每次收集依赖,都会执行
new Link
去生成一个Link节点。

并且传入两个参数,
activeSub
为当前active的订阅者,在这里就是
Sub1
(第一个
watchEffect
)。第二个参数为
this
,指向当前的Dep依赖对象,也就是
Dep1

counter1
变量)。

先不看
track
后面的代码,我们来看看
Link
这个class的代码,核心代码如下:

class Link {
// 指向Link链表的后一个节点(X轴)
nextDep: Link;
// 指向Link链表的前一个节点(X轴)
prevDep: Link;
// 指向Link链表的下一个节点(Y轴)
nextSub: Link;
// 指向Link链表的上一个节点(Y轴)
prevSub: Link;
- constructor(public sub: Subscriber, public dep: Dep) {
  // ...省略
}
}

细心的小伙伴可能发现了在
Link
中没有声明
sub

dep
属性,那么为什么前面我们会说Link节点中的
sub

dep
属性分别指向Sub订阅者和Dep依赖呢?

因为在constructor构造函数中使用了
public
关键字,所以
sub

dep
就作为属性暴露出来了。

执行完
let link = new Link(activeSub, this)
后,在响应式系统模型中初始化出来第一个Link节点,如下图:
step2

从上图可以看到
Link1

sub
属性指向
Sub1
订阅者,
dep
属性指向
Dep1
依赖。

我们接着来看
track
方法中剩下的代码,如下:

class Dep {
// 指向Link链表的尾部节点
subs: Link;
track() {
  let link = new Link(activeSub, this);
  if (!activeSub.deps) {
    activeSub.deps = activeSub.depsTail = link;
  } else {
    link.prevDep = activeSub.depsTail;
    activeSub.depsTail!.nextDep = link;
    activeSub.depsTail = link;
  }
  addSub(link);
}
}

先来看
if (!activeSub.deps)

activeSub
前面讲过了是
Sub1

activeSub.deps
就是
Sub1

deps
属性,也就是
Sub1
队列上的第一个Link。

从上图中可以看到此时的
Sub1
并没有箭头指向
Link1
,所以
if (!activeSub.deps)
为true,代码会执行

activeSub.deps = activeSub.depsTail = link;

deps

depsTail
属性分别指向
Sub1
队列的头部和尾部,当前队列中只有
Link1
这一个节点,那么头部和尾部当然都指向
Link1

执行完这行代码后响应式模型图就变成下面这样的了,如下图:
step3

从上图中可以看到
Sub1
的队列中只有
Link1
这一个节点,所以队列的头部和尾部都指向
Link1

处理完
Sub1
的队列,但是
Dep1
的队列还没处理,
Dep1
的队列是由
addSub(link)
函数处理的。
addSub
函数代码如下:

function addSub(link: Link) {
const currentTail = link.dep.subs;
if (currentTail !== link) {
  link.prevSub = currentTail;
  if (currentTail) currentTail.nextSub = link;
}
link.dep.subs = link;
}

由于
Dep1
队列中没有Link节点,所以此时在
addSub
函数中主要是执行第三块代码:
link.dep.subs = link
。`

link.dep
是指向
Dep1
,前面我们讲过了Dep依赖的
subs
属性指向队列的尾部。所以
link.dep.subs = link
就是将
Link1
指向
Dep1
的队列的尾部,执行完这行代码后响应式模型图就变成下面这样的了,如下图:
step4

到这里对第一个响应式变量
counter1
进行读操作进行的依赖收集就完了。

第一个
watchEffect

counter2
进行读操作

在第一个watchEffect中接着会对
counter2
变量进行读操作。同样会走到
get
拦截中,然后执行
track
函数,代码如下:

class Dep {
  // 指向Link链表的尾部节点
  subs: Link;
  track() {
    let link = new Link(activeSub, this);

    if (!activeSub.deps) {
      activeSub.deps = activeSub.depsTail = link;
    } else {
      link.prevDep = activeSub.depsTail;
      activeSub.depsTail!.nextDep = link;
      activeSub.depsTail = link;
    }

    addSub(link);
  }
}

同样的会执行一次
new Link(activeSub, this)
,然后把新生成的
Link2

sub

dep
属性分别指向
Sub1

Dep2
。执行后的响应式模型图如下图:
step5

从上面的图中可以看到此时
Sub1

deps
属性是指向
Link1
的,所以这次代码会走进
else
模块中。
else
部分代码如下:

link.prevDep = activeSub.depsTail;
activeSub.depsTail.nextDep = link;
activeSub.depsTail = link;

activeSub.depsTail
指向
Sub1
队列尾部的Link,值是
Link1
。所以执行
link.prevDep = activeSub.depsTail
就是将
Link2

prevDep
属性指向
Link1

同理
activeSub.depsTail.nextDep = link
就是将
Link1

nextDep
属性指向
Link2
,执行完这两行代码后
Link1

Link2
之间就建立关系了。如下图:
step6

从上图中可以看到此时
Link1

Link2
之间就有箭头连接,可以互相访问到对方。

最后就是执行
activeSub.depsTail = link
,这行代码是将
Sub1
队列的尾部指向
Link2
。执行完这行代码后模型图如下:
step7

Sub1
订阅者的队列就处理完了,接着就是处理
Dep2
依赖的队列。
Dep2
的处理方式和
Dep1
是一样的,让
Dep2
队列的队尾指向
Link2
,处理完了后模型图如下:
step8

到这里第一个watchEffect(也就是
Sub1
)对其依赖的两个响应式变量
counter1
(也就是
Dep1
)和
counter2
(也就是
Dep2
),进行依赖收集的过程就执行完了。

第二个
watchEffect

counter1
进行读操作

接着我们来看第二个
watchEffect
,同样的还是会对
counter1
进行读操作。然后触发其
get
拦截,接着执行
track
方法。回忆一下
track
方法的代码,如下:

class Dep {
  // 指向Link链表的尾部节点
  subs: Link;
  track() {
    let link = new Link(activeSub, this);

    if (!activeSub.deps) {
      activeSub.deps = activeSub.depsTail = link;
    } else {
      link.prevDep = activeSub.depsTail;
      activeSub.depsTail!.nextDep = link;
      activeSub.depsTail = link;
    }

    addSub(link);
  }
}

这里还是会使用
new Link(activeSub, this)
创建一个
Link3
节点,节点的
sub

dep
属性分别指向
Sub2

Dep1
。如下图:
step9

同样的
Sub2
队列上此时还没任何值,所以
if (!activeSub.deps)
为true,和之前一样会去执行
activeSub.deps = activeSub.depsTail = link;

Sub2
队列的头部和尾部都设置为
Link3
。如下图:
step10

处理完
Sub2
队列后就应该调用
addSub
函数来处理
Dep1
的队列了,回忆一下
addSub
函数,代码如下:

function addSub(link: Link) {
  const currentTail = link.dep.subs;
  if (currentTail !== link) {
    link.prevSub = currentTail;
    if (currentTail) currentTail.nextSub = link;
  }

  link.dep.subs = link;
}

link.dep
指向
Dep1
依赖,
link.dep.subs
指向
Dep1
依赖队列的尾部。从前面的图可以看到此时队列的尾部是
Link1
,所以
currentTail
的值就是
Link1

if (currentTail !== link)
也就是判断
Link1

Link3
是否相等,很明显不相等,就会走到if的里面去。

接着就是执行
link.prevSub = currentTail
,前面讲过了此时
link
就是
Link3

currentTail
就是
Link1
。执行这行代码就是将
Link3

prevSub
属性指向
Link1

接着就是执行
currentTail.nextSub = link
,这行代码是将
Link1

nextSub
指向
Link3

执行完上面这两行代码后
Link1

Link3
之间就建立联系了,可以通过
prevSub

nextSub
属性访问到对方。如下图:
step11

接着就是执行
link.dep.subs = link
,将
Dep1
队列的尾部指向
Link3
,如下图:
step17

到这里第一个响应式变量
counter1
进行依赖收集就完成了。

第二个
watchEffect

counter2
进行读操作

在第二个watchEffect中接着会对
counter2
变量进行读操作。同样会走到
get
拦截中,然后执行
track
函数,代码如下:

class Dep {
  // 指向Link链表的尾部节点
  subs: Link;
  track() {
    let link = new Link(activeSub, this);

    if (!activeSub.deps) {
      activeSub.deps = activeSub.depsTail = link;
    } else {
      link.prevDep = activeSub.depsTail;
      activeSub.depsTail!.nextDep = link;
      activeSub.depsTail = link;
    }

    addSub(link);
  }
}

这里还是会使用
new Link(activeSub, this)
创建一个
Link4
节点,节点的
sub

dep
属性分别指向
Sub2

Dep2
。如下图:
step12

此时的
activeSub
就是
Sub2

activeSub.deps
就是指向
Sub2
队列的头部。所以此时头部是指向
Link3
,代码会走到else模块中。

在else中首先会执行
link.prevDep = activeSub.depsTail

activeSub.depsTail
是指向
Sub2
队列的尾部,也就是
Link3
。执行完这行代码后会将
Link4

prevDep
指向
Link3

接着就是执行
activeSub.depsTail!.nextDep = link
,前面讲过了
activeSub.depsTail
是指向
Link3
。执行完这行代码后会将
Link3

nextDep
属性指向
Link4

执行完上面这两行代码后
Link3

Link4
之间就建立联系了,可以通过
nextDep

prevDep
属性访问到对方。如下图:
step13

接着就是执行
activeSub.depsTail = link
,将
Sub2
队列的尾部指向
Link4
。如下图:
step14

接着就是执行
addSub
函数处理
Dep2
的队列,代码如下:

function addSub(link: Link) {
  const currentTail = link.dep.subs;
  if (currentTail !== link) {
    link.prevSub = currentTail;
    if (currentTail) currentTail.nextSub = link;
  }

  link.dep.subs = link;
}

link.dep
指向
Dep2
依赖,
link.dep.subs
指向
Dep2
依赖队列的尾部。从前面的图可以看到此时队列的尾部是
Link2
,所以
currentTail
的值就是
Link2
。前面讲过了此时
link
就是
Link4

if (currentTail !== link)
也就是判断
Link2

Link4
是否相等,很明显不相等,就会走到if的里面去。

接着就是执行
link.prevSub = currentTail

currentTail
就是
Link2
。执行这行代码就是将
Link4

prevSub
属性指向
Link2

接着就是执行
currentTail.nextSub = link
,这行代码是将
Link2

nextSub
指向
Link4

执行完上面这两行代码后
Link2

Link4
之间就建立联系了,可以通过
prevSub

nextSub
属性访问到对方。如下图:
step15

最后就是执行
link.dep.subs = link

Dep2
队列的尾部指向
Link4
,如下图:
step16

至此整个依赖收集过程就完成了,最终就画出了Vue新的响应式模型。

依赖触发

当执行
counter1.value++
时,就会被变量
counter1
(也就是
Dep1依赖
)的set函数拦截。

此时就可以通过
Dep1

subs
属性指向队列的尾部,也就是指向
Link3

Link3
中可以直接通过
sub
属性访问到订阅者
Sub2
,也就是第二个
watchEffect
,从而执行第二个
watchEffect
的回调函数。

接着就是使用Link的
preSub
属性从队尾依次移动到队头,从而触发
Dep1
队列中的所有Sub订阅者。

在这里就是使用
preSub
属性访问到
Link1
(就到队列的头部啦),
Link1
中可以直接通过
sub
属性访问到订阅者
Sub1
,也就是第一个
watchEffect
,从而执行第一个
watchEffect
的回调函数。

总结

这篇文章讲了Vue新的响应式模型,里面主要有三个角色:
Dep依赖

Sub订阅者

Link节点

Dep依赖

Sub订阅者
不再有直接的联系,而是通过
Link节点
作为桥梁。

依赖收集的过程中会构建
Dep依赖
的队列,队列是由
Link节点
组成。以及构建
Sub订阅者
的队列,队列同样是由
Link节点
组成。

依赖触发时就可以通过
Dep依赖
的队列的队尾出发,
Link节点
可以访问和触发对应的
Sub订阅者

然后依次从队尾向队头移动,依次触发队列中每个
Link节点

Sub订阅者

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

另外欧阳写了一本开源电子书
vue3编译原理揭秘
,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。

随着 React 在前端开发中的流行,越来越多的 UI 框架和库开始涌现,以帮助开发者更高效地构建现代化、响应式的用户界面。其中,
Material-UI
是基于
Google Material Design
规范设计的一款开源
React UI
库,
Github Star高达 94K
,凭借其丰富的组件库、灵活的定制化选项以及无缝的开发体验,迅速成为了前端开发者的热门选择。今天我们将详细介绍
Material-UI
的显著特性、使用方式以及适用场景,帮助你更好地利用这一框架来构建现代化的用户界面。

简要介绍

Material-UI
(现已更名为 MUI)是一个用于
React
的流行 UI 框架,基于
Google

Material Design
规范构建。它提供了一套丰富的可定制的 UI 组件,帮助开发者快速构建现代化、响应式的用户界面。
Material-UI
提供了预先设计好的组件,如按钮、文本框、卡片、表单控件、导航栏等,旨在简化 UI 开发流程并提高开发效率。

Material Design 是由 Google 于 2014 年推出的一套设计语言和视觉设计规范。旨在通过一致的视觉、运动和交互模式,提供统一的用户体验。其设计理念受到了物理现实世界的启发,模拟了现实世界的材料和光线,强调了层次感、阴影、运动和响应式布局。

显著特性

  • 基于 Material Design
    :遵循 Google 的 Material Design 规范,保证了组件在视觉上的一致性和现代感。
  • 丰富的组件库
    :提供了大量的预构建组件,涵盖了表单、布局、导航、反馈、数据展示等常用 UI 模块。
  • 高度可定制化
    :支持通过主题(theme)和样式覆盖来自定义组件的外观,以满足不同项目的需求。
  • 响应式设计
    :内置响应式布局和组件,支持多种设备和屏幕尺寸,保证在不同终端上的良好表现。
  • 易于集成
    :与 React 无缝集成,提供直观的 API 和丰富的文档,便于快速上手和项目集成。
  • 生态系统完善
    :MUI 提供了附加的库,如 MUI X,用于高级表格和数据网格,支持更多复杂场景的开发。

使用使用

  1. 安装
npm install @mui/material @emotion/react @emotion/styled
// or
pnpm add @mui/material @emotion/react @emotion/styled
// or
yarn add @mui/material @emotion/react @emotion/styled
  1. 基础使用
import React from 'react';
import { Button, TextField, Container } from '@mui/material';

function App() {
return (
<Container>
<h1>Hello, Material-UI!</h1>
<TextField label="Name" variant="outlined" />
<Button variant="contained" color="primary">
Submit
</Button>
</Container>
);
}

export default App;
  1. 自定义主题
    Material-UI 允许通过创建自定义主题来改变组件的默认样式。可以使用 createTheme 函数创建主题,并通过 ThemeProvider 应用主题:
import React from 'react';
import { Button, ThemeProvider, createTheme } from '@mui/material';

const theme = createTheme({
palette: {
primary: {
main: '#1976d2',
},
},
});

function App() {
return (
<ThemeProvider theme={theme}>
<Button variant="contained" color="primary">
Custom Themed Button
</Button>
</ThemeProvider>
);
}

export default App;
  1. 响应式布局
    Material-UI 提供了简单且强大的响应式布局系统,允许开发者在不同设备上优化布局:
import Box from '@mui/material/Box';

function ResponsiveLayout() {
return (
<Box sx={{ display: { xs: 'block', md: 'flex' } }}>
<Box sx={{ width: { xs: '100%', md: '50%' } }}>Left content</Box>
<Box sx={{ width: { xs: '100%', md: '50%' } }}>Right content</Box>
</Box>
);
}

适用场景

  1. 企业级管理系统
    Material-UI 提供了大量复杂的表单和数据展示组件,适合开发后台管理系统和数据密集型的企业应用。其响应式设计和深度定制特性让它非常适合在复杂业务场景中应用。

  2. SaaS 平台
    对于需要用户友好界面和灵活定制的 SaaS 应用,Material-UI 提供了成熟的解决方案。开发者可以快速搭建可扩展的前端架构,并为用户提供一致、流畅的交互体验。

  3. 电子商务平台
    Material-UI 的数据展示和布局组件非常适合电子商务网站的构建。通过其响应式设计,开发者可以确保网站在移动端和桌面端均能提供良好的用户体验。

  4. 移动优先的 Web 应用
    Material-UI 的响应式特性使得它特别适合移动优先的 Web 应用开发。在移动设备上,Material-UI 能够自动调整组件的布局,确保最佳的用户体验。

如果你正在寻找一个能够大幅提高开发效率且高度可扩展的 UI 解决方案,不妨试试
Material-UI
,体验它带来的开发便捷性和设计一致性。


该模版已经收录到我的全栈前端一站式开发平台
“前端视界”
中(浏览器搜
前端视界
第一个),感兴趣的欢迎查看!

程序员开发利器:Your Commands网站上线

先上链接:
https://www.ycmds.cc

背景

各种命令行工具是我们IT行业日常工作离不开的,但是对于命令行工具的使用有一个痛点:文档上每一个命令行参数写的清清楚楚,但是怎么组合起来用却搞不清楚。所以为了解决这个问题每个人都应该有一个记事本,记录下来自己常用的完整命令行,每次用的时候翻出来直接用就可以。但存放到本地的记事本是非常不方便的,各种云记事本也非常不好用。所以有了一个想法,为什么不把这些命令行放到网络上,方便自己的同时也能便捷他人。

作者先收集了自己日常工作中常用的完整命令行,包括FFmpeg/Docker/Git/Tcpdump等等。此网站是开源的,非常欢迎其他大牛们把自己常用的命令行分享出来,赠人玫瑰,手留余香。

下面列举一部分:

FFmpeg

1、转推RTMP协议流

纯转推,没有编解码。

1.1、转推flv文件

ffmpeg -re -stream_loop -1 -i test.flv -c copy -f flv rtmp://localhost:1935/live/destination
参数解释
  • -re
    参数用于模拟实时读取输入流,输入数据的处理速度将与实际播放速度保持一致。

  • -stream_loop 是用来指定输入流的循环次数的选项。
    -1 作为 -stream_loop 的参数值,表示无限循环,即输入流将不断重复播放,直到手动停止或程序结束。

  • -c 是 -codec 的简写,用于指定编码器或解码器。copy 表示直接复制源流的音视频数据,不重新编解码。 也可以写成 -c:v copy -c:a copy

1.2、转推RTMP直播流

ffmpeg -i rtmp://localhost:1935/live/source -c copy -f flv rtmp://localhost:1935/live/destination

:::note
如果输入流本身就是实时流,可以不加-re参数,当输入流有GOP缓存,它将会被快递处理并转推出去,配置合适的播放策略比加-re参数能降低延迟。

:::

2、录制RTMP协议流

可以把直播流录制成flv文件:

ffmpeg -i  'rtmp://localhost:1935/live/test'  -c:v copy -c:a copy -f flv test.flv

3、转推RTSP协议流

3.1、基于TCP传输

ffmpeg -re -stream_loop -1  -i test.mp4 -c:v copy -c:a copy  -rtsp_transport tcp -f rtsp "rtsp://127.0.0.1:5544/live/test?token=123"
参数解释
  • -rtsp_transport tcp 表示基于TCP传输音视频数据,也就是Interleaved模式

3.2、基于UDP传输

 ffmpeg -re -stream_loop -1  -i test.mp4 -c:v copy  -c:a copy -f rtsp "rtsp://127.0.0.1:5544/live/test?token=123"

4、图片相关

4.1、PNG转YUV

 ffmpeg -i temp.jpg -s 1024x680 -pix_fmt yuvj420p 9.yuv
参数解释
  • -s 1024x680: 这个选项指定输出视频的尺寸为 1024x680 像素。-s 后面跟着想要的宽度和高度。

  • -pix_fmt yuvj420p: 这个选项指定输出的像素格式为 yuvj420p。

4.2、打开YUV

ffplay -f rawvideo -pixel_format yuv420p -video_size 1024x680 9.yuv

4.3、YUV转PNG

ffmpeg -y -s 1024x680 -i 9.yuv output.jpg

5、转码相关

ffmpeg转码主要涉及到:

变换编码方式:

  • H264转到H265:降码率,清晰度不变的情况下降低网络使用带宽。
  • H265转到H264:解决低端设备解不了H265的问题。

变换分辨率:

  • 降分辨率:降码率。
  • 升分辨率:官方的ffmpeg增加分辨率和码率没有很好的超分效果,需要使用第三方的SDK集成到ffmpeg中,比如英伟达的MAXINE。

降码率:

  • 恒定码率变动态码率:根据画面复杂度动态调整码率,节省网络带宽,提升用户体验。

5.1、RTMP直播流转码成720P H264

 ffmpeg -rw_timeout 5000000 -i 'rtmp://localhost:1935/live/source' -acodec libfdk_aac -b:a 64k -ac 2 -ar 44100 -profile:a aac_he  -vcodec libx264 -b:v 2000k -level 3.1 -vprofile high -vsync 2 -strict -2 -preset medium -bf 3 -force_key_frames source  -f flv -loglevel level+info -vf "scale='720:-2'"     'rtmp://localhost:1935/live/dest'
参数解释
  • -rw_timeout 5000000 设置读写超时时间,单位是微秒,5000000为5秒。如果在这个时间内没有完成读写操作,FFmpeg 将会停止操作并报告超时错误。

  • -acodec 执行音频编码器fdk_aac,这个编码库是开源的,支持LC、HE-AAC、HE-AAC-V2三种profile级别。

  • -b:a 指定音频码率

  • -ac 指定音频通道数2

  • -ar 指定音频采样率44100

  • -profile:a 指定音频profile级别为aac_he

  • -vcodec 执行视频编码器为x264

  • -b:v 指定视频码率为1700Kbits/s

  • -level 用于约束码率、帧率和分辨率

H264 Level

  • -vprofile 是用来定义一组编码工具和特性的集合,以满足不同使用场景和性能需求。

H264 Profile

  • -vsync 2 帧会连同其时间戳一起通过或丢弃,以防止 2 个帧具有相同的时间戳。

  • -preset medium 指定编码速速和压缩比,编码速度越快,压缩比越低。
    FFmpeg doc

  • -bf 3 指定B帧数目为3个,通常是两个P帧之间编码3个B帧。

  • -force_key_frames source 关键帧编码跟随源流,如果当前帧在源流中为关键帧,则编码输出关键帧,如果源流中的当前帧必须被丢弃,则下一帧输出关键帧。

  • -flv 指定封装格式为flv

  • -loglevel level+info 添加日志级别前缀、指定日志级别为info。

  • -vf "scale='720:-2'" 视频过滤器参数,scale 是用于缩放视频的过滤器,'720:-2' 指定了输出视频的宽度和高度:720 表示输出视频的宽度将被设置为 720 像素,-2 表示高度将自动计算,以保持原始视频的宽高比。

5.2、RTMP直播流转码成720P H265

 ffmpeg -rw_timeout 5000000 -i "rtmp://localhost:1935/live/source" -vcodec libx265 -b:v 2000k -acodec libfdk_aac -b:a 64k -ac 2 -ar 44100 -profile:a aac_he -preset veryfast -bf 3 -force_key_frames source -f flv -loglevel level+info -vf scale='720:-2' “rtmp://localhost:1935/live/dest”

Docker

1、基本命令

1.1、拉取docker镜像

docker pull docker.io/library/centos:7.9.2009

1.2、运行docker

docker run -it  docker.io/library/centos:7.9.2009 /bin/bash 
参数解释
  • -it 分配伪终端并保持标准输入打开,适合交互式操作。
  • /bin/bash 容器启动后运行的命令,这里是打开一个 Bash 终端。

1.3、查看所有docker实例

docker ps -a
参数解释
  • -a 列出所有容器,包括停止的,如果不加-a,则只显示正在运行的

1.4、进入docker内部

docker  exec -it <container ID or name> /bin/bash

1.5、查看所有镜像

docker images

1.6、清理docker镜像

1.6.1、删除未使用的镜像
docker image prune
1.6.2、强制删除未使用的镜像
docker image prune -a

这将删除所有未被容器使用的镜像,包括悬挂的镜像。

1.6.3、删除特定镜像
docker rmi <image_id_or_name>
1.6.4、导出容器到文件
docker export <container_id>  -o  image.tar
1.6.5、导出镜像到文件
docker save -o image.tar <repository_name>:<tag> or <image_id>
1.6.5、从文件加载成docker镜像
cat image.tar | docker import - <repository_name>:<tag>

其中,<repository_name>是你要上传到的镜像仓库名称,<tag>是镜像的标签。

2、高阶命令

2.1、拉取并运行镜像

docker run --cap-add=SYS_PTRACE -d -it --net=bridge --name centos7 --privileged=true -w /youcmds/workspace -e "PKG_CONFIG_PATH=/usr/local/lib/pkgconfig/" -e "LD_LIBRARY_PATH=/usr/local/lib64:/usr/local/lib:/usr/lib64:/usr/lib:/lib64:/lib" -p 1935-2935:1935-2935 -v /Users/yourcmds/workspace:/youcmds/workspace docker.io/library/centos:7.9.2009 /bin/bash 
参数解释
  • --cap-add 参数可以用于向 Docker 容器添加不同的权限,包括:

    NET_ADMIN: 允许容器拥有网络管理的能力。这意味着容器可以进行网络配置,比如更改接口的配置、添加或删除路由等。它赋予了容器更大的网络控制权限,适用于需要管理网络设置的应用场景。

    SYS_ADMIN:添加系统管理员权限,允许容器内的进程执行系统级别的管理操作,如挂载文件系统、设置时间、修改主机名等。

    SYS_PTRACE:添加系统追踪权限,允许容器内的进程使用 ptrace 系统调用,用于调试和监视其他进程。

    SYS_CHROOT:添加切换根目录权限,允许容器内的进程使用 chroot 系统调用,在指定的目录下创建一个新的根文件系统环境。

    SYS_MODULE:添加模块加载/卸载权限,允许容器内的进程加载和卸载内核模块。

    SYS_RAWIO:添加原始 I/O 权限,允许容器内的进程进行对设备的原始读写操作,绕过操作系统提供的文件系统抽象。

    SYS_TIME:添加时间管理权限,允许容器内的进程修改系统时间。

  • --net 设置docker的网络模式,常用的网络模式包括:

    bridge(桥接模式):默认模式,Docker 会创建一个虚拟网桥,容器通过这个桥接连接到主机的网络。适用于大多数常见场景。

    host(主机模式):容器直接使用主机的网络堆栈,适合对网络性能要求较高的应用,但会失去容器间的网络隔离。

    none(无网络模式):不连接任何网络,适合需要完全隔离的场景。

    container(容器模式):使新容器与另一个已存在的容器共享网络栈。这意味着它们共享同一个 IP 地址和端口。

    overlay(覆盖网络模式):用于跨多个 Docker 主机的集群环境(如 Docker Swarm 或 Kubernetes),允许容器在不同主机间通信。

  • -d:以后台模式运行容器。

  • --name centos7:为容器指定一个名称(centos7)。

  • -w /youcmds/workspace:设置容器的工作目录。

  • -e 设置环境变量。

  • -p 1935-2935:1935-2935:将主机的 1935-2935 端口映射到容器的同一端口范围。

  • -v /Users/yourcmds/workspace:/youcmds/workspace:将主机目录挂载到容器内的指定路径,实现文件共享。

  • docker.io/library/centos:7.9.2009:拉取镜像地址。

GIT

1、基本命令

1.1、查询远程仓库

用于显示当前仓库的所有远程仓库及其对应的 URL:

git remote -v

1.2、更新提交账户信息

git config --global user.name "你的名字"
git config --global user.email "你的邮箱@example.com"

如果只想更新当前项目

git config user.name "你的名字"
git config user.email "你的邮箱@example.com"

1.3、修改远程仓库地址

git remote set-url origin <new-url>

栈一种常见的特殊线性数据结构,其特殊之处在于其操作顺序,下面会详细介绍,也正因为其特性,因此栈可以轻松解决表达式求值、括号匹配、递归算法、回溯算法等等问题。

01
、定义

栈的特殊性表现为操作受限,其一只允许在栈的一端进行元素插入和删除运算,其二栈的运算操作遵循后进先出(Last In First Out,简称LIFO)的原则。

入栈
:当把元素插入到栈,这一行为叫做入栈,也称进栈或压栈;

出栈
:当把元素从栈中移除,这一行为叫做出栈,也称退栈;

栈顶
:允许元素进行插入和删除操作的一端称为栈顶;

栈底
:与栈顶对应的另一个端称为栈底,并且不允许进行元素操作;

空栈
:当栈中没有元素时叫做空栈。

满栈
:当栈是有限容量,并且容量已用完,则称为满栈。

栈容量
:当栈是有限容量,栈容量表示栈可以容纳的最大元素数量。

栈大小
:表示当前栈中的元素数量。

02
、分类

栈是逻辑结构,因此以存储方式的不同可以分为顺序栈和链栈。

顺序栈就是使用连续的地址空间存储所有栈元素,通常采用数组实现,因此导致栈的大小是固定的,不易扩扩容,容易浪费空间同时还要注意元素溢出等问题。

链栈顾名思义就是采用链式方式存储,通常采用单向链表实现,因此链栈可以无限扩容,按需使用,内存利用高效,同时也不存在满栈的情况。

03
、实现(顺序栈)

我们借助数组来实现顺序栈,其核心思想是把数组的起始位置作为栈底,把数组尾方向当作栈顶。

我们知道数组对插入、删除元素是不友好的,因为涉及到已存在元素移动的问题,但是如果直接在数组尾端插入、删除元素还是很方便的,因为不涉及元素移动问题,我们正是利用这一特点,把数组起始位置做为栈底,而插入、删除方便的数组尾端作为栈顶。

下面我们将一步一步实现一个泛型顺序栈。

1、ADT定义

我们首先来定义顺序栈的ADT。

ADT Stack{

数据对象:D 是一个非空的元素集合,D = {a1, a2, ..., an},其中 ai 表示栈中的第i个元素,n是栈的长度。

数据关系:D中的元素通过它们的索引(位置)进行组织,索引是从0到n-1的整数,并且遵循元素只能在栈顶操作,元素后进先出的原则。

基本操作:[

Init(n) :初始化一个指定容量的空栈。

Capacity:返回栈容量。

Length:返回栈长度。

Top:返回栈顶元素,当为空栈则报异常。

IsEmpty():返回是否为空栈。

IsFull():返回是否为满栈。

Push():入栈即添加元素,当为满栈则报异常。

Pop():出栈即返回栈顶元素并把其从栈中移除,当为空栈则报异常。

]

}

定义好栈ADT,下面我们就可以开始自己实现的栈。

2、初始化Init

初始化结构主要做几件事。

  • 初始化栈的容量;

  • 初始化存放栈元素数组;

  • 初始化栈顶索引;

具体实现代码如下:

//存放栈元素
private T[] _array;
//栈容量
private int _capacity;
//栈顶索引,为-1表示空栈
private int _top;
//初始化栈为指定容量
public MyselfArrayStack<T> Init(int capacity)
{
    //初始化栈容量为capacity
    _capacity = capacity;
    //初始化指定长度数组用于存放栈元素
    _array = new T[_capacity];
    //初始化为空栈
    _top = -1;
    //返回栈
    return this;
}

3、获取栈容量 Capacity

这个比较简单直接把栈容量私有字段返回即可。

//栈容量
public int Capacity
{
    get
    {
        return _capacity;
    }
}

4、获取栈长度 Length

因为栈顶索引表示数组下标,因此可以通过栈顶索引加1转为栈长度,同时因为我们定义了空栈是栈顶索引为-1,因此此时栈长等于[-1+1]为0,所以栈长度即为[栈顶索引+1]。

//栈长度
public int Length
{
    get
    {
        //栈长度等于栈顶元素加1
        return _top + 1;
    }
}

5、获取栈顶元素 Top

获取栈顶元素可以通过栈顶索引私有字段从数组中直接获取,同时要注意判断栈是否为空栈,如果为空栈则报异常。具体代码如下:

//获取栈顶元素
public T Top
{
    get
    {
        if (IsEmpty())
        {
            //空栈,不可以进行获取栈顶元素操作
            throw new InvalidOperationException("空栈");
        }
        return _array[_top];
    }
}

6、获取是否空栈 IsEmpty

是否空栈只需判断栈顶索引是否为小于0即可。

//是否空栈
public bool IsEmpty()
{
    //栈顶索引小于0表示空栈
    return _top < 0;
}

7、获取是否满栈 IsFull

是否满栈只需判断栈顶索引是否与栈容量减1相等,代码如下:

//是否满栈
public bool IsFull()
{
    //栈顶索引等于容量大小表示满栈
    return _top == _capacity - 1;
}

8、入栈 Push

入栈则是在把栈顶索引向数组后移动一位后,再把新元素赋值到栈顶索引所对应的元素上,同时还需要检查是否为满栈,如果是满栈则报错,具体实现代码如下:

//入栈
public void Push(T value)
{
    if (IsFull())
    {
        //栈顶索引大于等于容量大小减1,表明已经满栈,不可以进行入栈操作
        throw new InvalidOperationException("满栈");
    }
    //栈顶索引先向后移动1位,然后再存放栈顶元素
    _array[++_top] = value;
}

9、出栈 Pop

出栈则是首先取出栈顶元素后,然后把栈顶索引向数组前移动一位,最后返回取出的栈顶元素,同时还需要检查是否为空栈,如果是空栈则报错,具体实现代码如下:

//出栈
public T Pop()
{
    if (IsEmpty())
    {
        //栈顶索引小于1表示空栈,不可以进行出栈操作
        throw new InvalidOperationException("空栈");
    }
    //返回栈顶元素后,栈顶索引向前移动1位
    return _array[_top--];
}

04
、实现(链栈)

我们借助链表来实现链栈,其核心思想是把链表尾节点作为栈底,把链表首元节点当作栈顶。

这是因为如果我们想拿到链表的尾节点需要变量整个链表才可以拿到,但是要想获取首元节点则可以通过头指针直接获取到(无头节点链表),因此对于链表尾节点来说操作时不友好的适合来做栈底,链表首元节点对操作友好适合做为栈顶。

下面我们将一步一步实现一个泛型链栈。

1、ADT定义

相对于顺序栈的ADT来说,链栈的ADT少了两个方法即获取栈容量和是否满栈,这也是链表特性带来的好处。

2、初始化Init

初始化结构主要初始化栈顶节点为空和栈长度为0,具体实现如下:

public class MyselfStackNode<T>
{
    //数据域
    public T Data;
    //指针域,即下一个节点
    public MyselfStackNode<T> Next;
    public MyselfStackNode(T data)
    {
        Data = data;
        Next = null;
    }
}
public class MyselfStackLinkedList<T>
{
    //栈顶节点即首元节点
    private MyselfStackNode<T> _top;
    //栈长度
    private int _length;
    //初始化栈
    public MyselfStackLinkedList<T> Init()
    {
        //初始化栈顶节点为空
        _top = null;
        //初始化栈长度为0
        _length = 0;
        //返回栈
        return this;
    }
}

3、获取栈长度 Length

这个比较简单直接把栈长度私有字段返回即可。

//栈长度
public int Length
{
    get
    {
        return _length;
    }
}

4、获取栈顶元素 Top

获取栈顶元素可以通过栈顶节点直接获取,但是要注意判断栈是否为空栈,如果为空栈则报异常。具体代码如下:

//获取栈顶元素
public T Top
{
    get
    {
        if (IsEmpty())
        {
            //空栈,不可以进行获取栈顶元素操作
            throw new InvalidOperationException("空栈");
        }
        //返回首元节点数据域
        return _top.Data;
    }
}

5、获取是否空栈 IsEmpty

是否空栈只需判断栈顶节点是否为空即可。

//是否空栈
public bool IsEmpty()
{
    //栈顶节点为null表示空栈
    return _top == null;
}

6、入栈 Push

入栈则是首先创建一个新节点,然后把原栈顶节点赋值给新节点的指针域,最后把新节点变更为栈顶节点,同时栈长加1,具体实现代码如下:

//入栈
public void Push(T value)
{
    //创建新的栈顶节点
    var node = new MyselfStackNode<T>(value);
    //将老的栈顶节点赋值给新节点的指针域
    node.Next = _top;
    //把栈顶节点变更为新创建的节点
    _top = node;
    //栈长度加1
    _length++;
}

7、出栈 Pop

出栈则是首先取出栈顶节点对应的数据后,然后把栈顶节点指向原栈顶节点对应的下一个节点,同时栈长度减1,当然如果空栈则报错,具体实现代码如下:

//出栈
public T Pop()
{
    if (IsEmpty())
    {
        //空栈,不可以进行出栈操作
        throw new InvalidOperationException("空栈");
    }
    //获取栈顶节点数据
    var data = _top.Data;
    //把栈顶节点变更为原栈顶节点对应的下一个节点
    _top = _top.Next;
    //栈长度减1
    _length--;
    //返回栈顶数据
    return data;
}


:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。
https://gitee.com/hugogoos/Planner