手写Vue之核心梳理

本篇文件来记录Vue双向数据绑定的实现。

一、知识点

  • 什么是双向数据绑定(MVVM)?
    MVVM分别表示Model View View-Model,即模型(数据访问层)、视图(界面)、视图模型(模型和视图的通信),是一种软件架构模式。

    View层接收到交互信息,通过View-Model更新Model数据,同样,当Model数据发生变化后(一般是请求后端数据)通知View-Model使得视图发生更新。从而实现双向数据监听,并修改视图或者模型,这就是MVVM模式。
  • Vue是如何实现双向数据绑定的?
    实现双向数据绑定的关键在于如何监听数据发生了变化,Vue2.x及以前版本通过Javascript内置标准对象的Object.defineProperty()方法实现。由于Object.defineProperty()无法监听到数组更新(准确来说是通过length增加长度监听不到),所以Vue3.x使用Proxy作为新的数据监听方案,Proxy可以监听到整个对象的变更。
  • Object.defineProperty()
    此方法可直接在一个对象上定义一个新的属性,或者修改对象的现有属性,并返回此对象,MDN传送门
    // 添加属性
    const obj = {}
    Object.defineProperty(obj, 'name', {
      value: 'hua',
      writable: false
    })
    console.log(obj.name) // hua
    
    // getter setter,监听数据变化就借助这两个函数
    const obj = {}
    let age = 18
    Object.defineProperty(obj, 'age', {
      get() {
        console.log('get data')
        return age
      },
      set(newValue) {
        console.log('set data')
        age = newValue
      }
    })
    obj.age = 16 // 输出:set data
    console.log(obj.age) // 先输出:get data 再输出:16
    
  • Proxy
    此对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)MDN传送门
    // 赋值转发到target
    const target = {}
    const p = new Proxy(target, {})
    p.name = 'hua'
    console.log(target.name) // hua
    
    // 拦截
    const target = {}
    const p = new Proxy(target, {
      get(obj, prop) {
        return obj[prop]
      },
      set(obj, prop, value) {
        if (prop == 'name' && typeof value == 'string') {
          obj[prop] = value
        }
        return true
      }
    })
    
    p.name = 66
    console.log(target.name) // undefined
    p.name = 'hua'
    console.log(target.name) // hua
    

二、实现

