乐趣区

关于vue.js:手牵手带你实现minivue-京东云技术团队

1 前言

随着 Vue、React、Angularjs 等框架的诞生,数据驱动视图的理念也深入人心,就 Vue 来说,它领有着双向数据绑定、虚构 dom、组件化、视图与数据相拆散等等造福程序员的长处,那 Vue 的双向数据绑定实现原理是什么样的,如果让咱们本人去实现一个这样的双向数据绑定要怎么做呢,本文就与大家分享一下 Vue 的绑定原理及其简略实现

2 核心技术

大家都晓得 Vue2 双向绑定是基于 ES5 的 Object.defineProperty 办法 + 公布订阅者模式实现的 那咱们首先简略理解一下这两个模块都是做什么的,在 Vue 中充当了什么角色

2.1 Object.defineProperty

用来在对象上定义或者批改一个属性值,实现数据劫持,为批改数据后去调用视图更新做筹备

  const obj = {}
  let age = 18
  Object.defineProperty(obj, 'age',{get() {return age},
    set(newVal) {age = newVal + 1},
    enumerable: true
   })
 console.log(obj.age) // 18
 obj.age = 20
    console.log(obj.age) // 21

2.2 公布订阅者模式

此模式简略来讲就是分为公布和订阅两个概念,订阅意思就是咱们会定义很多个订阅者,每个订阅者都会有本人的 update 办法,把须要更新的订阅者放到数组中,而公布就代表告诉订阅者去顺次执行其 update 办法,从而实现数据更新

// 定义放订阅者的数组
function Dep() {this.subs = []
}
// 定义寄存订阅者的办法
Dep.prototype.addSub = function(sub) {this.subs.push(sub)
}
// 定义公布的办法
Dep.prototype.notify = function(sub) {
  this.subs.forEach(sub => {
    // 顺次告诉订阅者去执行 update 办法
    sub.update()})
}

写到这里咱们就把公布和订阅筹备好了,然而还短少订阅者,且订阅者要保障提供一个 update 办法才行,那咱们不禁想到是否去创立一个构造函数,通过这个构造函数创立的实例都会有 update 办法呢

function Watcher(fn) {this.fn = fn}
// 通过该构造函数创立的实例都会有 update 办法
Watcher.prototype.update = function() {this.fn()
}
// new 实例
const watcher1 = new Watcher(() => console.log('我是 watcher1'))
const watcher2 = new Watcher(() => console.log('我是 watcher2'))
const dep = new Dep()
// 把筹备好的事件放入到数组中
dep.addSub(watcher1)
dep.addSub(watcher2)
// 进行公布
dep.notify()
// 最终输入 我是 watcher1 我是 watcher2

3 具体实现

3.1 初始化

一个框架都是从它的初始化开始的,Vue 也不例外

<body>
    <div id="app">
      <p>a value: {{a.a}}</p>
      <div>b value: {{b}}</div>
      <span>v-model: </span><input type="text" v-model="b">
    </div>
    <script>
      // 模拟 Vue 的初始化和传入的参数
      let vue = new Vue({
        el: '#app',
        data: {a: { a: 'is a'},
          b: 'is b',
          c: 'is c'
        }
      })
    </script>
  </body>

3.2 数据劫持 observe

Vue 中在 data 中定义的属性才能够实现双向绑定,为了实现这个性能,咱们定义一个 Observe 用来劫持到对象的属性

// 给对象减少数据劫持
function Observe(data) {
  // 因 defineProperty 每次只能设置单个属性 所以需遍历
  for (let key in data) {let val = data[key]
    observe(val)
    Object.defineProperty(data, key, {
      enumerable: true,
      get() {return val},
      set(newVal) {if (newVal === val) return // 新值与旧值相等时 不做解决
        val = newVal // 之所以给 val 赋值 是因为取值时获得 val
        observe(newVal) // 当给变量值赋予一个新对象时 仍然须要劫持到其属性
      }
    })
  }
}
function observe(data) {if (typeof data !== 'object') return
  return new Observe(data)
}

