JavaScript:浏览器环境概述(BOM)

浏览器环境概述(BOM)

在这里插入图片描述
JavaScript是浏览器的内置脚本语言,一旦网页内嵌了JavaScript脚本,浏览器加载网页,就会去执行脚本,从而达到操作浏览器的目的,实现网页的各种动态效果

代码嵌入网页的方法
<script> 元素直接嵌入代码。

<script>
  var x = 1 + 5;
  console.log(x);
</script>

<script> 标签加载外部脚本

<script
src="https://www.example.com/script.js">
</script>

加载使用的协议
http协议

<script src="http://example.js"></script>

https协议

<script src="https://example.js"></script>

但是有时我们会希望,根据页面本身的协议来决定加载协议,这时可以采用下面的写法

<script src="//example.js"></script>

script 元素工作原理

工作原理
浏览器加载 JavaScript 脚本,主要通过 <script> 元素完成。正常的网页加载流程是这样的

1 浏览器一边下载 HTML 网页,一边开始解析。也就是说,不等到下载完,就开始解析
2 解析过程中,浏览器发现 <script> 元素,就暂停解析,把网页渲染的控制权转交给 JavaScript 引擎
3 如果 <script> 元素引用了外部脚本,就下载该脚本再执行,否则就直接执行代码
4 JavaScript 引擎执行完毕,控制权交还渲染引擎,恢复往下解析 HTML 网页

加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后,再继续渲染。原因是JavaScript代码可以修改DOM,所以必须把控制权让给它,否则会导致复杂的线程竞赛的问题。

如果外部脚本加载时间很长(一直无法完成下载),那么浏览器就会一直等待脚本下载完成,在成网页长时间失去响应,浏览器就i会呈现“假死”状态,这被称为“阻塞效应”。

为了避免这种情况,较好的做法是将<script>标签都放在页面底部,而不是头部。这样即使遇到脚本失去响应,网页主体的渲染也已经完成了,用户至少可以看到内容,而不是面对一张空白的页面。

defer 属性
为了解决脚本文件下载阻塞网页渲染的问题,一个方法是对 <script>元素加入 defer 属性。它的作用是延迟脚本的执行,等到 DOM 加载生成后,再执行脚本

<script src="./js/index.js" defer></script>

defer 属性的运行流程如下

1 浏览器开始解析 HTML 网页
2 解析过程中,发现带有 defer 属性的 <script> 元素
3 浏览器继续往下解析 HTML 网页,同时并行下载 <script> 元素加载的外部脚本
4 浏览器完成解析 HTML 网页,此时再回过头执行已经下载完成的脚本

有了 defer 属性,浏览器下载脚本文件的时候,不会阻塞页面渲染

async 属性
解决“阻塞效应”的另一个方法是对 <script> 元素加入 async 属性

<script src="./js/index1.js" async></script>
<script src="./js/index.js" async></script>

async 属性的作用是,使用另一个进程下载脚本,下载时不会阻塞渲染

1 浏览器开始解析 HTML 网页
2 解析过程中,发现带有 async 属性的 script 标签
3 浏览器继续往下解析 HTML 网页,同时并行下载 <script> 标签中的外部脚本
4 脚本下载完成,浏览器暂停解析 HTML 网页,开始执行下载的脚本
5 脚本执行完毕,浏览器恢复解析 HTML 网页

async 属性可以保证脚本下载的同时,浏览器继续渲染。需要注意的是,一旦采用这个属性,就无法保证脚本的执行顺序。哪个脚本先下载结束,就先执行那个脚本

一般来说,如果脚本之间没有依赖关系,就使用 async 属性,如果脚本之间有依赖关系,就使用 defer 属性。如果同时使用 asyncdefer 属性,后者不起作用,浏览器行为由 async 属性决定

回流和重绘

渲染树转换为网页布局,称为“布局流”(flow);布局显示到页面的这个过程,称为“绘制”(paint)。它们都具有阻塞效应,并且会耗费很多时间和计算资源。

页面生成以后,脚本操作和样式表操作,都会触发“回流”(reflow)和“重绘”(repaint)。

