件被看作是 JavaScript 与网页之间交互的桥梁,当事件发生时,可以通过 JavaScript 代码(函数)执行相关的操作。例如,用户可以通过鼠标拖曳登录框,改变登录框的显示位置;或者在阅读文章时,选中文本后自动弹出分享、复制选项。本章将对 DOM 中的事件进行详细讲解。
事件可被理解为是 JavaScript 侦测到的行为,这些行为指的就是页面的加载、鼠标单击页面、鼠标滑过某个区域、按下键盘等具体的动作,它对实现网页的交互效果起着重要的作用。在深入学习事件时,需要对一些非常基本又相当重要的概念有一定的了解。
事件处理程序指的就是 JavaScript 为响用户行为所执行的程序代码。例如,用户单击 button 按钮时,这个行为就会被 JavaScript 中的click 事件侦测到;然后让其自动执行,为 click 事件编写的程序代码,如在控制台输出“按钮被单击了”。
事件驱动是指,在 Web 页面中 JavaScript 的事件,侦测到的用户行为(如鼠标单击、鼠标移入等),并执行相应的事件处理程序的过程。
事件绑定指的是为某个元素对象的事件绑定事件处理程序。在 DOM 中提供了3种事件的绑定方式。下面将针对以3种事件绑定方式的语法以及各自的区别进行详细讲解。
事件的行内绑定式是通过HTML标签的属性设置实现的,具体语法格式如下。
<div onclick="alert('handle click')"></div>
在上述语法中,div 可以是任意的HTML标签,如 <button>、<input>标签等;事件是由 on 和事件名称组成的一个 HTML 属性,如单击事件对应的属性名为 onclick;事件的处理程序指的是 JavaScript 代码,如匿名函数等。
需要注意的是,由于开发中提倡 JavaScript 代码与 HTML 代码相分离。因此,不建议使用行内绑定事件。
动态的绑定方式很好地解决了JavaScript代码与HTML代码混合编写的问题。在JavaScript代码中,为需要事件处理的 DOM 元素对象,添加事件与事件处理程序。具体语法格式如下。
div.onclick=function handleClick () {
console.log('handle click');
};
在上述语法中,事件的处理程序一般都是匿名函数或有名的函数。在实际开发中,相对于行内绑定来说,事件的动态绑定的使用居多。
行内绑定与动态绑定除了实现的语法不同以外,本质是相同的,都是给 DOM 元素绑定了一个 onclick 属性。
为了给同一个 DOM 对象的同一个事件添加多个事件处理程序,DOM中引入了事件流的概念,可以让DOM对象通过事件监听的方式实现事件的绑定。由于不同浏览器采用的事件流实现方式不同,事件监听的实现存在兼容性问题。通常根据浏览器的内核可以划分为两大类,一类是早期版本的IE浏览器(如IE6~8),一类遵循W3C标准的浏览器(以下简称标准浏览器)。
接下来,将根据不同类型的浏览器,分别介绍事件监听的实现方式。
(1)早期版本的IE浏览器
在早期版本的IE浏览器中,事件监听的语法格式如下。
DOM对象.attachEvent(type,callback);
在上述语法中,参数 type 指的是为 DOM 对象绑定的事件类型,它是由 on 与事件名称组成的,如 onclick。。参数 callback 表示事件的处理程序。
(2)标准浏览器
标准浏览器包括IE8版本以上的IE浏览器(如IE9~11),新版的Firefox、Chrome等浏览器。具体语法格式如下。通过这种方式我们可以给元素注册多个事件处理函数,而 btn.onclick=fn 是赋值操作只能设置一个事件处理函数。
DOM对象.addEventListener(type, callback, [capture]);
在上述语法中,参数 type 指的是 DOM 对象绑定的事件类型,它是由事件名称设置的,如 click。参数 callback 表示事件的处理程序。参数 capture 默认值为 false,这个属性后面单独介绍,一般情况我们都使用它的默认值。
现在 IE 浏览器已经被淘汰,所以我们不需要再去记忆 attachEvent() 的用法,但是我们需要了解过去,过去在使用这种方式注册事件的时候需要处理浏览器的兼容性,下面我们演示下:
function addEventListener(element, type, listener) {
// 能力检测: 就是要看当前的浏览器是否支持此标签对象的属性或是方法
if (element.addEventListener) {
element.addEventListener(type, listener, false);
} else if (element.attachEvent) {
element.attachEvent('on' + type, listener);
} else {
element['on' + type]=listener;
}
}
在事件处理函数中的 this 指向当前触发该事件的 DOM 元素。
link.onclick=function handleLink () {
photo.src=this.href;
p.textContent=this.title;
return false;
};
但是通过行内绑定注册的事件,调用的函数中 this 指向的是 window。
<button onclick="handle()">按钮</button>
<script>
function handle () {
// 此处的 this 指向 window
console.log(this);
}
</script>
行内绑定事件 onclick="handle()" 中的 "" 双引号内部其实可以看做是一个匿名函数,"" 双引号内部的这个匿名函数才是事件处理函数,在事件处理函数中又调用了 handle() 方法。
<!-- 此处的 this 指向的是触发事件的对象 button -->
<button onclick="handle(this)">按钮</button>
<script>
function handle (btn) {
// 此处的 this 指向 window
console.log(btn);
}
</script>
绑定事件的元素可以解除绑定,例如:我们可以让按钮点击一次之后解除事件绑定。三种绑定事件的解除事件的方式不同,下面我们分别来介绍。
行内绑定事件和动态绑定事件本质上都是给 DOM 元素设置 onclick 属性,对应的解除绑定事件的方式都是把 onclick 属性重新设置为 null。
当按钮执行完点击事件的处理程序后立即解除事件的绑定
<button onclick="handle(this)">按钮</button>
<script>
function handle (btn) {
alert('Hello');
btn.onclick=null;
}
</script>
btn.onclick=function handle () {
this.onclick=null;
};
标准绑定事件使用 addEventListener(type, callback, [capture]); 方法,对应的解除绑定使用的方法是 removeEventListener(type, callback, [capture]),需要注意的是,如果注册的事件需要解除的话,使用 addEventListener() 注册事件的时候,传入的 callback 不能是匿名函数,因为解除事件绑定的时候还需要引用这个函数。
const div=document.querySelector('#div');
div.addEventListener('click', handle);
function handle () {
alert('hello');
this.removeEventListener('click', handle);
}
我们已经会使用 addEventListener(type, callback, [capture]),方法给元素注册事件,但是这个方法的第三个参数的作用我们还不清楚,下面我们就来介绍该方法的第三个参数,这里我们需要先来学习 DOM 中的事件流(事件模型)。
DOM (文档对象模型)结构是一个树型结构,当一个HTML元素产生一个事件时,该事件会在元素结点与根节点之间按特定的顺序传播,路径所经过的节点都会收到该事件,这个传播过程可称为 DOM 事件流。
事件顺序有两种类型:事件捕捉和事件冒泡。
这是 IE 浏览器对事件模型的实现,也是最容易理解的。冒泡,顾名思义,事件像个水中的气泡一样一直往上冒,直到顶端。
从DOM 树型结构上理解,就是事件由叶子节点沿祖先结点一直向上传递直到根节点;从浏览器界面视图 HTML 元素排列层次上理解就是事件由具有从属关系的触发事件的元素一直传递到根元素直到文档对象。
addEventListener(type, callback, [capture]),该方法的第三个参数为 false 的时候设置触发事件的方式为事件冒泡,该参数默认为 false。
一般情况下,我们都会使用事件冒泡的方式注册事件。
const outer=document.querySelector('#outer');
const inner=document.querySelector('#inner');
outer.addEventListener('click', function () {
console.log('点击了 outer');
}, false);
inner.addEventListener('click', function () {
console.log('点击了 inner');
}, false);
document.body.addEventListener('click', function () {
console.log('点击了 body');
}, false);
document.addEventListener('click', function () {
console.log('点击了 document');
}, false);
window.addEventListener('click', function () {
console.log('点击了 window');
}, false);
执行结果:
使用行内绑定和动态绑定事件的方式默认使用的是事件冒泡。
Netscape 的实现,它与冒泡型刚好相反,由 DOM 树最顶层元素一直到触发事件的元素。
addEventListener(type, callback, [capture]),该方法的第三个参数为 true 的时候设置触发事件的方式为事件捕获。
const outer=document.querySelector('#outer');
const inner=document.querySelector('#inner');
outer.addEventListener('click', function () {
console.log('点击了 outer');
}, true);
inner.addEventListener('click', function () {
console.log('点击了 inner');
}, true);
document.body.addEventListener('click', function () {
console.log('点击了 body');
}, true);
document.addEventListener('click', function () {
console.log('点击了 document');
}, true);
window.addEventListener('click', function () {
console.log('点击了 window');
}, true);
执行结果:
使用行内绑定和动态绑定事件的方式无法使用事件捕获。
我们已经对上面两个不同的事件模型进行了解释和对比。DOM 标准同时支持两种事件模型,即事件捕获与事件冒泡,但是,事件捕获先发生。两种事件流都会触发 DOM 中的所有对象,从 document对象开始,也在 document 对象结束(大部分兼容标准的浏览器会继续将事件是捕捉/冒泡延续到window 对象)。
如图:首先是捕获传递事件,接着是冒泡传递,所以,如果一个处理函数既注册了捕获型事件的监听,又注册冒泡型事件监听,那么在 DOM 事件模型中它就会被调用两次。
DOM 标准的事件模型最独特的性质是,文本节点也会触发事件(在IE不会)。
事件委托,通俗地来讲,就是把一个元素的处理事件的函数委托到另一个元素。
一般来讲,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。
举个例子,比如一个宿舍的同学同时快递到了,一种方法就是他们都傻傻地一个个去领取,还有一种方法就是把这件事情委托给宿舍长,让一个人出去拿好所有快递,然后再根据收件人一一分发给每个宿舍同学。
在这里,取快递就是一个事件,每个同学指的是需要响应事件的 DOM 元素,而出去统一领取快递的宿舍长就是代理的元素,所以真正绑定事件的是这个元素,按照收件人分发快递的过程就是在事件执行中,需要判断当前响应的事件应该匹配到被代理元素中的哪一个或者哪几个。
下面我们来做一个练习,为下面的每一个 li 注册点击事件,当点击当前 li 的时候打印 li 中的文本内容。
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
首先我们用传统的方式来实现,先获取到页面上所有的 li,然后遍历所有的 li,给每一个 li 注册点击事件,这里使用 addEventListener() 注册事件的时候省略了第三个参数,默认为 false,事件冒泡的方式。
这样做不好的地方有两点,第一:我们需要为每一个 li 元素创建一个新的事件处理函数,每次创建都需要销毁时间和内存。第二:当点击按钮往 ul 中添加新的 li 元素的时候需要给新创建的 li 注册点击事件。
const lis=document.querySelectorAll('#list li');
lis.forEach(function (li) {
li.addEventListener('click', function () {
console.log(this.textContent)
});
});
下面我们使用事件委托的方式优化上面的代码,把点击事件注册给父元素 ul,当点击 li 的时候通过事件冒泡把点击事件传递给父元素 ul。
const ul=document.querySelector('#list');
ul.addEventListener('click', function () {
console.log('test');
// 此处的 this 是注册事件的元素 ul
console.log(this);
});
代码改完之后点击 li,这段代码确实可以执行,但是我们的目标是打印 li 之间的内容,而通过打印发现此处的 this 不是我们想要的当前点击的 li,而是注册事件的元素 ul。所以这里需要强调一点,在注册事件的时候,事件源是注册事件的对象。
那如何获取当前触发事件的元素 li 呢?当事件被触发的时候事件处理函数会接收一个参数,这个参数叫做事件对象,事件对象可以提供触发事件的时候相关的数据,下一小节详细介绍,这里我们先用事件对象解决当前的问题,事件对象中有一个 target 属性,这个属性就是当前触发事件的对象。
在 IE 浏览器中获取事件对象的方式不同,IE 中是通过 window.event 获取事件对象,以前在获取事件对象的时候还要处理浏览器兼容性问题,IE 浏览器现在已经被淘汰所以浏览器兼容性的处理我们就不再演示。
const ul=document.querySelector('#list');
// 事件参数(对象) e
ul.addEventListener('click', function (e) {
// e.target 触发事件的元素
console.log(e.target.textContent);
// 注册事件的元素
console.log(this);
});
到这里这个案例就完成了,我们再来扩展下这个案例,如果想要点击特定的 li 来触发事件该如何实现?
<ul id="list">
<li>item 1</li>
<li class="cls">item 2</li>
<li class="cls">item 3</li>
......
<li>item n</li>
</ul>
如上代码,如果想点击具有特性类样式或者特定 id 的元素触发事件,可以通过判断当前点击的元素 e.target 的类样式或者 id 属性进行判断。
if (e.target.className==='cls') {
// ....
}
但是如果想像 CSS 选择器一样更加灵活的匹配的话,上面的判断不够灵活,这里可以使用 元素.matches(选择器) 来匹配特定元素。当元素匹配指定的选择器返回 true。
const ul=document.querySelector('#list');
ul.addEventListener('click', function (e) {
// matches 方法,当元素匹配指定的选择器返回 true
if (e.target.matches('.cls')) {
console.log(e.target.textContent);
}
});
利用事件冒泡的特性,将本应该注册在子元素上的处理事件注册在父元素上,这样点击子元素时发现其本身没有相应事件就到父元素上寻找作出相应。这样做的优势有:1. 减少内存消耗,避免重复创建相同事件处理函数,只需要把多个子元素的事件委托给父元素。2.随时可以添加子元素,添加的子元素会自动有相应的处理事件。
const tbody=document.querySelector('tbody');
tbody.addEventListener('click', function (e) {
// 注册事件的元素 tbody
// console.log(this);
// 触发事件的元素(你点击的那个元素)
// console.log(e.target)
// 判断元素是否是指定的元素
// console.log(e.target.matches('.del'))
if (e.target.matches('.del')) {
e.target.parentNode.parentNode.remove();
}
});
每当触发一个事件,就会产生一个事件对象 event,该对象包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。上一小节中我们使用事件对象获取触发事件的元素。
例如:鼠标操作产生的 event中会包含鼠标位置的信息;键盘操作产生的event中会包含与按下的键有关的信息。
所有浏览器都支持 event 对象,但支持方式不同,在标准 DOM 中 event 对象必须作为唯一的参数传给事件处理函数,在 IE 中 event 是 window 对象的一个属性。
成员 | 描述 | 备注 |
type | 触发的事件名称 | |
eventPhase | 事件流在传播阶段的位置 | |
target | 触发事件的元素 | |
srcElement | target 的别名,老版本的 IE 中使用 | |
clientX / clientY | 基于浏览器的可视区域,鼠标坐标值 | 可配合固定定位,基于窗口定位 |
pageX / pageY | 基于整个页面,页面滚动有关,鼠标在页面的坐标值 | 可配合绝对定位,基于页面定位 |
key | 获取按键输入 | |
preventDefault() | 取消默认行为 | |
stopPropagation() | 阻止事件冒泡 |
const img=document.querySelector('#img');
document.addEventListener('mousemove', function (e) {
// 鼠标位置 - 图片大小的一半
img.style.left=e.clientX - 96 / 2 + 'px';
img.style.top=e.clientY - 80 / 2 + 'px';
});
设置样式,让 body 的高度等于 1500px(垂直方向出现滚动条),滚动条下拉这时候移动鼠标,图片的纵向位置跟鼠标脱离。
原因是 clientX 和 clientY 获取的是鼠标在当前可视区域的位置。如果出现滚动条的话可以通过 pageX 和 pageY 获取鼠标在当前文档中的位置。
const img=document.querySelector('#img');
document.addEventListener('mousemove', function (e) {
img.style.left=e.pageX - 96 / 2 + 'px';
img.style.top=e.pageY - 80 / 2 + 'px';
});
这里获取图片大小的时候写的是具体值,将来图片替换后,还需要改变这里的大小。我们可以使用 getComputedStyle() 获取图片的大小。
const img=document.querySelector('#img');
img.addEventListener('load', function () {
const style=window.getComputedStyle(img, null);
const imgWidth=parseInt(style.width);
const imgHeight=parseInt(style.height);
document.addEventListener('mousemove', function (e) {
img.style.left=e.pageX - imgWidth / 2 + 'px';
img.style.top=e.pageY - imgHeight / 2 + 'px';
});
});
注意:这里需要在 img 标签加载完毕后获取图片的大小,否则获取到的图片大小是 0,因为 load 事件代表图片被加载,否则的话代码从上到下执行到这个位置,图片还没有被下载回来,这个时候获取图片的大小是 0。
#img {
width: 100px;
position: absolute;
left: 0;
top: 0;
}
.toLeft {
transform: rotateY(180deg);
}
const img=document.querySelector('#img');
let x=0;
let y=0;
document.addEventListener('keydown', function (e) {
switch (e.key) {
case 'ArrowLeft':
x -=10;
img.classList.add('toLeft');
break;
case 'ArrowRight':
x +=10;
img.classList.remove('toLeft');
break;
case 'ArrowUp':
y -=10;
break;
case 'ArrowDown':
y +=10;
break;
}
img.style.left=x + 'px';
img.style.top=y + 'px';
});
// contextmenu 鼠标右键事件
document.addEventListener('contextmenu', function(e) {
// 禁止点击的默认行为,即显示上下文菜单
e.preventDefault()
});
// 禁止选中文字事件
document.addEventListener('selectstart', function(e) {
// 禁止选中文字的默认行为,即不能选中文字
e.preventDefault()
})
const loginBg=document.querySelector('#bg');
const loginLink=document.querySelector('#link');
const loginBox=document.querySelector('#login');
const closeBtn=document.querySelector('#closeBtn');
const loginTitle=document.querySelector('#title');
loginLink.addEventListener('click', function () {
loginBox.style.display='block';
loginBg.style.display='block';
});
closeBtn.addEventListener('click', function () {
loginBox.style.display='none';
loginBg.style.display='none';
});
// 拖动事件的三个过程:鼠标按下 mousedowm,鼠标移动 mousemove,鼠标松开 mouseup
const style=window.getComputedStyle(loginBox, null);
// 模态框跟着鼠标走的原理
loginTitle.addEventListener('mousedown', function (e) {
const loginLeft=parseInt(style.left);
const loginTop=parseInt(style.top);
// 步骤一:当鼠标按下时,需要立即得到鼠标在盒子中的坐标
var x=e.pageX - loginLeft;
var y=e.pageY - loginTop;
// 为整个页面添加鼠标移动事件
document.addEventListener('mousemove', move);
function move(e) {
// 步骤二:模态框的left和top等于鼠标在页面的坐标减去鼠标在盒子内的坐标
// 注意:一定要加上px
login.style.left=e.pageX - x + 'px';
login.style.top=e.pageY - y + 'px';
}
// 步骤三:鼠标松开时取消整个页面的鼠标移动事件
document.addEventListener('mouseup', function (e) {
document.removeEventListener('mousemove', move);
});
});
在这之前我们已经使用过了单击事件、鼠标经过和鼠标离开的事件,浏览器给我们提供的事件种类非常多,下面我们列出一些常用的事件,使用的方式都是一样的。
描述 | 事件名称 |
鼠标单击 | click |
鼠标双击 | dblclick |
鼠标移入 | mouseover |
鼠标移出 | mouseout |
鼠标移动 | mousemove |
获取焦点 | focus |
失去焦点 | blur |
键盘按下 | keydown |
键盘弹起 | keyup |
不能识别功能键 ctrl、alt 等 | keypress |
文本框的输入事件 | input |
const search=document.querySelector('#search');
function hanldeFocus(e) {
if (e.key==='s') {
search.focus();
e.preventDefault();
}
}
document.addEventListener('keydown', hanldeFocus);
search.addEventListener('focus', function () {
document.removeEventListener('keydown', hanldeFocus);
});
search.addEventListener('blur', function () {
document.addEventListener('keydown', hanldeFocus);
});
动端的点透:当上层元素发生点击的时候, 下层元素也有点击(焦点)特性, 在300ms之后, 如果上层元素消失或者隐藏,
目标点就会"漂移"到下层元素身上, 就会触发点击行为。
<a href="http://baidu.com">百度一下</a>
<p>pc点击事件</p>
<div></div>
<script type="text/javascript">
//pc端事件是可以在移动端使用的 但是存在一些问题(事件点透)-pc端事件比移动端事件慢300ms
var oDiv=document.getElementsByTagName("div")[0];
var oP=document.getElementsByTagName("p")[0];
oP.onclick=function(){ //pc端事件
this.style.color="orange";
}
oDiv.addEventListener("touchend",function(){ //移动端事件
this.style.display="none";
},true);
</script>
如果我们在百度一下PC点击事件上方进行点击会发现当红色区域隐藏后还会触发其自身的事件。这种现象就是事件点透。
成因
我们上面说过, 移动设备能够响应click事件, 不过比较慢, 这是为什么?
因为click事件触发之后, 要等200ms到300ms左右, 因为浏览器有一些默认的手指快捷操作, 比如:快速双击两次屏幕视口会放大, 弹出辅助菜单等等。
大家用手机上网的时候, 尤其看一些PC端网站, 双击屏幕, 当前视口就变大了。
本质上:PC端事件比移动端的事件略慢, 大概是在300ms左右。所以我们也称这种现象为300毫秒延迟。
实例: 事件点透
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<style type="text/css">
*{
margin: 0;
padding: 0;
list-style: none;
font-size: 5vw;
}
div{
width: 100vw;
height: 50vh;
text-align: center;
background-color: rgba(255,0,0,0.5);
position: absolute;
left: 0;
top: 0;
}
</style>
</head>
<body>
<div></div>
<a href="http://xxxxxx.com">JavaScript</a>
<mark></mark>
<span>我没有点击行为</span>
<script type="text/javascript">
var div=document.getElementsByTagName("div")[0];
div.addEventListener("touchstart",function(){
this.style.display="none";
//this.style.backgroundColor="rgba(0,255,0,0.5)";
},false)
var mark=document.getElementsByTagName("mark")[0];
mark.onclick=function(){
this.style.color="red"; //字体变成红色
}
//事件点透:当上层元素(div)有点击行为时,当上层元素消失,这种点击行为会飘到下层(mark)(点击事件、具有点击行为)元素身上
//还需要注意上层元素是移动端事件 下层元素如果执行事件必须是PC端事件
//原因:300ms延迟
</script>
</body>
</html>
解决方法: 在移动端中全部使用移动端事件代替PC端事件 ontouchstart
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<style type="text/css">
*{
margin: 0;
padding: 0;
list-style: none;
font-size: 5vw;
}
div{
width: 100vw;
height: 50vh;
text-align: center;
background-color: rgba(255,0,0,0.5);
position: absolute;
left: 0;
top: 0;
}
</style>
</head>
<body>
<div></div>
<a href="http://xxxxxx.com">JavaScript</a>
<mark>JavaScript</mark>
<script type="text/javascript">
var div=document.getElementsByTagName("div")[0];
div.addEventListener("touchstart",function(){
this.style.display="none";
//this.style.backgroundColor="rgba(0,255,0,0.5)";
},false)
var mark=document.getElementsByTagName("mark")[0];
//在移动端中全部使用移动端事件代替PC端事件
mark.ontouchstart=function(){
this.style.color="red";
}
//事件点透:当上层元素有点击行为时,当上层元素消失,这种点击行为会飘到下层(点击事件、具有点击行为)元素身上
//还需要注意上层元素是移动端事件 下层元素如果执行事件必须是PC端事件
//原因:300ms延迟
</script>
</body>
</html>
超链接如何解决点透
文/ 观察者网专栏作者 陈蓝】
近几天,中国空间站两次紧急规避星链卫星事件成为热搜话题,在网上迅速发酵。
事出本月3日,中国通过联合国和平利用外层空间委员会(COPUOS)向联合国秘书长提交了一份照会,指出星链1095卫星从5月中旬至6月下旬持续降轨至382公里高度后保持在此高度运行。这是一个非常接近中国空间站轨道的高度。7月1日发生第一次近距离接近事件。为保证安全,中国空间站于7月1日晚做了紧急规避。
10月21日,星链2305卫星再次接近。这次星链卫星处于策略不明的连续机动状态。中国空间站再次做了避险规避。
这两次事件发生后,中国官方并未正式宣布,官媒也没报道,向联合国发照会同样也没官方和官媒消息。是网民从联合国网站上发现了这个事情。这就是为什么照会发出差不多四周,大家才知道。
不过到目前为止,热闹的主要还是中文媒体,尤其是中文自媒体。国外社交网络和专业圈子有少量讨论,而欧美主流媒体几乎都视而不见。
美国天文学家乔纳森·迈克道威尔(Jonathan McDowell)长期关注航天发射和航天器运行状况,定期发布相关信息和他自己的分析。这次也不例外,他为我们提供了更为详细的信息。
乔纳森·迈克道威尔推特截图
首先,他认为这两颗星链属于故障退役卫星。按他的统计,大概有300颗,即占在轨总数16%的星链卫星已经离开了550公里的工作轨道。它们都会在SpaceX控制下主动降轨并再入大气层。鉴于中国空间站处于稳定轨道,乔纳森认为SpaceX有主动规避并事先通知中方的责任。
乔纳森还对这两次事件的轨道做了分析。他指出星链1095和中国空间站最接近的时刻是7月1日13:15(世界协调时,下同),而中国空间站避险变轨发生在9:50,提前不到三个半小时。我们有理由推测,这是一个突发事件,发现情况时可能已经很迟,采取的是非常紧急的措施。
图1:7月星链1095(蓝线)和中国空间站(CSS,红线)轨道变化历史(两根线分别代表近地点和远地点高度)。
10月21日,中国空间站于3:16变轨。近19个小时后,星链2305于22:00接近空间站。这说明,我们对星链已经有了警惕,预测比较超前,采取措施的时间也比较充分。
乔纳森还提供了两个航天器的轨道和相邻距离变化的详细图表。图表显示,星链1095和中国空间站在7月相遇时,星链在降轨。它在和中国空间站相遇之际可能做过一些机动。
而10月那次相遇中,星链2305在持续升轨,且没有做任何规避机动。而中国空间站的机动比7月那次更明显,其远地点增加、近地点减少,轨道变得更椭圆。
图2:10月星链2305(红线)和中国空间站(CSS,蓝线)轨道变化历史(两根线分别代表近地点和远地点高度)。
图3:10月事件中,星链2305和中国空间站距离变化历史。
两个航天器每绕地球一圈就有一次相遇机会。图3显示了十月事件的两次相遇,右侧那次相距不到4公里。这在太空中是一个非常近的距离。考虑到在这之前已经做过规避机动,如果不做规避,这个距离肯定更小,不能排除相撞的可能性!所以,这两个事件的严重性无论怎么高估都不为过。
由于航天器飞行速度极高,碰撞造成的后果是毁灭性的。而且,一次碰撞产生的数以万计的碎片会继续在轨道上高速运行,进一步提高轨道上的碰撞风险。这种连锁反应的结果是,轨道上没有东西能够幸免。电影《地心引力》已经展现了这个令人恐怖的场景。
太空碰撞毁掉一颗卫星只是经济损失。但对载人航天来说,代价可能是宇航员的生命。近年来,轨道碎片的数量还在不断增加,碰撞的风险越来越大。所以,太空垃圾的消减、太空碰撞的预警和规避、载人航天器的太空救生等已经成为非常重要的课题,甚至已经成为政治问题和国际合作的重要内容。
在国际空间站,碰撞规避已经是常规操作。截至2020年底,它在22年的历史中做过25次紧急规避。而近年来这种操作更为频繁,平均每年有两次。
国际空间站目前的标准是,只要发现大于10厘米的太空碎片可能进入国际空间站轨道上下1.25公里、前后25公里的长方形“鞋盒”空间内,或者碰撞概率大于万分之一,空间站就必须开启变轨规避,宇航员必须躲进站上对接的载人飞船内,万一出现意外时可以随时逃离、返回地球。
国际空间站要求在可能的碰撞前留出五个多小时,其中规避变轨准备约三个小时,等待变轨生效约两个小时。中国空间站也有类似的规避准则。
然而,这两次事件的主角并非太空碎片,也非失控航天器,而是尚能由地面控制的卫星。在以往的太空碰撞及“潜在”太空碰撞事件中,总有一方是不受控的太空垃圾或失控航天器。历来威胁国际空间站的太空垃圾质量都很有限,更没遭遇过可控航天器的威胁。而这次威胁到中国空间站的是处于可控状态、260公斤之重的完整卫星,其破坏力可想而知。
毫无疑问,这次事件是非同寻常的。10月事件中星链卫星的持续升轨则带来了更多的疑问。按理说,退役卫星离轨再入是不需要升轨的。
星链卫星,图片来源:SpaceX
SpaceX和马斯克必须做出解释,为什么在卫星可控情况下还会发生这样的事件?为什么降轨规划时候没有考虑到和空间站的碰撞?为什么出现风险不提前做规避机动?为什么离轨再入中会出现升轨操作?
截至发稿,SpaceX和马斯克仍然没有做任何回应。
笔者不禁联想到今年中国空间站天和核心舱发射后的情况,当时西方媒体大肆渲染长征五号末级再入地球的风险。事实上,和太空碰撞相比,长五末级再入的可控性更强,风险和长期后果则更低。它的最终再入也如预测的那样,没有对地面造成任何损害。
中国空间站
但这次星链事件,西方主流媒体几乎集体失声(除了《镜报》这样的左翼小报),再次显示了他们所谓的新闻自由和客观报道的虚伪性。他们对中国的选择性和双标报道充满了傲慢和偏见。
当然,现在对事件的真正原因下结论还为时过早。但即便作最善意的假设,事件也可以引出更多的讨论和思考。类似星链这样的巨型星座会带来什么影响?数万颗卫星围绕地球,数百颗卫星在持续降轨,一定还会有失控卫星,太空碰撞的风险绝对不可低估。它对不断增加的太空人口来说可能是致命的。
目前,星链卫星已经占据了近地轨道近距离遭遇事件的一半。当12000颗星链卫星入轨后,这一比例预计将增加到90%。
在SpaceX和马斯克光环的带动下,很多公司计划或者已经走上了巨型星座的道路。这种任性的巨型星座和我们星辰大海的理想会不会背道而驰?这是人类正确的方向吗?地面交通需要交通管理部门、法规和警察的约束和监督,太空难道就不需要吗?
不过,太空没有边界。太空和航天器的管理无法依靠一国来完成。我们需要国际合作、需要基于一致认可的规则的行动。但很遗憾,目前的国际形势为合作投下了阴影。我们看到太空中出现了越来越多的冲突迹象而不是合作。也许,只有深刻的教训才能使局面有所改观。也许,我们只能等待,等待我们的下一代来解决这个难题。
至于马斯克,如果他要继续走下去,继续在太空拓展,最终实现他的火星梦想,他就必须在这一事件上表现出责任感和客观性,他也必须为巨型星座的问题找到实质性的解决办法。
让我们拭目以待吧。
本文系观察者网独家稿件,文章内容纯属作者个人观点,不代表平台观点,未经授权,不得转载,否则将追究法律责任。关注观察者网微信guanchacn,每日阅读趣味文章。
*请认真填写需求信息,我们会在24小时内与您取得联系。