404
Looks like we've got some broken links.Take me home.
diff --git a/404.html b/404.html new file mode 100644 index 0000000..07b8d65 --- /dev/null +++ b/404.html @@ -0,0 +1,19 @@ + + +
+ + +Looks like we've got some broken links.Take me home.
AJAX
全称(Async Javascript and XML)
即异步的 JavaScript
和 XML
,是一种创建交互式网页应用的网页开发技术,可以在不重新加载整个网页的情况下,与服务器交换数据,并且更新部分网页
Ajax
的原理简单来说通过XmlHttpRequest
对象来向服务器发异步请求,从服务器获得数据,然后用 JavaScript 来操作 DOM 而更新页面
流程图如下:
下面举个例子:
领导想找小李汇报一下工作,就委托秘书去叫小李,自己就接着做其他事情,直到秘书告诉他小李已经到了,最后小李跟领导汇报工作
浏览器可以发送HTTP
请求后,接着做其他事情,等收到 XHR 返回来的数据再进行操作
实现 Ajax 异步交互需要服务器逻辑进行配合,需要完成以下步骤:
通过XMLHttpRequest()
构造函数用于初始化一个 XMLHttpRequest
实例对象
const xhr = new XMLHttpRequest();
+
通过 XMLHttpRequest
对象的 open()
方法与服务器建立连接
xhr.open(method, url, [async][, user][, password])
+
参数说明:
method
:表示当前的请求方式,常见的有GET
、POST
url
:服务端地址async
:布尔值,表示是否异步执行操作,默认为true
user
: 可选的用户名用于认证用途;默认为null
password
: 可选的密码用于认证用途,默认为null
通过 XMLHttpRequest
对象的 send()
方法,将客户端页面的数据发送给服务端
xhr.send([body]);
+
body
: 在 XHR 请求中要发送的数据体,如果不传递数据则为 null
如果使用GET
请求发送数据的时候,需要注意如下:
open()
方法中的url
地址中send()
方法中参数设置为null
onreadystatechange
事件onreadystatechange
事件用于监听服务器端的通信状态,主要监听的属性为XMLHttpRequest.readyState
,关于XMLHttpRequest.readyState
属性有五个状态,如下图显示
只要 readyState
属性值一变化,就会触发一次 readystatechange
事件
XMLHttpRequest.responseText
属性用于接收服务器端的响应结果
举个例子:
const request = new XMLHttpRequest();
+request.onreadystatechange = function(e) {
+ if (request.readyState === 4) {
+ // 整个请求过程完毕
+ if (request.status >= 200 && request.status <= 300) {
+ console.log(request.responseText); // 服务端返回的结果
+ } else if (request.status >= 400) {
+ console.log("错误信息:" + request.status);
+ }
+ }
+};
+request.open("POST", "http://xxxx");
+request.send();
+
通过上面对XMLHttpRequest
对象的了解,下面来封装一个简单的Ajax
请求
//封装一个ajax请求
+function ajax(options) {
+ //创建XMLHttpRequest对象
+ const xhr = new XMLHttpRequest();
+
+ //初始化参数的内容
+ options = options || {};
+ options.type = (options.type || "GET").toUpperCase();
+ options.dataType = options.dataType || "json";
+ const params = options.data;
+
+ //发送请求
+ if (options.type === "GET") {
+ xhr.open("GET", options.url + "?" + params, true);
+ xhr.send(null);
+ } else if (options.type === "POST") {
+ xhr.open("POST", options.url, true);
+ xhr.send(params);
+ }
+ //接收请求
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState === 4) {
+ let status = xhr.status;
+ if (status >= 200 && status < 300) {
+ options.success && options.success(xhr.responseText, xhr.responseXML);
+ } else {
+ options.error && options.error(status);
+ }
+ }
+ };
+}
+
使用方式如下
ajax({
+ type: "post",
+ dataType: "json",
+ data: {},
+ url: "https://xxxx",
+ success: function(text, xml) {
+ //请求成功后的回调函数
+ console.log(text);
+ },
+ error: function(status) {
+ ////请求失败后的回调函数
+ console.log(status);
+ },
+});
+
ssh -p 22 root@ip
+//输入完之后会要输入密码,密码是不显示的,输入完回车就行
+
yum -y install gcc pcre-devel zlib-devel openssl openssl-devel
+
mkdir -p /www/nginx
+
cd /www/nginx
+
wget http://nginx.org/download/nginx-1.20.2.tar.gz
+
tar -zxvf nginx-1.20.2.tar.gz
+
cd nginx-1.20.2
+
./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-http_v2_module
+
make
+make install
+
whereis nginx
+cd /usr/local/nginx
+
./sbin/nginx
+
./sbin/nginx -s reload
+
git init
+
项目初始化之后会生成一个.git 文件,如果看不见,那就是电脑隐藏了以.开头的文件
git clone <项目地址>
+git clone -branch [tags标签] <项目地址> 或 git clone -b [tags标签] [项目地址]
+
表示从远程仓库拉取项目到本地
如果我们本地是有项目的,我们想要的是将本地项目和远程仓库关联起来,那么有如下两种情况
git init
+git remote add origin <项目地址>
+git add . //记得点(.)是要空格隔开
+git commit -m "<提交的信息记录>"
+git push --set-upstream origin master 或 git push -u origin master
+
git init
+git remote add origin xxxx
+git add .
+git commit -m 'init'
+git pull origin master --allow-unrelated-histories
+//若有冲突,先解决冲突
+git push --set-upstream origin master
+
可以看出来这两种情况只有在第 5 步的时候才是有区别的.
在上面的 commit 到本地仓库后,如果直接推送,它会报错:
refusing to merge unrelated histories
+
意思就是拒绝合并没有历史关系的分支,我们用下面的代码解决这个问题
然后添加下面这行代码:
git pull origin master --allow-unrelated-histories
+
我们允许拉取没有历史关系的分支,这步也就是我们代码的第 5 步
git branch //查看本地分支
+git branch -r //查看远程分支
+git branch -a //查看本地+远程分支
+git branch -v //分支最后一次提交的信息
+
git branch <BranchName> //创建新分支
+git checkout <BranchName> //切换分支
+git checkout -b <BranchName> //创建新分支并切换到分支
+
将远程 Git 仓库里的指定分支拉取到本地(本地不存在的分支)
git checkout -b <本地分支名> <origin/远程分支名>
+
git branch -d <BranchName> //删除本地分支
+git push origin :<BranchName> //删除远程分支
+
git branch -b <BranchName> //创建本地新分支并切换到新分支
+git push --set-upstream origin <BranchName>
+
不想提交当前代码到远程仓库
有时候,我们想要切换分支,但是又不想将当前分支代码提交到本地或远程仓库,直接切换过去的话代码会被覆盖,这时候我们可以使用暂存功能
git stash //存
+git stash pop //取出暂存
+
git add .
+git commit -m "<提交的信息记录>"
+git push
+
提交的时候,可以输入 emoji 表情,可爱又有趣,别人一眼看过来就知道你提交的是关于什么
🐛 //修复 BUG 💄 //更新样式 🔒 //解决安全问题 ♻️ //重构 ✨ //添加新功能
git log
+git log -3 //显示最近3次的更新
+
git reset --hard commit-id //回滚到commit-id
+git reset --hard HEAD~3 //将最近三次的提交回滚
+git reset --hard HEAD^ //将本地代码回退到上一个版本
+
有时候,我们的项目上线了,我们就需要标签功能,记录我们的版本
git tag //显示已有的标签
+git tag <v1.0.1> //创建一个轻量级标签
+git tag -a <v1.0.2> -m ‘<release version>’ //创建一个带有标注的标签
+git tag -d <tag_name> //删除标签
+git push //并不会把tag标签传送到远端服务器上,只有通过显式命令才能分享标签到远端仓库
+git push origin <tag_name> //push单个tag
+git push origin --tags //推送所有本地新增的标签
+
最后我们来说一下 config 配置
git config --global user.name //查看全局配置的用户名
+git config --global user.email //查看全局配置的邮箱
+git config --global user.name 'zhangsha' //配置全局的用户名
+git config --global user.email 'xx@xx.com' //配置全局的邮箱
+//下面是针对单独的项目进行配置和查看
+git config user.name 'lisi' //配置当前项目的用户名
+git config user.name //查看当前项目的用户名
+git config --list //查看多个配置
+
配置分为全局配置和项目配置,没有对项目进行单独配置的时候,会默认使用全局的配置.如果我们在一个项目中进行了单独的配置,那么就会使用项目中配置.这个选项在你切换不同项目(比如公司和个人项目)的时候,可以起到一定的作用.
HTTP
(HyperText Transfer Protocol),即超文本运输协议,是实现网络通信的一种规范。
在计算机和网络世界有存在不同的协议,如广播协议、寻址协议、路由协议等等......
而HTTP
是一个传输协议,即将数据由 A 传到 B 或 B 传到 A,并且 A 与 B 之间能够存放很多第三方,如
+A<=>X<=>Y<=>Z<=>B
传输的数据并不是计算机地层中的二进制包,二十完整的、有意义的数据,如 HTML 文件、图片文件、查询结果等超文本,能够被上层引用识别
在实际应用中,HTTP
常被用于Web
浏览器和网站服务器之间传递信息,以明文方式发送内容,不提供任何方式的数据加密
特点如下:
在上述介绍HTTP
中,了解到HTTP
传递信息是以明文的形式发送内容,这并不安全。而HTTPS
出现正式为了解决HTTP
不安全的特性
为了保证这些隐私数据能加密传输,让HTTP
安全运行的SSL/TLS
协议上,即 HTTPS=HTTP+SSL/TLS,通过SSL
证书来验证服务器的身份,并为浏览器和服务器之间的通信进行加密
SSL
协议位于TCP/IP
协议与各种应用层协议之间,浏览器和服务器之间在使用SSL
建立连接时需要选择一组恰当的加密算法来实现安全通信,为数据通信提供安全支持
流程图如下所示:
HTTP 状态码(英语:HTTP Status Code),用以表示网页服务超文本传输协议响应状态的 3 为数字代码
它由 RFC2616 规范定义的,并得到RFC 2518
、RFC 2817
、RFC 2295
、RFC 2774
与 RFC 4918
等规范扩展
简单来讲,http
状态码的作用是服务器告诉客户端当前请求响应的状态,通过状态码就能判断和分析服务器的运行状态
状态码第一位数字决定了不同的响应状态,有如下:
代表请求已被接受,需要继续处理。这类响应是临时响应,只包含状态行和某些可选的响应头信息,并以空行结束
常见的有:
代表请求已成功被服务器接收、理解、并接受
常见的有:
表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向
常见的有:
代表了客户端看起来可能发生了错误,妨碍了服务器的处理
常见的有:
表示服务器无法完成明显有效的请求。这类状态码代表了服务器在处理请求的过程中有错误或者异常状态发生
常见的有:
下面给出一些状态码的适用场景:
Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档
使用#
号可表示 1-6 级标题,一级标题对应一个#
号,二级标题对应两个#
号,以此类推。
# 一级标题
+## 二级标题
+### 三级标题
+#### 四级标题
+##### 五级标题
+###### 六级标题
+
效果如下:
也可以使用==
来表示 1 级标题,__
来表示 2 级标题
这是一级标题
+==
+这是二级标题
+--
+
效果如下:
*我是斜体*
+_我是斜体_
+**我是粗体**
+__我是粗体__
+***我是粗斜体***
+___我是粗斜体___
+
我是斜体
我是斜体
我是粗体
我是粗体
我是粗斜体
我是粗斜体
~~我是删除~~
+<u>这是一条下划线</u>
+这是一条分割线
+***
+这是一条分割线
+* * *
+这是一条分割线
+- - -
+这是一条分割线
+--------
+
我是删除
这是一条下划线
这是一条分割线
这是一条分割线
这是一条分割线
注意:三个或三个以上的-
或*
使用+
, -
, *
其中任何一个作为标记
* list1
++ list2
+- list3
+
使用数字 + .
来表示
1. list1
+2. list2
+3. list3
+
1. list1
+ - list1-1
+ - list1-2
+2. list2
+ - list2-1
+ - list2-2
+
注:此种写法在 vuepress 中无效,可以使用原生的复选框代替
- [ ] 吃饭
+- [x] 睡觉 //打x的表示已完成
+- [ ] 打豆豆
+
<label><input type="checkbox">吃饭</label>
+<label><input type="checkbox" checked>睡觉</label>
+<label><input type="checkbox">打豆豆</label>
+
可以无限引用,但是没什么太大的意义
> 前言:第一层引用
+>> 前言:第二层引用
+
前言:第一层引用
前言:第二层引用
[百度](https://www.baidu.com)
+
<https://www.baidu.com>
+
https://www.baidu.com (opens new window)
![alt 属性文本](图片链接)
+![占位符](https://iph.href.lu/200x200) //在线图片
+![占位符](/achievements-icon.png "测试") //本地图片
+
Markdown 使用 | 来分隔不同单元格,使用 - 来分隔表头和主体
|标题1|标题2|标题2
+| :- | :-: | -:
+|行1列1|行1列2|行1列3
+|行2列1|行2列2|行2列3
+
标题 1 | 标题 2 | 标题 2 |
---|---|---|
行 1 列 1 | 行 1 列 2 | 行 1 列 3 |
行 2 列 1 | 行 2 列 2 | 行 2 列 3 |
(```)
+console.log(1)
+(```)
+
这里在几个平台测试了一下,发现有的能显示,有的不能显示,所以这里在 ```外面包裹一层()只用于展示使用 效果如下:
console.log(1)
+
或者直接使用 4 个空格,也是可以展示一个代码片段
console.log(1)
+
Markdown 还支持流程图或甘特图之类的.但是因为不同的平台对其支持都是不同的,有的支持,有的不支持,因此就不展示流程图的代码了。
Markdown 的目标是成为一种适用于网络的书写语言.它的标签较少,只对应了 HTML 中的一部分.不在其覆盖范围内的,我们可以使用 html 标签来书写.(貌似也有的平台是不支持的)
<span style="color:orange;">orange</span>
+
效果如下:
orange
使用kbd
来展示按键:
<kbd>Ctrl</kbd>+<kbd>S</kbd>
+
Ctrl+S来保存文档
使用反斜杠转义特殊字符:
\*\* 正常显示 \*\*
+
** 正常显示 **
使用[^]
来定义注解:(vuepress中无效)
我是一个注解[^注解1]
+我也是一个注解[^注解2]
+去[百度][1]
+
+[^注解1]:我是注解1
+
+[^注解2]:我是注解2
+[1]: http://www.baidu.com
+
效果如下:
我是一个注解[^注解1]
+我也是一个注解[^注解2]
+去Google (opens new window)
[^注解1]: 我是注解 1 +[^注解2]: 我是注解 2
<title>页面标签</title>
+
<title/>
标签可以说是最主要、最重要的SEOn优化的元素。它将直接显示在搜索引擎的接口页里面,社交媒体分享,浏览器的标签页都将直接使用这个标题。
标题对于让用户快速了解搜索结果的内容至关重要。这是用户对你网页的第一印象,它通常是决定点击哪个结果的重要影响因素,因此在你的网页上使用高质量的标题很重要。
最佳实践:
该meta标签用于描述总结页面的内容。搜索引擎经常将其用于搜索结果的片段中,放在标题下面。它占据了搜索结果片段的很大部分的内容,内容的好坏很大程度上会影响用户是否点击这个结果,你需要设置一个引人注目的描述。谷歌不使用描述作为排名英语。
最佳实践:
<h1>h1</h1>
+<h1>h2</h1>
+<h1>h3</h1>
+<h1>h4</h1>
+<h1>h5</h1>
+<h1>h6</h1>
+
标题标签是HTML标签,HTML<h1>-<h6>
标题(Heading)元素呈现了六个不同的级别的标题,<h1>
级别最高,而<h6>
级别最低。它可以用来识别内容的结果层级。
现在标题标签不再是搜索引擎排名因素,标题标签中添加关键字,不一定可以使你的排名更好,但是对爬虫来说理解组织良好的内容更为容易。同时也方便用户理解页面内容。
最佳实践:
<img src="https://popeke.com/images/popeke@750w_20h.png" alt="popeke" />
+
图片的alt属性被添加到<img/>
标签以描述其内容。
Alt属性在页面优化方面很重要,原因有两个:
最佳实践:
机器人元标记告诉搜索引擎索引或不索引你的网页。
该标签对搜索引擎爬虫有四个主要值:
<meta name="robots" content="noindex, nofollow"> 表示不索引或不关注本网页。
+
+<meta name="robots" content="index, follow"> 表示索引并关注这个网页。
+
最佳实践:
<link rel="canonical" href="https://popeke.com/" />
+
rel=“canonical” link 标签为类似网页或重复网页指定权威网页。 它告诉搜索引擎哪个版本的页面是主要页面并希望被搜索引擎索引。
现如今有两个主要标准定义如何格式化此元数据:Twitter Cards 和 Facebook Open Graph 协议。
Open Graph (开放图谱协议)由 Facebook 的创建,简称 OG 协议或 OGP。它是 Facebook 在 2010 年 F8 开发者大会公布的一种网页元信息(Meta Information)标记协议,属于 Meta Tag (Meta 标签)的范畴,是一种为社交分享而生的 Meta 标签。它允许在 Facebook 上,其他网站能像 Facebook 内容一样具有丰富的媒体对象,进而促进 Facebook 和其他网站之间的集成。
Open Graph 标签不仅被 Facebook 使用,也被 LinkedIn 和 Twitter 等平台使用。请注意 Twitter 也有自己的一套 Twitter Cards 定义,如果网页提供了 Twitter 自己的定义,则优先使用自己的标准。
# 标题
+<meta property="og:title" content="HTML meta 标签详解">
+# 类型
+<meta property="og:type" content="article">
+# 网页的永久链接
+<meta property="og:url" content="https://popeke.com/">
+# 网页的需要展示图片
+<meta property="og:image" content="https://popeke.com/images/popeke@750w_20h.png">
+
对于 Open Graph 来说,每个页面里需要以上四个必需属性:
Open Graph 协议定义了一些网页类型, 包括 article、book、website、profile 等。
如果你的网页为个人主页,og:type
可以为 profile
;如果你的网页是一个视频为主的网页面,则可以为video
;如果你的网页是一篇博客文章则可以为article
。如果你的网页没什么特殊的类别,则可将og:type
设置为通用的website
。
除了以上四个必需属性之外,Open Graph 还有以下常用属性:
<meta property="og:description" content="Description Here">
+<meta property="og:site_name" content="Site Name">
+<meta property="og:locale" content="zh_CN">
+<meta property="og:video" content="link">
+<meta property="og:audio" content="link">
+
除了以上介绍的常用内容外,Open Graph 还有很多属性。有关 Open Graph 协议的更多信息,请访问官方 Open Graph 协议网站 https://ogp.me/ (opens new window)
如果你的网站设置了 Open Graph 你可以使用 facebook 的 debug 工具 (opens new window) 来调试及查看显示效果
大多数社交网站都遵循 Open Graph 协议,但是也有一些平台自己实现了一些定义,例如 Twitter。
Twitter Cards 是 Twitter 使用的元数据规范,用于在 Twitter 上共享链接时显示富文本、图像和视频。
Twitter Cards 有四种不同的类型:Summary, Summary Card With Large Image, App, 和 Player.
<meta name=twitter:card content="summary_large_image">
+<meta name=twitter:image content="https://popeke.com/images/popeke@750w_20h.png">
+<meta name=twitter:title content="HTML meta 标签详解">
+<meta name=twitter:description content="meta 标签标签定义了关于 HTML 文档的元数据">
+<meta name="twitter:creator" content="@yedaniu">
+<meta name="twitter:site" content="@site_account">
+
上面介绍了 Twitter Cards 一些常用属性,有关其他 Twitter Card 标签和选项的更多信息,请访问『官方 Twitter Cards 文档』( https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/abouts-cards) (opens new window)。
如果你的网站设置了 Open Graph 你可以使用 Twitter Cards (opens new window) 验证器 来调试及查看显示效果
Facebook 和 Twitter 提供了有关如何使用上述元标记的指南。在下表中,我们总结了有关它们的基本信息以及各自的建议。
元标签 | ||
---|---|---|
标题 | og:title 文章的标题,例如你的网站名称。 | twitter:title Twitter 卡片的必需参数,最大长度 70 个字符。 |
图片 | og:image 当有人将内容分享到 Facebook 时显示的图像的 URL | twitter:image 表示页面内容的唯一图像的 URL |
网站 | og:url 你页面的规范 URL | twitter:url 你页面的规范 URL |
描述 | og:description 对内容的简要描述,通常在 2 到 4 个句子之间。这将显示在 Facebook 帖子的标题下方 | Twitter:description 简明概括内容的描述,适合在推文中呈现。你不应重复使用标题作为描述或使用此字段来描述网站提供的一般服务 |
视口标记允许你配置页面在任何设备上的缩放和显示方式。
<meta name="viewport" content="width=device-width, initial-scale=1"/>
+
视口元标记与排名没有直接关系,但与用户体验有关。
同时谷歌在移动搜索结果中对移动端友好的网页的排名更高
最佳实践:
首屏时间(First Contentful Paint),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一样要全部渲染完成,但需要展示当前视窗需要的内容
首屏加载可以说是用户体验中重要的环节。
在页面渲染的过程,导致加载速度慢的因素可能如下:
常见的几种 SPA 首屏优化方式
后端返回的资源问题:
HTTP
缓存,设置Cache-Control
,Last-Modified
,Etag
等响应头Service Worker
离线缓存
+前端合理使用 LocalStorage图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素
+对于所有的图片资源,我们可以进行适当的压缩
+对于页面上使用到的 icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到一张图上,减少HTTP
请求次数
拆完包之后,我们在使用Gzip
做一下压缩,安装compression-webpack-plugin
yarn add compression-webpack-plugin -D
+
在vue.config.js
中引入并修改webpack
配置
const CompressionWebpackPlugin = require("compression-webpack-plugin");
+module.exports={
+ ...,//省略
+ configureWebpack:{
+ plugins:[
+ new CompressionWebpackPlugin({
+ test:/\.(js|css)$/,//匹配文件名
+ threshold:10240,//对超过10k的数据进行压缩
+ minRatio: 0.8, // 压缩比
+ deleteOriginalAssets: false //是否删除原文件
+ exclude: "/node_modules/",
+ })
+ ]
+ },
+ ...
+}
+
在服务器我们也要做相应的配置,比如Nginx
gzip on;
+gzip_disable "msie6";
+gzip_vary on;
+gzip_proxied any;
+gzip_comp_level 6;
+gzip_buffers 16 8k;
+gzip_http_version 1.1;
+gzip_min_length 256;
+gzip_types application/atom+xml application/geo+json application/javascript application/x-javascript application/json application/ld+json application/manifest+json application/rdf+xml application/rss+xml application/xhtml+xml application/xml font/eot font/otf font/ttf image/svg+xml text/css text/javascript text/plain text/xml;
+
SSR(Server side),也就是服务器渲染,组件或页面通过服务器生成 html 字符串,再发送到浏览器
+从头搭建一个服务器渲染是很复杂的,Vue
应用建议使用Nuxt.js
实现服务器渲染
减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分,资源加载优化和页面渲染优化
下图是更为全面的首屏优化的方案
大家可以根据自己项目的情况选择各种方式进行首屏渲染的优化
三次握手(Three-way Handshake)其实就是指建立一个 TCP 连接时,需要客户端和服务器总共发送 3 个包
主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备
过程如下:
上述每一次握手的作用如下:
第一次握手:客户端发送网络包,服务端收到了
+这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
第二次握手:服务端发包,客户端收到了
+这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常
第三次握手:客户端发包,服务端收到了。
+这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常
通过三次握手,就能确定双方的接收和发送能力是正常的。之后就可以正常通信了
如果是两次握手,发送端可以确定自己发送的信息能对方能收到,也能确定对方发的包自己能收到,但接收端只能确定对方发的包自己能收到 无法确定自己发的包对方能收到
并且两次握手的话, 客户端有可能因为网络阻塞等原因会发送多个请求报文,延时到达的请求又会与服务器建立连接,浪费掉许多服务器的资源
tcp
终止一个连接,需要经过四次挥手
过程如下:
四次挥手原因
服务端在收到客户端断开连接 Fin 报文后,并不会立即关闭连接,而是先发送一个 ACK 包先告诉客户端收到关闭连接的请求,只有当服务器的所有报文发送完毕之后,才发送 FIN 报文断开连接,因此需要四次挥手
一个完整的三次握手四次挥手如下图所示:
B 站图片接口 +https://api.bilibili.com/x/player/videoshot?aid=14326240
{
+ "code": 0,
+ "message": "0",
+ "ttl": 1,
+ "data": {
+ "pvdata": "//i0.hdslb.com/bfs/videoshot/23378913.bin",
+ "img_x_len": 10,
+ "img_y_len": 10,
+ "img_x_size": 160,
+ "img_y_size": 90,
+ "image": [
+ "//i0.hdslb.com/bfs/videoshot/23378913.jpg",
+ "//i0.hdslb.com/bfs/videoshot/23378913-1.jpg",
+ "//i0.hdslb.com/bfs/videoshot/23378913-2.jpg"
+ ],
+ "index": []
+ }
+}
+
字段 | 含义 | 详细 | 举例 |
---|---|---|---|
data | 缩略图 | ||
data.pvdata | 一个二进制文件 | 缩略图,并不是每一秒都对应一张缩略图,而是每一段对应一张,这个字段应该是时间段与缩略图的对应方式 | //i0.hdslb.com/bfs/videoshot/89359010.bin |
data.img_x_len | 每张雪碧图 x 方向图片数量 | 雪碧图是一个 10*10 的网格 | 10 |
data.img_y_len | 每张雪碧图 y 方向图片数量 | 雪碧图是一个 10*10 的网格 | 10 |
data.img_x_size | 单个缩略图图片的尺寸 | x 轴 | 160 |
data.img_y_size | 单个缩略图图片的尺寸 | y 轴 | 90 |
data.image | 雪碧图形式的所有缩略图 | 是一个数组,一次拿到所有的缩略图 | ["//i0.hdslb.com/bfs/videoshot/89359010.jpg"] |
<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title></title>
+ <link
+ rel="stylesheet"
+ href="./resources/font-awesome-4.7.0/css/font-awesome.min.css"
+ />
+ <link rel="stylesheet" type="text/css" href="./select/select.css" />
+ <style type="text/css">
+ #container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ margin: auto;
+ width: 170px;
+ height: 95px;
+ background-image: url(20210223214102298jpg); //背景图图片
+ background-size: 1700px;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="container"></div>
+ <script>
+ const container = document.querySelector("#container");
+ window.onload = function() {
+ // 鼠标移动到指定box上时,触发事件
+ //获取指定div元素
+ container.onmousemove = function(event) {
+ console.log("事件触发!");
+ // 获取盒子的位置
+ let boxX = container.offsetLeft;
+ let boxY = container.offsetTop;
+ // 获取鼠标的坐标
+ let pageX = event.pageX;
+ let pageY = event.pageY;
+ //计算鼠标在盒子中的坐标
+ let inBoxX = pageX - boxX;
+ let inBoxY = pageY - boxY;
+ // console.log("盒子中的坐标:x-->"+inBoxX+"--y-->"+inBoxY);
+ //计算X轴,并移动图片
+ //每移动5个值就改变一次图片
+ if (inBoxX % 5) {
+ //计算显示第几个图片
+ let imageIndex = parseInt(inBoxX / 5);
+ console.log("当前显示第几张图片:" + imageIndex);
+ //计算显示图片在大图中的位置
+ //计算显示图片的X轴(计算的是大图中的X轴)
+ let imageX = 170 * parseInt(imageIndex % 10);
+ //计算显示图片的Y轴(计算的是大图中的Y轴)
+ let imageY = 95 * parseInt(imageIndex / 10);
+ console.log("imageX:" + imageX);
+ console.log("imageY:" + imageY);
+ //修改背景图片
+ container.style.backgroundPosition =
+ "-" + imageX + "px" + " -" + imageY + "px";
+ }
+ };
+ };
+ </script>
+ </body>
+</html>
+
简单的分析,从输入 URL
到回车后发生的行为如下:
首先判断你输入的是一个合法的 URL
还是一个待搜索的关键词,并且根据你输入的内容进行对应操作
URL
的解析第过程中的第一步,一个URL
的结构解析如下:
在之前文章中讲过 DNS 的查询,这里就不再讲述了
整个查询过程如下图所示:
最终,获取到了域名对应的目标服务器IP
地址
在之前文章中,了解到tcp
是一种面向有连接的传输层协议
在确定目标服务器服务器的IP
地址后,则经历三次握手建立TCP
连接,流程如下:
当建立 tcp 连接之后,就可以在这基础上进行通信,浏览器发送 http 请求到目标服务器
请求的内容包括:
当服务器接收到浏览器的请求之后,就会进行逻辑操作,处理完成之后返回一个HTTP
响应消息,包括:
在服务器响应之后,由于现在http
默认开始长连接keep-alive
,当页面关闭之后,tcp
链接则会经过四次挥手完成断开
当浏览器接收到服务器响应的资源后,首先会对资源进行解析:
关于页面的渲染过程如下:
<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+ <title>auto complete</title>
+ <style>
+ bdi {
+ color: rgb(0, 136, 255);
+ }
+
+ li {
+ list-style: none;
+ }
+ </style>
+ </head>
+ <body>
+ <input class="inp" type="text" />
+ <section>
+ <ul class="container"></ul>
+ </section>
+ </body>
+ <script>
+ function debounce(fn, timeout = 300) {
+ let t;
+ return (...args) => {
+ if (t) {
+ clearTimeout(t);
+ }
+ t = setTimeout(() => {
+ fn.apply(fn, args);
+ }, timeout);
+ };
+ }
+
+ function memorize(fn) {
+ const cache = new Map();
+ return (name) => {
+ if (!name) {
+ container.innerHTML = "";
+ return;
+ }
+ if (cache.get(name)) {
+ container.innerHTML = cache.get(name);
+ return;
+ }
+ const res = fn.call(fn, name).join("");
+ cache.set(name, res);
+ container.innerHTML = res;
+ };
+ }
+
+ function handleInput(value) {
+ const reg = new RegExp(`\(${value}\)`);
+ const search = data.reduce((res, cur) => {
+ if (reg.test(cur)) {
+ const match = RegExp.$1;
+ res.push(`<li>${cur.replace(match, "<bdi>$&</bdi>")}</li>`);
+ }
+ return res;
+ }, []);
+ return search;
+ }
+
+ const data = [
+ "上海野生动物园",
+ "上饶野生动物园",
+ "北京巷子",
+ "上海中心",
+ "上海黄埔江",
+ "迪士尼上海",
+ "陆家嘴上海中心",
+ ];
+ const container = document.querySelector(".container");
+ const memorizeInput = memorize(handleInput);
+ document.querySelector(".inp").addEventListener(
+ "input",
+ debounce((e) => {
+ memorizeInput(e.target.value);
+ })
+ );
+ </script>
+</html>
+
SPA(single-page application)仅在 Web 页面初始化时加载相应的 HTML、CSS、JavaScript。一旦页面加载完成,SPA 不会因为用户的操作二进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户交互,避免页面的重新加载。
优点:
缺点:
v-if
是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做————知道条件第一次变为真时,才会开始渲染条件块。
v-show
就简单得多————不管初始条件是什么,元素都会被渲染,并且只是简单的基于 CSS 的"display:none"属性进行切换。
所以,v-if
适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show
则适用于需要非常频繁切换条件的场景。
Class 可以通过对象语法和数组语法进行动态绑定:
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>
+
+data: { isActive: true, hasError: false }
+
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
+
+data: { activeClass: 'active', errorClass: 'text-danger' }
+
Style 也可以通过对象语法和数组语法进行动态绑定:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
+
+data: { activeColor: 'red', fontSize: 30 }
+
<div v-bind:style="[styleColor, styleSize]"></div>
+
+data: { styleColor: { color: 'red' }, styleSize:{ fontSize:'23px' } }
+
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来不行。这样会防止从子组件意外改变父子组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生更新时,子组件中所有的 prop 都会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果这样子做,Vue 会在浏览器的控制台发出警告。子组件想修改时,只能通过$emit 派发一个自定义事件,父组件接收到后,由父组件修改。
有两种常见的试图改变一个 prop 的情形 :
props: ['initialCounter'],
+data: function () {
+ return {
+ counter: this.initialCounter
+ }
+}
+
props: ['size'],
+computed: {
+ normalizedSize: function () {
+ return this.size.trim().toLowerCase()
+ }
+}
+
computed:是计算属性,以来其它属性值,并且 computed 的值有缓存,只有它以来的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch:更多的是「观察」的作用,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续的操作;
运用场景:
由于 JavaScript 的限制,Vue 不能监测到以下数组的变动:
vm.items[indexOfItem]=newValue
vm.items.length = newLength
为了解决第一个问题,Vue 提供了以下操作方法:
//Vue.set
+Vue.set(vm.items, indexOfItem, newValue);
+//vm.$set
+vm.$set(vm.items, indexOfItem, newValue);
+//Array.prototype.splice
+vm.items.splice(indexOfItem, 1, newValue);
+
为了解决第二个问题,Vue 提供了以下操作方法:
//Array.prototype.splice
+vm.items.splice(newLength);
+
Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模板、挂载 DOM->渲染、更新->渲染、卸载等一些列过程,我们称这是 Vue 的生命周期。
生命周期 | 描述 |
---|---|
beforeCreate | 组件实例被创建之初,组件的属性生效之前 |
created | 组件实例已经完全创建,属性也绑定,但真实 DOM 还没有生成,$el 还不可用 |
beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用 |
mounted | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子 |
beforeUpdate | 组件数据更新之前被调用,发生在虚拟 DOM 打补丁之前 |
update | 组件数据更新之后 |
activited | keep-alive 专属,组件被激活时调用 |
deactivated | keep-alive 专属,组件被销毁时调用 |
beforeDestory | 组件销毁前调用 |
destoryed | 组件销毁后调用 |
Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
父 beforeUpdate -> 父 updated
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。但是本人推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
在钩子函数 mounted 被调用前,Vue 已经将编译好的模板挂载到页面上,所以在 mounted 中可以访问操作 DOM。
比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:
// Parent.vue
+<Child @mounted="doSomething"/>
+
+// Child.vue
+mounted() {
+ this.$emit("mounted");
+}
+
以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:
// Parent.vue
+<Child @hook:mounted="doSomething" ></Child>
+
+doSomething() {
+ console.log('父组件监听到 mounted 钩子函数 ...');
+},
+
+// Child.vue
+mounted(){
+ console.log('子组件触发 mounted 钩子函数 ...');
+},
+
+// 以上输出顺序为:
+// 子组件触发 mounted 钩子函数 ...
+// 父组件监听到 mounted 钩子函数 ...
+
当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听。
keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:
为什么组件中的 data 必须是一个函数,然后 return 一个对象,而 new Vue 实例里,data 可以直接是一个对象?
// data
+data() {
+ return {
+ message: "子组件",
+ childName:this.name
+ }
+}
+
+// new Vue
+new Vue({
+ el: '#app',
+ router,
+ template: '<App/>',
+ components: {App}
+})
+
因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。
我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
以 input 表单元素为例:
<input v-model='something'>
+
+相当于
+
+<input v-bind:value="something" v-on:input="something = $event.target.value">
+
如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:
父组件:
+<ModelChild v-model="message"></ModelChild>
+
+子组件:
+<div>{{value}}</div>
+
+props:{
+ value: String
+},
+methods: {
+ test1(){
+ this.$emit('input', '小红')
+ },
+},
+
Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。
props
/ $emit
适用 父子组件通信ref
与 $parent
/ $children
适用 父子组件通信ref
:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例$parent
/ $children
:访问父 / 子实例EventBus
($emit
/ $on
) 适用于 父子、隔代、兄弟组件通信这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。
$attrs
/$listeners
适用于 隔代组件通信$attrs
:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。$listeners
:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件provide
/ inject
适用于 隔代组件通信祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
Vuex
适用于 父子、隔代、兄弟组件通信Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
(1)Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
(2)改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
即:SSR 大致的意思就是 vue 在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的 html 片段直接返回给客户端这个过程就叫做服务端渲染。
服务端渲染 SSR 的优缺点如下:
(1)服务端渲染的优点:
(2) 服务端渲染的缺点:
vue-router 有 3 种路由模式:hash、history、abstract,对应的源码如下所示:
switch (mode) {
+ case "history":
+ this.history = new HTML5History(this, options.base);
+ break;
+ case "hash":
+ this.history = new HashHistory(this, options.base, this.fallback);
+ break;
+ case "abstract":
+ this.history = new AbstractHistory(this, options.base);
+ break;
+ default:
+ if (process.env.NODE_ENV !== "production") {
+ assert(false, `invalid mode: ${mode}`);
+ }
+}
+
其中,3 种路由模式的说明如下:
早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search':
https://www.word.com#search
+
hash 路由模式的实现主要是基于下面几个特性:
HTML5 提供了 History API 来实现 URL 的变化。其中做最主要的 API 有以下两个:history.pushState() 和 history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:
window.history.pushState(null, null, path);
+window.history.replaceState(null, null, path);
+
history 路由模式的实现主要基于存在下面几个特性:
Model–View–ViewModel (MVVM)是一种软件架构设计模式,有微软 WPF 和 Silverlight 的架构师 Ken Cooper 和 Ted Peters 开发,是一种简化用户界面的事件驱动编程方式。由 John Gossman(同样也是 WPF 和 Silverlight 的架构师)于 2005 年在他的博客发表
MVVM 源自于经典的 Model-View-Controller(MVC)模式,MVVM 的出现促进了前端开发与后端业务逻辑的分离,极大的提高了前端开发效率,MVVM 的核心是 ViewModel 层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向绑定,向下与 Model 层通过接口请求进行数据交互,启呈上启下作用,如下:
View 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建。
Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。
ViewModel 是由前端开发人员组织生成和维护的视图数据层。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。需要注意的是 ViewModel 所封装出来的数据模型包括视图的状态和行为两部分,而 Model 层的数据模型是只包含状态的,比如页面的这一块展示什么,而页面加载进来时发生什么,点击这一块发生什么,这一块滚动时发生什么这些都属于视图行为(交互),视图状态和行为都封装在了 ViewModel 里。这样的封装使得 ViewModel 可以完整地去描述 View 层。
MVVM 框架实现了双向绑定,这样 ViewModel 的内容会实时展现在 View 层,前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维护 ViewModel,更新数据视图就会自动得到相应更新。这样 View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的重要一环。
我们以下通过一个 Vue 实例来说明 MVVM 的具体实现,有 Vue 开发经验的同学应该一目了然:
(1)View 层
<div id="app">
+ <p>{{message}}</p>
+ <button v-on:click="showMessage()">Click me</button>
+</div>
+
(2)ViewModel 层
var app = new Vue({
+ el: "#app",
+ data: {
+ // 用于描述视图状态
+ message: "Hello Vue!",
+ },
+ methods: {
+ // 用于描述视图行为
+ showMessage() {
+ let vm = this;
+ alert(vm.message);
+ },
+ },
+ created() {
+ let vm = this;
+ // Ajax 获取 Model 层的数据
+ ajax({
+ url: "/your/server/data/api",
+ success(res) {
+ vm.message = res;
+ },
+ });
+ },
+});
+
(3) Model 层
{
+ "url": "/your/server/data/api",
+ "res": {
+ "success": true,
+ "name": "IoveC",
+ "domain": "www.cnblogs.com"
+ }
+}
+
Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:
即:
其中,View 变化更新 Data ,可以通过事件监听的方式来实现,所以 Vue 的数据双向绑定的工作主要是如何根据 Data 变化更新 View。
Vue 主要通过以下 4 个步骤来实现数据双向绑定的:
实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。
如果被问到 Vue 怎么实现数据双向绑定,大家肯定都会回答 通过 Object.defineProperty() 对数据进行劫持,但是 Object.defineProperty() 只能对属性进行数据劫持,不能对整个对象进行劫持,同理无法对数组进行劫持,但是我们在使用 Vue 框架中都知道,Vue 能检测到对象和数组(部分方法的操作)的变化,那它是怎么实现的呢?我们查看相关代码如下:
/**
+ * Observe a list of Array items.
+ */
+ observeArray (items: Array<any>) {
+ for (let i = 0, l = items.length; i < l; i++) {
+ observe(items[i]) // observe 功能为监测数据的变化
+ }
+ }
+
+ /**
+ * 对属性进行递归遍历
+ */
+ let childOb = !shallow && observe(val) // observe 功能为监测数据的变化
+
通过以上 Vue 源码部分查看,我们就能知道 Vue 框架是通过遍历数组 和递归遍历对象,从而达到利用 Object.defineProperty() 也能对对象和数组(部分方法的操作)进行监听。
Proxy 的优势如下:
Object.defineProperty 的优势如下:
受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value) 来实现为对象添加响应式属性,那框架本身是如何实现的呢?
我们查看对应的 Vue 源码:vue/src/core/instance/index.js
export function set(target: Array<any> | Object, key: any, val: any): any {
+ // target 为数组
+ if (Array.isArray(target) && isValidArrayIndex(key)) {
+ // 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
+ target.length = Math.max(target.length, key);
+ // 利用数组的splice变异方法触发响应式
+ target.splice(key, 1, val);
+ return val;
+ }
+ // key 已经存在,直接修改属性值
+ if (key in target && !(key in Object.prototype)) {
+ target[key] = val;
+ return val;
+ }
+ const ob = (target: any).__ob__;
+ // target 本身就不是响应式数据, 直接赋值
+ if (!ob) {
+ target[key] = val;
+ return val;
+ }
+ // 对属性进行响应式处理
+ defineReactive(ob.value, key, val);
+ ob.dep.notify();
+ return val;
+}
+
我们阅读以上源码可知,vm.$set 的实现原理是:
优点:
缺点:
虚拟 DOM 的实现原理主要包括以下 3 部分:
key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速。Vue 的 diff 过程可以概括为:oldCh 和 newCh 各有两个头尾的变量 oldStartIndex、oldEndIndex 和 newStartIndex、newEndIndex,它们会新节点和旧节点会进行两两对比,即一共有 4 种比较方式:newStartIndex 和 oldStartIndex 、newEndIndex 和 oldEndIndex 、newStartIndex 和 oldEndIndex 、newEndIndex 和 oldStartIndex,如果以上 4 种比较都没匹配,如果设置了 key,就会用 key 再进行比较,在比较的过程中,遍历会往中间靠,一旦 StartIdx > EndIdx 表明 oldCh 和 newCh 至少有一个已经遍历完了,就会结束比较。具体有无 key 的 diff 过程,可以查看作者写的另一篇详解虚拟 DOM 的文章《深入剖析:Vue 核心之虚拟 DOM》
所以 Vue 中 key 的作用是:key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速
更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快,源码如下:
function createKeyToOldIdx(children, beginIdx, endIdx) {
+ let i, key;
+ const map = {};
+ for (i = beginIdx; i <= endIdx; ++i) {
+ key = children[i].key;
+ if (isDef(key)) map[key] = i;
+ }
+ return map;
+}
+
Vue 3.0 正走在发布的路上,Vue 3.0 的目标是让 Vue 核心变得更小、更快、更强大,因此 Vue 3.0 增加以下这些新特性:
3.0 将带来基于代理 Proxy 的 observer 实现,提供全语言覆盖的反应性跟踪。这消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:
新的 observer 还提供了以下特性:
模板方面没有大的变更,只改了作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。
同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。
vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易。
此外,vue 的源码也改用了 TypeScript 来写。其实当代码的功能复杂之后,必须有一个静态类型系统来做一些辅助管理。现在 vue3.0 也全面改用 TypeScript 来重写了,更是使得对外暴露的 api 更容易结合 TypeScript。静态类型系统对于复杂代码的维护确实很有必要。
vue3.0 的改变是全面的,上面只涉及到主要的 3 个方面,还有一些其他的更改:
核心思路:
1、Promise.all 返回的肯定是一个 Promise 对象,所以可以直接写一个 return new Promise((resolve,reject)=>{})(这应该是一个惯性思维) +2、遍历传入的参数,用 Promise.resolve()将参数“包一层”,使其变成个 Promise 对象 +3、关键点是何时“决议”,也就是合适 resolve 出来,在这里做了计数器(count),每个内部 Promise 对象决议后就将计数器加一,并判断加一后的大小是否与传入对象的数量相等,如果相等则调用 resolve,如果任何一个 promise 对象失败,则调用 reject()方法。 +4、官网规定 Promise.all()接受的参数是一个可遍历的参数,所以未必一定是一个数组,所以用 Array.from()转化一下 +5、使用 for...of 进行遍历,因为凡事可遍历的变量应该都是部署了 iterator 方法,所以用 for...of 遍历最安全
Promise.all = function (iterater) {
+ let count = 0;
+ let len = iterater.length;
+ let res = [];
+ return new Promise((resolve, reject) => {
+ for (item of iterater) {
+ Promise.resolve(iterater[item])
+ .then((data) => {
+ res[item] = data;
+ if (++count === len) {
+ resolve(res);
+ }
+ })
+ .catch((err) => reject(err));
+ }
+ });
+};
+const promise1 = Promise.resolve(3);
+const promise2 = new Promise((resolve, reject) => {
+ setTimeout(resolve, 100, "foo");
+});
+const promise3 = 42;
+Promise.all([promise1, promise2, promise3]).then((values) =>
+ console.log(values)
+);
+
核心思路: +谁先决议那么就返回谁,所以将 all 的计数器和逻辑判断全部去除掉就可以了。
romise.race = function (iterater) {
+ return new Promise((resolve, reject) => {
+ for (item of iterater) {
+ Promise.resolve(iterater[item])
+ .then((data) => {
+ resolve(res);
+ })
+ .catch((err) => reject(err));
+ }
+ });
+};
+const promise1 = Promise.resolve(3);
+const promise2 = new Promise((resolve, reject) => {
+ setTimeout(resolve, 100, "foo");
+});
+const promise3 = 42;
+Promise.race([promise1, promise2, promise3]).then((values) =>
+ console.log(values)
+);
+
#你说什么
nextTick
支持两种形式使用方式:
Promise
,还支持 Promise.then
的形式。let pending = false,
+timeFunc,
+callbacks=[];
+
+//cb:执行的回调函数,context:执行上下文参数
+function nextTick(cb,context){
+ let _resolve=null;
+ callbacks.push(()=>{
+ if(cb){
+ try{
+ cb.call(context)
+ }catch(e){
+ handleError(e,ctx,'nextTick)
+ }
+ }else if(_resolve){
+ _resolve(context)
+ }
+ })
+ if(!pending){
+ pending=true;
+ timeFunc()
+ }
+ if(!cb&&typeof Promise !== "undefind"){
+ rteurn new Promise(resolve=>_resolve=resolve)
+ }
+}
+
+let isUsingMicroTask = false;
+if (typeof Promise !== 'undefined' && isNative(Promise)) {
+ //判断1:是否原生支持Promise
+ const p = Promise.resolve()
+ timerFunc = () => {
+ p.then(flushCallbacks)
+ if (isIOS) setTimeout(noop)
+ }
+ isUsingMicroTask = true
+} else if (!isIE && typeof MutationObserver !== 'undefined' && (
+ isNative(MutationObserver) ||
+ MutationObserver.toString() === '[object MutationObserverConstructor]'
+)) {
+ //判断2:是否原生支持MutationObserver
+ let counter = 1
+ const observer = new MutationObserver(flushCallbacks)
+ const textNode = document.createTextNode(String(counter))
+ observer.observe(textNode, {
+ characterData: true
+ })
+ timerFunc = () => {
+ counter = (counter + 1) % 2
+ textNode.data = String(counter)
+ }
+ isUsingMicroTask = true
+} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
+ //判断3:是否原生支持setImmediate
+ timerFunc = () => {
+ setImmediate(flushCallbacks)
+ }
+} else {
+ //判断4:上面都不行,直接用setTimeout
+ timerFunc = () => {
+ setTimeout(flushCallbacks, 0)
+ }
+}
+
+function flushCallbacks () {
+ pending = false
+ const copies = callbacks.slice(0)
+ callbacks.length = 0
+ for (let i = 0; i < copies.length; i++) {
+ copies[i]()
+ }
+}
+
+nextTick(()=>{
+ console.log("nextTick callback")
+})
+nextTick().then(()=>{
+ console.log("nextTick Promise")
+})
+