什么是回流和重绘
回流:当节点树中的一部分因为元素的规模尺寸,布局,隐藏等改变而需要重新构建

重绘:当节点数中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的

温馨提示
回流和重绘并不一定一起发生,回流必然导致重绘,重绘不一定需要回流。比如改变元素颜色,只会导致重绘,而不会导致回流;改变元素的布局,则会导致重绘和回流

什么时候会造成回流和重绘
回流

1 页面初始加载
2 改变字体,元素尺寸(width,height,border,position),改变元素内容
3添加或删除元素,如果元素本身被display:none会回流,visibility:hidden则会发生重绘
4定位为fixed的元素,滚动条拖动时会回流 5 调整窗口大小

重绘
在这里插入图片描述
优化技巧

1 读取DOM 或者写入DOM,尽量写在一起,不要混杂。不要读取一个DOM节点,然后立刻写入,接着再读取一个DOM节点
2 缓存DOM信息
3 不要一项一项地改变样式,而是使用CSS class一次性改变样式
4 使用documentFragment操作DOM
5 动画使用absolute定位或fixed定位,这样可以减少对其他元素的影响
6 只在必要时才显示隐藏元素
7 使用虚拟DOM(virtual DOM)库

// 引发两次回流
box.style.top = '100px';
console.log(box.style.top);//=>'100px'
box.style.left = '100px';
// 引发一次回流
box.style.top = '100px';
box.style.left = '100px';
console.log(box.style.top);//=>'100px'

定时器之 setTimeout()

JavaScript 提供定时执行代码的功能,叫做定时器(timer),主要由 setTimeout()setInterval() 这两个函数来完成。它们向任务队列添加定时任务

setTimeout 函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。

var timerId = setTimeout(func|code, delay);

setTimeout 函数接受两个参数,第一个参数 func|code 是将要推迟执行的函数名或者一段代码,第二个参数 delay 是推迟执行的毫秒数

setTimeout(function(){
    console.log("定时器")
},1000)

温馨提示
还有一个需要注意的地方,如果回调函数是对象的方法,那么 setTimeout 使得方法内部的 this关键字指向全局环境,而不是定义 时所在的那个对象

var name = "sxt";
var user = {
    name: "itbaizhan",
    getName: function () {
        setTimeout(function(){
            console.log(this.name);
       },1000)
   }
};
user.getName();

解决方案

var name = "sxt";
var user = {
    name: "itbaizhan",
    getName: function () {
        var that = this;
        setTimeout(function(){
            console.log(that.name);
       },1000)
   }
};
user.getName();

定时器可以进行取消

var id = setTimeout(f, 1000);
clearTimeout(id);

定时器之 setInterval()

在这里插入图片描述
setInterval 函数的用法与 setTimeout 完全一致,区别仅仅在于 setInterval 指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行

var timer = setInterval(function() {
  console.log(2);
}, 1000)

通过setInterval方法实现网页动画

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport"
content="width=device-width, initialscale=1.0">
 <title>Document</title>
 <style>
 #someDiv{
 width: 100px;
 height: 100px;
 background: red;
 }
 </style>
</head>
<body>
 <div id="someDiv"></div>
 <script>
 var div =
document.getElementById('someDiv');
 var opacity = 1;
 var fader = setInterval(function() {
  opacity -= 0.05;
  if (opacity > 0) {
    div.style.opacity = opacity;
 } else {
    clearInterval(fader);
 }
 }, 30);
 </script>
</body>
</html>

定时器可以进行取消

var id = setInterval(f, 1000);
clearInterval(id);

定时器实操

for (var i = 0; i < lis.length; i++) {
   (function (i) {
        lis[i].onmouseenter = function () {
            timer = setTimeout(function () {
                for (var j = 0; j < lis.length; j++) {
                  
					lis[j].removeAttribute("class");
					                  
					divs[j].removeAttribute("class")
               }
                lis[i].setAttribute("class","select")
              
					divs[i].setAttribute("class", "div-select")
           }, 300)
       }
   }(i))
    lis[i].onmouseout = function () {
        clearTimeout(timer);
   }
}

