胖蔡叨叨叨
你听我说

如何使用JQuery进行网站开发?

胖蔡阅读(97)

JQuery是较早一代的前端开发的利器, 极大地简化了 JavaScript 编程。就目前而言也是大多数网站使用的工具,就轻量型网站开发选择JQuery可以帮助我们快速实现网站的开发工作。本篇文章主要介绍如何使用JQuery进行简单的网站开发。

如何引入JQuery?

jquery就是通过JS的方式来操作界面的DOM,极大的简化我们操作DOM的复杂度。所以,应用方式也是一样的,代码示例如下:

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>

不同的是,我们可以根据jquery的来源分为两种类型:

1、本地jquery引入

这种方式就是将jquery的文件放在本地(或者说服务器地址),我们通过地址访问jquery,这种方式的引用优势是三方库可控性大大加强,jquery由我们自己保管维护。

<script src="//domain/jquery/3.4.1/jquery.min.js"></script>

2、CDN引入

CDN可以将一个地方的资源文件分布在不同的网络位置,极大的加快我们的访问速度,节省加载时间,这里介绍几个常用的Jquery免费CDN地址。

  • jQuery官网

jquery官方给我们提供了各个版本的jquery CDN地址,但由于外网环境,我们这边可能还是下载比较慢,可以考虑做英文站,海外服务器的时候使用。参考地址可以访问:https://releases.jquery.com/。使用示例如下:

<script src="http://code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
  • 微软CDN
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.0.min.js"></script>
  • CDNjs

CDNjs是用于专门提供前端CDN服务使用,但同样也是国外的CDN服务,大家慎重选择。

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==" crossorigin="anonymous"></script>

除了jquery,cdnjs中也可以引用其他公用库的CDN服务。

  • jsDeliver

jsDeliver是目前用的比较多的一个CDN服务,虽然服务商是国外的,但是并不太影响国内的使用(目前看来),和上方的CDNjs一样的,它本身也是一个CDN服务提供商,我们也可以使用它来代理请求我们其他需要使用的前端库,大家可以访问地址:https://www.jsdelivr.com/,搜索需要的npm库,jquery使用如下:

<script src="https://cdn.jsdelivr.net/npm/jquery@3.6/dist/jquery.min.js"></script>
  • 新浪CDN

新浪CDN可以通过http://lib.sinaapp.com/?path=/jquery 来查找你需要的具体版本Jquery,新浪提供了jquery各个版本的压缩版和未压缩版,可以自己选择使用,使用代码如下:

<script src="http://lib.sinaapp.com/js/jquery/1.10/jquery-1.10.0.min.js"></script>

另注:现有网上说的百度 Jquery CDN已经不可使用了,所以这里不列出来了。

3、版本选择

这里采用网上已有的一个方案进行选择,分属电脑端和手机端选择不同的jquery版本。选择建议如下:

  • 电脑端:为了兼容IE8,建议使用jquery 1.9版本,因为1.9之后jquery就宣布不再兼容IE浏览器了,若无IE兼容要求,可以直接使用jquery最新版本。
<script src="http://lib.sinaapp.com/js/jquery/1.9.1/jquery-1.9.1.min.js"></script>
  • 手机端:建议使用jquery最新版本,新版本的jquery对于性能和使用方式上都有很大的改善。

如何使用JQuery?

jquery引入和会在window下挂载一个$和JQuery对象,我们通过如下几个方面了解JQuery使用:

1、进入加载

// 等同于onload函数
$(function () {   
    ...  // 此处是页面 DOM 加载完成的入口
}) ; 
 

// 等同于ready函数
$(document).ready(function(){
   ...  //  此处是页面DOM加载完成的入口
});

2、jquery选择器

jquery最神奇和令人惊叹的就是jquery可以通过选择器快速选择html元素和操作:

//筛选ul中li的一个
$("ul li:first").css("color", "#ff0000");
//筛选ul中li的下标值为2的
$("ul li:eq(2)").css("color", "blue");
//筛选ul中li中下标值为奇数的
$("ol li:odd").css("color", "skyblue");
//筛选ul中li中下标值为偶数的
$("ol li:even").css("color", "pink");

3、事件绑定

通过jquery我们可以快速绑定键盘和鼠标事件,示例代码如下:

<script>
	var node = document.getElementById("xxx");
	$(node).css("color","red");//把原生的节点对象,转换成了jQuery对象
		
	//传统方式
	node.onclick = function() {}
	
	//法一:事件名(函数)  即事件名作为 方法名来用
	$("#btn").click(function() {console.log("点击了我");});
	$("#btn").mouseover(function() {console.log("over");});
	$("#btn").mouseout(function() {console.log("out")});
		
	//法二:bind('事件名', 函数)绑定
	$("#btn").bind('click', function() {console.log("点击");});
	$("#btn").bind('mouseover', function() {console.log("鼠标上来...")});
	$("#btn").bind('mouseout', function() {console.log("鼠标出去...")});
			
</script>

JavaScript中let、const和var的使用介绍

胖蔡阅读(150)

varJavaScriptES6之前使用的变量定义关键字。通过var声明的变量,会挂载在当前定义的函数执行上下文。若在最外层定则变量就挂载在全局执行上下文,即window对象下。而const和let则是ES6中提出的定义变量或常量关键字,其中const用于声明常量,let则是用于声明局部变量关键字。

var

var定义的变量可以修改,不强制要求初始化,未初始化则输出undefined。简单使用如下:

var a = 1; //此处声明的变量a为全局变量
function foo(){
   var a = 2;//此处声明的变量a为函数foo的局部变量
   console.log(a);//2
}
foo();
console.log(a);//1

特点:

  • 存在变量提升
  • 可多次声明,后面声明会覆盖之前的声明
  • 函数声明var变量会有隔离的效果,这时该变量为局部变量,外部无法访问

let

let是ES6之后提出的用于声明局部变量。其作用域为代码块区域,let的使用示例如下:

let a = 1;
console.log(a);//1
console.log(b);//Uncaught ReferenceError: b is not defined
let b = 2;
function foo(){
    let a = 1;
    let a = 2;//Uncaught SyntaxError: Identifier 'a' has already been declared
}

特点:

  • let作用域块状作用域,如函数内或代码块内,如for循环内
  • 同一个代码块内不允许重复声明
  • 不影响作用域链
  • 不同于var的是let不存在变量提升

const

const是和let一样在ES6之后别加入的关键词,const被设计专门用于声明常量,示例如下:

const a = 1;
console.log(a);//1
a = 2;
console.log(a);//Uncaught TypeError: Assignment to constant variable.

特点:

  • 块级作用域,和let一样
  • 不允许作用域内重复声明
  • 值不允许修改,对于引用类型而言不可修改引用类型指向,但可以修改引用类型内部属性值

前端开发中网络请求方式汇总

amiko阅读(166)

我们去聊前端开发,就绕不过网络请求,可以说网络请求支撑起来了前端的半边天,因为有网络数据的交互,才使得我们的网页变得更加“灵活”起来了,让我们的网站能及时反馈数据给用户。本篇文章主要介绍在前端开发中,我们常用的几种网络请求方式,首先来了解下常用的网络请求方式有哪几种:

  • ajax请求
  • axios请求
  • fetch请求
  • xhr原生请求
  • websocket请求

Ajax请求

ajax是由Jquery库提供的一种网络异步请求方法,通过ajax我们可以快速获取后端请求数据,并将其包装输出到页面元素上。请原理实现还是通过xhr封装请求服务器,通过服务器返回数据进行DOM渲染,我找到一个比较能描述的流程图像。

图1.1 ajax原理图

基础使用

# 格式
$.ajax({name:value, name:value, ... }) // 可以直接传入url 


# 使用示例
$.ajax({
    url:"http://api.mediastack.cn/base/captcha",
    dataType:"json",
    error:function(error){
         console.log("get catch error:",error)
    },
    success:function(result){
        // 后台成功返回
        console.log("get result:",result)
    }
});

