整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

VUE 响应式原理源码:带你一步精通 VUE - 原

VUE 响应式原理源码:带你一步精通 VUE - 原力计划

者 | 爱编程的小和尚

责编 | 王晓曼

出品 | CSDN博客

学过 VUE 如果不了解响应式的原理,怎么能说自己熟练使用 VUE,要是没有写过一个简易版的 VUE 怎么能说自己精通 VUE,这篇文章通过300多行代码,带你写一个简易版的 VUE,主要实现 VUE 数据响应式 (数据劫持结合发布者-订阅者)、数组的变异方法、编译指令,数据的双向绑定的功能。

本文需要有一定 VUE 基础,并不适合新手学习。

文章较长,且有些难度,建议大家,找一个安静的环境,并在看之前沐浴更衣,保持编程的神圣感。下面是实现的简易版VUE 的源码地址,一定要先下载下来!因为文章中的并非全部的代码。

Github源码地址:https://github.com/young-monk/myVUE.git

前言

在开始学习之前,我们先来了解一下什么是 MVVM ,什么是数据响应式。

我们都知道 VUE 是一个典型的 MVVM 思想,由数据驱动视图。

那么什么是 MVVM 思想呢?

MVVM是Model-View-ViewModel,是把一个系统分为了模型( model )、视图( view )和 view-model 三个部分。

VUE在 MVVM 思想下,view 和model 之间没有直接的联系,但是 view 和 view-model 、model和 view-model之间时交互的,当 view 视图进行 dom 操作等使数据发生变化时,可以通过 view-model 同步到 model 中,同样的 model 数据变化也会同步到 view 中。

那么实现数据响应式都有什么方法呢?1、发布者-订阅者模式:当一个对象(发布者)状态发生改变时,所有依赖它的对象(订阅者)都会得到通知。通俗点来讲,发布者就相当于报纸,而订阅者相当于读报纸的人。2、脏值检查:通过存储旧的数据,和当前新的数据进行对比,观察是否有变更,来决定是否更新视图。angular.js 就是通过脏值检查的方式。最简单的实现方式就是通过 setInterval 定时轮询检测数据变动,但这样无疑会增加性能,所以, angular 只有在指定的事件触发时进入脏值检测。3、数据劫持:通过 Object.defineProperty 来劫持各个属性的 setter,getter,在数据变动时触发相应的方法。VUE是如何实现数据响应式的呢?

VUE.js 则是通过数据劫持结合发布者-订阅者模式的方式。

当执行 new VUE 时,VUE 就进入了初始化阶段,VUE会对指令进行解析(初始化视图,增加订阅者,绑定更新函数),同时通过 Obserber会遍历数据并通过 Object.defineProperty 的 getter 和 setter 实现对的监听, 当数据发生变化的时候,Observer 中的 setter 方法被触发,setter 会立即调用Dep.notify, Dep 开始遍历所有的订阅者,并调用订阅者的 update 方法,订阅者收到通知后对视图进行相应的更新。

我来依次介绍一下图中的重要的名词:1、Observer:数据监听器,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者,内部采用 Object.defineProperty 的 getter 和 setter 来实现2、Compile:指令解析器,它的作用对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数3、Dep:订阅者收集器或者叫消息订阅器都可以,它在内部维护了一个数组,用来收集订阅者,当数据改变触发 notify 函数,再调用订阅者的 update 方法4、Watcher:订阅者,它是连接 Observer 和 Compile 的桥梁,收到消息订阅器的通知,更新视图5、Updater:视图更新所以我们想要实现一个 VUE 响应式,需要完成数据劫持、依赖收集、 发布者订阅者模式。下面我来介绍我模仿源码实现的功能:

1、数据的响应式、双向绑定,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者

2、解析 VUE 常用的指令 v-html,v-text,v-bind,v-on,v-model,包括( @ 和 : )

3、数组变异方法的处理

4、在 VUE 中使用 this 访问或改变 data 中的数据

我们想要完成以上的功能,需要实现如下类和方法:

1、实现 Observe r类:对所有的数据进行监听

2、实现 array 工具方法:对变异方法的处理

3、实现 Dep 类:维护订阅者

4、实现 Watcher 类:接收 Dep 的更新通知,用于更新视图

5、实现 Compile 类:用于对指令进行解析

6、实现一个 CompileUtils 工具方法,实现通过指令更新视图、绑定更新函数Watcher

7、实现 this.data 代理:实现对 this. data 代理:实现对 this.data 代理:实现对 this.data 代理,可以直接在 VUE 中使用 this 获取当前数据

我是使用了webpack作为构建工具来协同开发的,所以在我实现的VUE响应式中会用到ES6模块化,webpack的相关知识。

实现 Observer 类

我们都知道要用 Obeject.defineProperty 来监听属性的数据变化,我们需要对 Observer 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter ,这样的话,当给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。当然我们在新增加数据的时候,也要对新的数据对象进行递归遍历,加上 setter 和 getter 。