防抖(debounce)

在这里插入图片描述
防抖严格算起来应该属于性能优化的知识,但实际上遇到的频率相当高,处理不当或者放任不管就容易引起浏览器卡死。

从滚动条监听的例子说起

先说一个常见的功能,很多网站会提供这么一个按钮:用于返回顶部。

这个按钮只会在滚动到距离顶部一定位置之后才出现,那么我们现在抽象出这个功能需求-- 监听浏览器滚动事件,返回当前滚条与顶部的距离这个需求很简单,直接写

function showTop () {
    var scrollTop = document.documentElement.scrollTop;
    console.log('滚动条位置:' + scrollTop);
}
window.onscroll  = showTop

在运行的时候会发现存在一个问题:这个函数的默认执行频率,太!高!了!。 高到什么程度呢?以chrome为例,我们可以点击选中一个页面的滚动条,然后点击一次键盘的【向下方向键】,会
发现函数执行了8-9次!

然而实际上我们并不需要如此高频的反馈,毕竟浏览器的性能是有限的,不应该浪费在这里,所以接着讨论如何优化这种场景。

基于上述场景,首先提出第一种思路:在第一次触发事件时,不立即执行函数,而是给出一个期限值比如200ms,然后

1 如果在200ms内没有再次触发滚动事件,那么就执行函数
2 如果在200ms内再次触发滚动事件,那么当前的计时取消,重新开始计时

效果:如果短时间内大量触发同一事件,只会执行一次函数

实现:既然前面都提到了计时,那实现的关键就在于setTimeout这个函数,由于还需要一个变量来保存计时,考虑维护全局纯净,可以借助闭包来实现

function debounce(fn,delay){
    let timer = null //借助闭包
    return function() {
        if(timer){
            clearTimeout(timer)
       }
        timer = setTimeout(fn,delay) // 简化写法
   }
}
// 然后是旧代码
function showTop () {
var scrollTop = document.documentElement.scrollTop;
    console.log('滚动条位置:' + scrollTop);
}
window.onscroll = debounce(showTop,300)

到这里,已经把防抖实现了

防抖定义
对于短时间内连续触发的事件(上面的滚动事件),防抖的含义就是让某个时间期限(如上面的1000毫秒)内,事件处理函数只执行一次

节流(throttle)

在这里插入图片描述

节流严格算起来应该属于性能优化的知识,但实际上遇到的频率相当高,处理不当或者放任不管就容易引起浏览器卡死

继续思考,使用上面的防抖方案来处理问题的结果是如果在限定时间段内,不断触发滚动事件(比如某个用户闲着无聊,按住滚动不断的拖来拖去),只要不停止触发,理论上就永远不会输出当前距离顶部的距离

但是如果产品同学的期望处理方案是:即使用户不断拖动滚动条,也能在某个时间间隔之后给出反馈呢?

其实很简单:我们可以设计一种类似控制阀门一样定期开放的函数,也就是让函数执行一次后,在某个时间段内暂时失效,过了这段时间后再重新激活(类似于技能冷却时间)

效果:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效

实现
这里借助setTimeout来做一个简单的实现,加上一个状态位valid来表示当前函数是否处于工作状态

function throttle(fn,delay){
    let valid = true
    return function() {
       if(!valid){
           //休息时间 暂不接客
           return false
            }
       // 工作时间,执行函数并且在间隔期内把状态位设为无效
        valid = false
        setTimeout(function(){
            fn()
            valid = true;
       }, delay)
   }
}
function showTop () {
    var scrollTop = document.documentElement.scrollTop;
    console.log('滚动条位置:' + scrollTop);
}
window.onscroll = throttle(showTop,300)

如果一直拖着滚动条进行滚动,那么会以300ms的时间间隔,持续输出当前位置和顶部的距离

讲完了这两个技巧,下面介绍一下平时开发中常遇到的场景:

1 搜索看input事件,例如要支持输入实时搜索可以使用节流方案(间隔一段时间就必须查询相关内容),或者实现输入间隔大于某个值(如500ms),就当作用户输入完成,然后开始搜索,具体使用哪种方案要看业务需求
2 页面resize事件,常见于需要做页面适配的时候。需要根据最终呈现而的页面情况进行DOM渲染(这种情形一般是使用防抖,因为只需要判断最后一次的变化情况)

