Vue双向绑定原理及实现

5 篇文章 0 订阅
1 篇文章 0 订阅

双向绑定原理

原理

vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的

实现

  1. 首先,设置监听器Observer,对vue实例中的data数据进行劫持监听,核心方法就是Object.defineProperty(),递归遍历所有属性,添加get、set方法,其中利用订阅器Dep来收集订阅者,若访问属性则添加到订阅器,若数据变化则通知订阅者

  2. 其次,设置订阅者Watcher,对实例中data的属性绑定更新函数,当数据发生变化时执行相应函数,更新视图,其中订阅器Dep在监听器Observer和订阅者Watcher起到统一管理的作用

  3. 最后,实现指令解析器Compile,创建fragment文档片段接收节点元素并进行解析,解析过程是

    • 遍历子节点集合,如果是v-指令的元素节点,分别对v-model和v-on等不同类型的指令进行解析
    • 其中对v-model指令,将节点内容更新的方法加入到Watcher订阅者,并监听input输入事件,当数据变化时执行相应的函数更新视图;对v-on类型的事件指令添加监听方法,当事件触发时执行绑定的对应函数
    • 如果是{{}}文本节点,则初始化Watcher订阅者,并指定节点内容更新的方法,当然,如果有孩子节点则递归解析
    • 解析完毕,将文档片段追加到dom元素上

利用ES6 class关键字定义类的写法实现

1. Observer

  1. this接收Vue实例中的data
  2. 执行walk方法,若注入的是对象则遍历data,利用Object.defineProperty添加get、set方法,将其属性值转换为响应式数据
  3. 构造并初始化订阅器,如果调用该属性,则在执行get方法时添加至订阅器
  4. 数据改变执行set方法时,如果新赋值的是对象,添加set、get,将其内部的属性转换为响应式数据,并通知所有订阅者Watcher执行update函数,更新视图
/**
 * @description Observer-->数据监听器   (对Vue实例中的data进行监听)
 * @param       {Object}   data     Vue实例中的data
 */
class Observer {
  constructor (data) {
    this.data = data
    this.walk(data)
  }
  walk (data) {
    if (!data || typeof data !== 'object') return
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }
  defineReactive (data, key, val) {
    const self = this
    // 如果注入的是对象,则添加get、set,转化为响应式数据
    this.walk(val)
    // 初始化订阅器
    let dep = new Dep()
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get () {
        if (Dep.target) {
          dep.addSub(Dep.target)
        }
        return val
      },
      set (newVal) {
        if (newVal == val) return
        val = newVal
        // 如果新赋值的是对象,添加setter、getter,把val内部的属性转换为响应式数据
        self.walk(newVal)
        // 数据变化,通知所有订阅者Watcher
        dep.notify()
      }
    }) 
  }
}

/**
 * @description Dep-->订阅器
 * 1、this接收subs初始的空数组
 * 2、定义addSub方法,以便Observer监听器中属性调用get方法时,将订阅者Watcher添加至订阅器
 * 3、定义notify方法,用于Observer监听器中属性值变化时,调用update函数,更新视图
 */