但我们要注意数组,在处理数组时并不是把数组中的每一个元素都加上 setter 和 getter ,我们试想一下,一个从后端返回的数组数据是非常庞大的,如果为每个属性都加上 setter 和 getter ,性能消耗是十分巨大的。我们想要得到的效果和所消耗的性能不成正比,所以在数组方面,我们通过对数组的7 个变异方法来实现数据的响应式。只有通过数组变异方法来修改和删除数组时才会重新渲染页面。

那么监听到变化之后是如何通知订阅者来更新视图的呢?我们需要实现一个Dep(消息订阅器),其中有一个 notify 方法,是通知订阅者数据发生了变化,再让订阅者来更新视图。

我们怎么添加订阅者呢?我们可以通过 new Dep,通过 Dep 中的addSaubs 方法来添加订阅者。我们来看一下具体代码。

我们首先需要声明一个 Observer 类,在创建类的时候,我们需要创建一个消息订阅器,判断一下是否是数组,如果是数组,我们便改造数组,如果是对象,我们便需要为对象的每一个属性都加入 setter 和 getter 。

import { arrayMethods } from './array' //数组变异方法处理 
class Observer {
constructor(data) {
//用于对数组进行处理,存放数组的观察者watcher
this.dep=new Dep
if (Array.isArray(data)) {
//如果是数组,使用数组的变异方法
data.__proto__=arrayMethods
//把数组数据添加 __ob__ 一个Observer,当使用数组变异方法时,可以更新视图
data.__ob__=this
//给数组的每一项添加数据劫持(setter/getter处理)
this.observerArray(data)
} else {
//非数组数据添加数据劫持(setter/getter处理)
this.walk(data)
}
}
}

在上面,我们给 data 的__proto__原型链重新赋值,我们来看一下 arrayMethods 是什么,arrayMethods 是 array.js 文件中,抛出的一个新的 Array 原型:

// 获取Array的原型链
const arrayProto=Array.prototype;
// 重新创建一个含有对应原型的对象,在下面称为新Array
const arrayMethods=Object.create(arrayProto);
// 处理7个数组变异方法
['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'].forEach(ele=> {
//修改新Array的对应的方法
arrayMethods[ele]=function {
// 执行数组的原生方法,完成其需要完成的内容
arrayProto[ele].call(this, ...arguments)
// 获取Observer对象
const ob=this.__ob__
// 更新视图
ob.dep.notify
}
})
export {
arrayMethods
}

此时呢,我们就拥有了数组的变异方法,我们还需要通过 observerArray 方法为数组的每一项添加 getter 和setter ,注意,此时的每一项只是最外面的一层,并非递归遍历。

//循环遍历数组,为数组每一项设置setter/getter
observerArray(items) {
for (let i=0; i < items.length; i++) {
this.observer(items[i])
}
}

如果是一个对象的话,我们就要对对象 的每一个属性递归遍历,通过 walk 方法:

walk(data) {
//数据劫持
if (data && typeof data==="object") {
for (const key in data) {
//绑定setter和getter
this.defineReactive(data, key, data[key])
}
}
}

在上面的调用了 defineReactive ,我们来看看这个方法是干什么的?这个方法就是设置数据劫持的,每一行都有注释。

//数据劫持,设置 setter/getteer
defineReactive(data, key, value) {
//如果是数组的话,需要接受返回的Observer对象
let arrayOb=this.observer(value)
//创建订阅者/收集依赖
const dep=new Dep
//setter和getter处理
Object.defineProperty(data, key, {
//可枚举的
enumerable: true,
//可修改的
configurable: false,
get {
//当 Dep 有 watcher 时, 添加 watcher
Dep.target && dep.addSubs(Dep.target)
//如果是数组,则添加上数组的观察者
Dep.target && arrayOb && arrayOb.dep.addSubs(Dep.target)
return value
},
set: (newVal)=> {
//新旧数据不相等时更改
if (value !==newVal) {
//为新设置的数据添加setter/getter
arrayOb=this.observer(newVal);
value=newVal
//通知 dep 数据发送了变化
dep.notify
}
}
})
}
}

我们需要注意的是,在上面的图解中,在 Observer 中,如果数据发生变化,会通知消息订阅器,那么在何时绑定消息订阅器呢?就是在设置 setter 和 getter 的时候,创建一个 Dep,并为 Dep添加订阅者,Dep.target&& dep.addSubs(Dep.target),通过调用 dep 的 addSubs 方法添加订阅者。

实现 Dep

Dep 是消息订阅器,它的作用就是维护一个订阅者数组,当数据发送变化是,通知对应的订阅者,Dep中有一个 notify 方法,作用就是通知订阅者,数据发送了变化:

// 订阅者收集器
export default class Dep {
constructor {
//管理的watcher的数组
this.subs=
}
addSubs(watcher) {
//添加watcher
this.subs.push(watcher)
}
notify {
//通知watcher更新dom
this.subs.forEach(w=> w.update)
}
}

实现 watcher