ajax请求的字典值支持如下:

  • url:字符串,请求的url
  • async:布尔值,表示请求是否异步处理。默认是 true
  • beforeSend(xhr):发送请求前回调函数。
  • data:发送到服务器的数据。将自动转换为请求字符串格式。GET 请求中将附加在 URL 后面。
  • dataType:字符串类型, 预期服务器返回的数据类型。如:
    • xml:返回 XML 文档
    • html:返回纯文本 HTML 信息
    • script:把响应的结果当作 JavaScript 执行。
    • json:把响应的结果当作 JSON 执行,并返回一个JavaScript对象。
    • jsonp: 以 JSONP 的方式载入 JSON 数据块。会自动在所请求的URL最后添加 “?callback=?“。
    • text:返回纯文本字符串。
  • context:这个对象用于设置Ajax相关回调函数的上下文。
  • converters :一个数据类型到数据类型转换器的对象。每个转换器的值是一个函数,返回经转换后的请求结果。默认为{"* text": window.String, "text html": true, "text json": jQuery.parseJSON, "text xml": jQuery.parseXML}
  • crossDomain:如果你想在同一域中强制跨域请求(如JSONP形式),例如,想服务器端重定向到另一个域,那么需要将crossDomain设置为 true 。默认同域请求为false, 跨域请求为true
  • contentType:发送信息至服务器时内容编码类型。默认值是”application/x-www-form-urlencoded; charset=UTF-8“,适合大多数情况。
  • complete:请求完成后回调函数 (请求success 和 error之后均调用)。这个回调函数得到2个参数: jqXHR (在 jQuery 1.4.x中是 XMLHTTPRequest) 对象和一个描述请求状态的字符串("success""notmodified""error""timeout""abort", 或者 "parsererror") 。
  • cache:布尔类型,默认为true.如果设置为 false ,浏览器将不缓存此页面。
  • accepts:内容类型发送请求头(Content-Type),用于通知服务器该请求需要接收何种类型的返回结果。
  • error:函数类型,请求失败时调用此函数。有以下三个参数:jqXHR (在 jQuery 1.4.x前为XMLHttpRequest) 对象、描述发生错误类型的一个字符串 和 捕获的异常对象。如果发生了错误,错误信息(第二个参数)除了得到null之外,还可能是"timeout""error""abort" ,和 "parsererror"。 当一个HTTP错误发生时,errorThrown 接收HTTP状态的文本部分,比如: “Not Found”(没有找到) 或者 “Internal Server Error.”(服务器内部错误)。
  • jsonp:字符串类型,在一个jsonp请求中重写回调函数的名字。这个值用来替代在”callback=?“这种GETPOST请求中URL参数里的”callback“部分,比如{jsonp:'onJsonPLoad'}会导致将”onJsonPLoad=?"传给服务器。
  • jsonpCallback:为jsonp请求指定一个回调函数名。这个值将用来取代jQuery自动生成的随机函数名。这主要用来让jQuery生成一个独特的函数名,这样管理请求更容易,也能方便地提供回调函数和错误处理。
  • mimeType:一个mime类型用来覆盖XHR的 MIME类型。
  • success:请求成功后的回调函数。这个函数传递3个参数:从服务器返回的数据,并根据dataType参数进行处理后的数据,一个描述状态的字符串;还有 jqXHR(在jQuery 1.4.x前为XMLHttpRequest) 对象 。
  • timeout:设置请求超时时间(毫秒)。此设置将覆盖$.ajaxSetup() 里的全局设置。
  • type:请求方式 (“POST” 或 “GET“), 默认为 “GET“。

Axio请求

Axio是一个基于Promise的网络请求库,同时适配于浏览器端和服务端(nodejs)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests

图 1.2 axios

安装

$ npm install axios # 或者使用yarn 
$ yarn add axios

通过CDN加载:

<!--jsDelivr CDN -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

<!--unpkg CDN -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

示例

axios.request({
      url:'/article/home/index',
      method:'get',
      baseURL:'http://test.mediastack.cn/'
}).then(
     res => {
     console.log("get res:",res);
     var str=JSON.stringify(res);
     document.getElementById("content").innerHTML = str;
},error => {
     console.log("get request failed:",error);
     document.getElementById("content").innerHTML = error;
  }
);

具体使用方式,可参考React应用中封装axios

fetch请求

fetch API提供了一个标准的js接口,优化http请求和响应的操作方式,fetch提供了一种极简单的方式来进行异步跨域获取资源,和axios类似的是其依托Promise,通过返回Promise对象实现了请求的异步。如下为fetch常见使用示例:

        var url = "https://mock.yonyoucloud.com/mock/16388/test/cities";
        fetch(url, {
            method: "GET",
            mode: 'cors',
            cache: 'default'
        }).then(function(response){
            return response.json(); //响应内容json化
        }).then(function(res){
            console.log(res);  //打印响应值
        })

如上,配置参数支持如下:

  • method: 请求方法,如 GET、POST、PUT、DELETE。默认GET
  • headers: 请求头信息,形式为 Headers 的对象或包含 ByteString 值的对象字面量。比如:headers : { 'Content-Type' : 'application/json' },可以设置所有的header信息。
  • body: 请求的 body 信息:可能是一个 Blob、BufferSource、FormData、URLSearchParams 或者 USVString 对象。注意 GET HEAD 方法的请求不能包含 body 信息。
  • mode: 请求的模式,如 默认cors跨域、 no-cors不跨域 或者 same-origin
  • credentials: 请求的 credentials,如 默认omit不携带cookiesame-origin同源携带cookie 或者 include跨域携带cookie。但在Chrome,Chrome 47 之前的版本默认值为 same-origin ,自Chrome 47起,默认值为include
  • cache: 请求的 cache 模式: default、 no-store不缓存、 reload 、 no-cache有缓存但仍需要去服务器获取资源 、 force-cache 或者 only-if-cached
  • redirect: 可用的 redirect 模式: follow (自动重定向), error (如果产生重定向将自动终止并且抛出一个错误), 或者 manual (手动处理重定向). 在Chrome中默认使用followChrome 47之前的默认值是manual)。
  • referrer: 一个 USVString 可以是 no-referrer、client或一个 URL。默认是 client
  • referrerPolicy: 指定了HTTP头部referer字段的值。可能为以下值之一:
    • no-referrer
    • no-referrer-when-downgrade
    • origin
    • origin-when-cross-origin
    • unsafe-url

XHR原生请求

xhr请求是浏览器端网络请求的基础,我们现在常用的所有网络请求框架都是基于xhr实现的二度封装,接下来,我们来了解下不用网络框架如何使用xhr实现前端的网络请求。代码示例如下:

// 创建 XHR 对象
const xhr = new XMLHttpRequest();
// 监听 onreadystatechange 事件
xhr.onreadystatechange = () => {
     if (xhr.status == 200 && xhr.readyState == 4) {
         console.log('请求成功', xhr.responseText);
     }
};
// 调用 open 函数
xhr.open('POST', 'http://localhost:8080/login', true);

// post 请求时,设置请求头 Content-Type
// 用于指定请求参数,以什么样的格式发送给后端,告诉服务器,我前端发送的请求参数是什么类型的。
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

// 调用 send 函数
xhr.send('a=100&b=200');

如上,通过onreadystatechange监听的回调函数获取返回数据。

websocket请求

websocket网络请求与上述其他网络请求方式不同,上述其他请求基本都是基于http协议实现的单次请求,而websocket是通过socket实现的前后端的长连接网络请求,一般适用于实时性要求高的功能需求,如我们常见的在线聊天室、实时推送等。

let ws = new WebSocket(url)
ws.onopen = function () {
  // 指定连接成功后的回调函数
}         
ws.onclose = function () {
  // 指定连接关闭后的回调函数
}         
ws.onmessage = function () {
  // 指定收到服务器数据后的回调函数
}         
ws.onerror = function () {
  // 指定报错时的回调函数
}

ws.send(message)  向服务器发送数据
ws.close()  关闭当前连接

有关websocket更加详细的介绍可参考:前端开发之websocket

Vue2的patch流程和diff算法

胖蔡阅读(149)

Vue通过虚拟DOM实现对页面渲染的监听更新,降低对真实DOM的操作。而patch则是Vue虚拟DOM实现的基石,它能快速实现虚拟DOM的对比更新,并最终将vnode渲染成真实DOM。整个patch的过程就是:创建节点、删除节点和修改节点的过程。

创建节点

当因状态改变而新增的节点在DOM中并不存在时,我们需要创建一个节点并插入到DOM中。即当oldVnode不存在而vnode存在时,就需要使用vnode生成真实的DOM元素并将其插入到视图当中。

1、首次渲染时,DOM中不存在任何节点,即oldVnode是不存在的,我们需要使用vnode创建一个新DOM节点并渲染视图。