window 对象_属性

在这里插入图片描述
浏览器里面, window 对象(注意, w 为小写)指当前的浏览器窗口。它也是当前页面的顶层对象,即最高一层的对象,所有其他对象都是它的下属

a = 1;
window.a // 1

window.screenX,window.screenY
window.screenXwindow.screenY 属性,返回浏览器窗口左上角相对于当前屏幕左上角的水平距离和垂直距离(单位像素)。这两个属性只读

console.log(window.screenX);
console.log(window.screenY);

window.innerHeight,window.innerWidth
window.innerHeightwindow.innerWidth 属性,返回网页在当前窗口中可见部分的高度和宽度,即“视口”(viewport)的大小(单位像素)。这两个属性只读

console.log(window.innerHeight);
console.log(window.innerWidth);

温馨提示
这两个属性值包括滚动条的高度和宽度

window.outerHeight,window.outerWidth
window.outerHeightwindow.outerWidth 属性返回浏览器窗口的高度和宽度,包括浏览器菜单和边框(单位像素)。这两个属性只读

console.log(window.outerHeight);
console.log(window.outerWidth);

window.scrollX,window.scrollY
window.scrollX 属性返回页面的水平滚动距离, window.scrollY 属性返回页面的垂直滚动距离,单位都为像素。这两个属性只读

console.log(window.scrollX);
console.log(window.scrollY);

window.pageXOffset,window.pageYOffset
window.pageXOffset 属性和 window.pageYOffset 属性,是 window.scrollXwindow.scrollY 别名

window 对象_方法

在这里插入图片描述
window.alert(),window.prompt(),window.confirm()
window.alert() 、 window.prompt() 、 window.confirm() 都是浏览器与用户互动的全局方法。它们会弹出不同的对话框,要求用户做出回应。注意,这三个方法弹出的对话框,都是浏览器统一规定的式样,无法定制

window.alert()
window.alert() 方法弹出的对话框,只有一个“确定”按钮,往往用来通知用户某些信息

window.alert('Hello World');

window.prompt()
window.prompt() 方法弹出的对话框,提示文字的下方,还有一个输入框,要求用户输入信息,并有“确定”和“取消”两个按钮。它往往用来获取用户输入的数据

var result = prompt('您的年龄?', 25)
console.log(result);

window.confirm()
window.confirm() 方法弹出的对话框,除了提示信息之外,只有“确定”和“取消”两个按钮,往往用来征询用户是否同意confirm 方法返回一个布尔值,如果用户点击“确定”,返回 true ;如果用户点击“取消”,则返回 false

var result = confirm('你最近好吗?');
var okay = confirm('Please confirm thismessage.');
if (okay) {
  // 用户按下“确定”
} else {
  // 用户按下“取消”
}

window.open()
window.open 方法用于新建另一个浏览器窗口,类似于浏览器菜单的新建窗口选项。它会返回新窗口的引用,如果无法新建窗口,则返回null

window.open("https://itbaizhan.com")

Navigator 对象

在这里插入图片描述
window.navigator 属性指向一个包含浏览器和系统信息的 Navigator 对象。脚本通过这个属性了解用户的环境信息

Navigator.userAgent
navigator.userAgent 属性返回浏览器的 User Agent 字符串,表示用户设备信息,包含了浏览器的厂商、版本、操作系统等信息

navigator.userAgent
'Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/95.0.4638.69 Safari/537.36'

userAgent 可以大致准确地识别手机浏览器,方法就是测试是否包含mobi 字符串

var ua = navigator.userAgent.toLowerCase();
if (ua.indexOf("mobi") > -1) {
  // 手机浏览器
} else {
  // 非手机浏览器
}

Navigator.plugins
Navigator.plugins 属性返回一个类似数组的对象,成员是 Plugin 实例对象,表示浏览器安装的插件,比如 Flash、ActiveX