Watcher 就是订阅者, watcher 是 Observer 和 Compile 之间通信的桥梁,当数据改变时,接收到 Dep 的通知(Dep 的notify()方法),来调用自己的update方法,触发 Compile 中绑定的回调,达到更新视图的目的。

import Dep from './dep'
import { complieUtils } from './utils'
export default class Watcher {
constructor(vm, expr, cb) {
//当前的vue实例
this.vm=vm;
//表达式
this.expr=expr;
//回调函数,更新dom
this.cb=cb
//获取旧的数据,此时获取旧值的时候,Dep.target会绑定上当前的this
this.oldVal=this.getOldVal
}
getOldVal {
//将当前的watcher绑定起来
Dep.target=this
//获取旧数据
const oldVal=complieUtils.getValue(this.expr, this.vm)
//绑定完成后,将绑定的置空,防止多次绑定
Dep.target=
return oldVal
}
update {
//更新函数
const newVal=complieUtils.getValue(this.expr, this.vm)
if (newVal !==this.oldVal || Array.isArray(newVal)) {
//条用更新在compile中创建watcher时传入的回调函数
this.cb(newVal)
}
}
}

上面中用到了 ComplieUtils 中的 getValue 方法,会在下面讲,主要作用是获取到指定表达式的值。

我们把整个流程分成两条路线的话:

newVUE==> Observer数据劫持==> 绑定Dep==> 通知watcher==> 更新视图newVUE==> Compile解析模板指令==> 初始化视图 和 绑定watcher

此时,我们第一条线的内容已经实现了,我们再来实现一下第二条线。

实现 Compile

Compile 主要做的事情是解析模板指令,将模板中的变量替换成数据,初始化渲染页面视图。同时也要绑定更新函数,添加订阅者。

因为在解析的过程中,会多次的操作 dom,为提高性能和效率,会先将 VUE 实例根节点的 el 转换成文档碎片 fragment 进行解析编译操作,解析完成,再将 fragment 添加回原来的真实 dom 节点中。

class Complie {
constructor(el, vm) {
this.el=this.isNodeElement(el) ? el : document.querySelector(el);
this.vm=vm;
// 1、将所有的dom对象放到fragement文档碎片中,防止重复操作dom,消耗性能
const fragments=this.nodeTofragments(this.el)
// 2、编译模板
this.complie(fragments)
// 3、追加子元素到根元素
this.el.appendChild(fragments)
}
}

我们可以看到,Complie 中主要进行了三步,第一步 nodeTofragments 是讲所有的 dom 节点放到文档碎片中操作,最后一步,是把解析好的 dom 元素,从文档碎片重新加入到页面中,这两步的具体方法,大家去下载我的源码,看一下就明白了,有注释。我就不再解释了。

我们来看一下第二步,编译模板:

 complie(fragments) {
//获取所有节点
const nodes=fragments.childNodes;
[...nodes].forEach(ele=> {
if (this.isNodeElement(ele)) {
//1. 编译元素节点
this.complieElement(ele)
} else {
//编译文本节点
this.complieText(ele)
}
//如果有子节点,循环遍历,编译指令
if (ele.childNodes && ele.childNodes.length) {
this.complie(ele)
}
})
}

我们要知道,模板可能有两种情况,一种是文本节点(含有双大括号的插值表达式)和元素节点(含有指令)。我们获取所有节点后对每个节点进行判断,如果是元素节点,则用解析元素节点的方法,如果是文本节点,则调用解析文本的方法。

complieElement(node) {
//1.获取所有的属性
const attrs=node.attributes;
//2.筛选出是属性的
[...attrs].forEach(attr=> {
//attr是一个对象,name是属性名,value是属性值
const {name,value}=attr
//判断是否含有v-开头 如:v-html
if (name.startsWith("v-")) {
//将指令分离 text, html, on:click
const [, directive]=name.split("-")
//处理on:click或bind:name的情况 on,click
const [dirName, paramName]=directive.split(":")
//编译模板
complieUtils[dirName](node, value, this.vm, paramName)
//删除属性,在页面中的dom中不会再显示v-html这种指令的属性
node.removeAttribute(name)
} else if (name.startsWith("@")) {
// 如果是事件处理 @click='handleClick'
let [, paramName]=name.split('@');
complieUtils['on'](node, value, this.vm, paramName);
node.removeAttribute(name);
} else if (name.startsWith(":")) {
// 如果是事件处理 :href='...'
let [, paramName]=name.split(':');
complieUtils['bind'](node, value, this.vm, paramName);
node.removeAttribute(name);
}
})

}

我们在编译模板中调用了 complieUtils[dirName](node, value, this.vm, paramName)方法,这是工具类中的一个方法,用于处理指令。

我们再来看看文本节点,文本节点就相对比较简单,只需要匹配{{}}形式的插值表达式就可以了,同样的调用工具方法,来解析。

complieText(node) {
//1.获取所有的文本内容
const text=node.textContent
//匹配{{}}
if (/\{\{(.+?)\}\}/.test(text)) {
//编译模板
complieUtils['text'](node, text, this.vm)
}
}

