Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 久久免费视频3,精品国产专区91在线app,国产精品国产欧美综合一区

          整合營銷服務(wù)商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          VUE 響應(yīng)式原理源碼:帶你一步精通 VUE - 原

          VUE 響應(yīng)式原理源碼:帶你一步精通 VUE - 原力計劃

          者 | 愛編程的小和尚

          責編 | 王曉曼

          出品 | CSDN博客

          學過 VUE 如果不了解響應(yīng)式的原理,怎么能說自己熟練使用 VUE,要是沒有寫過一個簡易版的 VUE 怎么能說自己精通 VUE,這篇文章通過300多行代碼,帶你寫一個簡易版的 VUE,主要實現(xiàn) VUE 數(shù)據(jù)響應(yīng)式 (數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者)、數(shù)組的變異方法、編譯指令,數(shù)據(jù)的雙向綁定的功能。

          本文需要有一定 VUE 基礎(chǔ),并不適合新手學習。

          文章較長,且有些難度,建議大家,找一個安靜的環(huán)境,并在看之前沐浴更衣,保持編程的神圣感。下面是實現(xiàn)的簡易版VUE 的源碼地址,一定要先下載下來!因為文章中的并非全部的代碼。

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

          前言

          在開始學習之前,我們先來了解一下什么是 MVVM ,什么是數(shù)據(jù)響應(yīng)式。

          我們都知道 VUE 是一個典型的 MVVM 思想,由數(shù)據(jù)驅(qū)動視圖。

          那么什么是 MVVM 思想呢?

          MVVM是Model-View-ViewModel,是把一個系統(tǒng)分為了模型( model )、視圖( view )和 view-model 三個部分。

          VUE在 MVVM 思想下,view 和model 之間沒有直接的聯(lián)系,但是 view 和 view-model 、model和 view-model之間時交互的,當 view 視圖進行 dom 操作等使數(shù)據(jù)發(fā)生變化時,可以通過 view-model 同步到 model 中,同樣的 model 數(shù)據(jù)變化也會同步到 view 中。

          那么實現(xiàn)數(shù)據(jù)響應(yīng)式都有什么方法呢?1、發(fā)布者-訂閱者模式:當一個對象(發(fā)布者)狀態(tài)發(fā)生改變時,所有依賴它的對象(訂閱者)都會得到通知。通俗點來講,發(fā)布者就相當于報紙,而訂閱者相當于讀報紙的人。2、臟值檢查:通過存儲舊的數(shù)據(jù),和當前新的數(shù)據(jù)進行對比,觀察是否有變更,來決定是否更新視圖。angular.js 就是通過臟值檢查的方式。最簡單的實現(xiàn)方式就是通過 setInterval 定時輪詢檢測數(shù)據(jù)變動,但這樣無疑會增加性能,所以, angular 只有在指定的事件觸發(fā)時進入臟值檢測。3、數(shù)據(jù)劫持:通過 Object.defineProperty 來劫持各個屬性的 setter,getter,在數(shù)據(jù)變動時觸發(fā)相應(yīng)的方法。VUE是如何實現(xiàn)數(shù)據(jù)響應(yīng)式的呢?

          VUE.js 則是通過數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式。

          當執(zhí)行 new VUE 時,VUE 就進入了初始化階段,VUE會對指令進行解析(初始化視圖,增加訂閱者,綁定更新函數(shù)),同時通過 Obserber會遍歷數(shù)據(jù)并通過 Object.defineProperty 的 getter 和 setter 實現(xiàn)對的監(jiān)聽, 當數(shù)據(jù)發(fā)生變化的時候,Observer 中的 setter 方法被觸發(fā),setter 會立即調(diào)用Dep.notify, Dep 開始遍歷所有的訂閱者,并調(diào)用訂閱者的 update 方法,訂閱者收到通知后對視圖進行相應(yīng)的更新。

          我來依次介紹一下圖中的重要的名詞:1、Observer:數(shù)據(jù)監(jiān)聽器,能夠?qū)?shù)據(jù)對象的所有屬性進行監(jiān)聽,如有變動可拿到最新值并通知訂閱者,內(nèi)部采用 Object.defineProperty 的 getter 和 setter 來實現(xiàn)2、Compile:指令解析器,它的作用對每個元素節(jié)點的指令進行掃描和解析,根據(jù)指令模板替換數(shù)據(jù),以及綁定相應(yīng)的更新函數(shù)3、Dep:訂閱者收集器或者叫消息訂閱器都可以,它在內(nèi)部維護了一個數(shù)組,用來收集訂閱者,當數(shù)據(jù)改變觸發(fā) notify 函數(shù),再調(diào)用訂閱者的 update 方法4、Watcher:訂閱者,它是連接 Observer 和 Compile 的橋梁,收到消息訂閱器的通知,更新視圖5、Updater:視圖更新所以我們想要實現(xiàn)一個 VUE 響應(yīng)式,需要完成數(shù)據(jù)劫持、依賴收集、 發(fā)布者訂閱者模式。下面我來介紹我模仿源碼實現(xiàn)的功能:

          1、數(shù)據(jù)的響應(yīng)式、雙向綁定,能夠?qū)?shù)據(jù)對象的所有屬性進行監(jiān)聽,如有變動可拿到最新值并通知訂閱者

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

          3、數(shù)組變異方法的處理

          4、在 VUE 中使用 this 訪問或改變 data 中的數(shù)據(jù)

          我們想要完成以上的功能,需要實現(xiàn)如下類和方法:

          1、實現(xiàn) Observe r類:對所有的數(shù)據(jù)進行監(jiān)聽

          2、實現(xiàn) array 工具方法:對變異方法的處理

          3、實現(xiàn) Dep 類:維護訂閱者

          4、實現(xiàn) Watcher 類:接收 Dep 的更新通知,用于更新視圖

          5、實現(xiàn) Compile 類:用于對指令進行解析

          6、實現(xiàn)一個 CompileUtils 工具方法,實現(xiàn)通過指令更新視圖、綁定更新函數(shù)Watcher

          7、實現(xiàn) this.data 代理:實現(xiàn)對 this. data 代理:實現(xiàn)對 this.data 代理:實現(xiàn)對 this.data 代理,可以直接在 VUE 中使用 this 獲取當前數(shù)據(jù)

          我是使用了webpack作為構(gòu)建工具來協(xié)同開發(fā)的,所以在我實現(xiàn)的VUE響應(yīng)式中會用到ES6模塊化,webpack的相關(guān)知識。

          實現(xiàn) Observer 類

          我們都知道要用 Obeject.defineProperty 來監(jiān)聽屬性的數(shù)據(jù)變化,我們需要對 Observer 的數(shù)據(jù)對象進行遞歸遍歷,包括子屬性對象的屬性,都加上 setter 和 getter ,這樣的話,當給這個對象的某個值賦值,就會觸發(fā) setter,那么就能監(jiān)聽到了數(shù)據(jù)變化。當然我們在新增加數(shù)據(jù)的時候,也要對新的數(shù)據(jù)對象進行遞歸遍歷,加上 setter 和 getter 。

          但我們要注意數(shù)組,在處理數(shù)組時并不是把數(shù)組中的每一個元素都加上 setter 和 getter ,我們試想一下,一個從后端返回的數(shù)組數(shù)據(jù)是非常龐大的,如果為每個屬性都加上 setter 和 getter ,性能消耗是十分巨大的。我們想要得到的效果和所消耗的性能不成正比,所以在數(shù)組方面,我們通過對數(shù)組的7 個變異方法來實現(xiàn)數(shù)據(jù)的響應(yīng)式。只有通過數(shù)組變異方法來修改和刪除數(shù)組時才會重新渲染頁面。

          那么監(jiān)聽到變化之后是如何通知訂閱者來更新視圖的呢?我們需要實現(xiàn)一個Dep(消息訂閱器),其中有一個 notify 方法,是通知訂閱者數(shù)據(jù)發(fā)生了變化,再讓訂閱者來更新視圖。

          我們怎么添加訂閱者呢?我們可以通過 new Dep,通過 Dep 中的addSaubs 方法來添加訂閱者。我們來看一下具體代碼。

          我們首先需要聲明一個 Observer 類,在創(chuàng)建類的時候,我們需要創(chuàng)建一個消息訂閱器,判斷一下是否是數(shù)組,如果是數(shù)組,我們便改造數(shù)組,如果是對象,我們便需要為對象的每一個屬性都加入 setter 和 getter 。

          import { arrayMethods } from './array' //數(shù)組變異方法處理 
          class Observer {
          constructor(data) {
          //用于對數(shù)組進行處理,存放數(shù)組的觀察者watcher
          this.dep=new Dep
          if (Array.isArray(data)) {
          //如果是數(shù)組,使用數(shù)組的變異方法
          data.__proto__=arrayMethods
          //把數(shù)組數(shù)據(jù)添加 __ob__ 一個Observer,當使用數(shù)組變異方法時,可以更新視圖
          data.__ob__=this
          //給數(shù)組的每一項添加數(shù)據(jù)劫持(setter/getter處理)
          this.observerArray(data)
          } else {
          //非數(shù)組數(shù)據(jù)添加數(shù)據(jù)劫持(setter/getter處理)
          this.walk(data)
          }
          }
          }

          在上面,我們給 data 的__proto__原型鏈重新賦值,我們來看一下 arrayMethods 是什么,arrayMethods 是 array.js 文件中,拋出的一個新的 Array 原型:

          // 獲取Array的原型鏈
          const arrayProto=Array.prototype;
          // 重新創(chuàng)建一個含有對應(yīng)原型的對象,在下面稱為新Array
          const arrayMethods=Object.create(arrayProto);
          // 處理7個數(shù)組變異方法
          ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'].forEach(ele=> {
          //修改新Array的對應(yīng)的方法
          arrayMethods[ele]=function {
          // 執(zhí)行數(shù)組的原生方法,完成其需要完成的內(nèi)容
          arrayProto[ele].call(this, ...arguments)
          // 獲取Observer對象
          const ob=this.__ob__
          // 更新視圖
          ob.dep.notify
          }
          })
          export {
          arrayMethods
          }

          此時呢,我們就擁有了數(shù)組的變異方法,我們還需要通過 observerArray 方法為數(shù)組的每一項添加 getter 和setter ,注意,此時的每一項只是最外面的一層,并非遞歸遍歷。

          //循環(huán)遍歷數(shù)組,為數(shù)組每一項設(shè)置setter/getter
          observerArray(items) {
          for (let i=0; i < items.length; i++) {
          this.observer(items[i])
          }
          }

          如果是一個對象的話,我們就要對對象 的每一個屬性遞歸遍歷,通過 walk 方法:

          walk(data) {
          //數(shù)據(jù)劫持
          if (data && typeof data==="object") {
          for (const key in data) {
          //綁定setter和getter
          this.defineReactive(data, key, data[key])
          }
          }
          }

          在上面的調(diào)用了 defineReactive ,我們來看看這個方法是干什么的?這個方法就是設(shè)置數(shù)據(jù)劫持的,每一行都有注釋。

          //數(shù)據(jù)劫持,設(shè)置 setter/getteer
          defineReactive(data, key, value) {
          //如果是數(shù)組的話,需要接受返回的Observer對象
          let arrayOb=this.observer(value)
          //創(chuàng)建訂閱者/收集依賴
          const dep=new Dep
          //setter和getter處理
          Object.defineProperty(data, key, {
          //可枚舉的
          enumerable: true,
          //可修改的
          configurable: false,
          get {
          //當 Dep 有 watcher 時, 添加 watcher
          Dep.target && dep.addSubs(Dep.target)
          //如果是數(shù)組,則添加上數(shù)組的觀察者
          Dep.target && arrayOb && arrayOb.dep.addSubs(Dep.target)
          return value
          },
          set: (newVal)=> {
          //新舊數(shù)據(jù)不相等時更改
          if (value !==newVal) {
          //為新設(shè)置的數(shù)據(jù)添加setter/getter
          arrayOb=this.observer(newVal);
          value=newVal
          //通知 dep 數(shù)據(jù)發(fā)送了變化
          dep.notify
          }
          }
          })
          }
          }

          我們需要注意的是,在上面的圖解中,在 Observer 中,如果數(shù)據(jù)發(fā)生變化,會通知消息訂閱器,那么在何時綁定消息訂閱器呢?就是在設(shè)置 setter 和 getter 的時候,創(chuàng)建一個 Dep,并為 Dep添加訂閱者,Dep.target&& dep.addSubs(Dep.target),通過調(diào)用 dep 的 addSubs 方法添加訂閱者。

          實現(xiàn) Dep

          Dep 是消息訂閱器,它的作用就是維護一個訂閱者數(shù)組,當數(shù)據(jù)發(fā)送變化是,通知對應(yīng)的訂閱者,Dep中有一個 notify 方法,作用就是通知訂閱者,數(shù)據(jù)發(fā)送了變化:

          // 訂閱者收集器
          export default class Dep {
          constructor {
          //管理的watcher的數(shù)組
          this.subs=
          }
          addSubs(watcher) {
          //添加watcher
          this.subs.push(watcher)
          }
          notify {
          //通知watcher更新dom
          this.subs.forEach(w=> w.update)
          }
          }

          實現(xiàn) watcher

          Watcher 就是訂閱者, watcher 是 Observer 和 Compile 之間通信的橋梁,當數(shù)據(jù)改變時,接收到 Dep 的通知(Dep 的notify()方法),來調(diào)用自己的update方法,觸發(fā) Compile 中綁定的回調(diào),達到更新視圖的目的。

          import Dep from './dep'
          import { complieUtils } from './utils'
          export default class Watcher {
          constructor(vm, expr, cb) {
          //當前的vue實例
          this.vm=vm;
          //表達式
          this.expr=expr;
          //回調(diào)函數(shù),更新dom
          this.cb=cb
          //獲取舊的數(shù)據(jù),此時獲取舊值的時候,Dep.target會綁定上當前的this
          this.oldVal=this.getOldVal
          }
          getOldVal {
          //將當前的watcher綁定起來
          Dep.target=this
          //獲取舊數(shù)據(jù)
          const oldVal=complieUtils.getValue(this.expr, this.vm)
          //綁定完成后,將綁定的置空,防止多次綁定
          Dep.target=
          return oldVal
          }
          update {
          //更新函數(shù)
          const newVal=complieUtils.getValue(this.expr, this.vm)
          if (newVal !==this.oldVal || Array.isArray(newVal)) {
          //條用更新在compile中創(chuàng)建watcher時傳入的回調(diào)函數(shù)
          this.cb(newVal)
          }
          }
          }

          上面中用到了 ComplieUtils 中的 getValue 方法,會在下面講,主要作用是獲取到指定表達式的值。

          我們把整個流程分成兩條路線的話:

          newVUE==> Observer數(shù)據(jù)劫持==> 綁定Dep==> 通知watcher==> 更新視圖newVUE==> Compile解析模板指令==> 初始化視圖 和 綁定watcher

          此時,我們第一條線的內(nèi)容已經(jīng)實現(xiàn)了,我們再來實現(xiàn)一下第二條線。

          實現(xiàn) Compile

          Compile 主要做的事情是解析模板指令,將模板中的變量替換成數(shù)據(jù),初始化渲染頁面視圖。同時也要綁定更新函數(shù),添加訂閱者。

          因為在解析的過程中,會多次的操作 dom,為提高性能和效率,會先將 VUE 實例根節(jié)點的 el 轉(zhuǎn)換成文檔碎片 fragment 進行解析編譯操作,解析完成,再將 fragment 添加回原來的真實 dom 節(jié)點中。

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

          我們可以看到,Complie 中主要進行了三步,第一步 nodeTofragments 是講所有的 dom 節(jié)點放到文檔碎片中操作,最后一步,是把解析好的 dom 元素,從文檔碎片重新加入到頁面中,這兩步的具體方法,大家去下載我的源碼,看一下就明白了,有注釋。我就不再解釋了。

          我們來看一下第二步,編譯模板:

           complie(fragments) {
          //獲取所有節(jié)點
          const nodes=fragments.childNodes;
          [...nodes].forEach(ele=> {
          if (this.isNodeElement(ele)) {
          //1. 編譯元素節(jié)點
          this.complieElement(ele)
          } else {
          //編譯文本節(jié)點
          this.complieText(ele)
          }
          //如果有子節(jié)點,循環(huán)遍歷,編譯指令
          if (ele.childNodes && ele.childNodes.length) {
          this.complie(ele)
          }
          })
          }

          我們要知道,模板可能有兩種情況,一種是文本節(jié)點(含有雙大括號的插值表達式)和元素節(jié)點(含有指令)。我們獲取所有節(jié)點后對每個節(jié)點進行判斷,如果是元素節(jié)點,則用解析元素節(jié)點的方法,如果是文本節(jié)點,則調(diào)用解析文本的方法。

          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);
          }
          })

          }

          我們在編譯模板中調(diào)用了 complieUtils[dirName](node, value, this.vm, paramName)方法,這是工具類中的一個方法,用于處理指令。

          我們再來看看文本節(jié)點,文本節(jié)點就相對比較簡單,只需要匹配{{}}形式的插值表達式就可以了,同樣的調(diào)用工具方法,來解析。

          complieText(node) {
          //1.獲取所有的文本內(nèi)容
          const text=node.textContent
          //匹配{{}}
          if (/\{\{(.+?)\}\}/.test(text)) {
          //編譯模板
          complieUtils['text'](node, text, this.vm)
          }
          }

          上面用來這么多工具方法,我們來看看到底是什么。

          實現(xiàn) ComplieUtils 工具方法

          這個方法主要是對指令進行處理,獲取指令中的值,并在頁面中更新相應(yīng)的值,同時我們在這里要綁定 watcher 的回調(diào)函數(shù)。

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

          import Watcher from './watcher'
          export const complieUtils={
          //處理text指令
          text(node, expr, vm) {
          let value;
          if (/\{\{.+?\}\}/.test(expr)) {
          //處理 {{}}
          value=expr.replace(/\{\{(.+?)\}\}/g, (...args)=> {
          //綁定觀察者/更新函數(shù)
          new Watcher(vm, args[1],=> {
          //第二個參數(shù),傳入回調(diào)函數(shù)
          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)
          }
          //調(diào)用更新函數(shù)
          this.updater.updaterText(node, value)
          },
          }

          Text 處理函數(shù)是對 dom 元素的 TextContent 進行操作的,所以有兩種情況,一種是使用 v-text 指令,會更新元素的 textContent,另一種情況是{{}} 的插值表達式,也是更新元素的 textContent。

          在此方法中我們先判斷是哪一種情況,如果是 v-text 指令,那么就綁定一個 watcher 的回調(diào),獲取到 textContent 的值,調(diào)用 updater.updaterText 在下面講,是更新元素的方法。如果是雙大括號的話,我們就要對其進行特殊處理,首先是將雙大括號替換成指定的變量的值,同時為其綁定 watcher 的回調(diào)。

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

          獲取 textContent 的值是用一個 reduce 函數(shù),用法在最后面的鏈接中,因為數(shù)據(jù)可能是 person.name 我們需要獲取到最深的對象的值。

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

          updater.updaterText更新dom的方法,其實就是對 textContent 重新賦值。

          我們再來將一下v-model指令,實現(xiàn)雙向的數(shù)據(jù)綁定,我們都知道,v-model其實實現(xiàn)的是 input 事件和 value 之間的語法糖。所以我們這里同樣的監(jiān)聽一下當前 dom 元素的 input 事件,當數(shù)據(jù)改變時,調(diào)用設(shè)置新值的方法:

          //處理model指令
          model(node, expr, vm) {
          const value=this.getValue(expr, vm)
          //綁定watcher
          new Watcher(vm, expr, (newVal)=> {
          this.updater.updaterModel(node, newVal)
          })
          //雙向數(shù)據(jù)綁定
          node.addEventListener("input", (e)=> {
          //設(shè)值方法
          this.setVal(expr, vm, e.target.value)
          })
          this.updater.updaterModel(node, value)
          },

          這個方法同樣是通過 reduce 方法,為對應(yīng)的變量設(shè)置成新的值,此時數(shù)據(jù)改變了,會自動調(diào)用更新視圖的方法,我們在之前已經(jīng)實現(xiàn)了。

          //通過表達式,vm,輸入框的值,實現(xiàn)設(shè)置值,input中v-model雙向數(shù)據(jù)綁定
          setVal(expr, vm, inputVal) {
          expr.split(".").reduce((data, currentVal)=> {
          data[currentVal]=inputVal
          }, vm.$data)
          },

          實現(xiàn)VUE

          最后呢,我們就要來整合這些類和工具方法,在創(chuàng)建一個 VUE 實例的時候,我們先獲取 options 中的參數(shù),然后對起進行數(shù)據(jù)劫持和編譯模板:

          class Vue {
          constructor(options) {
          //獲取模板
          this.$el=options.el;
          //獲取data中的數(shù)據(jù)
          this.$data=options.data;
          //將對象中的屬性存起來,以便后續(xù)使用
          this.$options=options
          //1.數(shù)據(jù)劫持,設(shè)置setter/getter
          new Observer(this.$data)
          //2.編譯模板,解析指令
          new Complie(this.$el, this)
          }
          }

          此時我們想要使用 VUE 中的數(shù)據(jù),比如我們想要在 vm 對象中使用person.name, 必須用 this.$data.person.name 才能獲取到,如果我們想在 vm 對象中使用 this.person.name 直接修改數(shù)據(jù),就需要代理一下 this.$data 。其實就是將當前的 this.$data 中的數(shù)據(jù)放到全局中進行監(jiān)聽。

          export default class Vue {
          constructor(options) {
          //...
          //1.數(shù)據(jù)劫持,設(shè)置setter/getter
          //2.編譯模板,解析指令
          if (this.$el) { //如果有模板
          //代理this
          this.proxyData(this.$data)
          }
          }
          proxyData(data) {
          for (const key in data) {
          //將當前的數(shù)據(jù)放到全局指向中
          Object.defineProperty(this, key, {
          get {
          return data[key];
          },
          set(newVal) {
          data[key]=newVal
          }
          })
          }
          }
          }

          文章到了這里,就實現(xiàn)了一個簡易版的 VUE,建議大家反復(fù)學習,仔細體驗,細細品味。

          在文章的最后,我通過問、答的形式,來解答一些常見的面試題:

          問:什么時候頁面會重新渲染?

          答:數(shù)據(jù)發(fā)生改變,頁面就會重新渲染,但數(shù)據(jù)驅(qū)動視圖,數(shù)據(jù)必須先存在,然后才能實現(xiàn)數(shù)據(jù)綁定,改變數(shù)據(jù),頁面才會重新渲染。

          問:什么時候頁面不會重新渲染?

          答:有3種情況不會重新渲染:

          1、未經(jīng)聲明和未使用的變量,修改他們,都不會重新渲染頁面

          2、通過索引的方式和更改長度的方式更改數(shù)組,都不會重新渲染頁面

          3、增加和刪除對象的屬性,不會重新渲染頁面

          問:如何使 未聲明/未使用的變量、增加/刪除對象屬性可以使頁面重新渲染?

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

          問:如何更改數(shù)組可以使頁面重新渲染?

          答:可以使用數(shù)組的變異方法(共 7 個):push、pop、unshift、shift、splice、sort、reverse

          問:數(shù)據(jù)更新后,頁面會立刻重新渲染么?

          答:更改數(shù)據(jù)后,頁面不會立刻重新渲染,頁面渲染的操作是異步執(zhí)行的,執(zhí)行完同步任務(wù)后,才會執(zhí)行異步的

          同步隊列,異步隊列(宏任務(wù)、微任務(wù))

          問:如果更改了數(shù)據(jù),想要在頁面重新渲染后再做操作,怎么辦?

          答:可以使用 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 此時數(shù)據(jù)已更改
          console.log(vm.$el.innerHTML); // monk 此時頁面還未重新渲染
          // 1. 使用vm.$nextTick
          vm.$nextTick(=> {
          console.log(vm.$el.innerHTML); // the young monk 此時數(shù)據(jù)已更改
          })
          // 2. 使用Vue.nextTick
          Vue.nextTick(=> {
          console.log(vm.$el.innerHTML); // the young monk 此時數(shù)據(jù)已更改
          })
          </script>

          問:vm.$nextTick 和 VUE.nextTick 有什么區(qū)別呢 ?

          答:VUE.nextTick 內(nèi)部函數(shù)的 this 指向 Window,vm.$nextTick 內(nèi)部函數(shù)的 this 指向 VUE 實例對象。

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

          問:vm.$nextTick 和 VUE.nextTick 是通過什么實現(xiàn)的呢?

          答:二者都是等頁面渲染后執(zhí)行的任務(wù),都是使用微任務(wù)。

           if(typeof Promise !=='undefined') {
          // 微任務(wù)
          // 首先看一下瀏覽器中有沒有promise
          // 因為IE瀏覽器中不能執(zhí)行Promise
          const p=Promise.resolve;

          } else if(typeof MutationObserver !=='undefined') {
          // 微任務(wù)
          // 突變觀察
          // 監(jiān)聽文檔中文字的變化,如果文字有變化,就會執(zhí)行回調(diào)
          // vue的具體做法是:創(chuàng)建一個假節(jié)點,然后讓這個假節(jié)點稍微改動一下,就會執(zhí)行對應(yīng)的函數(shù)
          } else if(typeof setImmediate !=='undefined') {
          // 宏任務(wù)
          // 只在IE下有
          } else {
          // 宏任務(wù)
          // 如果上面都不能執(zhí)行,那么則會調(diào)用setTimeout
          }

          同樣的這也是 VUE 的一個小缺點:VUE 一直是等主線程執(zhí)行完以后再執(zhí)行渲染任務(wù),如果主線程卡死,則永遠渲染不出來。

          問:利用 Object.defineProperty 實現(xiàn)響應(yīng)式有什么缺點?

          答:

          1、天生就需要進行遞歸

          2、監(jiān)聽不到數(shù)組不存在的索引的改變

          3、監(jiān)聽不到數(shù)組長度的改變

          4、監(jiān)聽不到對象的增刪

          版權(quán)聲明:本文為CSDN博主「愛編程的小和尚」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接及本聲明。

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

          ?雷軍:4G 手機已清倉,全力轉(zhuǎn) 5G;QQ音樂播放中途插語音廣告引熱議;Wine 5.9 發(fā)布 | 極客頭條

          ?中國 AI 應(yīng)用元年來了!

          ?新基建東風下,開發(fā)者這樣抓住工業(yè)互聯(lián)網(wǎng)風口!

          ?15 歲黑進系統(tǒng),發(fā)挑釁郵件意外獲 Offer,不惑之年捐出全部財產(chǎn),Twitter CEO 太牛了!

          ?避坑!使用 Kubernetes 最易犯的 10 個錯誤

          ?必讀!53個Python經(jīng)典面試題詳解

          ?贈書 | 1月以來 Tether 增發(fā)47億 USDT,美元都去哪兒了?

          期回顧

          「JavaScript 從入門到精通」1.語法和數(shù)據(jù)類型

          「JavaScript 從入門到精通」2.流程控制和錯誤處理

          「JavaScript 從入門到精通」3.循環(huán)和迭代

          「JavaScript 從入門到精通」4.函數(shù)

          「JavaScript 從入門到精通」5.表達式和運算符

          「JavaScript 從入門到精通」6.數(shù)字

          「JavaScript 從入門到精通」7.時間對象

          前置知識:

          JS中的正則表達式是用來匹配字符串中指定字符組合的模式。

          另外需要記?。赫齽t表達式也是對象。

          1.創(chuàng)建正則表達式

          • 使用一個正則表達式字面量:

          • 使用RegExp對象:
          • new RegExp(str[, attr])接收2個參數(shù),str是一個字符串,指定正則表達式匹配規(guī)則,attr可選,表示匹配模式,值有g(shù)(全局匹配),i(區(qū)分大小寫的匹配)和m(多行匹配)。

          正則表達式的返回值,是一個新的RegExp對象,具有指定的模式和標志。

          返回信息介紹:

          關(guān)于正則表達式的一些方法屬性,文章后面介紹,這里先復(fù)習定義和使用。

          2.使用正則表達式

          JS的正則表達式可以被用于:

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

          2.1 RegExp對象方法

          • 2.1.1 exec(str)

          str: 需要檢索的字符串。

          若檢索成功,返回匹配的數(shù)組,否則返回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匹配的所有結(jié)果的一個數(shù)組,數(shù)組的第一項是進行匹配完整的字符串,之后的項是用圓括號捕獲的結(jié)果,否則返回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: 指定替換文本
          • 返回替換成功之后的字符串,不改變源字符串內(nèi)容。

          val可以使用特殊變量名:

          • 2.2.4 split

          將一個字符串,按照指定符號分割成一個字符串數(shù)組。

          str.split(sub[, maxlength]):

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

          2.3 使用情況

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

          3.正則表達式符號介紹

          詳細的每個符號的用法,可以查閱 W3school JavaScript RegExp 對象

          3.1 修飾符

          修飾符描述i執(zhí)行對大小寫不敏感的匹配。g執(zhí)行全局匹配(查找所有匹配而非在找到第一個匹配后停止)。m執(zhí)行多行匹配。

          3.2 方括號

          用于查找指定返回之內(nèi)的字符:

          .3 元字符

          元字符是擁有特殊含義的字符:

          3.4 量詞

          4. 正則表達式拓展(ES6)

          4.1 介紹

          在ES5中有兩種情況。

          • 參數(shù)是字符串,則第二個參數(shù)為正則表達式的修飾符。

          • 參數(shù)是正則表達式,返回一個原表達式的拷貝,且不能有第二個參數(shù),否則報錯。

          ES6中使用:

          第一個參數(shù)是正則對象,第二個是指定修飾符,如果第一個參數(shù)已經(jīng)有修飾符,則會被第二個參數(shù)覆蓋。

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

          4.2 字符串的正則方法

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

          4.3 u修飾符

          添加u修飾符,是為了處理大于uFFFF的Unicode字符,即正確處理四個字節(jié)的UTF-16編碼。

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

          由于ES5之前不支持四個字節(jié)UTF-16編碼,會識別為兩個字符,導(dǎo)致第二行輸出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修飾符,就無法識別非規(guī)范的K字符。
          /[a-z]/i.test('\u212A') // false
          /[a-z]/iu.test('\u212A') // true
          

          檢查是否設(shè)置u修飾符: 使用unicode屬性。

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

          4.4 y修飾符

          y修飾符與g修飾符類似,也是全局匹配,后一次匹配都是從上一次匹配成功的下一個位置開始。區(qū)別在于,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修飾符解決四個字節(jié)的UTF-16字符,另一個是行終止符。

          終止符即表示一行的結(jié)束,如下四個字符屬于“行終止符”:

          • 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
          復(fù)制代碼

          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
          復(fù)制代碼

          距離過年還有90天~

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

          該方法用于檢測給出的日期位于今年的第幾天:

          const dayOfYear=(date)=> Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);
          
          dayOfYear(new Date());   // 307
          復(fù)制代碼

          2021年已經(jīng)過去300多天了~

          4. 時間格式化

          該方法可以用于將時間轉(zhuǎn)化為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
          復(fù)制代碼

          二、字符串處理

          1. 字符串首字母大寫

          該方法用于將英文字符串的首字母大寫處理:

          const capitalize=str=> str.charAt(0).toUpperCase() + str.slice(1)
          
          capitalize("hello world")  // Hello world
          復(fù)制代碼

          2. 翻轉(zhuǎn)字符串

          該方法用于將一個字符串進行翻轉(zhuǎn)操作,返回翻轉(zhuǎn)后的字符串:

          const reverse=str=> str.split('').reverse().join('');
          
          reverse('hello world');   // 'dlrow olleh'
          復(fù)制代碼

          3. 隨機字符串

          該方法用于生成一個隨機的字符串:

          const randomString=()=> Math.random().toString(36).slice(2);
          
          randomString();
          復(fù)制代碼

          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...'
          復(fù)制代碼

          5. 去除字符串中的HTML

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

          const stripHtml=html=> (new DOMParser().parseFromString(html, 'text/html')).body.textContent || '';
          復(fù)制代碼

          三、數(shù)組處理

          1. 從數(shù)組中移除重復(fù)項

          該方法用于移除數(shù)組中的重復(fù)項:

          const removeDuplicates=(arr)=> [...new Set(arr)];
          
          console.log(removeDuplicates([1, 2, 2, 3, 3, 4, 4, 5, 5, 6]));
          復(fù)制代碼

          2. 判斷數(shù)組是否為空

          該方法用于判斷一個數(shù)組是否為空數(shù)組,它將返回一個布爾值:

          const isNotEmpty=arr=> Array.isArray(arr) && arr.length > 0;
          
          isNotEmpty([1, 2, 3]);  // true
          復(fù)制代碼

          3. 合并兩個數(shù)組

          可以使用下面兩個方法來合并兩個數(shù)組:

          const merge=(a, b)=> a.concat(b);
          
          const merge=(a, b)=> [...a, ...b];
          復(fù)制代碼

          四、數(shù)字操作

          1. 判斷一個數(shù)是奇數(shù)還是偶數(shù)

          該方法用于判斷一個數(shù)字是奇數(shù)還是偶數(shù):

          const isEven=num=> num % 2===0;
          
          isEven(996); 
          復(fù)制代碼

          2. 獲得一組數(shù)的平均值

          const average=(...args)=> args.reduce((a, b)=> a + b) / args.length;
          
          average(1, 2, 3, 4, 5);   // 3
          復(fù)制代碼

          3. 獲取兩個整數(shù)之間的隨機整數(shù)

          該方法用于獲取兩個整數(shù)之間的隨機整數(shù)

          const random=(min, max)=> Math.floor(Math.random() * (max - min + 1) + min);
          
          random(1, 50);
          復(fù)制代碼

          4. 指定位數(shù)四舍五入

          該方法用于將一個數(shù)字按照指定位進行四舍五入:

          const round=(n, d)=> Number(Math.round(n + "e" + d) + "e-" + d)
          
          round(1.005, 2) //1.01
          round(1.555, 2) //1.56
          復(fù)制代碼

          五、顏色操作

          1. 將RGB轉(zhuǎn)化為十六機制

          該方法可以將一個RGB的顏色值轉(zhuǎn)化為16進制值:

          const rgbToHex=(r, g, b)=> "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
          
          rgbToHex(255, 255, 255);  // '#ffffff'
          復(fù)制代碼

          2. 獲取隨機十六進制顏色

          該方法用于獲取一個隨機的十六進制顏色值:

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

          六、瀏覽器操作

          1. 復(fù)制內(nèi)容到剪切板

          該方法使用 navigator.clipboard.writeText 來實現(xiàn)將文本復(fù)制到剪貼板:

          const copyToClipboard=(text)=> navigator.clipboard.writeText(text);
          
          copyToClipboard("Hello World");
          復(fù)制代碼

          2. 清除所有cookie

          該方法可以通過使用 document.cookie 來訪問 cookie 并清除存儲在網(wǎng)頁中的所有 cookie:

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

          3. 獲取選中的文本

          該方法通過內(nèi)置的 getSelection 屬性獲取用戶選擇的文本:

          const getSelectedText=()=> window.getSelection().toString();
          
          getSelectedText();
          復(fù)制代碼

          4. 檢測是否是黑暗模式

          該方法用于檢測當前的環(huán)境是否是黑暗模式,它是一個布爾值:

          const isDarkMode=window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
          
          console.log(isDarkMode)
          復(fù)制代碼

          5. 滾動到頁面頂部

          該方法用于在頁面中返回頂部:

          const goToTop=()=> window.scrollTo(0, 0);
          
          goToTop();
          復(fù)制代碼

          6. 判斷當前標簽頁是否激活

          該方法用于檢測當前標簽頁是否已經(jīng)激活:

          const isTabInView=()=> !document.hidden; 
          復(fù)制代碼

          7. 判斷當前是否是蘋果設(shè)備

          該方法用于檢測當前的設(shè)備是否是蘋果的設(shè)備:

          const isAppleDevice=()=> /Mac|iPod|iPhone|iPad/.test(navigator.platform);
          
          isAppleDevice();
          復(fù)制代碼

          8. 是否滾動到頁面底部

          該方法用于判斷頁面是否已經(jīng)底部:

          const scrolledToBottom=()=> document.documentElement.clientHeight + window.scrollY >=document.documentElement.scrollHeight;
          復(fù)制代碼

          9. 重定向到一個URL

          該方法用于重定向到一個新的URL:

          const redirect=url=> location.href=url
          
          redirect("https://www.google.com/")
          復(fù)制代碼

          10. 打開瀏覽器打印框

          該方法用于打開瀏覽器的打印框:

          const showPrintDialog=()=> window.print()
          復(fù)制代碼

          七、其他操作

          1. 隨機布爾值

          該方法可以返回一個隨機的布爾值,使用Math.random()可以獲得0-1的隨機數(shù),與0.5進行比較,就有一半的概率獲得真值或者假值。

          const randomBoolean=()=> Math.random() >=0.5;
          
          randomBoolean();
          復(fù)制代碼

          2. 變量交換

          可以使用以下形式在不適用第三個變量的情況下,交換兩個變量的值:

          [foo, bar]=[bar, foo];
          復(fù)制代碼

          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
          復(fù)制代碼

          4. 華氏度和攝氏度之間的轉(zhuǎn)化

          該方法用于攝氏度和華氏度之間的轉(zhuǎn)化:

          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
          復(fù)制代碼

          5. 檢測對象是否為空

          該方法用于檢測一個JavaScript對象是否為空:

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

          主站蜘蛛池模板: 亚欧色一区W666天堂| 亚洲一区二区三区首页| 高清精品一区二区三区一区| 中文字幕人妻AV一区二区| 无码人妻精品一区二| 国产一区二区三区乱码网站| 国产一区二区三区在线影院| 国产一区二区三区内射高清| 亚洲视频一区二区三区| 色欲综合一区二区三区| 亚洲AV无码一区二区三区系列| 亚洲av区一区二区三| 无码人妻AⅤ一区二区三区水密桃| 天堂Av无码Av一区二区三区| 国产高清一区二区三区 | 极品少妇伦理一区二区| 亚洲一区二区三区乱码在线欧洲| 亚欧成人中文字幕一区| 无码人妻精品一区二区三| 精品久久一区二区三区| 久久亚洲国产精品一区二区| 精品aⅴ一区二区三区| 久久国产高清一区二区三区| 午夜精品一区二区三区在线观看| 日本一区二区在线不卡| 极品少妇伦理一区二区| 中文字幕一区二区三区在线不卡| 国产精品一区12p| 精品一区二区三区免费视频| 中文字幕一区二区区免| 日韩人妻无码一区二区三区综合部 | 色欲AV无码一区二区三区| 男人免费视频一区二区在线观看 | 久久久久人妻精品一区三寸蜜桃| 国产成人综合精品一区| 狠狠色成人一区二区三区| 亚洲AV综合色一区二区三区| 亚洲综合一区二区精品久久| 亚洲av无码成人影院一区 | 色噜噜狠狠一区二区三区果冻| 69福利视频一区二区|