2、当vnode oldVnode完全不是同一个节点时,即oldVnode是一个被废弃的节点,vnode是一个全新的节点,此时,我们需要使用vnode创建一个新DOM节点,用它去替换oldVnode所对应的真实DOM节点。

vnode是有类型的,所以当我们在创建节点时,最重要的是根据vnode的类型来创建出相同类型的DOM元素。事实上,只有三种类型的节点会被创建并插入到DOM中:元素节点注释节点文本节点

删除节点

因为渲染视图时,需以vnode为标准,所以vnode中不存在的节点都属于被废弃的节点,需要从DOM中删除。当vnode oldVnode完全不是同一个节点时,在DOM中需要使用vnode创建新节点替换oldVnode所对应的旧节点,而替换的过程就是将新创建的DOM节点插入到旧节点的旁边,然后再将旧节点删除。

在要删除节点的父元素上调用removeChild方法即可。

更新节点

无论是新增节点,还是删除节点,他们之间都有一个共同点,那就是两个虚拟节点是完全不同的。而当新旧两个节点是相同的节点时,我们需要对这两个节点进行比较细致的比对,然后对oldVnode在视图中所对应的真实节点进行更新。

更新节点需要对以下3种情况进行判断并分别处理:

  • 如果VNodeoldVNode均为静态节点:无需处理
  • 如果VNode是文本节点:如果VNode是文本节点即表示这个节点内只包含纯文本,那么只需看oldVNode是否也是文本节点,如果是,那就比较两个文本是否不同,如果不同则把oldVNode里的文本改成跟VNode的文本一样。如果oldVNode不是文本节点,那么不论它是什么,直接调用setTextNode方法把它改成文本节点,并且文本内容跟VNode相同。
  • 如果VNode是元素节点:如果VNode是元素节点,则又细分两种情况
    • 该节点不包含子节点
    • 该节点包含子节点

patch流程

1、当oldVnode不存在时,直接使用vnode渲染视图;

2、当oldVnodevnode都存在但并不是同一个节点时,使用vnode创建的DOM元素替换旧的DOM元素;

3、当oldVnodevnode是同一个节点时,使用更详细的对比操作对真实的DOM节点进行更新。

patch过程中最关键的是运营Vue-Diff算法,完成新旧vnode的精细化对比。通过Diff算法,我们可以计算出虚拟DOM中被改变的部分,然后针对该部分进行原生DOM操作,而不用重新渲染整个页面,从而提升性能。

原始diff算法

原始diff算法就是,两个虚拟DOM树,进行不分层级的逐一比对,也就是说,一个虚拟DOM树,从根节点到以后分支的每一个节点,都要单独拿出来跟新生成的节点做比较,这就是最原始的diff算法。

这个diff算法的时间复杂度表面上看是(n ^2),因为单独一个个的去跟另外的n个相比较,肯定是n ^2次就比较结束了,但是实际上不是的,比较完之后还要计算如何在最优的地方放置最佳的节点,所以就是O(n ^3)了。


虽然原始的Diff算法从功能上解决了先对比再处理实际DOM的需求,但是实际上我们的流程变得更加的复杂和笨拙。

优化Diff算法

优化Diff算法只比较同一层级 ,不做跨级比较。因为在实际的web展示中,非同级的节点移动是非常少的,所以可以选择做同级比较。

所谓同级比较,即只比较同层的节点,不同层不做比较。不同层的只需要删除原节点,并且新建插入更新节点。

Vue-Diff算法,采用的就是优化过的Diff算法,同层比较,不会跨级,且其比较是从两侧向中间进行的,这种方式相对于从左到右依次比对的方式来说,更高效。

Vue-diff 策略

1、Tree Diff

Tree Diff是对树每一层进行遍历,找出不同。

2、Component Diff

Vue是基于组件构建的,对于组件间的比较采用的策略如下:

  • 如果是同一类型的组件,则按照原策略比较组件的虚拟 DOM 树,否则不需要比较。
  • 如果是不同类型的组件,则将该组件判断为dirty component,从而替换整个组件下的所有子节点。

3、Element Diff

在进行组件对比的时候,如果两个组件类型相同,则需要进行元素级别的对比,这就是Element DiffElement Diff时,提供了3种节点操作,分别为INSERT_MARKUP(插入),MOVE_EXISTING(移动),REMOVE_NODE(删除)。

  • INSERT_MARKUP:新的组件类型不在旧集合中,即全新的节点,需要对新节点进行插入操作。
  • MOVE_EXISTING:旧集合中有新组件类型,且element是可更新的类型,这时候就需要做移动操作,可以复用以前的DOM节点。
  • REMOVE_NODE:旧组件类型,在新集合里也有,但对应的element不同则不能直接复用和更新,需要执行删除操作,或者旧组件不在新集合里的,也需要执行删除操作。

Vue-diff过程

定义4个指针:OldStartIdx、OldEndIdx、NewStartIdx、NewEndIdx。比较的是两个指针对应的节点的虚拟DOM是否为同一个。

具体步骤如下:

  1. 比较OldStartIdxNewStartIdx
  2. 如果两个startIdx相同,则两个指针都会+1,也就是向后移一位,重新生成OldStartIdxNewStartIdx指针。
  3. 如果两个startIdx不一致,则比较两个endIdx
  4. 比较OldEndIdxNewEndIdx
  5. 如果两个endIdx一致,则两个endIdx都减1,也就是向前移一位,再执行步骤1。
  6. 如果两个startIdx和两个endIdx都不一致,则比较捺向的oldStartIdxNewEndIdx
  7. 如果oldStartIdxNewEndIdx一致,则把oldStartIdx指向的虚拟DOM里的真实DOM节点,挪到OldEndIdx位置之后,oldStartIdx加1向后移一位,newEndIdx减1向前移动一位。
  8. 如果竖向和捺向都不一致,则比较撇向oldEndIdxNewStartIdx
  9. 如果撇向一致,则把oldEndIdx指向的真实dom节点挪到oldStartIdx所在的真实dom前,同时oldEndIdx减1向前移动一位,newStartIdx加1向后移动一位。
  10. 如果竖向、捺向、撇向都不一致,则看有没有key。
  11. 如果有key,就能快速找到,并挪到oldStartIdx前。
  12. 如果没有key,就遍历oldStartIdxoldEndIdx之间的所有节点,寻找newStartIdx指向的节点是否存在于这些老的vdom中。如果有,就把它挪到oldStartIdx前;没有就在oldStartIdx之前创建一个节点,newEndIdx减1向前移动一位。这样比较下去,一直到newEndIdx<newStartIdx

newEndIdx<newStartIdx,这时new vnode生成完毕,然后将old vnode中多余的部分删掉即可,也就是oldStartIdxoldEndIdx指向的dom及中间的部分。


Vue2的变化侦测机制

胖蔡阅读(97)

Vue最引人所乐道的就是其数据双向绑定式模式,以数据驱动视图。通过数据的改变实现视图的实时更新的这一过程其实就是对数据的一个状态的追踪,那么Vue2是通过何种方式实现界面的实时动态变化的呢?本篇文章主要介绍Vue2的这种变化侦测机制的实现原理。

Object的变化侦测

由于JSObjectArray提供的方法机制不同,所以Vue针对ObjectArray,采用了两套不同的变化侦

测机制。本章,我们先来详细介绍一下Object的变化侦测。

Object变化如何侦测

Vue变化侦测机制的关键点在于观测数据变化,那么我们如何观测Object的变化呢?

Vue为我们定义了Observer类。通过Observer类,可以将一个正常的数据转换成可观测的数据。

例如:

let apple = new Observer({
'weight':'1斤',
'price':10
})

这样,apple的两个属性都变得可观测了。

那么, observer 类究竟是怎样的一个存在呢?我们来看看源码:

/**
* Observer类会通过递归的方式把一个对象的所有属性包括子属性都转化成可观测对象
*/
export class Observer {
constructor (value) {
this.value = value
// 给value新增一个__ob__属性,值为该value的Observer实例
// 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作
def(value,'__ob__',this)
if (Array.isArray(value)) {
// 当value为数组时的逻辑
// ...
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}

看到这里,大家可能还是不知道Observer类究竟是如何监测数据变化,实现数据可观测呢?

我们再把上述代码中的defineReactive方法代码放出来,大家就明白了。

/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
function defineReactive (obj,key,val) {
// 如果只传了obj和key,那么val = obj[key]
if (arguments.length === 2) {
val = obj[key]
}
if(typeof val === 'object'){
new Observer(val)
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
console.log(`${key}属性被读取了`);
return val;
},
set(newVal){
if(val === newVal){
return
}
console.log(`${key}属性被修改了`);
val = newVal;
}
})
}