上面用来这么多工具方法,我们来看看到底是什么。

实现 ComplieUtils 工具方法

这个方法主要是对指令进行处理,获取指令中的值,并在页面中更新相应的值,同时我们在这里要绑定 watcher 的回调函数。

我来以 v-text 指令来解释,其他指令都有注释,大家自己看。

import Watcher from './watcher'
export const complieUtils={
//处理text指令
text(node, expr, vm) {
let value;
if (/\{\{.+?\}\}/.test(expr)) {
//处理 {{}}
value=expr.replace(/\{\{(.+?)\}\}/g, (...args)=> {
//绑定观察者/更新函数
new Watcher(vm, args[1],=> {
//第二个参数,传入回调函数
this.updater.updaterText(node, this.getContentVal(expr, vm))
})
return this.getValue(args[1], vm)
})
} else {
//v-text
new Watcher(vm, expr, (newVal)=> {
this.updater.updaterText(node, newVal)
})
//获取到value值
value=this.getValue(expr, vm)
}
//调用更新函数
this.updater.updaterText(node, value)
},
}

Text 处理函数是对 dom 元素的 TextContent 进行操作的,所以有两种情况,一种是使用 v-text 指令,会更新元素的 textContent,另一种情况是{{}} 的插值表达式,也是更新元素的 textContent。

在此方法中我们先判断是哪一种情况,如果是 v-text 指令,那么就绑定一个 watcher 的回调,获取到 textContent 的值,调用 updater.updaterText 在下面讲,是更新元素的方法。如果是双大括号的话,我们就要对其进行特殊处理,首先是将双大括号替换成指定的变量的值,同时为其绑定 watcher 的回调。

//通过表达式, vm获取data中的值, person.name
getValue(expr, vm) {
return expr.split(".").reduce((data, currentVal)=> {
return data[currentVal]
}, vm.$data)
},

获取 textContent 的值是用一个 reduce 函数,用法在最后面的链接中,因为数据可能是 person.name 我们需要获取到最深的对象的值。

 //更新dom元素的方法
updater: {
//更新文本
updaterText(node, value) {
node.textContent=value
}
}

updater.updaterText更新dom的方法,其实就是对 textContent 重新赋值。

我们再来将一下v-model指令,实现双向的数据绑定,我们都知道,v-model其实实现的是 input 事件和 value 之间的语法糖。所以我们这里同样的监听一下当前 dom 元素的 input 事件,当数据改变时,调用设置新值的方法:

//处理model指令
model(node, expr, vm) {
const value=this.getValue(expr, vm)
//绑定watcher
new Watcher(vm, expr, (newVal)=> {
this.updater.updaterModel(node, newVal)
})
//双向数据绑定
node.addEventListener("input", (e)=> {
//设值方法
this.setVal(expr, vm, e.target.value)
})
this.updater.updaterModel(node, value)
},

这个方法同样是通过 reduce 方法,为对应的变量设置成新的值,此时数据改变了,会自动调用更新视图的方法,我们在之前已经实现了。

//通过表达式,vm,输入框的值,实现设置值,input中v-model双向数据绑定
setVal(expr, vm, inputVal) {
expr.split(".").reduce((data, currentVal)=> {
data[currentVal]=inputVal
}, vm.$data)
},

实现VUE

最后呢,我们就要来整合这些类和工具方法,在创建一个 VUE 实例的时候,我们先获取 options 中的参数,然后对起进行数据劫持和编译模板:

class Vue {
constructor(options) {
//获取模板
this.$el=options.el;
//获取data中的数据
this.$data=options.data;
//将对象中的属性存起来,以便后续使用
this.$options=options
//1.数据劫持,设置setter/getter
new Observer(this.$data)
//2.编译模板,解析指令
new Complie(this.$el, this)
}
}

此时我们想要使用 VUE 中的数据,比如我们想要在 vm 对象中使用person.name, 必须用 this.$data.person.name 才能获取到,如果我们想在 vm 对象中使用 this.person.name 直接修改数据,就需要代理一下 this.$data 。其实就是将当前的 this.$data 中的数据放到全局中进行监听。

export default class Vue {
constructor(options) {
//...
//1.数据劫持,设置setter/getter
//2.编译模板,解析指令
if (this.$el) { //如果有模板
//代理this
this.proxyData(this.$data)
}
}
proxyData(data) {
for (const key in data) {
//将当前的数据放到全局指向中
Object.defineProperty(this, key, {
get {
return data[key];
},
set(newVal) {
data[key]=newVal
}
})
}
}
}

文章到了这里,就实现了一个简易版的 VUE,建议大家反复学习,仔细体验,细细品味。

在文章的最后,我通过问、答的形式,来解答一些常见的面试题:

问:什么时候页面会重新渲染?

答:数据发生改变,页面就会重新渲染,但数据驱动视图,数据必须先存在,然后才能实现数据绑定,改变数据,页面才会重新渲染。

问:什么时候页面不会重新渲染?