3.3 构造函数编写

 function Vue(options = {}) {
  //  模拟 Vue 把属性挂载到 $options 且能够通过 this._data 拜访属性
  this.$options = options
  const data = this._data = this.$options.data
  observe(data) // 给 data 减少数据劫持
}

[]()

上图中咱们模拟 Vue 对 options 和 data 减少了一些可拜访形式,给 data 减少了数据劫持,也在咱们的实例中看到了成果,那这时又有了新的问题,Vue 中拜访数据都是 this.xxx 可间接通过实例拜访,这样比咱们图中的拜访形式还要更不便一些,那咱们也是否把属性间接挂载到实例上呢,当然是能够的

3.4 数据代理

如果想要间接把属性挂载到实例上,那咱们须要保障通过实例间接拜访的属性值是实时无误的,且去批改该属性值还可能被劫持到,否则会影响前面的双向数据绑定,既然 data 中的数据咱们曾经通过 Observe(在 3.2 节)做了劫持,那咱们在通过 this.xxx 间接批改属性时只须要去批改 data 对应中的属性就能够触发 Observe 劫持

 function Vue(options = {}) {
  //  模拟 Vue 把属性挂载到 $options
  this.$options = options
  const data = this._data = this.$options.data
  observe(data)
  // 将以后 this 传入办法 将属性挂载到 this 上
  proxyData.call(this, data)
}

// this 代理 this._data
function proxyData(data) {
  const vm = this
  for (let key in data) {
    Object.defineProperty(vm, key, {
      enumerable: true,
      get() {return vm._data[key]
      },
      set(newVal) {
// 间接批改 data 中对应属性 触发 data 中劫持 保持数据对立
        vm._data[key] = newVal 
      }
    })
  }
}

[]()

3.5 实现 compile

先在内存中创立一个文档碎片来递归所有 dom 节点,用正则匹配 {{}} 相符的节点,获取到括号里的 key,最初在 data 中拿到对应 key 的属性值,替换到节点上(因为次要是实现双向绑定,所以咱们将 dom 的操作放到文档碎片中操作来代替虚构 dom)

function Compile(el, vm) {vm.$el = document.querySelector(el)
  // 建设文档碎片 将 el 下的所有元素挪进文档碎片 防止死循环
  const fragment = document.createDocumentFragment()
  // 将 el 中的元素都移入碎片中
  while (child = vm.$el.firstChild) {fragment.appendChild(child)
  }
  // 匹配节点中的{{}} 将其替换为对应的值
  replace(fragment)
  function replace(fragment) {
    // 循环每一层节点
    Array.from(fragment.childNodes).forEach(node => {
      const text = node.textContent
// 定义正则表达式
      const reg = /{{(.*)}}/
      // 此判断为当节点是文本节点(因为变量都是文本)且被蕴含在 {{}} 中的文本节点时
// 文本节点 Node.TEXT_NODE: 3
      if (node.nodeType === Node.TEXT_NODE && reg.test(text)) {
// 以下三行为了获取到 key 对应的 value 值 页面初始化后失常将变量替换为值
        const arr = RegExp.$1.split('.') // [a, a]  [b]
        let val = vm
        arr.forEach(k => (val = val[k]))
        node.textContent = text.replace(reg, val)
      }
      if (node.childNodes) {replace(node)
      }
    })
  }
  // 将解决好的文档碎片塞回 dom 中
  vm.$el.appendChild(fragment)
}

初始化 Vue 时调用 compile

 function Vue(options = {}) {
  //  模拟 Vue 把属性挂载到 $options
  this.$options = options
  const data = this._data = this.$options.data
  observe(data)
  // 将以后 this 传入办法 将属性挂载到 this 上
  proxyData.call(this, data)
  new Compile(options.el, this)
}