defineReactive方法用到了JS一个非常重要的原生方法Object.defineProperty。通过

Object.defineProperty() 方法,可以给数据对象定义一个属性(key),并把这个属性的读和写分别

使用 get() set() 进行拦截,每当该属性进行读或写操作的时候就会触发 get() set() ,从而使得

数据变化可以观测。

谁使用了数据成为依赖

现在,我们知道了数据什么时候发生变化,该去通知视图去更新了。但是,视图那么大,我们到底该通

知谁去更新?总不能一个数据变化了,把整个视图全部更新一遍吧。

理想的情况是:视图里谁用到了这个数据、谁依赖了这个数据,就更新谁!

Vue当然是这么做的。

那么,Vue究竟怎么用代码的形式,来描述这个“谁”呢?请看以下代码:

/**
* @param { vm } vue对象实例
* @param { expOrFn } 对象的属性
* @param { cb } 真正包含数据更新逻辑的函数
*/
export default class Watcher {
constructor (vm,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn) //parsePath方法把一个形如'data.a.b.c'的字符串路
径所表示的值,从真实的data对象中取出来
//初始化的时候触发数据属性的get方法
this.value = this.get()
}
// 触发数据属性的get方法,访问数据属性即可实现
get () {
// 把Watcher实例保存到Dep类的target属性上
Dep.target = this;
const vm = this.vm
let value = this.getter.call(vm, vm)
Dep.target = null;
return value
}
// 当update被触发时,此时获取到的数据属性值时已经被修改后的新值
update () {
const oldValue = this.value
this.value = this.get()
// 触发传递给Watcher的更新数据的函数
this.cb.call(this.vm, this.value, oldValue)
}
}

从上述代码可以知道,在创建 Watcher 实例的过程中会自动的把自己添加到这个数据对应的依赖管理器

中,以后这个 Watcher 实例就代表这个依赖,当数据变化时,我们就通知 Watcher 实例,由 Watcher

实例再去通知真正的依赖。

依赖放在哪里集中管理

一个数据可能被多处使用,产生众多依赖,这些依赖如何管理?

简单的做法是,我们给每个数据都建一个依赖数组,谁依赖了这个数据,我们就把谁放入这个依赖数组

中,那么当这个数据发生变化的时候,我们就去它对应的依赖数组中,把每个依赖都通知一遍。

更好的做法是,我们为每一个数据都建立一个依赖管理器,把这个数据所有的依赖都管理起来。所以,

vue创建了Dep类。

/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 删除一个依赖
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
/*依赖收集,当存在Dep.target的时候添加依赖*/
depend () {
if (Dep.target) {
Dep.target.addDep(this) // *Watcher里的addDep方法,添加一个依赖关系到Deps集合中
*/
}
}
/*通知所有依赖更新*/
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() //Watcher里面的调度者接口,当依赖发生改变的时候进行回调
}
}
}
/*依赖收集完需要将Dep.target设为null,防止后面重复添加依赖。*/
Dep.target = null
const targetStack = []
/*将watcher实例设置给Dep.target,用以依赖收集。同时将该实例存入target栈中*/
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
/*将观察者实例从target栈中取出并设置给Dep.target*/
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}

Object依赖何时收集何时更新

数据变化知道了,依赖是谁也知道了,依赖放哪也明确了,那么何时才能收集依赖,又是何时才能去通

知依赖进行更新:

function defineReactive (obj,key,val) {
if (arguments.length === 2) {
val = obj[key]
}
if(typeof val === 'object'){
new Observer(val)
}
const dep = new Dep() //实例化一个依赖管理器,生成一个依赖管理数组dep
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
dep.depend() // 在getter中收集依赖
return val;
},
set(newVal){
if(val === newVal){
return
}
val = newVal;
dep.notify() // 在setter中通知依赖更新
}
})
}

可观测的数据被获取时会触发 getter 属性,所以我们就可以在 getter 中收集这个依赖。同样,当这个

数据变化时会触发 setter 属性,那么我们就可以在 setter 中通知依赖更新。

关于Object侦测的问题

前面介绍了Object类型数据的变化侦测原理,了解了数据的变化是通过getter/setter来追踪的。

但是,也正是由于这种追踪方式,有些语法中即便是数据发生了变化,Vue也追踪不到。例如:

data() {
return {
obj: {}
}
}
methods: {
addKey() { //在obj上面新增name属性
this.obj.name = 'apple'
}
deleteKey() {//在obj上面删除name属性
delete this.obj.name
}
}

在上面代码中,无论是为obj新增name属性,还是删除name属性,Vue都无法侦测到。

Vue是通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但

getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性,所以才会导致上述问题。

为了解决这一问题,Vue增加了两个全局API:Vue.set和Vue.delete。关于这两个API的实现原理,我们

后续再介绍。

总结

Object通过Observer类将其所有数据包括子数据都转换成getter/setter的形式来追踪变化。

我们在getter中收集依赖,在setter中通知依赖。

收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖、删除依赖和向依

赖发送消息等。

所谓的依赖,其实就是Watcher。只有Watcher触发的getter来会收集依赖,哪个Watcher触发了

getter,就把哪个Watcher收集到Dep中,当数据发生变化时,会循环依赖列表,把所有的watcher都通

知一遍。

数据与Observer、Dep、watcher之间的运转流程如下:

  • 数据 通过 Observer 转换成可侦测的对象。
  • 当外界通过 Watcher 读取数据时,会将 Watcher 添加到 Dep 中
  • 当数据发生变化时,则会向 Dep 中的依赖即 Watcher 发送通知。
  • Watcher 接收到通知后,会向外界发送通知。外界接收到通知后进行相应的更新。

Array的变化侦测

我们知道,Object的变化是靠setter来追踪的,只要一个数据发生变化,就一定会触发setter。那Array

是不是这样呢?

我们先来看下面这个例子:

this.list.push(1)

该例子中,我们使用了push方法向list中新增了数字1,改变了list数组,但并没有触发setter。

也就是说,我们可以通过Array原型上的方法来改变数组的内容,而无需触发setter,所以Object那种通

过getter/setter来实现侦测的方式用在Array身上就行不通了。

为此,Vue中,专门创建了Array变化侦测机制。

虽然Object和Array的变化侦测机制不同,但是在讲述Object变化侦测机制中提到的Observer、Dep、

watcher三个类及其概念,同样适用于Array。

下面我们正式开始Array的变化侦测的介绍。

Array变化如何侦测

前面的例子使用push来改变数组的内容,那么我们只要能在用户使用push操作数组的时候得到通知,

那么就能追踪数据的变化了。来看看下面这段代码:

let arr = [1,2,3]
arr.push(4)
Array.prototype.newPush = function(val){
console.log('arr被修改了')
this.push(val)
}
arr.newPush(4)

我们针对数组的原生push方法定义了一个新的newPush方法,这个newPush方法内部调用了原生push

方法,这样既能保证新的newPush方法跟原生push方法具有相同的功能,而且我们还能得知数组变化。

创建拦截器

基于上述思想,Vue中创建了一个数组方法拦截器,重写了操作数组的方法,并把它拦截在数组实例与

Array.prototype之间。当数组实例使用操作数组方法时,其实使用的是拦截器中重写的方法,而不再使

用Array.prototype上的原生方法。

具体实现代码如下:

const arrayProto = Array.prototype
// 创建一个对象作为拦截器
export const arrayMethods = Object.create(arrayProto)
// 改变数组自身内容的7个方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
const original = arrayProto[method] // 缓存原生方法
Object.defineProperty(arrayMethods, method, {
enumerable: false,
configurable: true,
writable: true,
value:function mutator(...args){
const result = original.apply(this, args)
return result
}
})
})

在上面的代码中,我们创建了变量arrayMethods,它继承自Array.prototype,具备其所有功能。

接下来,在arrayMethods上使用Object.defineProperty方法,将那些可以 改变数组自身内容的方法

(push,pop,shift,unshift,splice, sort, reverse)进行封装。

所以,当使用push方法的时候,其实调用的是arrayMethods.push,执行的是mutator函数,而