答:有3种情况不会重新渲染:

1、未经声明和未使用的变量,修改他们,都不会重新渲染页面

2、通过索引的方式和更改长度的方式更改数组,都不会重新渲染页面

3、增加和删除对象的属性,不会重新渲染页面

问:如何使 未声明/未使用的变量、增加/删除对象属性可以使页面重新渲染?

答:添加利用 vm.$set/VUE.set,删除利用vm.$delete/VUE.delete方法

问:如何更改数组可以使页面重新渲染?

答:可以使用数组的变异方法(共 7 个):push、pop、unshift、shift、splice、sort、reverse

问:数据更新后,页面会立刻重新渲染么?

答:更改数据后,页面不会立刻重新渲染,页面渲染的操作是异步执行的,执行完同步任务后,才会执行异步的

同步队列,异步队列(宏任务、微任务)

问:如果更改了数据,想要在页面重新渲染后再做操作,怎么办?

答:可以使用 vm.$nextTick 或 VUE.nextTick

问:来介绍一下vm.$nextTick 和 VUE.nextTick 吧。

答:我们来看个小例子就明白啦:

<div id="app">{{ name }}</div>
<script>
const vm=new Vue({
el: '#app',
data: {
name: 'monk'
}
})
vm.name='the young monk';
console.log(vm.name); // the young monk 此时数据已更改
console.log(vm.$el.innerHTML); // monk 此时页面还未重新渲染
// 1. 使用vm.$nextTick
vm.$nextTick(=> {
console.log(vm.$el.innerHTML); // the young monk 此时数据已更改
})
// 2. 使用Vue.nextTick
Vue.nextTick(=> {
console.log(vm.$el.innerHTML); // the young monk 此时数据已更改
})
</script>

问:vm.$nextTick 和 VUE.nextTick 有什么区别呢 ?

答:VUE.nextTick 内部函数的 this 指向 Window,vm.$nextTick 内部函数的 this 指向 VUE 实例对象。

Vue.nextTick(function  {
console.log(this); // window
})
vm.$nextTick(function {
console.log(this); // vm实例
})

问:vm.$nextTick 和 VUE.nextTick 是通过什么实现的呢?

答:二者都是等页面渲染后执行的任务,都是使用微任务。

 if(typeof Promise !=='undefined') {
// 微任务
// 首先看一下浏览器中有没有promise
// 因为IE浏览器中不能执行Promise
const p=Promise.resolve;

} else if(typeof MutationObserver !=='undefined') {
// 微任务
// 突变观察
// 监听文档中文字的变化,如果文字有变化,就会执行回调
// vue的具体做法是:创建一个假节点,然后让这个假节点稍微改动一下,就会执行对应的函数
} else if(typeof setImmediate !=='undefined') {
// 宏任务
// 只在IE下有
} else {
// 宏任务
// 如果上面都不能执行,那么则会调用setTimeout
}

同样的这也是 VUE 的一个小缺点:VUE 一直是等主线程执行完以后再执行渲染任务,如果主线程卡死,则永远渲染不出来。

问:利用 Object.defineProperty 实现响应式有什么缺点?

答:

1、天生就需要进行递归

2、监听不到数组不存在的索引的改变

3、监听不到数组长度的改变

4、监听不到对象的增删

版权声明:本文为CSDN博主「爱编程的小和尚」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/Newbie___/article/details/105973085

?雷军:4G 手机已清仓,全力转 5G;QQ音乐播放中途插语音广告引热议;Wine 5.9 发布 | 极客头条

?中国 AI 应用元年来了!

?新基建东风下,开发者这样抓住工业互联网风口!

?15 岁黑进系统,发挑衅邮件意外获 Offer,不惑之年捐出全部财产,Twitter CEO 太牛了!

?避坑!使用 Kubernetes 最易犯的 10 个错误

?必读!53个Python经典面试题详解

?赠书 | 1月以来 Tether 增发47亿 USDT,美元都去哪儿了?

期回顾

「JavaScript 从入门到精通」1.语法和数据类型

「JavaScript 从入门到精通」2.流程控制和错误处理

「JavaScript 从入门到精通」3.循环和迭代

「JavaScript 从入门到精通」4.函数

「JavaScript 从入门到精通」5.表达式和运算符

「JavaScript 从入门到精通」6.数字

「JavaScript 从入门到精通」7.时间对象

前置知识:

JS中的正则表达式是用来匹配字符串中指定字符组合的模式。

另外需要记住:正则表达式也是对象。

1.创建正则表达式

  • 使用一个正则表达式字面量:

  • 使用RegExp对象:
  • new RegExp(str[, attr])接收2个参数,str是一个字符串,指定正则表达式匹配规则,attr可选,表示匹配模式,值有g(全局匹配),i(区分大小写的匹配)和m(多行匹配)。

正则表达式的返回值,是一个新的RegExp对象,具有指定的模式和标志。

返回信息介绍:

关于正则表达式的一些方法属性,文章后面介绍,这里先复习定义和使用。