var pluginsLength = navigator.plugins.length;
for (var i = 0; i < pluginsLength; i++) {
  console.log(navigator.plugins[i].name);
  console.log(navigator.plugins[i].filename);
  console.log(navigator.plugins[i].description);
  console.log(navigator.plugins[i].version);
}

Navigator.platform
Navigator.platform 属性返回用户的操作系统信息,比如 MacIntel 、 Win32 、Linux x86_64

navigator.platform
// 'Win32'

Navigator.language,Navigator.languages
Navigator.language 属性返回一个字符串,表示浏览器的首选语言。该属性只读

navigator.language
// 'zh-CN'

Navigator.languages 属性返回一个数组,表示用户可以接受的语言

navigator.languages  
// ['zh-CN', 'zh']

Screen 对象

Screen 对象表示当前窗口所在的屏幕,提供显示设备的信息。window.screen 属性指向这个对象

Screen.height
浏览器窗口所在的屏幕的高度(单位像素)。除非调整显示器的分辨率,否则这个值可以看作常量,不会发生变化。显示器的分辨率与浏览器设置无关,缩放网页并不会改变分辨率

console.log(screen.height);

Screen.width
浏览器窗口所在的屏幕的宽度(单位像素)

console.log(screen.width);

Screen.availHeight
浏览器窗口可用的屏幕高度(单位像素)。因为部分空间可能不可用,比如系统的任务栏或者 Mac 系统屏幕底部的 Dock 区,这个属性等于 height 减去那些被系统组件的高度

screen.availHeight

Screen.availWidth
浏览器窗口可用的屏幕宽度(单位像素)

screen.availWidth

Screen.pixelDepth
整数,表示屏幕的色彩位数,比如 24 表示屏幕提供24位色彩

screen.pixelDepth
// 24

Screen.orientation
返回一个对象,表示屏幕的方向。该对象的 type 属性是一个字符串,表示屏幕的具体方向, landscape-primary 表示横放, landscape-secondary表示颠倒的横放, portrait-primary 表示竖放, portrait-secondary 表示颠倒的竖放。

screen.orientation
// {angle: 270, type: 'portrait-primary',
onchange: null}

History 对象

window.history 属性指向 History 对象,它表示当前窗口的浏览历史History 对象保存了当前窗口访问过的所有页面网址

window.history.length

History.back()
History.back() :移动到上一个网址,等同于点击浏览器的后退键。对于第一个访问的网址,该方法无效果

history.back();

History.forward()
History.forward() :移动到下一个网址,等同于点击浏览器的前进键。对于最后一个访问的网址,该方法无效果

history.forward();

History.go()
History.go() :接受一个整数作为参数,以当前网址为基准,移动到参数指定的网址,比如 go(1) 相当于 forward()go(-1) 相当于 back() 。如果参数超过实际存在的网址范围,该方法无效果;如果不指定参数,默认参数为 0 ,相当于刷新当前页面

history.go(-2);

温馨提示
移动到以前访问过的页面时,页面通常是从浏览器缓存之中加载,而不是重新要求服务器发送新的网页。

Cookie 对象

在这里插入图片描述

Cookie 是服务器保存在浏览器的一小段文本信息,每个 Cookie 的大小一般不能超过4KB。浏览器每次向服务器发出请求,就会自动附上这段信息

Cookie 的目的就是区分用户,以及放置状态信息,它的使用场景主要如下

1 对话(session)管理:保存登录状态、购物车等需要记录的信息
2 个性化信息:保存用户的偏好,比如网页的字体大小、背景色等等
3 追踪用户:记录和分析用户行为

Cookie 不是一种理想的客户端存储机制。它的容量很小(4KB),缺乏数据操作接口,而且会影响性能。客户端存储建议使用 Webstorage API 。只有那些每次请求都需要让服务器知道的信息,才应
该放在 Cookie 里面

每个 Cookie 都有以下几方面的元数据

Cookie 的名字
Cookie 的值(真正的数据写在这里面)
到期时间(超过这个时间会失效)
所属域名(默认为当前域名)
生效的路径(默认为当前网址)

