者 | 愛編程的小和尚
責編 | 王曉曼
出品 | 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中的正則表達式是用來匹配字符串中指定字符組合的模式。
另外需要記住:正則表達式也是對象。
正則表達式的返回值,是一個新的RegExp對象,具有指定的模式和標志。
返回信息介紹:
關于正則表達式的一些方法屬性,文章后面介紹,這里先復習定義和使用。
JS的正則表達式可以被用于:
2.1 RegExp對象方法
str: 需要檢索的字符串。
若檢索成功,返回匹配的數組,否則返回null。
返回信息介紹:
str:需要檢索的字符串。
若匹配成功返回true否則false。
等價于 reg.exec(str) !=null。
^str表示匹配以str開頭的字符串,這些符號文章后面會介紹。
2.2 String對象方法
str.search(reg):
str:被檢索的源字符串。
reg:可以是需要檢索的字符串,也可以是需要檢索的RegExp對象,可以添加標志,如i。
若檢索成功,返回第一個與RegExp對象匹配的字符串的起始位置,否則返回-1。
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' 屬性是被解析的原始字符串。
將字符串中指定字符替換成其他字符,或替換成一個與正則表達式匹配的字符串。
str.replace(sub/reg,val):
val可以使用特殊變量名:
將一個字符串,按照指定符號分割成一個字符串數組。
str.split(sub[, maxlength]):
2.3 使用情況
詳細的每個符號的用法,可以查閱 W3school JavaScript RegExp 對象
3.1 修飾符
修飾符描述i執行對大小寫不敏感的匹配。g執行全局匹配(查找所有匹配而非在找到第一個匹配后停止)。m執行多行匹配。
3.2 方括號
用于查找指定返回之內的字符:
.3 元字符
元字符是擁有特殊含義的字符:
3.4 量詞
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修飾符后,會改變下面正則表達式的行為:
var a=""; /^.$/.test(a); // false /^.$/u.test(a); // true
/\u{61}/.test('a'); // false /\u{61}/u.test('a'); // true /\u{20BB7}/u.test(''); // true
/a{2}/.test('aa'); // true /a{2}/u.test('aa'); // true /{2}/.test(''); // false /{2}/u.test(''); // true
/[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'
在正則表達式中,點(.)可以表示任意單個字符,除了兩個:用u修飾符解決四個字節的UTF-16字符,另一個是行終止符。
終止符即表示一行的結束,如下四個字符屬于“行終止符”:
/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不沖突,兩者一起使用的情況下,.匹配所有字符,而^和$匹配每一行的行首和行尾。
公眾號:前端自習課
該方法用于檢測給出的日期是否有效:
const isDateValid=(...val)=> !Number.isNaN(new Date(...val).valueOf());
isDateValid("December 17, 1995 03:24:00"); // true
復制代碼
該方法用于計算兩個日期之間的間隔時間:
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天~
該方法用于檢測給出的日期位于今年的第幾天:
const dayOfYear=(date)=> Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);
dayOfYear(new Date()); // 307
復制代碼
2021年已經過去300多天了~
該方法可以用于將時間轉化為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
復制代碼
該方法用于將英文字符串的首字母大寫處理:
const capitalize=str=> str.charAt(0).toUpperCase() + str.slice(1)
capitalize("hello world") // Hello world
復制代碼
該方法用于將一個字符串進行翻轉操作,返回翻轉后的字符串:
const reverse=str=> str.split('').reverse().join('');
reverse('hello world'); // 'dlrow olleh'
復制代碼
該方法用于生成一個隨機的字符串:
const randomString=()=> Math.random().toString(36).slice(2);
randomString();
復制代碼
該方法可以從指定長度處截斷字符串:
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...'
復制代碼
該方法用于去除字符串中的HTML元素:
const stripHtml=html=> (new DOMParser().parseFromString(html, 'text/html')).body.textContent || '';
復制代碼
該方法用于移除數組中的重復項:
const removeDuplicates=(arr)=> [...new Set(arr)];
console.log(removeDuplicates([1, 2, 2, 3, 3, 4, 4, 5, 5, 6]));
復制代碼
該方法用于判斷一個數組是否為空數組,它將返回一個布爾值:
const isNotEmpty=arr=> Array.isArray(arr) && arr.length > 0;
isNotEmpty([1, 2, 3]); // true
復制代碼
可以使用下面兩個方法來合并兩個數組:
const merge=(a, b)=> a.concat(b);
const merge=(a, b)=> [...a, ...b];
復制代碼
該方法用于判斷一個數字是奇數還是偶數:
const isEven=num=> num % 2===0;
isEven(996);
復制代碼
const average=(...args)=> args.reduce((a, b)=> a + b) / args.length;
average(1, 2, 3, 4, 5); // 3
復制代碼
該方法用于獲取兩個整數之間的隨機整數
const random=(min, max)=> Math.floor(Math.random() * (max - min + 1) + min);
random(1, 50);
復制代碼
該方法用于將一個數字按照指定位進行四舍五入:
const round=(n, d)=> Number(Math.round(n + "e" + d) + "e-" + d)
round(1.005, 2) //1.01
round(1.555, 2) //1.56
復制代碼
該方法可以將一個RGB的顏色值轉化為16進制值:
const rgbToHex=(r, g, b)=> "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
rgbToHex(255, 255, 255); // '#ffffff'
復制代碼
該方法用于獲取一個隨機的十六進制顏色值:
const randomHex=()=> `#${Math.floor(Math.random() * 0xffffff).toString(16).padEnd(6, "0")}`;
randomHex();
復制代碼
該方法使用 navigator.clipboard.writeText 來實現將文本復制到剪貼板:
const copyToClipboard=(text)=> navigator.clipboard.writeText(text);
copyToClipboard("Hello World");
復制代碼
該方法可以通過使用 document.cookie 來訪問 cookie 并清除存儲在網頁中的所有 cookie:
const clearCookies=document.cookie.split(';').forEach(cookie=> document.cookie=cookie.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date(0).toUTCString()};path=/`));
復制代碼
該方法通過內置的 getSelection 屬性獲取用戶選擇的文本:
const getSelectedText=()=> window.getSelection().toString();
getSelectedText();
復制代碼
該方法用于檢測當前的環境是否是黑暗模式,它是一個布爾值:
const isDarkMode=window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
console.log(isDarkMode)
復制代碼
該方法用于在頁面中返回頂部:
const goToTop=()=> window.scrollTo(0, 0);
goToTop();
復制代碼
該方法用于檢測當前標簽頁是否已經激活:
const isTabInView=()=> !document.hidden;
復制代碼
該方法用于檢測當前的設備是否是蘋果的設備:
const isAppleDevice=()=> /Mac|iPod|iPhone|iPad/.test(navigator.platform);
isAppleDevice();
復制代碼
該方法用于判斷頁面是否已經底部:
const scrolledToBottom=()=> document.documentElement.clientHeight + window.scrollY >=document.documentElement.scrollHeight;
復制代碼
該方法用于重定向到一個新的URL:
const redirect=url=> location.href=url
redirect("https://www.google.com/")
復制代碼
該方法用于打開瀏覽器的打印框:
const showPrintDialog=()=> window.print()
復制代碼
該方法可以返回一個隨機的布爾值,使用Math.random()可以獲得0-1的隨機數,與0.5進行比較,就有一半的概率獲得真值或者假值。
const randomBoolean=()=> Math.random() >=0.5;
randomBoolean();
復制代碼
可以使用以下形式在不適用第三個變量的情況下,交換兩個變量的值:
[foo, bar]=[bar, foo];
復制代碼
該方法用于獲取一個變量的類型:
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
復制代碼
該方法用于攝氏度和華氏度之間的轉化:
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
復制代碼
該方法用于檢測一個JavaScript對象是否為空:
const isEmpty=obj=> Reflect.ownKeys(obj).length===0 && obj.constructor===Object;
*請認真填寫需求信息,我們會在24小時內與您取得聯系。