mutator是可控的,我们可以在里面做一些其他的事情,诸如发送变化通知等。

挂载拦截器

有了拦截器之后,想要它生效,就需要使用它去覆盖Array.prototype。但是我们不能直接覆盖,因为这

样会污染全局的Array,所以我们可以这样操作:

const hasProto = '__proto__' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export class Observer {
constructor (value) {
this.value = value
if (Array.isArray(value)) {
//挂载关键代码
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value)
}
}
}
function protoAugment (target, src: Object, keys: any) {
target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}

上面代码中首先判断了浏览器是否支持 __proto__ ,如果支持,则调用protoAugment函数覆盖value

原型功能;如果不支持,则调用copyAugment函数把拦截器中重写的7个方法循环加入到value上。这

样,拦截器就可以生效了。

拦截器生效以后,当数组数据再发生变化时,我们就可以在拦截器中通知变化了。

Array依赖在哪收集

我们创建了拦截器,让我们具备了当数组内容发生变化时得到通知的能力。但是变化了通知谁呢?当然

是用到了Array型数据的那些依赖。那么这些依赖,我们该如何收集呢?

在这之前,我们先简单回顾一下Object的依赖的在哪收集的。

Object的依赖是在Object.defineProperty中的getter里收集的,每个key都会有一个对应的Dep列表来存

储依赖。

那么,数组在哪里收集依赖呢?其实,数组也是在getter中收集依赖的。为什么这么说呢?我们来看看

下面这个例子。

data(){
return {
list:[1,2,3]
}
}

想想看,list这个数据始终都存在于一个object数据对象中,而且我们也说了,谁用到了数据谁就是依

赖,那么要用到list这个数据,是不是得先从object数据对象中获取一下list数据,而从object数据对象中

获取list数据自然就会触发list的getter,所以我们就可以在getter中收集依赖。

总结一句话就是:Array型数据还是在getter中收集依赖。

Array依赖列表放在哪

知道在哪收集依赖了,那么将收集来的的依赖列表放在哪呢?Vue中,把Array的依赖列表放在Observer

中。

export class Observer {
constructor (value) {
this.value = value
this.dep = new Dep() // 实例化一个依赖管理器,用来收集数组依赖
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value)
}
}
}

从上面的介绍中,我们知道,Array在getter中收集依赖,在拦截器中通知依赖更新。所以这个依赖保存

的位置就很关键,它必须在getter和拦截器中都可以访问到,而Observer实例正好在getter中、拦截器

中都能访问到。

Array依赖如何收集

把Dep实例保存在Observer的属性上之后,我们可以在getter中像下面这样访问并收集依赖。

function defineReactive (obj,key,val) {
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
//收集依赖
if (childOb) {
childOb.dep.depend()
}
return val;
},
set(newVal){
}
})
}
/**
* 尝试为value创建一个0bserver实例,如果创建成功,直接返回新创建的Observer实例。
* 如果 Value 已经存在一个Observer实例,则直接返回它
*/
export function observe (value, asRootData){
if (!isObject(value) || value instanceof VNode) {
return
}
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}

在上面的代码中,我们通过 observe函数,创建一个Observer实例,如果value已经是响应式数据,则

不需要再次创建Observer实例,直接返回已经创建的Observer实例即可,避免了重复侦测value变化的

问题。

Observer实例childOb创建后,我们就可以访问其dep属性 ,调用该属性上的depend()方法,收集依赖

了。

如何通知Array依赖更新

到现在为止,依赖已经收集好了,并且也已经存放好了,那么我们该如何通知依赖呢?

因为我们是在拦截器中获知数组变化的,所以我们应该在拦截器里通知依赖,而要想通知依赖,首先要

能访问到依赖。

export class Observer实例。 {
constructor (value) {
this.value = value
this.dep = new Dep()
def(value,'__ob__',this) //通过def工具函数,在value中新增一个__ob__属性,这个属性的
值就是当前的Observer实例。
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value)
}
}
}

上述代码中,我们通过def工具函数,在value中新增一个 __ob__ 属性,这个属性的值就是当前的

Observer实例。然后我们就可以在拦截器中,通过 __ob__ 属性拿到Observer实例,然后就可以拿到

__ob__ 的dep了,从而调用dep身上的notify()方法通知依赖更新。具体代码如下:

methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__ //拿到Observer实例
ob.dep.notify()//拿到Observer实例的dep属性,调用notify()方法通知依赖更新
return result
})
})

Array的深度监测

我们上面说的侦测数组的变化,指的是数组自身的变化,比如是否新增一个元素,是否删除一个元素

等。

实际上,数组的子数据的变化也要侦测。比如数组中Object身上某个属性的值发生了变化也要发送通

知。再比如,使用了push往数组中新增了元素,这个新增元素的变化也要侦测。

1、侦测数组中元素的变化

如何侦测数组中子数据的变化,我们来看看下面这段代码:

export class Observer {
value: any;
dep: Dep;
constructor (value: any) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value) // 将数组中的所有元素都转化为可被侦测的响应式
} else {
this.walk(value)
}
}
// 侦测数组中的每一项
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}

在上面代码中,我们调用observeArray方法,循环Array中的每一项,执行observe函数,将数组中的每

个元素都执行一遍new Observer,即可将这个数组的所有子数据转换成响应式的。

2、侦测新增元素的变化

对于数组中已有的元素我们已经可以将其全部转化成可侦测的响应式数据了,但是如果向数组里新增一

个元素的话,我们也需要将新增的这个元素转化成可侦测的响应式数据。

如若达到此目的,我们需要拿到新增的这个元素,然后调用observeArray函数将其转化即可。

我们知道,可以向数组内新增元素的方法有3个,分别是:push、unshift、splice。我们只需对这3中方

法分别处理,拿到新增的元素,再将其转化即可。

methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args // 如果是push或unshift方法,那么传入参数就是新增的元素
break
case 'splice':
inserted = args.slice(2) // 如果是splice方法,那么传入参数列表中下标为2的就是新
增的元素
break
}
if (inserted) ob.observeArray(inserted) // 调用observeArray函数将新增的元素转化成
响应式
// notify change
ob.dep.notify()
return result
})
})

在上述代码中,我们从 this.__ob__ 上拿到Observer实例后,如果有新增元素,则使用

ob.observeArray来侦测这些新元素的变化。

关于Array侦测的问题

从前面的介绍,我们知道,Vue2中对Array的变化侦测是通过拦截原型中操作数组的方法的方式实现

的,但是,其实我们是可以不通过使用数组原型方法来改变数组的。例如:

this.list[0] = 2 // 改变数组的第一个值
this.list.length = 0 // 清空数组

如果使用上述方式改变数组,Vue是侦测不到的。

为了解决这一问题,与Object一样,同样需用到了Vue.set和Vue.delete这两个API。

总结

Array追踪变化的方式和Object不一样。因为它是通过方法来改变内容的,所以我们通过创建拦截器去覆

盖数组原型的方式来追踪变化。

Array收集依赖的方式和Object一样,都是在getter中收集。但是由于使用依赖的位置不同,数组要在拦

截器中向依赖发送消息,所以依赖不能像Object那样保存在defineReactive中,而是把依赖保存在了

Observer实例上。

除了侦测数组自身的变化外,数组中元素发生的变化也要侦测。

除了侦测已有数据外,当使用push等方法向数组中新增数据时,新增的数据也要进行变化侦测。

JS中如何理解执行上下文?

胖蔡阅读(149)

执行上下文是JS中一个比较重要的概念,当前函数、变量所处的执行上下文直接决定当前变量、对象可访问哪些数据。一般地,根据执行区域可以将执行上下文分为两种类型:

  • 全局执行上下文
  • 函数执行上下文
执行上下文

全局执行上下文

一般地,全局执行上下文是最外层的执行上下文,总在执行栈底部,我们可以理解为window对象作为一个全局执行上下文的对象。因此,所有通过var定义的全局变量和函数都可以通过window访问对应的属性和方法。全局执行上下文在一个页面打开自动的时候创建,直到这个页面关闭后全局执行上下文会被回收。

函数执行上下文

如上,在JS中执行是按顺序执行的,JS启动的时候会创建一个执行上下文的一个栈结构,并同时创建一个全局执行上下文压在栈底,然后按顺序执行。这时候,若是遇到需要执行一个函数的话,JS就会创建一个对应的函数执行上下文入栈,并在该函数执行结束后,将该函数执行上下文出栈。