按照MVVM模式进行分解实现,即先实现View触发Model更新,再实现Model变化而更新View。

  • 创建项目
    我们先创建mvvm项目,在项目下创建vue.jsindex.html,分别作为Vue双向数据绑定“引擎”和Vue的使用场景。

  • 回忆如何使用Vue创建项目
    我们先来看以下代码:

    <div id="app">
      {{ message }}
    </div>
    
    const app = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue'
      }
    })
    

    是不是很熟悉,这个是Vue官网提供的声明式渲染,我们也将以此开始我们Vue双向数据绑定的开始。

  • 构造函数(类)Vue
    我们发现,Vue其实就是一个构造函数,接受一个对象作为参数,然后将message的值赋值给了id为app的div,那么我们来实现

    function Vue(op) {
      const el = document.querySelector(op.el)
      el.innerHTML = op.data.meaasge
    }
    

    至此,我们可以在页面上看到效果:
    到这里,我们以及有了一丝丝进步,但是接下来我们要实现在输入框输入之后,输入信息在div中显示出来。

  • input输入改变div中的值

    • 改造html
    <input />
    <div id="app"></div>
    
    • js操作Dom实现
    const app = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue'
      }
    })
    
    const input = document.querySelector('input')
    const el = document.querySelector('#app')
    
    input.oninput = function(e) {
      el.innerHTML = e.target.value
    }
    

    至此,我们实现了input输入改变div内容的需求,但是会发现,这个改变与Vue并没有关系。那如果与Vue有关我们应该怎么做呢?我们发现,在Vue构造函数中,为div初始化内容的时候是这样一段代码el.innerHTML = op.data.meaasge,也就是说,我们可以通过改变op.data.meaasge的值达到我们想要的结果。因此,我们可以这样改进代码:

    const app = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue'
      }
    })
    
    const input = document.querySelector('input')
    const el = document.querySelector('#app')
    
    input.oninput = function(e) {
      app.data.message = e.target.value
    }
    

    但是,当我们这样改之后,并没有达到我们的效果,为什么呢?因为最终要改变div中的值,还是的借助innerHTML实现,那么我们在什么时候去调用该方法呢?这里就需要利用对data进行劫持来触发。

  • Object.defineProperty数据劫持
    我们继续来改造我们的构造函数Vue,监听data的改变,然后调用innerHTML进行赋值。

    function observe(obj, el) {
      if (typeof obj !== 'object') {
        return
      }
    
      for (let key in obj) {
        observe(obj[key])
    
        let temp = obj[key]
    
        Object.defineProperty(obj, key, {
          get() {
            return temp
          },
          set(newValue) {
            if (temp !== newValue) {
              console.log('data changed', newValue)
              temp = newValue
              el.innerHTML = temp
            }
          }
        })
      }
    }
    
    function Vue(op) {
      const el = document.querySelector(op.el)
    
      this.data = op.data
    
      // 数据监听
      observe(op.data, el)
    
      // 初始化
      el.innerHTML = op.data.message
    }
    

    至此,我们实现了通过监听data变化而改变div内容的需求,但是你会发现,这里只要有数据发生变化了,都会改变div中的内容,如果我们只想在message改变的时候才改变div中的内容,怎么办呢?接下来我们来引入我们的监听者Watcher,完善我们的Vue.

  • 订阅者Watcher、订阅器Dep、拦截器observe

    // 拦截器
    function observe(obj, el) {
      if (typeof obj !== 'object') {
        return
      }
      for (let key in obj) {
        observe(obj[key])
    
        let temp = obj[key]
    
        let dep = new Dep()
    
        Object.defineProperty(obj, key, {
          get() {
            // 将watcher添加到订阅器中
            dep.depend()
            return temp
          },
          set(newValue) {
            if (temp !== newValue) {
              temp = newValue
              dep.notify()
            }
          }
        })
      }
    }
    
    
    // 会有多个订阅者,所以使用订阅器进行管理
    class Dep {
      constructor() {
        this.subs = []
      }
    
      // 添加订阅者
      depend() {
        if (Dep.target) {
          this.addSubs(Dep.target)
        }
      }
    
      // 通知订阅者更新
      notify() {
        this.subs.forEach(item => {
          item.update()
        })
      }
    
      addSubs(sub) {
        this.subs.push(sub)
      }
    
    }
    
    Dep.target = null
    
    // 订阅者
    class Watcher {
      constructor(vm, key, cb) {
        this.vm = vm
        this.key = key
        this.cb = cb
        this.value = this.get()
      }
    
      // 订阅者更新视图
      update() {
        const newValue = this.vm.data[this.key]
        const oldValue = this.value
        if (newValue !== oldValue) {
          // 这里相当于将cb添加到vm对象,然后进行调用
          this.cb.call(this.vm, newValue, oldValue)
        }
      }
      
      // 初始化value,并利用闭包形式将watcher绑定到订阅器中
      get () {
        Dep.target = this
        const value = this.vm.data[this.key]
        Dep.target = null
        return value
      }
    }
    
    function Vue(op) {
      const el = document.querySelector(op.el)
    
      this.data = op.data
    
      // 数据监听
      observe(op.data, el)
    
      // 初始化
      el.innerHTML = op.data.message
    
      // 监听message变化,只有message变化才能触发
      new Watcher(this, 'message', function(newValue, ondValue) {
        // 在这里对我们的div进行内容赋值
        el.innerHTML = newValue
      })
    }
    

    到这里,我们的Vue双向数据绑定已经完成了,让我们一起测试我们的代码吧!

  • 测试

    • View改变Model
      输入框输入,div内容随之变化
    • Model改变View
    const app = new Vue({
        el: '#app',
        data: {
          message: 'Hello Vue'
        }
      })
    
      const input = document.querySelector('input')
      const el = document.querySelector('#app')
    
      input.oninput = function(e) {
        app.data.message = e.target.value
      }
    
      setTimeout(function() {
        app.data.message = '过了两秒执行'
      }, 2000)
    

原文地址:https://www.cnblogs.com/huiwenhua/p/13648068.html