zoukankan      html  css  js  c++  java
  • 理解Vue响应式原理,实现一个Mini vue

    MVVM(Model-View-ViewModel)是一种程序的架构设计,相比于MVC,ViewModel充当了控制层(Control),Vuejs的核心就是实现这个ViewModel,用一张图表示,就是下面这样

    在JS中,Model可以看做是Object对象,View就是HTML网页。在传统开发中,当数据发生变化时,开发者需要手动更新DOM,进而改变视图层。而在Vue中,一切视图的改变都是基于数据的变动,也就是说,开发者不需要再操作DOM。ViewModel的核心就是建立视图和数据之间的联系,做到数据驱动视图的变化。所谓双向数据绑定,不过就是数据驱动视图变化,视图驱动数据变化

    Vue实现ViewModel是通过Object.defineProperty实现的,关于defineProperty的基本语法可移步至此。Vue在实例化的过程中,会遍历data中的所有属性,然后通过Object.defineProperty把这些属性全部转为 getter/setter,内部用一个Observer对象监听数据的设置和读取,这个过程也叫作数据劫持。每一个Vue实例都会有一个Watcher(订阅者)对象,在模板编译过程中,需要插入数据的地方都会用getter去访问data属性,比如v-model或者v-text等等,Watcher会把用到的data属性记为依赖,在内部用Dep对象统一管理这些依赖,这样就建立了视图和数据之间的联系。当渲染视图的数据依赖发生变化时,setter函数会被调用,Watcher会对比前后两个的数值是否发生变化,然后确定是否通知视图进行重新渲染。

    用一张图表示就是下面这样

    下面实现一个简版的Vue.js

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      
      <title>Mini Vue.js</title>
    </head>
    <div id="app"></div>
    <body>
      <script>
      // vue 实例,接收一个 option(Object) 参数
      class Vue {
        constructor(options = {}) {
          this.$options = options
          // 简化了对data的处理
          let data = this._data = this.$options.data
          // 遍历data, 将所有data最外层属性代理到Vue实例上
          // this.key 就能访问到 data 对象中的数据
          Object.keys(data).forEach(key => this._proxy(key))
          // 监听数据
          observe(data)
    
          // 获取dom节点
          this.$el = document.querySelector(options.el)
    
    
          // 渲染DOM
          this._randerDom()
        }
        _randerDom(val) {
          // 解析字符串模版,为了简单直接用了innerHTML
          if (this.$el && this.$options && this.$options.template) {
            this.$el.innerHTML = this.$options.template(this._data)
          }
        }
        // 暴露一个在vue实例外使用订阅者的接口,在实例内部主要在指令中使用
        // 指令的实现需要编写complie函数,为了简单,不写该函数也不影响理解vue原理
        $watch(expOrFn, cb) {
          // 添加订阅者
          new Watcher(this, expOrFn, cb)
        }
        _proxy(key) {
          // 把这data属性全部转为 getter/setter。
          Object.defineProperty(this, key, {
            configurable: true,
            enumerable: true,
            get: () => this._data[key],
            set: (val) => {
              this._data[key] = val
            }
          })
        }
      }
    
      function observe(value) {
        // 当值不存在,或者不是复杂数据类型时,不再需要继续深入监听
        if (!value || typeof value !== 'object') {
          return
        }
        return new Observer(value)
      }
    
      /*
       * Observer.js
       */
      class Observer {
        constructor(value) {
          this.value = value
          this.walk(value)
        }
        walk(value) {
          // 遍历传入的data, 将所有data的属性添加set&get
          Object.keys(value).forEach(key => this.convert(key, value[key]))
        }
        convert(key, val) {
          // 添加set&get方法
          defineReactive(this.value, key, val)
        }
      }
    
      function defineReactive(obj, key, val) {
        var dep = new Dep()
        // 给传入的data内部对象递归的调用observe,来实现深度监听
        var chlidOb = observe(val)
    
        Object.defineProperty(obj, key, {
          enumerable: true, // 可枚举
          configurable: true, // 可修改
          get: () => {
            // Watcher实例在实例化过程中,会为Dep添加一个target属性,在读取data中的某个属性,会触发当前get方法。
            // 如果Dep类存在target属性,将订阅者添加到dep实例的subs数组中
            if (Dep.target) {
              dep.addSub(Dep.target)
            }
            return val
          },
          set: (newVal) => {
            // 在设置data中的某个属性,会触发当前set方法。
            if (val === newVal) return
            val = newVal
            // 对新值进行监听
            chlidOb = observe(newVal)
            // 通知所有订阅者,数值被改变了
            dep.notify()
          }
        })
      }
    
      // 订阅者管理员,管理所有订阅者队列
      class Dep {
        constructor() {
          this.subs = [] // 订阅者队列
        }
        addSub(sub) {
          this.subs.push(sub) // 添加订阅者
        }
        notify() {
          // 当改变data中的属性值时,会通知所有的订阅者(Watcher),触发订阅者的相应逻辑处理
          this.subs.forEach((sub) => sub.update())
        }
      }
    
      /*
       * Watcher订阅者
       */
      class Watcher {
        constructor(vm, expOrFn, cb) {
          this.vm = vm // 被订阅的数据一定来自于当前Vue实例
          this.cb = cb // 当数据更新时需要执行的回调函数
          this.expOrFn = expOrFn // 被监听的数据(表达式或函数)
          this.oldVal = this.get() // 维护更新之前的数据
        }
        // 对外暴露的接口,用于在订阅的数据被更新时,由订阅者管理员(Dep)调用
        update() {
          this.vm._randerDom() // 检测的数据变动后,更新dom 
          this.run()
        }
        run() {
          const newVal = this.get()
          if (newVal !== this.oldVal) {
            this.cb.call(this.vm, this.oldVal, newVal)
            this.oldVal = newVal;
          }
        }
        get() {
          // 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者
          Dep.target = this
          const val = this.vm._data[this.expOrFn]
          // 置空,用于下一个Watcher使用
          Dep.target = null
          return val;
        }
      }
    
    
      let app = new Vue({
        el: '#app',
        template(data) {
          return `
            <p>${data.title}</p>`
        },
        data: {
          'title': 'Hello Vue'
        }
      });
      app.$watch('title', function(oldVal, newVal) {
        console.log('oldVal:' + oldVal)
        console.log('newVal:' + newVal)
      })
      </script>
    </body>
    </html>
    

    不到200行代码,实现了Vue的核心功能,包括依赖收集、数据劫持、视图渲染。当然,由于是简版的Vue,只是为了便于理解Vue的原理,所以很多功能都剔除了。示例可以直接在chrome下运行,不需要babel编译。

    运行效果

    优秀文章首发于聚享小站,欢迎关注!
  • 相关阅读:
    重新整理 .net core 实践篇————配置系统之盟约[五]
    重新整理 .net core 实践篇————依赖注入应用之援军[四]
    重新整理 .net core 实践篇————依赖注入应用之生命法则[三]
    重新整理 .net core 实践篇————依赖注入应用[二]
    重新整理 .net core 实践篇————配置应用[一]
    spring cloud 学习笔记 客户端(本地)均衡负载(三)
    Leetcode之插入区间
    Leetcode之两棵二叉搜索树中的所有元素
    Leetcode之二叉树的层序遍历
    LeetCode之验证二叉搜索树
  • 原文地址:https://www.cnblogs.com/yesyes/p/15356913.html
Copyright © 2011-2022 走看看