class Dep {
  constructor () {
    this.subs = []
  }
  addSub (sub) {
    this.subs.push(sub)
  }
  notify () {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

Dep.target = null

2. Watcher

  1. this接收vm, exp, cb参数
  2. value接收get方法返回Vue实例中data对象的属性值,Dep.target缓存Watcher订阅器自己,value接收指令或文本节点绑定的属性值并返回,释放自己
  3. 定义update更新方法,oldVal接收之前存入的value为旧值,newVal重新接收Vue实例中data对象的属性值,将新值newVal赋于value,传新值、旧值入Watcher绑定的更新函数cb,利用call函数将this指向Vue实例本身并执行函数
/**
 * @description Watcher-->订阅者   (对vue实例的某个属性exp绑定更新函数cb)
 * @param vm      Vue对象
 * @param exp     node节点的v-指令、{{}}绑定的属性名
 * @param cb      Watcher绑定的更新函数
 */
class Watcher {
  constructor (vm, exp, cb) {
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.value = this.get() // 将自己添加到订阅器的操作
  }
  get () {
    Dep.target = this
    let value = this.vm.data[this.exp]
    Dep.target = null
    return value
  }
  update () {
    let oldVal = this.value
    let newVal = this.vm.data[this.exp]
    if (newVal === oldVal) return
    this.value = newVal
    this.cb.call(this.vm, newVal, oldVal)
  }
}

3. Compile

  1. this接收vm实例、el节点,创建fragment文档片段
  2. 初始化,将元素节点转换为文档片段,解析节点,最后将文档片段追加到#app的dom元素上
  3. 解析节点:遍历子节点集合childNodes判断元素节点(节点类型为1)还是文本节点(节点类型为3), 再遍历子节点的属性对不同指令进行不同的操作
/**
 * @description Compile -->解析指令   (对vue实例的dom元素el进行解析)
 * @param el      dom节点,如#app
 * @param vm      Vue实例
 */
class Compile {
  constructor (el, vm) {
    this.vm = vm
    this.el = document.querySelector(el)
    // 暂存处理后的新节点,最后追加到dom元素上,确保一次内容渲染刷新,提高性能
    this.fragment = null
    this.init()
  }
  // ------------初始化------------
  // 如果节点存在,则转换为文档片段对象,并解析节点,最后将文档片段追加到根节点#app的dom元素上
  init () {
    if (this.el) {
      this.fragment = this.nodeToFragment(this.el)
      this.compileElement(this.fragment)
      this.el.appendChild(this.fragment)
    } else {
      console.error('Dom元素不存在')
    }
  }
  // ------------节点转文档片段对象------------
  // 创建文档片段,循环将dom元素的第一个子节点放入fragment文档片段
  nodeToFragment (el) {
    let fragment = document.createDocumentFragment()
    let child = el.firstChild
    while (child) {
      fragment.appendChild(child) // 将Dom元素移入fragment中
      child = el.firstChild
    }
    return fragment
  }
  // ------------解析节点------------
  // 对nodeList类型的子节点集合,利用数组原型上的slice方法,并结合call在其作用域中调用,转换为数组从而使用forEach遍历
  // 如果是元素节点且有v-指令,则解析元素节点
  // 如果是文本节点有{{}},则解析文本节点
  // 如果有孩子节点则递归
  compileElement (el) {
    // [].slice === Array.prototype.slice  true
    [].slice.call(el.childNodes).forEach(node => {
      let reg = /\{\{(.*)\}\}/
      let text = node.textContent
      if (this.isElementNode(node)) {
        this.compile(node)
      } else if (this.isTextNode(node) && reg.test(text)) {
        this.compileText(node, reg.exec(text)[1])
      }

      if (node.childNodes && node.childNodes.length) {
        this.compileElement(node)
      }
    })
  }
  //  ------------遍历节点属性,解析元素节点------------
  // 如果是v-开头的指令节点,再判断如果是v-model解析model指令、v-on则解析事件指令
  // 最后移除属性
  compile (node) {
    Array.prototype.forEach.call(node.attributes, attr => {
      let attrName = attr.name // v-model   v-on:click
      let exp = attr.value // name          clickMe

      if (this.isDirective(attrName)) { // v-开头
        if (this.isEventDirective(attrName)) {  // v-on:click事件指令(截取v-后on开头)
          this.compileEvent(node, exp, attrName)
        } else {  // model指令
          this.compileModel(node, exp)
        }
        node.removeAttribute(attrName)
      }
    })
  }
  // ------------解析文本节点------------
  // 定义{{}}属性的值
  // 将节点内容更新的方法加入到订阅者
  compileText (node, exp) {
    let initText = this.vm[exp]
    this.updateText(node, initText)
    new Watcher(this.vm, exp, value => {
      this.updateText(node, value)
    })
  }
  // ------------解析事件指令------------
  // 如果事件类型和方法存在,则对节点元素添加监听方法,当事件触发时执行相应函数
  compileEvent  (node, exp, attrName) {
    let eventType = attrName.substring(2).split(':')[1] // click
    let cb = this.vm.methods && this.vm.methods[exp] // clickMe方法名

    if (eventType && cb) {
      node.addEventListener(eventType, cb.bind(this.vm))
    }
  }
  // ------------解析model指令------------
  // 定义v-model属性的值
  // 将节点内容更新的方法加入到订阅者
  // 监听input输入,如果变化的新值不等于v-model属性的值,将v-model属性的值修改为新值
  compileModel  (node, exp) {
    let value = this.vm[exp] // Vue实例中的属性值
    this.modelUpdater(node, value)
    new Watcher(this.vm, exp, value => {
      this.modelUpdater(node, value)
    })

    node.addEventListener('input', e => {
      let newValue = e.target.value
      if (value === newValue) return
      this.vm[exp] = newValue
      value = newValue
    })
  }
  // {{}}更新
  updateText  (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value
  }
  //model更新
  modelUpdater (node, value) {
    node.value = typeof value == 'undefined' ? '' : value
  }
  isDirective (attr) {
    return attr.indexOf('v-') == 0
  }
  isEventDirective (attrName) {
    return attrName.substring(2).indexOf('on:') === 0
  }
  isElementNode  (node) {
    return node.nodeType == 1
  }
  isTextNode (node) {
    return node.nodeType == 3
  }
}

4. Vue模拟对象

  1. this接收Vue实例中的el、data、mounted、methods参数
  2. 执行run方法,遍历Vue实例中的data数据,对其属性添加get、set函数, 利用Object.defineProperty将其转换为响应式数据
  3. 初始化Observer监听器实例,将data数据传入,对数据进行劫持监听
  4. 初始化Compile指令解析器实例,将el节点#app,Vue实例对象注入,解析指令
  5. 调用mounted方法执行挂载函数
/**
 * @description Vue-->小型Vue   (对vue实例的data属性进行监听,dom节点el进行解析,mounted挂载函数执行)
 * @param       {Object}  options  vue实例注入的对象,包含data、mehtods、mounted等
 */
class Vue {
  constructor (options) {
    this.el = options.el
    this.data = options.data
    this.mounted =options.mounted
    this.methods = options.methods
    this.run()
  }
  run () {
    Object.keys(this.data).forEach(key => {
      this.proxyKeys(key)
    })

    new Observer(this.data)
    new Compile(this.el, this)
    this.mounted.call(this) // 所有事情处理好后执行mounted函数
  }
  proxyKeys (key) {
    let self = this
    Object.defineProperty(this, key, {
      enumerable: true,
      configurable: true,
      get () {
        return self.data[key]
      },
      set (newVal) {
        if (newVal === self.data[key]) return
        self.data[key] = newVal
      }
    })
  }
}

5. index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>two-way Binding principle</title>
</head>
<style>
  #app {
    text-align: center;
  }
</style>
<body>
  <div id="app">
    <h2>{{title}}</h2>
    <input v-model="name">
    <h1>绑定值:{{name}}</h1>
    <button v-on:click="clickMe">click me!</button>
  </div>
</body>
<script src="js/observer.js"></script>
<script src="js/watcher.js"></script>
<script src="js/compile.js"></script>
<script src="js/index.js"></script>
<script type="text/javascript">
  new Vue({
    el: '#app',
    data: {
      title: 'hello, world',
      name: 'yang'
    },
    mounted() {
      window.setTimeout(() => {
        this.title = '你好,世界'
      }, 1000)
    },
    methods: {
      clickMe () {
        this.title = 'hello, universe'
      }
    }
  })
</script>
</html>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值