var a=1;//1.进入全局执行上下文环境
function outter(){
  var b=2;
  function inner(){
    var c=3;
    console.log(a+b+c);
  }
  inner();//3.进入inner函数上下文环境
}
outter();//2.进入outter函数上下文环境

上述代码,是比较常见介绍执行上下文的代码。如上所示,开始执行JS时,会创建全局执行上下文,然后运行到outter()函数将触发创建函数执行上下文环境,在outter内部又会创建一个子函数执行上下文inner(),我们简单的梳理下上下文创建、销毁的流程:

  1. 打开页面,运行JS代码时创建全局执行上下文环境
  2. 调用outter()函数,创建outter()函数执行上下文环境
  3. 运行执行outter函数,调用内部函数inner()函数,创建inner()函数执行上下文环境
  4. inner()函数执行完毕,销毁inner函数执行上下文环境
  5. outter()函数执行完毕,销毁outter函数执行上下文环境
  6. 关闭当前页面,销毁全局执行上下文环境

执行上下文组成

执行上下文环境一般的由变量环境和词法环境组成。变量环境可以理解为我们的全局变量、全局函数,或者是函数执行上下文里的函数变量部分。词法环境主要就是js代码执行的调用链即代码作用域。

JavaScript中的正则表达式基础使用介绍

胖蔡阅读(116)

什么是正则表达式?它有什么作用?

又称规则表达式,(Regular Expression,在代码中常简写为regex、regexp或RE),是一种文本模式,包括普通字符(例如,a 到 z 之间的字母)和特殊字符(称为”元字符”),是计算机科学的一个概念。

正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串,通常被用来检索、替换那些符合某个模式(规则)的文本。

JS 正则表达式

如何使用正则表达式

定义表达式

  • 普通方式

let reg=/表达式/修饰符(可选)

修饰符
g:代表可以进行全局匹配(查找所有匹配而非在找到第一个匹配后停止)。
i:代表不区分大小写匹配。
m:代表可以进行多行匹配。
例如
var reg=/a*b/;
var reg=/abc+f/g;
  • 构造函数方式

let reg=new RegExp(“表达式”,”附加参数”);

例如

var reg=new RegExp(“a*b”); 
var reg=new RegExp(“abc+f”,”g”);
  • 两者区别

1、普通方式中的表达式必须是一个常量字符串,
2、构造函数中的表达式可以是常量字符串,也可以是一个js变量,
3、例如根据用户的输入来作为表达式参数等等:var reg=new RegExp(动态变量,”g”);

表达式操作

1、test(str) 判断字符串str是否匹配表达式,返回一个布尔值。

let regx=/123/; 
let flag=regx.test('123sdsd'); // 匹配123sdsd中是否包含123, 结果 flag 返回 true

2、exec(str) 返回str中与表达式相匹配的第一个字符串,而且以数组的形式表现

var str="the name 123 and 456";
var reg=/\d/g; reg.lastIndex=15; // 在全局模式下可以使用lastIndex属性设置在字符串中查找指定字符时开始的位置。 
console.log(reg.exec(str)); ['4', index: 17, input: 'the name 123 and 456', groups: undefined] 

如果有多个合适的匹配,则第一次执行exec返回一个第一个匹配,此时继续执行exec,则依次返回第二个第三个匹配。 例如:

var regx=/user\d/g; 
var rs=regx.exec('ddduser1dsfuser2dd'); 
var rs1=regx.exec('ddduser1dsfuser2dd'); 
console.log(rs) ['user1', index: 3, input: 'ddduser1dsfuser2dd', groups: undefined] console.log(rs1)['user2', index: 11, input: 'ddduser1dsfuser2dd', groups: undefined] 
// 当然注意regx中的g参数是必须的,否则无论exec执行多少次,都返回第一个匹配。后面还有相关内容涉及到对此想象的解释

3、match(expr) 返回与expr相匹配的一个字符串数组,如果没有加参数g,则返回第一个匹配,加入参数g则返回所有的匹配

var regx=/user\d/g; 
var str='user13userddduser345'; 
var rs=str.match(regx); 
console.log(rs) ['user1', 'user3']

4、search(expr),返回字符串中与expr相匹配的第一个匹配的index值。

var regx=/user\d/g; 
var str='user13userddduser345'; 
var rs=str.search(regx); 
console.log(rs) 、// 0

5、replace(expr,str),将字符串中匹配expr的部分替换为str。另外在replace方法 中,str中可以含有一种变量符号$,格式为$n,代表匹配中被记住的第n的匹配字符串(注意小括号可以记忆匹配)

示例一:

var regx=/user\d/g;
var str='user13userddduser345';
var rs=str.replace(regx,'00');
console.log(rs) 003userddd0045

示例二:

var regx=/u(se)r\d/g; 
var str=“user13userddduser345”; 
var rs=str.replace(regx,”$1”); 
console.log(rs) // se3userdddse45 

对于replace(expr,str)方法还要特别注意一点,如果expr是一个表达式对象则会进行全局替换(此时表达式必须附加参数g,否则也只是替换第一个匹配),如果expr是一个字符串对象,则只会替换第一个匹配的部分,例如:

var regx='user'; 
var str='user13userddduser345'; 
var rs=str.replace(regx,'00'); 
console.log(rs)

6、split(expr),将字符串以匹配expr的部分做分割,返回一个数组,而且表达式是否附加参数g都没有关系,结果是一样的。

var regx=/user\d/g; 
var str='user13userddduser345'; 
var rs=str.split(regx); 
console.log(rs) // ['', '3userddd', '45']

正则表达式模式

方括号查询范围内符合的内容

正则表达式模式说明
[abc]查找方括号之间的任何字符。
[0-9]查找任何从 0 至 9 的数字。
(xy)
元字符是拥有特殊含义的字符
元字符说明
—-—-
\d查找数字。
\D查找非数字字符
\s查找空白字符。
\S查找非空白字符
\b匹配单词边界
\B匹配非单词边界
量词:
元字符说明
—-—-
^匹配开头,在多行检测中,会匹配一行的开头
$匹配结尾,在多行检测中,会匹配一行的结尾
n+匹配任何包含至少一个 n 的字符串。
n*匹配任何包含零个或多个 n 的字符串。
n?匹配任何包含零个或一个 n 的字符串。
n{x}匹配包含 x 个 n 的序列的字符串
n{x,y}匹配包含最少 x 个、最多 y 个 n 的序列的字符串
n{x,}匹配包含至少 x 个 n 的序列的字符串
正则说明

示例如下:

var s = "how are you";
var r = /\w+$/;
var a = s.match(r); //返回数组["you"]
var r = /^\w+/;var a = s.match(r); //返回数组["how"]
var r = /\w+/g;var a = s.match(r); //返回数组["how","are","you"]

常用一些表达式

1、手机校验

