ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

vue数据双向绑定原理总结加完整代码

2021-11-28 13:32:37  阅读:167  来源: 互联网

标签:vue const 绑定 vm value watcher key 双向 obj


双向数据绑定分步实现

1.数组的reduce()方法

应用场景:下次操作的初始值,依赖于上次操作的返回值

(1)数组的累加计算

普通实现

const arr = [1,2,3,4,5,6]
 let total = 0;
 arr.forEach(item=>{
 total+=item;
})
 console.log(total);

通过reduce实现

基础知识

  • 使用reduce()方法实现
  • 使用方法: 数组.reduce(函数,初始值)
  • 循环当前数组,侧重于“滚雪球”
    • 数组.reduce((上次计算的结果,当前循环的item项) => {},0)
    • 数组.reduce((上次计算的结果,当前循环的item项) => {return上次计算的结果,当前循环的item项},0)
    • const 累加的结果 = 数组.reduce((上次计算的结果,当前循环的item项) => {return上次计算的结果

代码

const total = arr.reduce((val,item) => {
  return val + item;
},0)
console.log(total);
(2)链式获取对象属性的值
const obj = {
  name:'xx',
  info:{
    address:{
      location:'北京顺义',
    }
  }
}

普通实现

const location = obj.info.address.location
console.log(location)//北京顺义

通过reduce实现–初始给了一个数组

基础思路

//第一次 reduce
         初始值是 obj这个对象,
         当前的item项是 info
         第一次reduce的结果是 obj.info的属性对应的对象

//第二次 reduce
         初始值是 obj.info这个对象,
         当前的item项是 address
         educe的结果是 obj.info.address的属性对应的对象

//第三次 reduce
         初始值是 obj.info.address这个对象,
         当前的item项是 location
         educe的结果是 obj.info.address.location的属性对应的值

代码

const attrs = ['info','address','location']
const location =  attrs.reduce((newobj,key) => {
  return newobj[key]
},obj)
console.log(location)

通过reduce实现升级操作–初始给了一个字符串,分割

代码

const attrs = 'info.address.location'
const location = attrs.split('.').reduce((newobj,key) =>
 newobj[key]
 ,obj)

2.发布订阅模式

一.代码解析

(1)Dep类
  • 负责进行依赖收集
    • 第一,有个数组,专门来存放所有的订阅信息
    • 第二,提供一个向数组中追加订阅信息的方法
    • 第三,提供一个循环,循环触发数组中的每个订阅信息

代码

//收集依赖/订阅者
class Dep{
  //constructor() 方法是一种特殊的方法,用于创建和初始化在类中创建的对象。
  // 当初始化类时,constructor() 方法会被自动调用,并且它必须使用确切的名称 "constructor"
  constructor() {
    //这个subs数组,用来存放所有订阅者的信息
    this.subs = []
  }

  //向subs数组中,添加订阅者信息,接受一个实例
  addSub(watcher){
    this.subs.push(watcher)
  }

  //发布通知的方法
  notify(){
    this.subs.forEach((watcher) => watcher.update())
  }
}
(2)Watcher类
  • 负责订阅一些事件

代码

//订阅者的类
class Watcher{
 constructor(cb) {
   this.cb =cb//将cb挂载到自己身上
 }
  //触发回调的方法
  update(){
    this.cb()
  }
}
//创建类
const w1 = new Watcher(() => {
  console.log('我是第一个订阅者')
})
// w1.update();调用这个函数才会打印我是第一个订阅者
const w2 = new Watcher(() => {
  console.log('我是第二个订阅者')
})


const dep = new Dep()
dep.addSub(w1)
dep.addSub(w2)
dep.notify()

二.运作

(1)介绍vue发布订阅模式如何运作
  • 只要我们为vue中的data数据重新赋值了,这个赋值的动作,会被vue监听到,然后vue要把数据的变化,通知到每个订阅者,接下来,订阅者(DOM元素)要根据最新的数据,更新自己的类容
  • 在数据更新完一瞬间,会触发update,里面写的代码就是根据新的数据进行的操作

3.使用Object.defineProperty()进行数据劫持

(1).通过get()劫持取值操作–叫做getter,函数名是get
(2).通过set()劫持赋值操作–叫做setter,函数名是set
 const obj = {
  name:'zs',
   age:20,
 }

 Object.defineProperty(obj,'name',{
   enumerable:true,//当前属性,允许被循环 for-in
   configurable:true,//当前属性,允许被配置例如delete,允许对象中属性被删除
   get(){
     console.log('有人获取obj.name的值')
     return '我不是zs'
   },
   set(newval){
     console.log('不要值',newval)
     dep.notify()
   }
 })
 //取值操作
 console.log(obj.name)
 //赋值操作
 obj.name = 'ls'

在这里插入图片描述

前期实现双向绑定铺垫代码学习
  • html
    • 实现getter,setter
    • 属性代理
    • 数据劫持
    • 递归为每一个添加set,get
    • 解决赋值后无set,get的情况
<body>
<div id="app">
  <h1>姓名:{{name}}</h1>
</div>
<script src="05-vue.js"></script>
<script>
  const vm = new Vue({
    el:'#app',
    data:{
      name:'zs',
      age:20,
      info:{
        a:'a1',
        c:'c1'
      }
    }
  })
  console.log(vm.name);//这里写vm.name为undefined
  //这里访问的话需要访问vm.$data.name
  //通过属性代理,可以使用vm.name访问
  • js
    • this指最终new出来那个实例
//实现vue中的getter
class Vue{
  constructor(options) {
    this.$data = options.data
    //调用数据劫持的方法
    observe(this.$data)


    //属性代理
    Object.keys(this.$data).forEach((key) => {
      Object.defineProperty(this,key,{
        enumerable:true,
        configurable:'true',
        get() {
          return this.$data[key]
        },
        set(newvalue) {
          this.$data[key] = newvalue
        }
      })
    })
  }
}

//定义一个数据劫持的方法
function observe(obj) {

  //这是递归的终止条件
  if(!obj || typeof obj !== 'object') return//是空的不是一个对象

  //通过object.keys(obj)获取当前obj的每个属性
  Object.keys(obj).forEach((key) => {
    //当前被循环的key所对应的属性值
    let value = obj[key]

    //把value这个子节点,进行递归
    observe(value)

    //需要为当前的key所对应的属性,添加getter和setter
    Object.defineProperty(obj,key,{
      enumerable:true,
      configurable:true,
      get() {
        console.log(`有人获取了${key}的值`);
        return value
      },
      set(newvalue) {
        value = newvalue;

        //这里如果给对象里面的值a,c重新赋值,那么新的
        //a和c就不会再有新的get和set,所以需要重新调用给新的这里修改的是info,还是一个对象vm.$data.info = {a:4,c:9},
        observe(value)
      }
    })
  })
}
  • 文档碎片–提高性能,内容变了,页面需要重绘,定位位置变了,重排。为了性能,加文档碎片,也就是将节点存进去,在内存里进行修改,就不会进行重绘,编辑好以后,将结果重新渲染到页面上
  • 将数据拿出来放到文本碎片里。进行操作,在还给vm
 //调用模板编译的函数,编译需要模板结构,拿到数据传this,创建就调用
    Compile(options.el,this)


//对HTML结构进行模板编译的方法
function Compile(el,vm) {
  //获取el对应的Dom元素
  vm.$el = document.querySelector(el)
  //获取文档中 id="demo" 的元素:document.querySelector("#demo");
  //创建文档碎片,提高Dom操作的性能
  const fragment = document.createDocumentFragment()

  //拿出来
  while((childNode = vm.$el.firstChild)){
    fragment.appendChild(childNode)
  }

  //进行模板编译
  replace(fragment)

  //放进去
  vm.$el.appendChild(fragment)

  function replace(node) {
    //定义匹配插值表达式的正则
    const regMustache = /\{\{\s*(\S+)\s*\}\}/
    //\S匹配任何非空白字符
    //\s匹配任何空白字符,包括空格、制表符、换页符等等。
    //()非空白字符提取出来,用一个小括号进行分组

    //证明当前的node节点是一个文本子节点,需要进行正则的替换
    if(node.nodeType == 3){
      //注意:文本子节点,也是一个Dom对象,如果要获取文本子节点的字符串内容,需要调用textContent属性获取
      // console.log(node.textContent);
      //终止递归的条件
      return
    }
   //证明不是一个文本结点,可能是一个Dom元素,需要进行递归处理
    node.childNodes.forEach((child) => replace(child))
  }

在这里插入图片描述

  • 文本子节点进行正则的匹配和替换,到这里是将数据拿到渲染到页面上
 if(node.nodeType == 3){
      //注意:文本子节点,也是一个Dom对象,如果要获取文本子节点的字符串内容,需要调用textContent属性获取
      // console.log(node.textContent);
      const text = node.textContent
      //进行字符串的正则匹配与提取
      const execRusult = regMustache.exec(text)//为一个数组,索引为0的为{{name}},为1的为name,exec() 方法用于检索字符串中的正则表达式的匹配。
      if(execRusult){
        const value = execRusult[1].split('.').reduce((newobj,k) => newobj[k],vm)
        // console.log(value);
        node.textContent = text.replace(regMustache,value)
      }
  • 发布订阅–当数据被更新时,页面会跟着渲染
    • 创建两个类,创建watcher类的实例
  //在这个时候,创建watcher类的实例,将这个方法存到watcher身上,调update就执行

        new Watcher(vm,execRusult[1],(newValue) => {
          node.textContent = text.replace(regMustache,newValue)
        })

//依赖收集的类
class Dep{
  constructor() {
    //所有的watcher都要存到这个数组中
    this.subs = []
  }
  //像数组中,添加watcher方法
  addSub(watcher){
    this.subs.push(watcher)
  }
  //负责通知每个watcher的方法
  notify(){
    this.subs.forEach((watcher) => watcher.update())
  }
}
//订阅者的类
class Watcher{
  //cb回调函数中,记录着当前watcher如和更新自己的文本内容
  //同时,需要拿到最新的数据,因此,在new watcher 期间,需要传进来vm
  //要知道在vm身上众多的数据中,那个数据,才是自己当前所需要的数据,在new watcher 期间,指定watcher对应数据的名字
  constructor(vm,key,cb) {
    this.vm = vm
    this.key = key
    this.cb = cb
  }
  //watcher实例,需要update函数,让发布者能够通知我们进行更新
  update(){
    this.cb()
  }
}
  • 将watcher实例存储到dep.subs数组中
get() {
        //只要执行了下面这一行,那么刚才new的watcher实例,就加到了dep.subs这个数组中了
        Dep.target&&dep.addSub(Dep.target)
      }
      

    //下面三行代码,负责把创建的watcher实例存到dep实例的subs数组中
    Dep.target = this//自定义属性,watcher实例
    key.split('.').reduce((newobj,k) => newobj[k],vm)//这里主要是想执行这一行代码,跳到get,返回name,age,
    Dep.target = null
  
  • 数据到view视图的单向绑定–修改之后显示在页面上
 set(newvalue) {
        dep.notify()
        }
        
 update(){
    const value = this.key.split('.').reduce((newobj,k) => newobj[k],this.vm)
    this.cb(value)
  }

在这里插入图片描述

  • 实现文本框的单向数据绑定
//判断当前的node节点是否为input输入框
    if(node.nodeType === 1&& node.tagName.toUpperCase() === 'INPUT'){
      //得到当前元素的所有属性节点
      const attrs = Array.from(node.attributes)
      const findResult = attrs.find((x) => x.name === 'v-model')
      if(findResult){
        //获取到当前v-model属性的值 v-model=‘name’ v-model='info.a'
        const expStr = findResult.value
        const value = expStr.split('.').reduce((newobj,k) => newobj[k],vm)
        node.value = value
        //创建Watcher的实例
        new Watcher(vm,expStr,(newValue) => {
          node.value = newValue
        })
      }
    }

在这里插入图片描述

  • 实现文本框的双向数据绑定
//监听文本框的input事件,拿到文本框的最新的值,把最新的值,更新到vm上
        node.addEventListener('input',(e) => {
          const keyArr = expStr.split('.')
          const obj = keyArr.slice(0,keyArr.length-1).reduce((newobj,k) => newobj[k],vm)
          obj[keyArr[keyArr.length-1]] = e.target.value
        })

在这里插入图片描述

完整代码

  • HTML

<div id="app">
  <h3 >姓名:{{name}}</h3>
  <h3>年龄是:{{age}}</h3>
  <h3>info.a的值是:{{info.a}}</h3>
  <div>name的值:<input type="text" v-model="name"> </div>
  <div>info.a的值:<input type="text" v-model="info.a"> </div>
</div>5
<script src="05-vue.js"></script>
<script>
  const vm = new Vue({
    el:'#app',
    data:{
      name:'zs',
      age:20,
      info:{
        a:'a1',
        c:'c1'
      }
    }
  })
  console.log(vm.name);//这里写vm.name为undefined
  //这里访问的话需要访问vm.$data.name
  //通过属性代理,可以使用vm.name访问
</script>
</body>
  • js
//实现vue中的getter
class Vue{
  constructor(options) {
    this.$data = options.data
    //调用数据劫持的方法
    observe(this.$data)


    //属性代理
    Object.keys(this.$data).forEach((key) => {
      Object.defineProperty(this,key,{
        enumerable:true,
        configurable:'true',
        get() {
          return this.$data[key]
        },
        set(newvalue) {
          this.$data[key] = newvalue
        }
      })
    })

    //调用模板编译的函数,编译需要模板结构,拿到数据传this,创建就调用
    Compile(options.el,this)
  }
}



//定义一个数据劫持的方法
function observe(obj) {

  //这是递归的终止条件
  if(!obj || typeof obj !== 'object') return//是空的不是一个对象
  const dep = new Dep()

  //通过object.keys(obj)获取当前obj的每个属性
  Object.keys(obj).forEach((key) => {
    //当前被循环的key所对应的属性值
    let value = obj[key]

    //把value这个子节点,进行递归
    observe(value)

    //需要为当前的key所对应的属性,添加getter和setter
    Object.defineProperty(obj,key,{
      enumerable:true,
      configurable:true,
      get() {
        //只要执行了下面这一行,那么刚才new的watcher实例,就加到了dep.subs这个数组中了
        Dep.target&&dep.addSub(Dep.target)
        return value
      },
      set(newvalue) {
        value = newvalue;
        dep.notify()
        //这里如果给对象里面的值a,c重新赋值,那么新的
        //a和c就不会再有新的get和set,所以需要重新调用给新的这里修改的是info,还是一个对象vm.$data.info = {a:4,c:9},
        observe(value)
      }
    })
  })
}


//对HTML结构进行模板编译的方法
function Compile(el,vm) {
  //获取el对应的Dom元素
  vm.$el = document.querySelector(el)
  //获取文档中 id="demo" 的元素:document.querySelector("#demo");
  //创建文档碎片,提高Dom操作的性能
  const fragment = document.createDocumentFragment()

  //拿出来
  while((childNode = vm.$el.firstChild)){
    fragment.appendChild(childNode)
  }

  //进行模板编译
  replace(fragment)

  //放进去
  vm.$el.appendChild(fragment)

  function replace(node) {
    //定义匹配插值表达式的正则
    const regMustache = /\{\{\s*(\S+)\s*\}\}/
    //\S匹配任何非空白字符
    //\s匹配任何空白字符,包括空格、制表符、换页符等等。
    //()非空白字符提取出来,用一个小括号进行分组

    //证明当前的node节点是一个文本子节点,需要进行正则的替换
    if(node.nodeType == 3){
      //注意:文本子节点,也是一个Dom对象,如果要获取文本子节点的字符串内容,需要调用textContent属性获取
      // console.log(node.textContent);
      const text = node.textContent
      //进行字符串的正则匹配与提取
      const execRusult = regMustache.exec(text)//为一个数组,索引为0的为{{name}},为1的为name,exec() 方法用于检索字符串中的正则表达式的匹配。
      if(execRusult){
        const value = execRusult[1].split('.').reduce((newobj,k) => newobj[k],vm)
        // console.log(value);
        node.textContent = text.replace(regMustache,value)
        //在这个时候,创建watcher类的实例,将这个方法存到watcher身上,调update就执行

        new Watcher(vm,execRusult[1],(newValue) => {
          node.textContent = text.replace(regMustache,newValue)
        })
      }

      //终止递归的条件
      return
    }

    //判断当前的node节点是否为input输入框
    if(node.nodeType === 1&& node.tagName.toUpperCase() === 'INPUT'){
      //得到当前元素的所有属性节点
      const attrs = Array.from(node.attributes)
      const findResult = attrs.find((x) => x.name === 'v-model')
      if(findResult){
        //获取到当前v-model属性的值 v-model=‘name’ v-model='info.a'
        const expStr = findResult.value
        const value = expStr.split('.').reduce((newobj,k) => newobj[k],vm)
        node.value = value

        //创建Watcher的实例
        new Watcher(vm,expStr,(newValue) => {
          node.value = newValue
        })

        //监听文本框的input事件,拿到文本框的最新的值,把最新的值,更新到vm上
        node.addEventListener('input',(e) => {
          const keyArr = expStr.split('.')
          const obj = keyArr.slice(0,keyArr.length-1).reduce((newobj,k) => newobj[k],vm)
          obj[keyArr[keyArr.length-1]] = e.target.value
        })
      }
    }


   //证明不是一个文本结点,可能是一个Dom元素,需要进行递归处理
    node.childNodes.forEach((child) => replace(child))
  }
}

//依赖收集的类
class Dep{
  constructor() {
    //所有的watcher都要存到这个数组中
    this.subs = []
  }
  //像数组中,添加watcher方法
  addSub(watcher){
    this.subs.push(watcher)
  }
  //负责通知每个watcher的方法
  notify(){
    this.subs.forEach((watcher) => watcher.update())
  }
}


//订阅者的类
class Watcher{
  //cb回调函数中,记录着当前watcher如和更新自己的文本内容
  //同时,需要拿到最新的数据,因此,在new watcher 期间,需要传进来vm
  //要知道在vm身上众多的数据中,那个数据,才是自己当前所需要的数据,在new watcher 期间,指定watcher对应数据的名字
  constructor(vm,key,cb) {
    this.vm = vm
    this.key = key
    this.cb = cb
    //下面三行代码,负责把创建的watcher实例存到dep实例的subs数组中
    Dep.target = this//自定义属性
    key.split('.').reduce((newobj,k) => newobj[k],vm)
    Dep.target = null
  }
  //watcher实例,需要update函数,让发布者能够通知我们进行更新
  update(){
    const value = this.key.split('.').reduce((newobj,k) => newobj[k],this.vm)
    this.cb(value)
  }
}

ye

标签:vue,const,绑定,vm,value,watcher,key,双向,obj
来源: https://blog.csdn.net/weixin_51398691/article/details/121580823

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有