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 的更新.

源码在这里

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