不同浏览器对 Cookie 数量和大小的限制,是不一样的。一般来说,单个域名设置的 Cookie 不应超过30个,每个 Cookie 的大小不能超过 4KB。超过限制以后,Cookie 将被忽略,不会被设置

读取cookie

document.cookie

Cookie 属性

在这里插入图片描述
Expires
Expires 属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个 Cookie。它的值是 UTC 格式,可以使用Date.prototype.toUTCString() 进行格式转换如果不设置该属性,或者设为 nullCookie 只在当前会话(session)有效,浏览器窗口一旦关闭,当前 Session 结束,该Cookie 就会被删除。另外,浏览器根据本地时间,决定 Cookie 是否过期,由于本地时间是不精确的,所以没有办法保证 Cookie 一定会在服务器指定的时间过期。

document.cookie = "name=iwen;Expires=Fri, 31
Dec 2021 16:00:00 GMT"

Max-Age
Max-Age 属性指定从现在开始 Cookie 存在的秒数,比如 60 * 60 * 24 * 365(即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie
如果同时指定了 ExpiresMax-Age ,那么 Max-Age 的值将优先生效

document.cookie = "name=iwen;Max-Age=3600"

Domain
Domain 属性指定 Cookie 属于哪个域名,以后浏览器向服务器发送HTTP 请求时,通过这个属性判断是否要附带某个 Cookie

Path
Path 属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个Cookie。只要浏览器发现, Path 属性是 HTTP 请求路径的开头一部分,就会在头信息里面带上这个 Cookie。比如, Path 属性是 / ,那么请求 /docs 路径也会包含该 Cookie。当然,前提是 Domain 属性必须符合条件

Secure
Secure 属性指定浏览器只有在加密协议 HTTPS 下,才能将这个Cookie 发送到服务器。另一方面,如果当前协议是 HTTP,浏览器会自动忽略服务器发来的 Secure 属性。该属性只是一个开关,不需要指定值。如果通信是 HTTPS 协议,该开关自动打开

HttpOnly
HttpOnly 属性指定该 Cookie 无法通过 JavaScript 脚本拿到,主要是document.cookie 属性、 XMLHttpRequest 对象和 Request API 都拿不到该属性。这样就防止了该 Cookie 被脚本读到,只有浏览器发出 HTTP请求时,才会带上该 Cookie

封装cookie操作

var cookie = {
set:function(name,value,days){
        var d = new Date();
        d.setDate(d.getDate() + days);
        document.cookie = name + "=" + value +";expires=" + d+";";
   },
    get:function(name){
        var cookiesArr = document.cookie.split("; ")
        for(var i = 0;i< cookiesArr.length;i++){
            var newArr = cookiesArr[i].split("=");
            if(name === newArr[0]){
                return newArr[1]
           }
       }
   },
    unset:function(name){
        this.set(name,"",-1)
   }
}

apply、call和bind函数

在这里插入图片描述
无论是apply、call还是bind其实都是改变this的指向,我们先来看一个例子

var obj = {
    name:"小张",
    getName:function(){
        console.log(this.name)
   }
}
obj.getName(); // 小张

下面我们来看一下通过他们来改变this指向

var obj = {
    name:"小张",
    getName:function(){
        console.log(this.name)
   }
}
var newObj = {
    name:'小王'
}
obj.getName.call(newObj); // 小王
obj.getName.apply(newObj); // 小王
obj.getName.bind(newObj)(); // 小王

由此得出结论,bind 返回的是一个新的函数,你必须调用它才会被执行

我们再来看一下applycall的区别

var obj = {
    name:"小张",
    getName:function(city){
        console.log(this.name,city)
   }
}
var newObj = {
    name:'小王'
}
obj.getName.call(newObj,'北京');
obj.getName.apply(newObj,['上海']);
obj.getName.bind(newObj,'深圳')();

第一个参数:this的指向,第二个参数为方法传递参数

常见应用场景

var arr = [10,20,30]
Math.max.apply(null,arr) // 30
var arr = [10,20,30]
Math.max.call(null,...arr)

null代表指向window对象,这里是因为Array本身时候window对象的子元素

面向对象编程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值