2.使用正则表达式

JS的正则表达式可以被用于:

  • RegExp对象的exec和test方法;
  • String对象的match、replace、search和split方法。

2.1 RegExp对象方法

  • 2.1.1 exec(str)

str: 需要检索的字符串。

若检索成功,返回匹配的数组,否则返回null。

返回信息介绍:

  • 2.1.2 test(str)

str:需要检索的字符串。

若匹配成功返回true否则false。

等价于 reg.exec(str) !=null。

^str表示匹配以str开头的字符串,这些符号文章后面会介绍。

2.2 String对象方法

  • 2.2.1 search

str.search(reg):

str:被检索的源字符串。

reg:可以是需要检索的字符串,也可以是需要检索的RegExp对象,可以添加标志,如i。

若检索成功,返回第一个与RegExp对象匹配的字符串的起始位置,否则返回-1。

  • 2.2.2 match

str.match(reg):

str:被检索的源字符串。

reg:可以是需要检索的字符串,也可以是需要检索的RegExp对象,可以添加标志,如i。

若检索成功,返回与reg匹配的所有结果的一个数组,数组的第一项是进行匹配完整的字符串,之后的项是用圆括号捕获的结果,否则返回null。

'see Chapter 3.4.5.1' 是整个匹配。

'Chapter 3.4.5.1' 被'(chapter \d+(\.\d)*)'捕获。

'.1' 是被'(\.\d)'捕获的最后一个值。

'index' 属性(22)是整个匹配从零开始的索引。

'input' 属性是被解析的原始字符串。

  • 2.2.3 replace

将字符串中指定字符替换成其他字符,或替换成一个与正则表达式匹配的字符串。

str.replace(sub/reg,val):

  • str: 源字符串
  • sub: 使用字符串来检索被替换的文本
  • reg: 使用RegExp对象来检索来检索被替换的文本
  • val: 指定替换文本
  • 返回替换成功之后的字符串,不改变源字符串内容。

val可以使用特殊变量名:

  • 2.2.4 split

将一个字符串,按照指定符号分割成一个字符串数组。

str.split(sub[, maxlength]):

  • str: 源字符串
  • sub: 指定的分割符号或正则
  • maxlength: 源字符串

2.3 使用情况

  • 当我们想要查找一个字符串中的一个匹配是否找到,可以用test或search方法。
  • 当我们想要得到匹配的更多信息,我们就需要用到exec或match方法。

3.正则表达式符号介绍

详细的每个符号的用法,可以查阅 W3school JavaScript RegExp 对象

3.1 修饰符

修饰符描述i执行对大小写不敏感的匹配。g执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)。m执行多行匹配。

3.2 方括号

用于查找指定返回之内的字符:

.3 元字符

元字符是拥有特殊含义的字符:

3.4 量词

4. 正则表达式拓展(ES6)

4.1 介绍

在ES5中有两种情况。

  • 参数是字符串,则第二个参数为正则表达式的修饰符。

  • 参数是正则表达式,返回一个原表达式的拷贝,且不能有第二个参数,否则报错。

ES6中使用:

第一个参数是正则对象,第二个是指定修饰符,如果第一个参数已经有修饰符,则会被第二个参数覆盖。

new RegExp(/abc/ig, 'i');

4.2 字符串的正则方法

常用的四种方法:match()、replace()、search()和split()。

4.3 u修饰符

添加u修饰符,是为了处理大于uFFFF的Unicode字符,即正确处理四个字节的UTF-16编码。

/^\uD83D/u.test('\uD83D\uDC2A'); // false
/^\uD83D/.test('\uD83D\uDC2A'); // true

由于ES5之前不支持四个字节UTF-16编码,会识别为两个字符,导致第二行输出true,加入u修饰符后ES6就会识别为一个字符,所以输出false。

注意:

加上u修饰符后,会改变下面正则表达式的行为:

  • (1)点字符 点字符(.)在正则中表示除了换行符以外的任意单个字符。对于码点大于0xFFFF的Unicode字符,点字符不能识别,必须加上u修饰符。
var a="";
/^.$/.test(a); // false
/^.$/u.test(a); // true
  • (2)Unicode字符表示法 使用ES6新增的大括号表示Unicode字符时,必须在表达式添加u修饰符,才能识别大括号。
/\u{61}/.test('a'); // false
/\u{61}/u.test('a'); // true
/\u{20BB7}/u.test(''); // true
  • (3)量词 使用u修饰符后,所有量词都会正确识别码点大于0xFFFF的 Unicode 字符。
/a{2}/.test('aa'); // true
/a{2}/u.test('aa'); // true
/{2}/.test(''); // false
/{2}/u.test(''); // true
  • (4)i修饰符 不加u修饰符,就无法识别非规范的K字符。
/[a-z]/i.test('\u212A') // false
/[a-z]/iu.test('\u212A') // true

检查是否设置u修饰符: 使用unicode属性。

const a=/hello/;
const b=/hello/u;
a.unicode // false
b.unicode // true

4.4 y修饰符

