美文网首页
Vue.js 双向数据绑定原理剖析

Vue.js 双向数据绑定原理剖析

作者: 梦晓半夏_d68a | 来源:发表于2020-08-05 08:25 被阅读0次

  在理解Object.defineProperty(),我提到了Vue.js 通过Object.defineProperty 方法进行数据劫持从而实现双向数据绑定的,并在Object.defineProperty() 实现双向数据绑定小案例中手动实现了一个简单的双向数据绑定。
  其实只通过Object.defineProperty()已经可以实现数据绑定了,只是结合发布订阅者模式可以做到‘一对多’的模式,效率更高,Vue.js就是通过Object.defineProperty() + 发布订阅者模式 来实现双向数据绑定的。

  要实现mvvm的双向数据绑定,需要实现以下几点:

  1. 实现一个观察者Observer,能够对data数据对象的所有的属性都分别创建一个发布者Dep, 并通过Object.defineProperty() 对所有属性进行监听。
  2. 实现一个发布者Dep,建立一个数组subs保存该data属性的所有订阅者(Watcher),并定义增加订阅者(Watcher) 和更新subs数组中所有订阅者(Watcher)的方法。
  3. 实现一个订阅者Watcher,记录每个使用data数据对象的属性的地方,并定义了更新视图的update方法。
  4. 实现一个编译器Compiler,对Vue管理入口元素'#app'中的所有子元素节点的指令进行扫描和解析,根据指令模板替换数据,添加订阅者(Watcher)并更新视图

  流程图如下(PS:建议看后面代码的时候都结合这张图看):

双向数据绑定流程图

接下来就自己来实现双向数据绑定吧,先来看效果:


自己实现双向数据绑定效果.gif



  代码开始了,先准备入口页面和入口文件

index.html

<!-- 入口页面  -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    {{message}}
    <hr>
    <input v-model="message"><br>
    <h2>{{message}}</h2>
    <p>{{message}}</p>
  </div>
   <!-- 导入自己实现Vue.js 双向数据绑定 -->
   <script src="./bundle.js"></script>
</body>
</html>

main.js

// 入口文件 
import Vue from './vue'
const vue = new Vue({
  el: '#app',
  data: {
    message: '自己实现双向数据绑定'
  }
})
window.vue = vue // 绑定到window全局

vue.js

  自己写一个vue.js文件,并在index.html 中导入。

// 自己写的vue文件
import Obeserver from './Obeserver'
import Compiler from './Compiler'
class Vue {
  constructor(options) {
    // 1.保存创建Vue 实例传入的Vue管理入口元素'#app'、data数据
    this.$options = options
    this.$el = this.$options.el
    this.data = this.$options.data
    // 2.将创建Vue 实例传入的data数据进行遍历,使用this.message返回this.data.message的值
    Object.keys(this.data).forEach(item => {
      this._proxy(item)
    })
    // 3.使用观察者Obeserver监听data中数据的改变
    new Obeserver(this.data)
    // 4.使用编译器Compiler编译模板
    new Compiler(this.$el, this)
  }

  _proxy(key){
    let that = this
    Object.defineProperty(this, key, {
      get() {
        return that.data[key]
      },
      set(newVal) {
        that.data[key] = newVal
      }
    })
  }
}
export default Vue

观察者Observer

  实现一个观察者Observer,通过Object.defineProperty()对所有属性的存取进行监听,当监听到数据的变化之后就需要通知订阅者了,因此可以实现一个发布者Dep,在发布者Dep里面维护一个数组subs,用来收集订阅者,数据变动触发notify就可以通知到订阅者了,订阅者再调用update方法更新视图,data中属性的存取如下:
(1) 当属性值变化时自动调用Object.defineProperty()set方法,拿到最新值并通知发布者Dep,发布者Dep再遍历所有订阅者Watcher修改对应订阅者Watcher对应的节点值,更新视图;
(2) 当获取属性值时自动调用Object.defineProperty()get方法,调用发布者Dep的addSub方法(判断是否是新的订阅者Watcher,是则添加到发布者Dep的subs数组中)

// 观察者-Obeserver
import Dep from './Dep'
class Obeserver {
  constructor(data) {
    this.data = data // vue中的data
    // 使用Object.defineProperty监听data数据对象的所有属性
    Object.keys(this.data).forEach(key => {
      let value = this.data[key]
      let dep = new Dep(key) // 创建dep对象
      Object.defineProperty(this.data, key, {
        set(newValue) {
          if (value === newValue) return
          console.log(`监听到data中${key}改变,新值为:${newValue}`)
          value = newValue
          dep.notify()
        },
        //获取this.data中key的值都会触发 get函数, 该方法将Watcher存放到subs数组中
        get() {
          console.log(`获取到${key}对应的值为:${value}`)
          if (Dep.target) {
            dep.addSub(Dep.target)
            console.log('添加watcher')
          }
          return value
        }
      })
    })
  }
  
}
export default Obeserver

Dep 发布者

  实现一个发布者Dep,建立一个数组subs保存该data属性的所有订阅者(Watcher),并定义了增加订阅者(Watcher) 的addSub方法和更新subs数组中所有Watcher中对应节点的值的notify方法。

// Dep-发布者
import Watcher from './Watcher'
class Dep {
  constructor(data, value) {
    this.subs = []
    this.data = data
    this.value = value
  }
  addSub(watcher) {
    this.subs.push(watcher)
  }
  // 遍历subs中存放的所有Watcher,调用update方法,更新视图
  notify(value) {
    console.log(this.subs)
    this.subs.forEach(item => {
      item.update(value)
    })
  }
}
Dep.prototype.target = null
export default Dep

Watcher订阅者

  实现一个订阅者Watcher,记录每个使用data数据对象的属性的地方,并定义了更新视图的update方法。