[]()

3.6 Model -> ViewModel -> View

目前咱们曾经实现的性能:数据劫持、this 代理、编译模板,最终咱们要达到批改数据、视图自动更新的成果,还须要以下工作

1)第一步咱们须要创立一个订阅者,其 update 事件就是接管到咱们更新后的数据值而后去更新 dom, 因为要更新 dom,所以此订阅者是在 compile 中定义的,并且大家会发现咱们在编译过程中,是循环每一层节点去判断的,也就意味着咱们页面有多少个符合条件的文本节点,就会新建多少个 watcher,那这时就须要把文本节点的对应 key 和 value 传入 watcher 中,用来判断更新的哪个节点值

[]()

2)既然咱们的 watcher 新增了参数(vue 实例、节点变量)所以咱们须要对 watcher 办法做出更改

[]()

3)当 watcher 定义好后,还须要批改下其 update 办法,因为咱们的 watcher 第三个参数也就是回调函数中新增了参数,须要给其传参

Watcher.prototype.update = function() {
  // this.exp 可取到 key 值 从 vm 中凭借 key 就能够取到属性值
  let val = this.vm
  const arr = this.exp.split('.')
  arr.forEach(k => (val = val[k]))
  this.fn(val) // 传入 newVal
}

4)订阅者都筹备好了,还须要增加订阅者到 dep 数组并且在数据改变后调用公布,这个过程须要在 observe 中实现

[]()

5)最终成果如图所示

[]()

3.7 View -> ViewModel -> Model

下面咱们实现了从数据到视图的更新,那视图从数据的更新呢,首先咱们想到一个最常见的例子(v-model), 要想实现它,咱们须要做以下两步

1)把 value 值展现到绑定 v-model 的 input 中

[]()

[]()

2)其次是每次咱们更改值时都应该把值更新到界面上,所以咱们还须要新建一个 watcher,在绑定 v-model 的 input 上绑定事件,当输出文案时获取到输出的值,扭转 data 只对应的属性值

[]()

3)最终成果如图所示

[]()

4 总结

4.1 实现思路

[]()

4.2 优缺点

长处:胜利达到了数据和视图的双向驱动,像在操作表单时应用会更不便,省略了很多反复的 onChange 事件去解决数据的变动,也省略了给 dom 增加值的操作,代码量会更少,更不便保护

毛病:批改数据时会使得咱们无奈追踪数据的扭转源头,且在数据劫持那步须要去循环, 为一个对象的每一个属性减少劫持,无奈间接在一个对象上减少所有属性的劫持(该毛病 Vue3 已躲避,大家可自行学习)

4.3 小结

在工作中咱们很多我的项目会用到框架,理解它的一些原理有助于咱们更好的去应用,便于咱们造就本人的‘造轮子’能力,遇到问题时能更好的解决,缩小不必要的 bug,更好的去调试代码,一些很简单的组件如果找不到开源的话,本人也能去实现不至于一头雾水

4.4 参考资料

  • 可运行的该源码实例:https://coding.jd.com/zhangtingting155/mini-vue.git
  • Vue 双向绑定:https://github.com/answershuto/learnVue/blob/master/docs/%E4%BB%8E%E6%BA%90%E7%A0%81%E8%A7%92%E5%BA%A6%E5%86%8D%E7%9C%8B%E6%95%B0%E6%8D%AE%E7%BB%91%E5%AE%9A.MarkDown
  • 数据劫持:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Refer…
  • 很乏味的设计模式:https://refactoringguru.cn/design-patterns

5 思考

至此,一个双向数据绑定性能就根本实现了,本文咱们的实现是基于 Vue2 的双向数据绑定原理,目前 Vue3 曾经趋于稳定,咱们能够思考下,如果是基于 Vue3 的原理去做,那须要怎么去实现呢。

作者:京东物流 张婷婷

起源:京东云开发者社区

退出移动版