y修饰符与g修饰符类似,也是全局匹配,后一次匹配都是从上一次匹配成功的下一个位置开始。区别在于,g修饰符只要剩余位置中存在匹配即可,而y修饰符是必须从剩余第一个开始。

lastIndex属性: 指定匹配的开始位置:

返回多个匹配:

一个y修饰符对match方法只能返回第一个匹配,与g修饰符搭配能返回所有匹配。

'a1a2a3'.match(/a\d/y); // ["a1"]
'a1a2a3'.match(/a\d/gy); // ["a1", "a2", "a3"]

检查是否使用y修饰符:

使用sticky属性检查。

const a=/hello\d/y;
a.sticky; // true

4.5 flags属性

flags属性返回所有正则表达式的修饰符。

/abc/ig.flags; // 'gi'

5. 正则表达式拓展(ES9)

在正则表达式中,点(.)可以表示任意单个字符,除了两个:用u修饰符解决四个字节的UTF-16字符,另一个是行终止符。

终止符即表示一行的结束,如下四个字符属于“行终止符”:

  • U+000A 换行符(\n)
  • U+000D 回车符(\r)
  • U+2028 行分隔符(line separator)
  • U+2029 段分隔符(paragraph separator)
/foo.bar/.test('foo\nbar')
// false

上面代码中,因为.不匹配\n,所以正则表达式返回false。

换个醒,可以匹配任意单个字符:

/foo[^]bar/.test('foo\nbar')
// true

ES9引入s修饰符,使得.可以匹配任意单个字符:

/foo.bar/s.test('foo\nbar') // true

这被称为dotAll模式,即点(dot)代表一切字符。所以,正则表达式还引入了一个dotAll属性,返回一个布尔值,表示该正则表达式是否处在dotAll模式。

const re=/foo.bar/s;
// 另一种写法
// const re=new RegExp('foo.bar', 's');
re.test('foo\nbar') // true
re.dotAll // true
re.flags // 's'

/s修饰符和多行修饰符/m不冲突,两者一起使用的情况下,.匹配所有字符,而^和$匹配每一行的行首和行尾。

公众号:前端自习课

、日期处理

1. 检查日期是否有效

该方法用于检测给出的日期是否有效:

const isDateValid=(...val)=> !Number.isNaN(new Date(...val).valueOf());

isDateValid("December 17, 1995 03:24:00");  // true
复制代码

2. 计算两个日期之间的间隔

该方法用于计算两个日期之间的间隔时间:

const dayDif=(date1, date2)=> Math.ceil(Math.abs(date1.getTime() - date2.getTime()) / 86400000)
    
dayDif(new Date("2021-11-3"), new Date("2022-2-1"))  // 90
复制代码

距离过年还有90天~

3. 查找日期位于一年中的第几天

该方法用于检测给出的日期位于今年的第几天:

const dayOfYear=(date)=> Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);

dayOfYear(new Date());   // 307
复制代码

2021年已经过去300多天了~

4. 时间格式化

该方法可以用于将时间转化为hour:minutes:seconds的格式:

const timeFromDate=date=> date.toTimeString().slice(0, 8);
    
timeFromDate(new Date(2021, 11, 2, 12, 30, 0));  // 12:30:00
timeFromDate(new Date());  // 返回当前时间 09:00:00
复制代码

二、字符串处理

1. 字符串首字母大写

该方法用于将英文字符串的首字母大写处理:

const capitalize=str=> str.charAt(0).toUpperCase() + str.slice(1)

capitalize("hello world")  // Hello world
复制代码

2. 翻转字符串

该方法用于将一个字符串进行翻转操作,返回翻转后的字符串:

const reverse=str=> str.split('').reverse().join('');

reverse('hello world');   // 'dlrow olleh'
复制代码

3. 随机字符串

该方法用于生成一个随机的字符串:

const randomString=()=> Math.random().toString(36).slice(2);

randomString();
复制代码

4. 截断字符串

该方法可以从指定长度处截断字符串:

const truncateString=(string, length)=> string.length < length ? string : `${string.slice(0, length - 3)}...`;

truncateString('Hi, I should be truncated because I am too loooong!', 36)   // 'Hi, I should be truncated because...'
复制代码

5. 去除字符串中的HTML

该方法用于去除字符串中的HTML元素:

const stripHtml=html=> (new DOMParser().parseFromString(html, 'text/html')).body.textContent || '';
复制代码

三、数组处理

1. 从数组中移除重复项

该方法用于移除数组中的重复项:

const removeDuplicates=(arr)=> [...new Set(arr)];

console.log(removeDuplicates([1, 2, 2, 3, 3, 4, 4, 5, 5, 6]));
复制代码

2. 判断数组是否为空

该方法用于判断一个数组是否为空数组,它将返回一个布尔值:

const isNotEmpty=arr=> Array.isArray(arr) && arr.length > 0;

isNotEmpty([1, 2, 3]);  // true
复制代码

3. 合并两个数组

可以使用下面两个方法来合并两个数组:

