• 手写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)
      

  • 相关阅读:
    Lc1049_最后一块石头的重量II
    Lc343_整数拆分
    MySQL使用Limit关键字限制查询结果的数量效率问题
    Lc62_不同路径
    Java几种序列化方式对比
    3、你平时工作用过的JVM常用基本配置参数有哪些?
    2、你说你做过JVM调优和参数配置,请问如何盘点查看MM系统默认值
    强引用、软引用、弱引用、虚引用分别是什么?
    零拷贝
    并发编程面试题-锁的优化 和 happen-before原则
  • 原文地址:https://www.cnblogs.com/huiwenhua/p/13648068.html
Copyright © 2020-2023  润新知