<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      淺析MVVM原理,實現一個mini-vue

      目錄

      前言

      MVVM

      mini-vue實現

      Compile(指令解析)

      Updater(視圖更新)

      Proxy(代理data)

      Observer(數據劫持)

      Dep(調度中心)

      Watcher(數據觀察)

      函數的連接

      寫在最后


      前言

      MVVM實際上是MVC的改進版,其立足于MVP框架。使用Vue時,我們會體會到其與React的區別,綁定表單數據時react對輸入框讀寫需要input事件設置state,以及value綁定,而vue則只需將數據與model綁定即可,這種數據驅動視圖卻與視圖解耦的編程方式使用起來很方便。以前面試官問vue原理,若能說出雙向綁定實現和Object.defineProperty就已經夠了,現在隨著對vue深入的學習,面試官已經不僅僅局限于此。所以,為了深入體驗mvvm模式,我實現了一個mini-vue。

      MVVM

      在開始前,我們先試著參照下圖實現一個簡單的雙向綁定案例

       

      DOM通過eventListener修改Model,Model通過修改data驅動視圖

       在html>body中添加以下代碼就可以實現

          <input id="input-box" type="text">
          <div id="show-text"></div>
          <script>
              const showText = document.querySelector('#show-text')
              const inputBox = document.querySelector('#input-box')
              class VM {
                  data = {
                      value: ''
                  }
                  constructor() {
                      Object.defineProperty(this.data, 'value', {
                          set(v) {
                              showText.textContent = v
                          },
                          get() {
                              return showText.textContent
                          }
                      })
                  }
              }
      
              const vm = new VM()
              inputBox.addEventListener('input', function (e) {
                  vm.data.value = e.target.value
                  console.log(vm.data.value)
              })
          </script>

      在上述代碼中,我們可以使用Object.defineProperty將data和textContent的值綁定,從而達到數據驅動視圖的效果,那么這樣就夠了嗎?

      mini-vue實現

      下面是雙向綁定的流程

      通過上圖我們可以得知:new MVVM后會進行兩步操作,一是compile指令解析,將v-if,@click,{{  }}解析出來,獲取data中的數據,并且都與watcher綁定,第一次初始化和watcher監聽到數據變化時會執行updater,重新渲染頁面,二是observer數據劫持,將data中的數據通過defineProperty添加讀寫監聽,并將數據變化與watcher綁定在一起,那么此時watcher就是連接數據變化和視圖更新的樞紐。

      下面我們一步一步實現上述代碼

      Compile(指令解析

      我們回顧一下vue是如何使用的,標簽中各種v-if,v-show,v-html,以及@click等等屬性,綁定著data中的屬性和methods中的函數

          <div id="app">
              <span v-text='title.name'></span>
              <div v-if='isRender'>
                  <span>1</span>
                  <span>2</span>
                  <span>3</span>
              </div>
              <ul>
                  <li v-if='isRender'>{{info.name}}---{{info.age}}---{{modelData}}---{{inputVal.item.value}}</li>
                  <li v-show='isShow'>{{info.age}}</li>
                  <li v-if='isRender'>{{modelData}}</li>
                  <li v-show='isShow'>{{inputVal.item.value}}</li>
              </ul>
              <span v-text='inputVal.item.value'></span>
              <div v-html='htmlTemp'></div>
              <div v-show='isShow'>world</div>
              <button v-on:click='handlerShow'>點擊顯示</button>
              <button @click='handlerRender'>點擊渲染</button>
              <input v-model='modelData' type="text">
              <input v-model='inputVal.item.value' type="text">
          </div>

      而實例化vue則是將數據和函數初始化到vue中

              let vm = new Vue({
                  el: '#app',
                  data: {
                      title: {
                          name: 'hello'
                      },
                      info: {
                          name: '張三',
                          age: 23,
                      },
                      isShow: true,
                      isRender: true,
                      modelData: 123,
                      htmlTemp: '<span style="color:red;">html</span>',
                      inputVal: {
                          item: {
                              value: 'abc'
                          }
                      }
                  },
                  methods: {
                      handlerShow() {
                          this.isShow = !this.isShow
                      },
                      handlerRender() {
                          this.isRender = !this.isRender
                      }
                  },
              })

      那么,我們要如何去讓js識別這些指令并渲染視圖呢
      首先,創建標簽碎片,將Dom元素獲取到DocumentFragment中,以便于解析指令及根據指令對視圖響應,其次,將標簽屬性分離,每種指令對應一種響應方式(updater)。最后綁定watcher監聽到數據變化時,再次觸發updater
      以下是compile.js,用來解析標簽內容和屬性

      // 指令解析器
      const textRegex = /\{\{(.+?)\}\}/g //解析{{}}的正則
      class Compile {
          constructor(elem, vm) {
              this.elem = isElemNode(elem) === '1' ? elem : document.querySelector(elem)
              this.vm = vm
              const fragment = this.createFragment(this.elem)
              this.getTemp(fragment, this.vm)
              this.elem.appendChild(fragment);
          }
          // 遞歸子元素,查找所有元素
          getTemp(fragment, vm) {
              const fragmentChild = Array.from(fragment.childNodes)
              fragmentChild.forEach(item => {
                  this.filterElem(item, vm)
                  item.childNodes && item.childNodes.length && this.getTemp(item, vm)
              })
          }
          // 創建標簽碎片,將dom元素添加到標簽碎片中
          createFragment(elem) {
              const fragment = document.createDocumentFragment();
              while (elem.firstChild) {
                  fragment.append(elem.firstChild)
              }
              return fragment
          }
      
          // 針對不同元素節點進行分離
          filterElem(elem, vm) {
              switch (isElemNode(elem)) {
                  case 1: //元素節點
                      this.renderNode(elem, vm)
                      break;
                  case 3: //文本節點
                      this.renderText(elem, vm)
                      break;
              }
          }
          // 渲染文本,主要解析‘{{}}’及多個‘{{}}’
          renderText(elem, vm) {
              textRegex.test(elem.textContent) && updater(elem, vm, elem.textContent, 'text-content')
          }
          // 渲染標簽
          renderNode(elem, vm) {
              //取出所有屬性和值
              Array.from(elem.attributes).forEach(attr => {
                  const {
                      name,
                      value
                  } = attr;
                  // 過濾‘v-’和‘@’操作,并移除標簽屬性
                  name.startsWith('v-') ? (this.compileV_Command(elem, vm, name, value), removeAttr(elem, name)) : name.startsWith('@') ? (this.compileEventComment(elem, vm, name.split('@')[1], value), removeAttr(elem, name)) : null
              })
          }
          // v- 指令解析,指令
          compileV_Command(elem, vm, name, value) {
              const key = name.split('v-')
              const eventCommand = key[1] && key[1].split(':')[1]
              // v-model事件
              key[1] === 'model' && this.compileEventComment(elem, vm, 'input', value, e => {
                  setDeepData(vm, value, e.target.value)
              })
              // 過濾指令是否為事件
              eventCommand ? this.compileEventComment(elem, vm, eventCommand, value) : updater(elem, vm, value, key[1])
          }
          // @ 指令解析,事件
          compileEventComment(elem, vm, name, value, fn) {
              !fn && elem.addEventListener(name, vm.options.methods[value].bind(vm))
              fn && elem.addEventListener(name, fn.bind(vm))
          }
      }

      Updater(視圖更新)

      指令解析完后自然需要updater.js,對當前元素進行下一步渲染,在此之前,我們的值需要從vue.data中取,這樣才能將data數據綁定到標簽中,lodash有兩個函數一個是_.get(),另一個是_.set(),作用是獲取和設置對象某一層某個值,所以我們需要在utils(工具函數)中實現一下

      utils.js

      //lodash中的 _.get(),獲取對象多級屬性
      function getDeepData(object, path, defaultValue) {
          const paths = path.split('.')
          for (const i of paths) { //逐層遍歷path
              object = object[i]
              if (object === undefined) { //不能用 '!object' null,0,false等等會等于false
                  return defaultValue
              }
          }
          return object
      }
      //lodash中的 _.set(),賦值對象某級屬性
      function setDeepData(object, path, value) {
          const paths = path.split('.')
          const last = paths[paths.length - 1]//為何要在length - 1時賦值:因為object的引用關系使得我們可以一級一級賦值,而當最后一項是基本類型時,無法將引用的值賦給原始的object
          let _obj = object
          for (const i of paths) {
              last === i && (_obj[last] = value)
              _obj = _obj[i]
          }
      }
      // 移除屬性值
      function removeAttr(elem, key) {
          elem.removeAttribute(key)
      }
      // 獲取標簽類型
      function isElemNode(elem) {
          return elem.nodeType
      }

      updater.js 

      // 更新視圖,標簽中指令屬性處理
      function updater(elem, vm, value, type) {
          switch (type) {
              case 'text':
                      elem.textContent = getDeepData(vm.data, value)
                  break;
              case 'text-content':
                      elem.textContent = value.replace(textRegex, (..._) => getDeepData(vm.data, _[1]))
                  break;
              case 'html':
                      elem.innerHTML = getDeepData(vm.data, value)
                  break;
              case 'model':
                      elem.value = getDeepData(vm.data, value)
                  break;
              case 'if':
                  const temp = document.createTextNode('')
                  elem.parentNode.insertBefore(temp, elem);
                      getDeepData(vm.data, value) ? temp.parentNode.insertBefore(elem, temp) : temp.parentNode.removeChild(elem)
                  break;
              case 'show':
                      elem.hidden = !getDeepData(vm.data, value)
                  break;
          }
      }

      完成這一步后,我們在vue.js中調用

      class VueDemo {
          constructor(options) {
              this.options = options //配置信息
              this.data = options.data;
              // 判斷options.el是否存在
              (this.el = options.el) && Object.defineProperties(this, {
                  compile: {
                      value: new Compile(options.el, this) //指令解析器
                  }
              })
          }
      }

      效果出來了,指令被解析出來并且在頁面中顯示

      Proxy(代理data)

      我們雖然將vue.data中的數據渲染到了頁面,但是還是需要通過this.data來獲取數據,而vue可以中直接通過this來拿到數據,此時我們需要新建一個proxy.js將this.data代理到this上

      // data數據代理到vue
      class DataProxy {
          constructor(data, vm) {
              for (const key in data) {
                  Object.defineProperty(vm, key, {
                      get() {
                          return data[key];
                      },
                      set(val) {
                          data[key] = val;
                      }
                  })
              }
              return data
          }
      }

      在vue.js中調用,并將updater.js 中的vm.data改成vm

      class VueDemo {
          constructor(options) {
              this.options = options //配置信息
              this.$data = options.data;
              // 判斷options.el是否存在
              (this.el = options.el) && Object.defineProperties(this, {
                  proxy: {
                      value: new DataProxy(options.data, this) //data代理到this
                  },
                  compile: {
                      value: new Compile(options.el, this) //指令解析器
                  }
              })
          }
      }

      寫到這里,compile和updater已經實現了,接下來將是數據劫持的實現方式

      Observer(數據劫持)

      這一步的作用是將data中的數據都加上讀寫響應控制,給所有數據綁定可以更新視圖的函數

      // 發布模式
      class Observer {
          constructor(data) {
              this.initObserver(data)
          }
          // 劫持所有數據
          initObserver(data) {
              if (data && typeof data === 'object') {
                  for (const key in data) {
                      this.defineReactive(data, key, data[key])
                  }
              }
          }
          // 響應攔截器,遞歸監聽所有層級
          defineReactive(data, key, val) {
              this.initObserver(val) //劫持子項
              Object.defineProperty(data, key, {
                  enumerable: true, // 允許枚舉
                  configurable: false, // 不能被定義
                  get: _ =>  val,//初始化獲取值時對dep綁定
                  set: newVal => val = newVal
              })
          }
      }

      Dep(調度中心)

      watcher的作用是將上面的observer與視圖的刷新函數updater進行連接,當observer監測到數據變化時會通過dep告訴watcher,watcher就會執行updater更新視圖,于是,我們需要先實現observer與watcher之間的觀察者dep,我們先假定watcher中更新視圖的函數名字叫compareVal,將watcher注冊到調度中心中

      // 調度中心(觀察者模式)
      class Dep {
          observerList = [] //調度中心,存放與屬性綁定的事件
          //觸發所有與該屬性綁定的事件
          fireEvent() {
              this.observerList.forEach(target => {
                  target.compareVal()
              })
          }
          //注冊事件
          subscribe(target) {
              target.compareVal && this.observerList.push(target)
          }
      }

      Watcher(數據觀察)

      watcher的作用是連接observer和compile,使數據和視圖綁定
      以下是watcher.js的實現

      // 訂閱模式(比較綁定值的變化)
      class Watcher {
          constructor(vm, val, update) {
              this.vm = vm
              this.val = val;
              this.update = update
              this.oldVal = getDeepData(this.vm, this.val)
              update() //首次渲染初始化
          }
          // 對比數據,更新視圖
          compareVal() {
              const newVal = getDeepData(this.vm, this.val);
              newVal !== this.oldVal && (this.update(), this.oldVal = newVal) //更新視圖后將新值賦到oldVal上
          }
      }

      函數的連接

      我們來回顧一下以上功能的實現

      整個流程中的函數部分已經全部實現,只剩下如何將他們聯系在一起,這時如果你對整個功能實現還有些模糊,那請認真分析一下這張流程圖,并繼續看下去吧

      首先我們把watcher和指令解析以及updater之間的關系實現。
      在updater中給予每一個指令一個watcher,將更新視圖操作綁定到watcher中,由compareVal來更新視圖

      // 更新視圖,標簽中指令屬性處理
      function updater(elem, vm, value, type) {
          switch (type) {
              case 'text':
                  new Watcher(vm, value, _ => {
                      elem.textContent = getDeepData(vm, value)
                  })
                  break;
              case 'text-content':
                  value.replace(textRegex, (..._) => { //外面的content.replace獲取所有{{}}中的屬性
                      new Watcher(vm, _[1], _ => { //里面的content.replace獲取data中綁定的值
                          elem.textContent = value.replace(textRegex, (..._) => getDeepData(vm, _[1]))
                      })
                  })
                  break;
              case 'html':
                  new Watcher(vm, value, _ => {
                      elem.innerHTML = getDeepData(vm, value)
                  })
                  break;
              case 'model':
                  new Watcher(vm, value, _ => {
                      elem.value = getDeepData(vm, value)
                  })
                  break;
              case 'if':
                  const temp = document.createTextNode('')
                  elem.parentNode.insertBefore(temp, elem);
                  new Watcher(vm, value, _ => {
                      getDeepData(vm, value) ? temp.parentNode.insertBefore(elem, temp) : temp.parentNode.removeChild(elem)
                  })
                  break;
              case 'show':
                  new Watcher(vm, value, _ => {
                      elem.hidden = !getDeepData(vm, value)
                  })
                  break;
          }
      }

      那么如何告訴watcher數據發生了改變呢?
      在watcher中我們獲取oldvalue時采用this.oldVal = getDeepData(this.vm, this.val)
      這個操作會使observer中data屬性的get被觸發,此時如果我們將watcher注冊到dep中即可對所有數據變化進行監聽,然鵝,在實現的時候,發現了一些問題,由于defineReactive將data所有屬性都監聽了,導致取屬性時使用{{info.name}}時,data.info和data.info.name都會被劫持,而我們只需要info.name,所以,當dep注冊watcher時需要設置一個開關,并且在observer中根據開關添加監聽,修改的watcher和observer如下:
      watcher.js

      // 訂閱模式(比較綁定值的變化)
      class Watcher {
          constructor(vm, val, update) {
              this.vm = vm
              this.val = val;
              this.update = update
              this.oldVal = this.getOldVal() //獲取初始值,觸發observer中屬性的get
              update() //首次渲染初始化
          }
          getOldVal() {
              Dep.target = this //將watcher暫存到Dep上,在Observer中通過dep.subscribe將watcher傳到dep的observerList(調度中心)中,后續當值發送修改時通過fireEvent觸發watcher.compareVal來更新視圖
              const oldVal = getDeepData(this.vm, this.val) //觸發Observer中的getter,將watcher注冊到dep中
              Dep.target = null
              return oldVal
          }
          // 對比數據,更新視圖
          compareVal() {
              const newVal = getDeepData(this.vm, this.val);
              newVal !== this.oldVal && (this.update(), this.oldVal = newVal) //更新視圖后將新值賦到oldVal上
          }
      
      }

      observer.js中的defineReactive

          // 響應攔截器,遞歸監聽所有層級
          defineReactive(data, key, val) {
              this.initObserver(val) //劫持子項
              const dep = new Dep() //將observer與watcher連接,當watcher觸發數據變化后,將watcher中的回調函數注冊到dep中
              Object.defineProperty(data, key, {
                  enumerable: true, // 允許枚舉
                  configurable: false, // 不能被定義
                  get: _ => {
                      Dep.target && dep.subscribe(Dep.target); //獲取屬性值時,將watcher中的回調函數注冊到dep中(在頁面初始化時調用)
                      return val
                  },
                  set: newVal => newVal !== val && (val = newVal) //設置屬性時,對比新值和舊值有無差別
              })
          }

      現在,我們只剩下當數據發生改變時,如何通知watcher,因為上述的defineReactive中已經將watcher注冊到了dep,此時我們只需在數據變化時也就是defineReactive的set中對數據更新進行響應,當某條數據被設置時,我們將dep中watcher觸發即可

          // 響應攔截器,遞歸監聽所有層級
          defineReactive(data, key, val) {
              this.initObserver(val) //劫持子項
              const dep = new Dep() //將observer與watcher連接,當watcher觸發數據變化后,將watcher中的回調函數注冊到dep中
              Object.defineProperty(data, key, {
                  enumerable: true, // 允許枚舉
                  configurable: false, // 不能被定義
                  get: _ => {
                      Dep.target && dep.subscribe(Dep.target); //獲取屬性值時,將watcher中的回調函數注冊到dep中(在頁面初始化時調用)
                      return val
                  },
                  set: newVal => newVal !== val && (val = newVal, this.initObserver(newVal), dep.fireEvent()) //設置屬性時,對比新值和舊值有無差別,若修改的值是引用型時,將屬性重新注冊到dep中,并更新視圖
              })
          }

      至此,流程圖中的所有功能均已實現,讓我們在vue.js中實例化observer試試效果

      class VueDemo {
          constructor(options) {
              this.options = options //配置信息
              this.$data = options.data;
              // 判斷options.el是否存在
              (this.el = options.el) && Object.defineProperties(this, {
                  //observer和compile的順序不要錯,否則監聽不到compile中的數據
                  observer: {
                      value: new Observer(options.data) // 數據監聽器
                  },
                  proxy: {
                      value: new DataProxy(options.data, this) //data代理到this
                  },
                  compile: {
                      value: new Compile(options.el, this) //指令解析器
                  }
              })
          }
      }

      寫在最后

      感謝你看到了最后,希望文章能對你有幫助,同時也歡迎你提出寶貴的建議

      最后附上源碼地址
      喜歡這篇文章別忘了點個贊,你的支持是作者創作的動力

      posted @ 2021-08-08 23:08  阿宇的編程之旅  閱讀(324)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 麻豆国产成人AV在线播放| 毛片亚洲AV无码精品国产午夜| 国产亚洲精品第一综合麻豆| 国产精品午夜福利在线观看| 色欲国产精品一区成人精品| 久久久无码精品国产一区| 久久99精品国产99久久6男男 | 成人看的污污超级黄网站免费| 国产口爆吞精在线视频2020版| 久久夜色精品国产亚洲av| 日韩欧美亚洲综合久久| 2018av天堂在线视频精品观看| 欧美成本人视频免费播放| 一本色道久久加勒比综合| 中文国产乱码在线人妻一区二区| 国产精品久久久久久福利| 神马久久亚洲一区 二区| 91精品亚洲一区二区三区| 成人无号精品一区二区三区| 成人无码一区二区三区网站| 久久国产乱子精品免费女| 奇米777四色成人影视| 亚洲日韩精品无码一区二区三区| 精品日本乱一区二区三区| 日韩深夜视频在线观看| 精品人妻伦九区久久aaa片69| 精品人妻中文无码av在线| 91一区二区三区蜜桃臀| 久久高清超碰AV热热久久| 中文字幕在线精品人妻| 在线视频中文字幕二区| 国产美女久久久亚洲综合| 亚洲欧洲一区二区天堂久久| 色综合天天综合网天天看片| 熟妇无码熟妇毛片| 欧美人与动欧交视频| 国产成人精品无码专区| av无码久久久久不卡网站蜜桃| аⅴ天堂中文在线网| 麻豆精产国品一二三产| 巨爆乳中文字幕爆乳区|