最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 真没想到我看着这篇文章,立马搞懂了vue数据驱动视图的原理

    正文概述 掘金(console没有log)   2021-08-23   387

    前言

    ​ 大家好,我是console没有log,一名大二的专科学生,这篇文章讲给大家分享一下vue的数据驱动视图的原理,也夹杂着模板编译的知识点。我会把这个文章写的非常简单,相信我,你一定看的懂。

    ​ 这个文章我主要从需求、流程图、代码里讲解原理的逻辑,话不多说了我现在开始写文章。

    走进项目的小知识点

    闭包

    function run() {
      let a = 11
      return a
    }
    
    let num = run() // 当此次代码执行完之后在run方法局部作用域的变量a就会呗垃圾回收机制处理掉
    let num2 = run() // 执行这次代码的时候,则是会重新创建一个局部作用域,产生一个变量a,等待函数执行完毕则会再次被垃圾回收机制处理掉
    
    
    function run () {
      let a = 11
      let b = 12
    
      function asd() {
        a = a+1
        return a
      }
    
      return asd
    }
    
    let me = run() // 此时按照原来的方案,run的局部作用域里面的变量会在run方法执行完毕之后,会被垃圾回收机制回收掉,但是发现a变量被另一个局部作用域使用了,则就会产生一个效果,就是run这个局部作用域下的其他变量会被垃圾回收机制处理掉,a变量会保存到内存中。
    let num = me() // 而这里就会读取到内存中变量a的值并且加一返回出来,这里会吧a变量销毁吗? 答案是并不会,a还在内存中 这里num的值为 12
    console.log(num);  // -- 12
    
    let num2 = me() // 这里的打印结果是13,就很显而易见,调用了两次返回的me方法用的是同一个内存地址的a变量
    console.log(num2); // -- 13
    
    

    这里提一句函数的参数也是享有这样的权利的,函数声明变量,也就是在局部作用域声明了一个变量只是没有用 var let const 这几个声明标志而已,看一下代码

    function run(a) {
    
      function asd() {
        a += 1
        return a
      }
    
      return asd
    }
    
    let me = run(1)
    let num = me()
    console.log(num); // -- 2
    let num2 = me()
    console.log(num2); // -- 3
    
    // 原理就不说了,就是想表达函数的参数也是可以实现的。
    

    Object.defineProperty

    可以直接看这个链接Object.defineProperty() - JavaScript | MDN (mozilla.org),如果看不懂的话再来看我的解释。

    let obj = {}
    
    Object.defineProperty(obj, 'a', {
      value: '111',
    })
    
    console.log(obj); // -- { a : '111' }
    
    Object.keys(obj).forEach(key => console.log(key)); // -- 'a'
    
    // 此方法会直接在一个对象上定义一个新属性,并可以设置值
    
    let obj = {}
    
    Object.defineProperty(obj, 'a', {
      value: '111',
      enumerable: false // 这里可以设置这个键的是否可以枚举,如果是false的话我们不管是用for...in...,还是Object.keys等都不会把它读取出来
    })
    
    console.log(obj); // -- { a : '111' }
    
    Object.keys(obj).forEach(key => console.log(key)); // -- 没有打印
    
    for (let key in obj) {
      console.log(key); // 没有打印
    }
    
    
    let obj = {}
    
    Object.defineProperty(obj, 'a', {
      value: '111',
      writable: false // 设置这个值是否可以修改,如果是false则是不可以修改
    })
    
    console.log(obj); // -- { a : '111' }
    
    obj.a = '222'
    
    console.log(obj) // -- { a : '111' }
    
    // 下面的这个图片就是此代码块运行之后的打印,发现超出最大的栈内存了,这是为什么呢
    let obj = {
      a: 11
    }
    
    Object.defineProperty(obj, 'a', {
      get() {
        console.log('obj.a被使用了');
        return '我是a'
      },
      set(val) {
    		// console.log(val) // -- 12
        obj.a = val; // 这是因为,如果你直接给obj.a 赋值的话就相当于由一次出发了set方法,无限触发set就会爆仓
      }
    })
    
    let a = obj.a // -- ‘我是a’
    
    obj.a = 12
    
    真没想到我看着这篇文章,立马搞懂了vue数据驱动视图的原理

    怎么样解决这个问题呢,看以下代码

    解决方法一

    // 我们可以通过一个变量来接受我们设置的新的值,get的时候也返回我们设置了新的值的变量
    
    let obj = {
      a: 11
    }
    let temp;
    
    Object.defineProperty(obj, 'a', {
      get() {
        console.log('obj.a被使用了');
        return temp
      },
      set(val) {
        temp = val;
      }
    })
    
    obj.a = 12
    
    let as = obj.a
    console.log('as的值是:', as);
    // 下面是运行结果
    
    真没想到我看着这篇文章,立马搞懂了vue数据驱动视图的原理

    解决方法二

    let obj = {
      a: 11
    }
    function defineReactive(obj, key, value) {
      Object.defineProperty(obj, key, {
        get() {
          return value;
        },
        set(newValue) {
          value = newValue;
        }
      }) 
    }
    
    defineReactive(obj)
    let as = obj.a 
    console.log(as);
    obj.a = 22
    console.log(obj);
    
    真没想到我看着这篇文章,立马搞懂了vue数据驱动视图的原理

    设计模式--订阅发布模式

    我觉得这片文章写的挺好,大家可以去看一看观察者模式 vs 发布订阅模式 - 知乎 (zhihu.com)看完各篇文章以后,再回来看我的解释

    构建基础目录

    整体功能

    因为我们只要研究数据驱动视图,所以我们只实现data的功能就好了

    理论知识

    理论实现

    数据驱动序章

    1. index.js写入vue类的基本配置

      • 保存要挂载的dom元素,默认我们绑定id为app的dome元素
      • 执行传进来的data函数,获取他返回的数据对象
      class Vue {
        constructor(option) {
          this.$el = option.el ?
            document.querySelector(option.el)
          :
            document.querySelector('#app')
          
          this.$data = option.data()
          
        }
      }
      
      
    2. 我们想要直接可以用this访问data中的数据

      • 在Vue这个类中建立_proxyData方法,接收一个参数(data)
      • 通过Object.keys方法吧对象中的所有的键提取成一个数组
      • forEach遍历由data键值形成的数组
      • 通过Object.defineProperty方法实现可以实现this访问data数据的需求
      • 在构造函数中执行_proxyData
      class Vue {
        constructor(option) {
          this.$el = option.el ?
            document.querySelector(option.el)
          :
            document.querySelector('#app')
          
          this.$data = option.data()
          
          this._proxyData(this.$data)
        }
      
      
        _proxyData(data) {
          Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
              get() {
                return data[key]
              },
              set(newValue) {
                if(newValue === data[key]) return
      
                data[key] = newValue
              }
            })
          })
        }
        
      }
      

      写到这先不着急往下面看,咱们敲一下代码试试到底可不可以

      let vue = new Vue({
        data() {
          return {
            name: '我是console没有log'
          }
        }
      })
      
      console.log(vue.$data.name); // 我是console没有log ---本来是要这么写的
      console.log(vue.name); // 我是console没有log
      

      完全没问题哦,这样就可以了,嘿嘿。

    3. 实现观察者类Observer

      • 在根目录创建Observer.js,并且创建Observer类
      • 上边我们用到Object.defineProperty就只是为了实现this可以访问data的数据,这次我们重新给data中的数据使用Object.defineProperty,这次就是为了监听data中的数据变化了
      export class Observer {
        constructor(data) {
          this.walk(data)
        }
      
        walk(data) {
          if(!data || typeof data !== 'object') return
          Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key])
          })
        }
      
        defineReactive(obj, key, value) {
          const self = this
          self.walk(value)
      
          Object.defineProperty(obj, key, {
            get() {
              return value
            },
            set(newValue) {
              if(newValue === value) return
              value = newValue
              self.walk(value)
            }
          })
        }
        
      }
      

      稍等,我画一个流程图,再给大家解释一下......

      Ok!画好了

      真没想到我看着这篇文章,立马搞懂了vue数据驱动视图的原理

      这里里面还没有任何更改视图的操作,别着急因为还有三个类要写,我们现在Vue这个类中用上Observer类好伐~

      import {Observer} from './Observer'
      
      class Vue {
        constructor(option) {
          this.$el = option.el ?
            document.querySelector(option.el)
          :
            document.querySelector('#app')
          
          this.$data = option.data()
      
          this._proxyData(this.$data)
      
          new Observer(this.$data)
        }
      
      
        _proxyData(data) {
          Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
              get() {
                return data[key]
              },
              set(newValue) {
                if(newValue === data[key]) return
      
                data[key] = newValue
              }
            })
          })
        }
      }
      

      这样好了,已经写了好一会的逻辑但是并不能看到实际的效果,下面我们现实现一个compiler(编译器好了,让我们能直接看到页面)

    4. compiler编译器类

      • 实现对dom文本节点的编译
      • 在根目录创建Compiler.js文件
      export class Compiler {
        constructor(vm) {
          this.vm = vm
          this.el = vm.$el
      
          this.compile(this.el)
        }
      
        compile(el) {
          // 取到元素的所有子节点
          let childNodes = [...el.childNodes]
          childNodes.forEach(node => {
      			// 如果是文本节点
            if(this.isTextNode(node)) {
              this.compileText(node)
              
              // 如果是元素节点
            } else if(this.isElementNode) {
      
            }
      			
            // 判断元素下面还有没有子节点,如果有则进行递归
            if(node.childNodes && node.childNodes.length) {
              this.compile(node)
            }
      
          })
        }
        compileText(node) {
          let val = node.textContent
          let reg = /\{\{(.+?)\}\}/
      
          if(reg.test(val)) {
            // 取到双大括号里面的值
            let key = RegExp.$1.trim()
            node.textContent = val.replace(reg, this.vm[key])
          }
        }
      
        compileElement(node) {
          // 这里就先啥也不用写
        }
      
        isTextNode(node) {
          return node.nodeType === 3
        }
      
        isElementNode(node) {
          return node.nodeType === 1
        }
      
      }
      

      还是得等我写一个流程图,稍等!

      ok,又好了

      真没想到我看着这篇文章,立马搞懂了vue数据驱动视图的原理

      嘿嘿这个时候我们就立马可以看到效果了!!!!!!!!!

      我们在index.html中这么写!

      <div id="app">
        {{name}}
      </div>
      
      <script type="module" src="./index.js"></script>
      

      在Vue类中用上Compiler类并且,并且那么写

      import {Observer} from './Observer'
      import {Compiler} from './Compiler'
      
      class Vue {
        constructor(option) {
          this.$el = option.el ?
            document.querySelector(option.el)
          :
            document.querySelector('#app')
          
          this.$data = option.data()
      
          this._proxyData(this.$data)
      
          new Observer(this.$data)
      
          new Compiler(this) // 要用上哦
        }
      
      
        _proxyData(data) {
          Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
              get() {
                return data[key]
              },
              set(newValue) {
                if(newValue === data[key]) return
      
                data[key] = newValue
              }
            })
          })
        }
      }
      // 那么写在这哦
      new Vue({
        data() {
          return {
            name: '我是console没有log'
          }
        }
      })
      

      接下来看我们的页面!!!!

      真没想到我看着这篇文章,立马搞懂了vue数据驱动视图的原理

      这等好事,真的赞

      下面的这两个类解释起来有点麻烦,我的解决办法就是,打算一口气写完这两个类,直接画一个整个的流程图!!直接秒懂。

    5. Dep类

      • 在根目录创建Dep.js
      • addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
      • notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作
      export class Dep {
        constructor() {
          this.subs = []
        }
      
        addSub(sub) {
          if(sub && sub.update) {
            this.subs.push(sub)
          }
        }
      
        notify() {
          this.subs.forEach(sub => {
            sub.update()
          })
        }
      }
      
    6. watcher类

      • 在根目录创建Watcher.js
      • 它的作用就是,给每一个在页面上使用的变量添加一个监听者,每当data中数据发生改变,它就会接收到通知,从而更新页面。如果页面上并没有使用该变量,那么让他绑定视图更新的方法这样岂不是浪费。 ---- 请注意看这句话
      
      import {Dep} from './Dep'
      
      export class Watcher {
        constructor(vm, key, cd) {
          this.vm = vm
          this.key = key
          this.cd = cd
          Dep.nb = this
      
          this.oldValue = vm[key]
      
          Dep.nb = null
          
        }
      
        update() {
          let newValue = this.vm[this.key]
      
          if(newValue === this.oldValue) return
      
          this.cd(newValue)
        }
      }
      

      现在我们把这两个类用上

      watcher用到Compiler里

      import { Watcher } from "./Watcher"
      
      export class Compiler {
        constructor(vm) {
          this.vm = vm
          this.el = vm.$el
      
          this.compile(this.el)
        }
      
        compile(el) {
          let childNodes = [...el.childNodes]
          childNodes.forEach(node => {
      
            if(this.isTextNode(node)) {
              this.compileText(node)
            } else if(this.isElementNode) {
      
            }
      
            if(node.childNodes && node.childNodes.length) {
              this.compile(node)
            }
      
          })
        }
        compileText(node) {
          let val = node.textContent
          let reg = /\{\{(.+?)\}\}/
      
          if(reg.test(val)) {
            let key = RegExp.$1.trim()
            node.textContent = val.replace(reg, this.vm[key])
      			
            new Watcher(this.vm, key, (newValue) => { // 在这里哦 -- 页面使用到的变量我们才会给他设置更新页面的方法
              node.textContent = newValue
            })
          }
        }
      
        compileElement(node) {
          // 这里就先啥也不用写
        }
      
        isTextNode(node) {
          return node.nodeType === 3
        }
      
        isElementNode(node) {
          return node.nodeType === 1
        }
      
      }
      

      Dep用到Observer里面

      import { Dep } from "./Dep"
      
      export class Observer {
        constructor(data) {
          this.walk(data)
        }
      
        walk(data) {
          if(!data || typeof data !== 'object') return
          Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key])
          })
        }
      
        defineReactive(obj, key, value) {
          const self = this
          self.walk(value)
          let dep = new Dep() // 这里
          Object.defineProperty(obj, key, {
            get() {
              Dep.nb && dep.addSub(Dep.nb) // 这里
              return value
            },
            set(newValue) {
              if(newValue === value) return
              value = newValue
              self.walk(value)
              dep.notify() // 这里
            }
          })
      
        }
      }
      

      再看一下这个理论,然后看全部的流程图

      真没想到我看着这篇文章,立马搞懂了vue数据驱动视图的原理

      提醒一下就是,如果data中数据发生改变的时候会运行Observer中绑定的set方法,可以结合着我这个流程图看

      玩一下

      到这数据影响视图的原理就结束了,敲了这么长时间的代码,竟然还需要我们自己绑定事件,来改变data中的数据,这我不能忍,下面我们来玩一下

      记得我们当时判断如果是元素节点的时候啥也么有干吗,接下来我们就干点什么

      给Vue类添加$methods

      import {Observer} from './Observer'
      import {Compiler} from './Compiler'
      
      class Vue {
      
        constructor(option) {
          this.$el = option.el ?
            document.querySelector(option.el)
          :
            document.querySelector('#app')
          
          this.$data = option.data()
      
          this.$methods = option.methods ? option.methods : {}
          this._proxyData(this.$data)
          this._initMethods(this.$methods)
          this._proxyMethods(this.$methods)
      
          new Observer(this.$data)
          new Compiler(this)
        }
      
      
        _proxyData(data) {
          Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
              get() {
                return data[key]
              },
              set(newValue) {
                if(newValue === data[key]) return
                data[key] = newValue
              }
            })
          })
        }
        _proxyMethods(methods) {
          Object.keys(methods).forEach(key => {
            Object.defineProperty(this, key, {
              get() {
                return methods[key]
              },
              set(newValue) {
                if(newValue === methods[key]) return;
                methods[key] = newValue
              }
            })
          })
        }
        _initMethods(methods) {
          Object.keys(methods).forEach(key => {
            methods[key] = methods[key].bind(this)
          })
        }
      }
      
      
      

      添加Compiler中的compileElement方法

      compileElement(node) {
      		let self = this,
      			props = node.getAttributeNames(),
      			reg = /^c-(\w+)/,
      			reg2 = /\((.+)\)/,
      			reg3 = /\'(.+)\'/,
      			reg4 = /^\d+$/,
      			reg5 = /(\w+)\(/,
      			value,
      			methodName
      		props.forEach((key) => {
      			if (reg.test(key)) {
      				methodName = RegExp.$1.trim()
      			}
      
      			let qian = node.getAttribute(key)
      
      			if (reg2.test(qian)) {
      				value = RegExp.$1.trim()
      				if (reg4.test(value)) {
      					value = parseInt(value)
      				}
      				if (reg3.test(value)) {
      					value = RegExp.$1.trim
      				}
      			}
      
      			if (reg5.test(qian)) {
      				qian = RegExp.$1.trim
      			}
      			console.log(qian)
      			console.log(methodName)
      			console.log(value)
      			node.addEventListener(methodName, function (e) {
      				self.vm[qian](value ? (self.vm[value] ? self.vm[value] : value) : e)
      			})
      		})
      	}
      
      
      

      现在我们就可以在实例化的Vue对象中绑定方法啦 在index.js文件中加上

      let vue = new Vue({
        data() {
          return {
            name: '我是console没有log'
          }
        },
        methods: {
          run() {
            this.name = '真的好棒哦'
          }
        }
      })
      

      在html文件中加上

      <div id="app">
        <div>
        	{{name}}
        </div>
      <button c-click="run">改变</button>
      </div>
      
      <script type="module" src="./index.js"></script>
      

    结束语

    这是我在掘金的第一篇文章,其实第一篇文章我纠结了好久,根本不知道发什么,想发这个一看那个博主已经写了而且写的很好,诶有点自卑了。不过我又想了想每个人的学习方式不一样吗,指不定看我的能看懂呢嘿嘿

    在这里我想特别感谢彭哥的指导,真的帮了我好多,人也特别好。这个是彭哥的掘金地址,希望大家也多多关注彭哥,真的非常厉害

    gitee的仓库地址

    如果大家喜欢我这种风格的文章请在点赞和关注,还有评论告诉我!!

    拜拜啦~下次见


    起源地下载网 » 真没想到我看着这篇文章,立马搞懂了vue数据驱动视图的原理

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元