//手机号校验 
const phoneReg = /^[1][3,4,5,6,7,8,9][0-9]{9}$/  
//身份证的校验 
const sfzReg = /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
//邮箱的校验
const emailReg = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/
//URL的校验
const urlReg = /^((https?|ftp|file):\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/
//IPv4的校验
const ipv4Reg = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
//16进制颜色的校验
const color16Reg = /^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/
//日期 YYYY-MM-DD
const dateReg = /^\d{4}(\-)\d{1,2}\d{1,2}$/
//日期 YYYY-MM-DD hh:mm:ss
const dateReg = /^(\d{1,4})(-|\/)(\d{1,2})(\d{1,2}) (\d{1,2}):(\d{1,2}):(\d{1,2})$/
//整数的校验
const intReg = /^[-+]?\d*$/
//小数的校验
const floatReg = /^[-\+]?\d+(\.\d+)?$/
//保留n位小数
function checkFloat(n) {
  return new RegExp(`^([1-9]+[\d]*(.[0-9]{1,${n}})?)$`)
}
// 保留2位小数
const floatReg = checkFloat(2)

const floatNum1 = 1234.5
console.log(floatReg.test(floatNum1)) // true
//邮政编号的校验
const postalNoReg = /^\d{6}$/
//QQ号的校验5-11位数字
const qqReg = /^[1-9][0-9]{4,10}$/
//微信号的校验 说明:6至20位,以字母开头,字母,数字,减号,下划线
const wxReg = /^[a-zA-Z]([-_a-zA-Z0-9]{5,19})+$/
//车牌号的校验
const carNoReg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/
//只含字母的字符串
const letterReg = /^[a-zA-Z]+$/
//包含中文的字符串
const cnReg = /[\u4E00-\u9FA5]/
//密码强度的校验  说明:密码中必须包含字母、数字、特称字符,至少8个字符,最多30个字符
const passwordReg = /(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[^a-zA-Z0-9]).{8,30}/
//字符串长度n的校验
function checkStrLength(n) {
  return new RegExp(`^.{${n}}$`)
}
// 校验长度为3的字符串
const lengthReg = checkStrLength(3)

const str1 = 'hhh'
console.log(lengthReg.test(str1)) // true
//文件拓展名的校验
function checkFileName (arr) {
  arr = arr.map(name => `.${name}`).join('|')
  return new RegExp(`(${arr})$`)
}
const filenameReg = checkFileName(['jpg', 'png', 'txt'])
//匹配img和src
const imgReg = /<img.*?src=[\"|\']?(.*?)[\"|\']?\s.*?>/ig

const htmlStr = '<div></div><img src="sunshine.png" /><img src="sunshine111.png" />'

console.log(imgReg.exec(htmlStr))

2、邮箱校验

var reg = new RegExp("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$");
     //校验
     if (value==null){
         alert("邮箱地址不能为空");
         return false;
     }
     if(!reg.test(value)){
         alert("请输入有效的邮箱地址");
         return false;
     }
     return  true;

3、身份证号校验

const validateIdent = {
    aIdentityCode_City: { // 城市代码列表  
        11: "北京", 12: "天津", 13: "河北", 14: "山西", 15: "内蒙古", 21: "辽宁", 22: "吉林",
        23: "黑龙江 ", 31: "上海", 32: "江苏", 33: "浙江", 34: "安徽", 35: "福建", 36: "江西",
        37: "山东", 41: "河南", 42: "湖北 ", 43: "湖南", 44: "广东", 45: "广西", 46: "海南",
        50: "重庆", 51: "四川", 52: "贵州", 53: "云南", 54: "西藏 ", 61: "陕西", 62: "甘肃",
        63: "青海", 64: "宁夏", 65: "新疆", 71: "台湾", 81: "香港", 82: "澳门", 91: "国外 "
    },
    IdentityCode_isCardNo(card) {//检查号码是否符合规范,包括长度,类型  
        var reg = /(^\d{15}$)|(^\d{17}(\d|X)$)/; //身份证号码为15位或者18位,15位时全为数字,18位前17位为数字,最后一位是校验位,可能为数字或字符X  
        if (reg.test(card) === false) {
            return false;
        }
        return true;
    },
    IdentityCode_checkProvince(card) { //取身份证前两位,校验省份    
        var province = card.substr(0, 2);
        if (validateIdent.aIdentityCode_City[province] == undefined) {
            return false;
        }
        return true;
    },
    IdentityCode_checkBirthday(card) { //检查生日是否正确,15位以'19'年份来进行补齐。  
        var len = card.length;
        //身份证15位时,次序为省(3位)市(3位)年(2位)月(2位)日(2位)校验位(3位),皆为数字    
        if (len == '15') {
            var re_fifteen = /^(\d{6})(\d{2})(\d{2})(\d{2})(\d{3})$/;
            var arr_data = card.match(re_fifteen); // 正则取号码内所含出年月日数据  
            var year = arr_data[2];
            var month = arr_data[3];
            var day = arr_data[4];
            var birthday = new Date('19' + year + '/' + month + '/' + day);
            return validateIdent.IdentityCode_verifyBirthday('19' + year, month, day, birthday);
        }
        //身份证18位时,次序为省(3位)市(3位)年(4位)月(2位)日(2位)校验位(4位),校验位末尾可能为X    
        if (len == '18') {
            var re_eighteen = /^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$/;
            var arr_data = card.match(re_eighteen); // 正则取号码内所含出年月日数据  
            var year = arr_data[2];
            var month = arr_data[3];
            var day = arr_data[4];
            var birthday = new Date(year + '/' + month + '/' + day);
            return validateIdent.IdentityCode_verifyBirthday(year, month, day, birthday);
        }
        return false;
    },
    IdentityCode_verifyBirthday(year, month, day, birthday) {//校验日期 ,15位以'19'年份来进行补齐。
        var now = new Date();
        var now_year = now.getFullYear();
        //年月日是否合理    
        if (birthday.getFullYear() == year
            && (birthday.getMonth() + 1) == month
            && birthday.getDate() == day) {
            //判断年份的范围(3岁到150岁之间)    
            var time = now_year - year;
            if (time >= 3 && time <= 150) {
                return true;
            }
            return false;
        }
        return false;
    },
    IdentityCode_checkParity(card) { //校验位的检测  
        card = validateIdent.IdentityCode_changeFivteenToEighteen(card); // 15位转18位    
        var len = card.length;
        if (len == '18') {
            var arrInt = new Array(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2);
            var arrCh = new Array('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2');
            var cardTemp = 0, i, valnum;
            for (i = 0; i < 17; i++) {
                cardTemp += card.substr(i, 1) * arrInt[i];
            }
            valnum = arrCh[cardTemp % 11];
            if (valnum == card.substr(17, 1)) {
                return true;
            }
            return false;
        }
        return false;
    },
    IdentityCode_changeFivteenToEighteen(card) {  //15位转18位身份证号   
        if (card.length == '15') {
            var arrInt = new Array(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2);
            var arrCh = new Array('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2');
            var cardTemp = 0, i;
            card = card.substr(0, 6) + '19' + card.substr(6, card.length - 6);
            for (i = 0; i < 17; i++) {
                cardTemp += card.substr(i, 1) * arrInt[i];
            }
            card += arrCh[cardTemp % 11];
            return card;
        }
        return card;
    },
    IdentityCodeValid(card) {//   身份证号码检验主入口 
        let pass = true;
        let sex = ''
        //是否为空    
        if (pass && card === '')
            pass = false;
        //校验长度,类型    
        if (pass && validateIdent.IdentityCode_isCardNo(card) === false)
            pass = false;
        //检查省份    
        if (pass && validateIdent.IdentityCode_checkProvince(card) === false)
            pass = false;
        //校验生日    
        if (pass && validateIdent.IdentityCode_checkBirthday(card) === false)
            pass = false;
        //检验位的检测    
        if (pass && validateIdent.IdentityCode_checkParity(card) === false)
            pass = false;
        if (pass) {
            var iCard = validateIdent.IdentityCode_changeFivteenToEighteen(card);
            if (parseInt(iCard.charAt(16)) % 2 == 0) {
                sex = "0"; // 女生  
            } else {
                sex = "1"; // 男生  
            }
            return true
        } else {
            return false
        }
    }
}
 
 
 
 
export default validateIdent.IdentityCodeValid   //导出

一款轻量级处理cookie的javascript库推荐,无依赖

胖蔡阅读(283)

Js-cookie是一款用于处理 cookie 的简单、轻量级 JavaScript API。接下来我们就来了解下js-cookie,并学会快速的使用它。

  • 支持所有浏览器
  • 接受任何类型编码字符
  • 大量的测试用例
  • 无依赖性,不需要依赖其他任何包
  • 支持 ES 模块
  • 支持 AMD/CommonJS
  • RFC 6265 兼容
  • 启用自定义编码/解码
  • < 800 字节压缩!

安装

1、使用NPM安装

js-cookie 支持npm下载安装,安装代码如下:

$ npm i js-cookie

npm 包有一个 module 字段指向库的 ES 模块变体,主要是为 ES 模块感知捆绑器提供支持,而它的 browser 字段指向 UMD 模块以完全向后兼容。

2、直接下载引用

小伙伴可以直接点击访问js-cookie库的github仓库进行下载。文章的最后我也会提供百度网盘的下载链接给大家,以供哪些网速不是太好的同学使用。

需要注意的是,从版本 3 开始,发行版包含该库的两个变体,一个 ES 模块和一个 UMD 模块。注意不同的扩展名:.mjs 表示 ES 模块,而 .js 是 UMD 模块。如何在浏览器中加载 ES 模块的示例:

<script type="module" src="/path/to/js.cookie.mjs"></script>
<script type="module">
  import Cookies from '/path/to/js.cookie.mjs'

  Cookies.set('foo', 'bar')
</script>

并非所有浏览器都原生支持 ES 模块。出于这个原因,npm 包/发布提供了 ES UMD 模块变体,您可能希望将 ES 模块与 UMD 后备一起包含以解决此问题:

<script type="module" src="/path/to/js.cookie.mjs"></script>
<script nomodule defer src="/path/to/js.cookie.js"></script>

这里我们以延迟方式加载 nomodule 脚本,因为 ES 模块默认是延迟的。根据您使用库的方式,这可能不是绝对必要的。有关nomodule的使用,可以参考之前写的:你真的会用script吗?noscript又是什么?这篇文章的介绍。

3、使用CDN

官方问题提供了一个JSDELiVR的CDN服务地址。大家可以点击访问。可以根据需求选择配置CDN包含的文件,我这边给一个包含完整js-cookie的CDN使用代码:

<script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.1/dist/js.cookie.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.1/index.min.js"></script>

ES Module

如何从另一个模块导入 ES 模块的示例:

import Cookies from 'js-cookie'

Cookies.set('foo', 'bar')

基本用法

  • 创建一个在整个网站上有效的 cookie
Cookies.set('name', 'value')
  • 创建一个从现在起 7 天后过期的 cookie,在整个站点中有效
Cookies.set('name', 'value', { expires: 7 })
  • 创建一个过期cookie,对当前页面的路径有效
Cookies.set('name', 'value', { expires: 7, path: '' })
  • 读取cookie
Cookies.get('name') // => 'value'
Cookies.get('nothing') // => undefined
  • 读取所有可见cookie
Cookies.get() // => { name: 'value' }

注意:无法通过传递其中一个 cookie 属性(在写入相关 cookie 时可能会或可能不会使用)来读取特定 cookie:

Cookies.get('foo', { domain: 'sub.example.com' }) // `domain` won't have any effect...!

名称为 foo 的 cookie 仅在 .get() 上可用,前提是它从调用代码的位置可见;阅读时域和/或路径属性将不起作用。

  • 删除cookie
Cookies.remove('name')
  • 删除一个对当前页面路径有效的cookie
Cookies.set('name', 'value', { path: '' })
Cookies.remove('name') // fail!
Cookies.remove('name', { path: '' }) // removed!

非常重要的一点,删除 cookie 并且您不依赖默认属性时,您必须传递用于设置 cookie 的完全相同的路径和域属性:

Cookies.remove('name', { path: '', domain: '.yourdomain.com' })


注意:删除不存在的 cookie 既不会引发任何异常,也不会返回任何值。

命名空间冲突

如果存在与命名空间 Cookie 冲突的任何危险,则 noConflict 方法将允许您定义一个新的命名空间并保留原来的命名空间。这在第三方站点上运行脚本时特别有用,例如作为小部件或 SDK 的一部分。

// Assign the js-cookie api to a different variable and restore the original "window.Cookies"
var Cookies2 = Cookies.noConflict()
Cookies2.set('name', 'value')

注意: .noConflict 方法在使用 AMD 或 CommonJS 时不是必需的,因此它不会在这些环境中公开。

编码问题

该项目符合 RFC 6265。 cookie-name 或 cookie-value 中不允许的所有特殊字符都使用百分比编码使用每个对应的 UTF-8 Hex 进行编码。
cookie-name 或 cookie-value 中唯一允许且仍在编码的字符是百分比 % 字符,它被转义以便将百分比输入解释为文字。
请注意,默认的编码/解码策略意味着只能在 js-cookie 读取/写入的 cookie 之间进行互操作。要覆盖默认的编码/解码策略,您需要使用转换器。

Cookie属性


Cookie 属性默认值可以通过 withAttributes() 创建 api 实例来全局设置,或者通过将普通对象作为最后一个参数传递来单独为每次调用 Cookies.set(…) 设置。每次调用属性会覆盖默认属性。

expires

定义何时删除 cookie。值必须是一个数字,它将被解释为从创建之日算起的天数或一个 Date 实例。如果省略,cookie 将成为会话 cookie。

要创建在不到一天内过期的 cookie,您可以查看 Wiki 上的常见问题解答。

  • 默认值:当用户关闭浏览器时,Cookie 被删除。
  • 示例
Cookies.set('name', 'value', { expires: 365 })
Cookies.get('name') // => 'value'
Cookies.remove('name')

path

一个字符串,指示 cookie 可见的路径。

  • 默认值:/
  • 示例
Cookies.set('name', 'value', { path: '' })
Cookies.get('name') // => 'value'
Cookies.remove('name', { path: '' })

关于 Internet Explorer 的注意事项:

由于底层 WinINET InternetGetCookie 实现中的一个模糊错误,如果使用包含文件名的路径属性设置 IE 的 document.cookie,它将不会返回 cookie。

 Internet Explorer Cookie Internals (FAQ)


这意味着不能使用 window.location.pathname 设置路径,以防此类路径名包含如下文件名:/check.html(或至少,无法正确读取此类 cookie)。

事实上,您绝不应该允许不受信任的输入来设置 cookie 属性,否则您可能会受到 XSS 攻击。

domain

一个字符串,指示 cookie 应该可见的有效域。该 cookie 也将对所有子域可见。

  • 默认值:Cookie 仅对创建 cookie 的页面的域或子域可见,Internet Explorer 除外(见下文)。
  • 示例:

假设在 enjoytoday.cn 上创建了一个 cookie

Cookies.set('name', 'value', { domain: 'cookie.enjoytoday.cn' })
Cookies.get('name') // => undefined (need to read at 'cookie.enjoytoday.cn')

关于 Internet Explorer 默认行为的注意事项:

Q3:如果我没有为 cookie 指定 DOMAIN 属性,IE 还是会将它发送到所有嵌套的子域?
答:是的,example.com 上设置的 cookie 将被发送到 sub2.sub1.example.com。
Internet Explorer 在这方面不同于其他浏览器。

Internet Explorer Cookie Internals (FAQ)

这意味着如果您省略domain属性,它将在 IE 中对子域可见。

secure


true 或 false,指示 cookie 传输是否需要安全协议 (https)。

  • 默认值:没有安全协议要求。
  • 示例
Cookies.set('name', 'value', { secure: true })
Cookies.get('name') // => 'value'
Cookies.remove('name')

sameSite

一个字符串,允许控制浏览器是否与跨站点请求一起发送 cookie。

  • 默认值:不设置
  • 示例
Cookies.set('name', 'value', { sameSite: 'strict' })
Cookies.get('name') // => 'value'
Cookies.remove('name')

设置默认值:

const api = Cookies.withAttributes({ path: '/', domain: '.example.com' })

转换

读取

创建一个覆盖默认解码实现的新 api 实例。所有依赖于正确解码才能工作的 get 方法,例如 Cookies.get() Cookies.get('name'),将为每个 cookie 运行给定的转换器。返回的值将用作 cookie 值。

读取只能使用转义函数解码的 cookie 之一的示例:

document.cookie = 'escaped=%u5317'
document.cookie = 'default=%E5%8C%97'
var cookies = Cookies.withConverter({
  read: function (value, name) {
    if (name === 'escaped') {
      return unescape(value)
    }
    // Fall back to default for all other cookies
    return Cookies.converter.read(value, name)
  }
})
cookies.get('escaped') // 北
cookies.get('default') // 北
cookies.get() // { escaped: '北', default: '北' }

写入

创建一个覆盖默认编码实现的新 api 实例:

Cookies.withConverter({
  write: function (value, name) {
    return value.toUpperCase()
  }
})

TypeScript 申明

js-cookie提供了typesrcipt的types包以供下载查阅。

$ npm i @types/js-cookie

关于

如果你还想要了解js-cookie更多信息,可参考下方信息进行获取了解:

  • github仓库地址:https://github.com/js-cookie/js-cookie
  • 百度网盘下载地址
链接:https://pan.baidu.com/s/1NLYm6mbikUF_WLpsjcQq1A
提取码:1qct

当前版本为v3.0.1, 如需更新版本请访问github自行下载。

Python Sanic 使用开发指南

胖蔡阅读(335)

Python Sanic是一款Python的异步Web框架,Sanic框架是基于Python 3.5+实现的一个类Flask框架,Sanic基于asyncio 库使用async/await实现python的异步语法,编写高效非阻塞代码。如下是Sanic的 21.3.x版本的中文使用文档。

基础

进阶

测试与部署