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

双向数据绑定分步实现

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'

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

前期实现双向绑定铺垫代码学习
  • 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))
  }

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

  • 文本子节点进行正则的匹配和替换,到这里是将数据拿到渲染到页面上
 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)
  }

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

  • 实现文本框的单向数据绑定
//判断当前的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
        })
      }
    }

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

  • 实现文本框的双向数据绑定
//监听文本框的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
        })

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

完整代码

  • 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

上一篇:Python-使用列表实现筛选法求素数


下一篇:《计算机视觉:模型、学习和推理》一2.5 贝叶斯公式