Vue源碼解析06-手寫自己的Vue

Vue 源碼解析 06-手寫自己的 Vue

最近一段時間一直在研究 Vue 的源碼,突然間想寫一個乞丐的 Vue 實現,爲了理一下自己的思路,同時也作爲一個階段性的總結.

實現雙向數據綁定

Vue 雙向綁定看這裏
Vue2.0/1.0 雙向數據綁定簡單來說就是利用了 Object.defineProperty()和觀察者模式對 data 數據進行數據劫持.
在這裏插入圖片描述

廢話不多說,直接上代碼

Watcher 實現數據的更新操作

//Watcher,觀察者,真正執行更新操作的角色
 class Watcher{
   constructor(vm,key,update){
     //保存傳入的選項
     this.vm = vm
     this.key = key
     this.update = update
     //將當前watcher添加到Dep中
     Dep.target = this
     this.vm[this.key]
     Dep.target = null
   }
  //用來執行我們的更新函數
   update(){
     this.update&&this.update(this.vm[this.key])
   }
 }

Dep 維護更新隊列和通知更新

//Dep負責維護一個更新隊列,
 class Dep{
   constructor(){
     this._dep = []
   }
   //添加更新隊列(這是一個乞丐版的)
   addDep(watcher){
     this._dep.push(watcher)
   }
   //通知隊列更新
   notify(){
     this._dep.forEach(item=>item())
   }
 }

observer 的實現

 class Vue{
   //構造函數接收配置項,將其保存起來
   constructor(options){
     this.$options = options||{}
     this.$data = options.data||{}
     //執行響應化處理
     this.$data&&this.observe(this.$data)
     //執行編譯
    new Compile(options.el, this)
   }

   observe(obj){
     //響應化的數據必須是對象
     if(!obj||typeof obj !=='object')return
      //遍歷
     Object.keys(obj).forEach(key=>{
       //執行響應化
       this.defineReactive(obj,key,obj[key])
       //代理數據(將data數據代理到當前vue實例上)
       this.proxyData(key)
     })
   }

  //數據劫持
  defineReactive(obj,key,val){
    //當前val爲對象時,遞歸處理
    this.observe(val)
    //每一個key都對應一個dep,用來維護更新隊列
    const dep = new Dep()
    Object.defineProperty(obj,key,{
      get(){
        Dep.target&&dep.addDep(Dep.target)
        return val
      },
      set(newVal){
        if(newVal!==val){
          val = newVal
          //通知更新隊列更新
          dep.notify()
        }
      }
    })
  }
   //代理數據
   proxyData(key){
     Object.defineProperty(this,key,{
       get(){
         return this.$data[key]
       },
       set(newVal){
          this.$data[key]=newVal
          //如果新添加的是一個對象,繼續響應化處理
          this.observe(this.$data[key])
       }
     })
   }
 }

編譯的實現

編譯的實現原理很簡單,我們可以簡化爲三步:

  • 獲取並遍歷 DOM 樹
  • 如果是文本節點,獲取{{}}的內容並解析替換
  • 如果是元素節點,訪問節點特性,截取“v-”和@開頭的並解析

現在看一下代碼的實現

  class Compile{
    //接收一個宿主元素,確定掛載目標,同時接收一個當前vue實例
    constructor(vm,el){
      this.$vm = vm
      this.$el = document.querySelector(el)
      //如果el存在,則執行編譯
      this.$el&&this.compile(this.$el)
    }
    //編譯
    compile(el){
      //獲取所有子元素,獲取DOM樹
      const childNodes = el.childNodes
      //遍歷所有子元素
      Array.from(childNodes).forEach(node=>{
        //判斷節點類型
        if(this.isElement(node)){
          //如果是元素節點,執行元素節點更新
          this.compileElement(node)
        }else if(this.isText(node)){
          //如果是文本節點,執行文本節點更新
          this.compileText(node)
        }
        //如果子元素下面還有子元素,遞歸處理
        if(node.childNodes&&node.childNodes.length>0){
          this.compile(node)
        }
      })
    }
    //編譯元素節點
    compileElement(node){
      //獲取節點特性
      const attrs = node.attributes
      //遍歷處理特性
      Array.from(attrs).forEach(item=>{
        //獲取特性信息
        const name = item.name
        const val = item.value
        //如果包含“v-”,我們認爲是指令
        if(name.indexOf('v-')>-1){
          //截取指令名稱
          const dir = name.substring('2')
          //如果存在指令更新函數,則進行更新
          this.update(node,val,dir)
        }else if(name.indexOf('@')>-1){
          //@認爲是監聽事件
          const dir = name.substring(1)
          node.addEventListener(dir,this.$vm[val].bind(this.$vm));
        }
      })
    }
    //編譯文本節點
    compileText(node){
      //獲取數據內容
      const exp = RegExp.$1
      //執行更新
      this.text(node,exp)
    }
    text(node,key){
      this.update(node,key,'text')
    }
    //雙向數據綁定
    model(node,key){
      //v-model的原理就是一個監聽事件+“類似v-text”的語法糖
      this.update(node,key,'model')
      //添加input的監聽事件
      node.addEventListener('input',event=>{
        this.$vm[key]=event.target.value
      })
    }
    html(node,key){
      this.update(node,key,'html')
    }
    //update函數
    update(node,key,dir){
      //獲取具體的更新函數
      const updater =this[dir+'Updater']
      updater&&updater.call(this,node,this.$vm[key])
      //添加Watcher
      new Watcher(this.$vm,key,val=>{
        updater&&updater.call(this,node,val)
      })
    }
    //更新text文本
    textUpdater(node,val){
      node.textContent =val
    }
    //更新v-html
    htmlUpdater(node,val){
      node.innerHTML= val
      //響應式的編譯新添加的子節點
      this.compile(node)
    }
    //model更新函數
    modelUpdater(node,val){
      //value屬性賦值
      node.value = val
    }

    //判斷是否爲元素節點
    isElement(node){
      //元素節點的nodeType類型爲1
      return node.nodeType===1
    }
    //判斷是否爲文本節點
    isText(node){
      //不僅要判斷元素類型,並且文本內容包含{{}}
      const res = node.nodeType===3&&/\{\{.*\}\}/.test(node.textContent)
      return res
    }
  }

寫在最後

以上,就是一個乞丐版的 Vue,簡單的實現了 Vue 的雙向數據綁定和 DOM 編譯解析更新.這裏只是實現了一個簡單的函數更新,Vue2.0 裏面的 Watcher.run()函數是進行虛擬 DOM 的更新.
雖然是乞丐版的實現,但是感覺思路是相通的:通過 Object.defineProperty 實現數據劫持,通過 Compile 模塊實現 DOM 的更新.

源碼在這裏

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章