// Watcher-订阅者
import Dep from "./Dep"

class Watcher {
  constructor(node, name, vm, type) {
    // 编译器传过来的
    this.node = node
    this.name = name
    this.vm = vm
    this.type = type
    Dep.target = this
    this.update()
    Dep.target = null
  }
  // 更新视图
  update(){
    /* 执行this.vm[this.name],会调用Observer中Object.defineProperty()的get方法,get方法会判断是否是新的订阅者Watcher,是则将订阅者Watcher 存放到subs数组中
      再将this.vm[this.name]的值赋给该节点的值,完成页面渲染
    */
    if (this.type === 'twoEle') {
      this.node.value = this.vm[this.name] // 元素节点包含v-model更新视图
    }else if(this.type === 'ele'){
      this.node.innerText = this.vm[this.name] // 元素节点包含插值更新视图
    }
    else if(this.type === 'text'){
      this.node.nodeValue = this.vm[this.name] // 文本节点更新视图
    }
  }
}
export default Watcher

编译器Compiler

  实现一个编译器Compiler,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。

import Watcher from './Watcher'
const reg = /\{\{(.*)\}\}/
class Compiler {
  constructor(el, vm) {
    this.el = document.querySelector(el)
    this.vm = vm
    this.frag = this._createFragment()
    this.el.appendChild(this.frag)
  }
  // appendChild() 方法的作用是从一个元素向另一个元素中移动元素, 因此通过while可以遍历到this.el(#app)中的所有子元素,添加到frag中
  _createFragment() {
    // createDocumentFragment适合处理有大量dom元素,效率比createElement高
    let frag = document.createDocumentFragment() 
    while(this.el.firstChild) {
      this._compile(this.el.firstChild)
      frag.appendChild(this.el.firstChild)
    }
    return frag
  }
  // 判断节点类型,填加订阅者Watcher并更新视图
  _compile(node) {
    // node.nodeType: 1元素节点  3文本节点
    let type = null
    if (node.nodeType === 1) {
      console.log(node.attributes)
      let attr = node.attributes
      /* 元素节点中有v-model: view --> model  model --> view
        (1) 此处以input输入框为例,监听input 事件,更新data中数据
        (2) 设置input输入框的值为data中对应的数据
      */
      if(attr.hasOwnProperty('v-model')) {
        let name = attr['v-model'].nodeValue
        node.addEventListener('input', (e) => {
          this.vm[name] = e.target.value
        })
        type = 'twoEle'
        new Watcher(node, name, this.vm, type)
      }
      // 元素节点中有插值 {{}}
      if (reg.test(node.innerText)) {
        // 获取元素节点{{}}中的数据
        let name = RegExp.$1
        name = name.trim()
        type = 'ele'
        // 将获取到的数据用Watcher订阅者记录并更新视图
        new Watcher(node, name, this.vm, type)
      }
    }else if (node.nodeType === 3) {
      if (reg.test(node.nodeValue)) {
        // 测试文本节点是否有插值{{}},则获取{{}}中的数据
        let name = RegExp.$1
        name = name.trim()
        type = 'text'
        // 将获取到{{}}中的数据用Watcher订阅者记录
        new Watcher(node, name, this.vm, type)
      }
    }
  }
}
export default Compiler

  好了,这就实现了一个简单的双向数据绑定了,其思想和原理大都来自vue源码,最后和官方的Vue.js 做一个效果对比。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    {{message}}
    <hr>
    <input v-model="message"><br>
    <h2>{{message}}</h2>
    <p>{{message}}</p>
    {{message}}
  </div>
  <!-- 开发环境版本,包含了有帮助的命令行警告 -->
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
    const vue = new Vue({
      el: '#app',
      data: {
        message: '使用Vue官方提供的双向数据绑定'
      }
    })
  </script>
</body>
</html>
Vue官方的双向数据绑定效果.gif

  通过对比可以知道,自己实现的vue.js 和官方的vue.js 双向数据绑定效果一致,收工 。



文中有不足或者读者有疑问或更好的见解,欢迎留言讨论。
如果觉得该篇文章对您有帮助,别忘了留下您的足迹,点个赞❤噢

相关文章

  • Vue双向数据绑定原理

    剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模...

  • 关于双向绑定的问题

    剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模...

  • vue双向数据绑定

    剖析Vue原理、实现双向绑定MVVM 几种实现双向绑定的做法 目前几种主流的mvc(vm)框架都实现了单向数据绑定...

  • vue双向绑定原理

    Vue.js双向绑定的实现原理

  • Vue.js 双向数据绑定原理剖析

      在理解Object.defineProperty(),我提到了Vue.js 通过Object.definePr...

  • Vue原理研究之双向数据绑定

    前言 本篇文章主要研究Vue的双向数据绑定的学习笔记。具体细节请参考《剖析Vue原理&实现双向绑定MVVM》。 原...

  • Vue之表单双向数据绑定和组件

    三、表单双向数据绑定和组件 目录:双向数据绑定、组件 1.双向数据绑定 1)什么是双向数据绑定Vue.js是一个M...

  • vue数据双向绑定原理

    vue数据双向绑定原理 vue.js 采用数据劫持结合发布者-订阅者模式的方式,通过Object.definePr...

  • 深入Vue响应式原理

    1.Vue的双向数据绑定 参考 vue的双向绑定原理及实现Vue双向绑定的实现原理Object.definepro...

  • js实现双向数据绑定

    js双向绑定几种方法的介绍 使用Object.defineProperty实现简单的js双向绑定剖析Vue原理&实...

网友评论

      本文标题:Vue.js 双向数据绑定原理剖析

      本文链接:https://www.haomeiwen.com/subject/rgldrktx.html