const merge=(a, b)=> a.concat(b);

const merge=(a, b)=> [...a, ...b];
复制代码

四、数字操作

1. 判断一个数是奇数还是偶数

该方法用于判断一个数字是奇数还是偶数:

const isEven=num=> num % 2===0;

isEven(996); 
复制代码

2. 获得一组数的平均值

const average=(...args)=> args.reduce((a, b)=> a + b) / args.length;

average(1, 2, 3, 4, 5);   // 3
复制代码

3. 获取两个整数之间的随机整数

该方法用于获取两个整数之间的随机整数

const random=(min, max)=> Math.floor(Math.random() * (max - min + 1) + min);

random(1, 50);
复制代码

4. 指定位数四舍五入

该方法用于将一个数字按照指定位进行四舍五入:

const round=(n, d)=> Number(Math.round(n + "e" + d) + "e-" + d)

round(1.005, 2) //1.01
round(1.555, 2) //1.56
复制代码

五、颜色操作

1. 将RGB转化为十六机制

该方法可以将一个RGB的颜色值转化为16进制值:

const rgbToHex=(r, g, b)=> "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);

rgbToHex(255, 255, 255);  // '#ffffff'
复制代码

2. 获取随机十六进制颜色

该方法用于获取一个随机的十六进制颜色值:

const randomHex=()=> `#${Math.floor(Math.random() * 0xffffff).toString(16).padEnd(6, "0")}`;

randomHex();
复制代码

六、浏览器操作

1. 复制内容到剪切板

该方法使用 navigator.clipboard.writeText 来实现将文本复制到剪贴板:

const copyToClipboard=(text)=> navigator.clipboard.writeText(text);

copyToClipboard("Hello World");
复制代码

2. 清除所有cookie

该方法可以通过使用 document.cookie 来访问 cookie 并清除存储在网页中的所有 cookie:

const clearCookies=document.cookie.split(';').forEach(cookie=> document.cookie=cookie.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date(0).toUTCString()};path=/`));
复制代码

3. 获取选中的文本

该方法通过内置的 getSelection 属性获取用户选择的文本:

const getSelectedText=()=> window.getSelection().toString();

getSelectedText();
复制代码

4. 检测是否是黑暗模式

该方法用于检测当前的环境是否是黑暗模式,它是一个布尔值:

const isDarkMode=window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches

console.log(isDarkMode)
复制代码

5. 滚动到页面顶部

该方法用于在页面中返回顶部:

const goToTop=()=> window.scrollTo(0, 0);

goToTop();
复制代码

6. 判断当前标签页是否激活

该方法用于检测当前标签页是否已经激活:

const isTabInView=()=> !document.hidden; 
复制代码

7. 判断当前是否是苹果设备

该方法用于检测当前的设备是否是苹果的设备:

const isAppleDevice=()=> /Mac|iPod|iPhone|iPad/.test(navigator.platform);

isAppleDevice();
复制代码

8. 是否滚动到页面底部

该方法用于判断页面是否已经底部:

const scrolledToBottom=()=> document.documentElement.clientHeight + window.scrollY >=document.documentElement.scrollHeight;
复制代码

9. 重定向到一个URL

该方法用于重定向到一个新的URL:

const redirect=url=> location.href=url

redirect("https://www.google.com/")
复制代码

10. 打开浏览器打印框

该方法用于打开浏览器的打印框:

const showPrintDialog=()=> window.print()
复制代码

七、其他操作

1. 随机布尔值

该方法可以返回一个随机的布尔值,使用Math.random()可以获得0-1的随机数,与0.5进行比较,就有一半的概率获得真值或者假值。

const randomBoolean=()=> Math.random() >=0.5;

randomBoolean();
复制代码

2. 变量交换

可以使用以下形式在不适用第三个变量的情况下,交换两个变量的值:

[foo, bar]=[bar, foo];
复制代码

3. 获取变量的类型

该方法用于获取一个变量的类型:

const trueTypeOf=(obj)=> Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();

trueTypeOf('');     // string
trueTypeOf(0);      // number
trueTypeOf();       // undefined
trueTypeOf(null);   // null
trueTypeOf({});     // object
trueTypeOf([]);     // array
trueTypeOf(0);      // number
trueTypeOf(()=> {});  // function
复制代码

4. 华氏度和摄氏度之间的转化

该方法用于摄氏度和华氏度之间的转化:

const celsiusToFahrenheit=(celsius)=> celsius * 9/5 + 32;
const fahrenheitToCelsius=(fahrenheit)=> (fahrenheit - 32) * 5/9;

celsiusToFahrenheit(15);    // 59
celsiusToFahrenheit(0);     // 32
celsiusToFahrenheit(-20);   // -4
fahrenheitToCelsius(59);    // 15
fahrenheitToCelsius(32);    // 0
复制代码

5. 检测对象是否为空

该方法用于检测一个JavaScript对象是否为空:

const isEmpty=obj=> Reflect.ownKeys(obj).length===0 && obj.constructor===Object;