【二十三】数据驱动

在这里插入图片描述

前言

本篇博客主要回顾了vue.js的两大核心之一,数据驱动。

参考博客:https://juejin.cn/post/6989106100582744072;https://juejin.cn/post/6844904067525935118

面试回答

1.数据驱动:vue的核心是数据驱动,vue2中是利用Object.defineproperty进行数据劫持,同时结合发布者订阅者模式来实现数据的双向绑定,首先有一个发布者类,它提供添加订阅者方法以及通知方法,这两个方法中都要求订阅者类需要有更新方法;然后有一个数据劫持类,它主要是将数据转换成getter、setter,并且在getter中触发添加订阅者类方法,在setter中触发通知方法,订阅者类中则提供更新方法。当数据改变时,就会触发数据劫持类中的setter方法,去调用发布者类中的通知方法,从而调用订阅者中的更新方法;当然还有模板编译类中,用来处理元素节点上的指令以及文本节点的差值表达式这些;最后汇总在vue.js中,new一个数据劫持类、模板编译类,并且把响应式的数据绑定到vue.js上,完成数据的双向绑定。

2.vue3和vue2的区别:从原理上来说,vue2 的双向数据绑定是利⽤ES5的Object.definePropert来对数据进⾏劫持再结合发布订阅模式来实现的。而vue3中使⽤了es6的Proxy对数据进行代理,通过reactive函数给每⼀个对象都包⼀层Proxy来监听属性的变化。vue3这种方式能够避免definePropery不能进行全对象监听并且需要对数组进行特异性操作的一个缺陷。从算法上来说,vue3优化了diff算法,主要从三个方向进行优化,第一个优化是节点更新添加标记,对动态数据进行标记,对于静态数据则直接使用缓存,就是不再对所有的dom进行对比。第二个优化是数据循环优化,主要是对vnode节点遍历循环的方法进行优化,比如头部循环优化、尾部循环优化以及最长链循环优化。最后一个是事件优化,对于各类事件用闭包进行缓存,从而不引起重新渲染,来达到加速的目的。从使用上来说,就是一个选项式Api与组合式Api的区别,vue2是选项式Api,将data、methods、watch、computed都分开管理,而vue3是组合式Api,将所有的逻辑放到setup里面。

知识点

vue核心的本质:利用Object.defineProperty(vue2)/Proxy(vue3)进行数据劫持各个属性的getter、setter,同时结合观察者模式,来实现数据双向绑定以及响应式。其中Proxy是JS2015(ES6)的一个新特性。Proxy的代理是针对整个对象的,而不是对象的某个属性,因此不同于Object.defineProperty的必须遍历对象每个属性,Proxy只需要做一层代理就可以监听统计结构下的所有属性变化,当然对于深层结构,递归还是需要进行的。此外,Proxy支持代理数组的变化,不过Proxy兼容性不太好。

1.数据拦截

这个知识点应用于实现MVVM库/框架的数据绑定,即改变数据的同时,可以同步更改页面上对应的数据。

let test = {
	a:1,
	b:'zxp',
	c:{abc:'abc'}
}
1.ES5:defineProperty()

Object.defineProperty(obj, prop, descriptor) :obj,要在其上定义属性的对象。prop,要定义或修改的属性的名称。descriptor,将被定义或修改的属性描述符,也就是方法。

  • ES5出来的方法
  • 三个参数:对象(必填)、属性值(必填)、描述符(可选)
  • defineProterty的描述符属性
数据属性:value,writable,configurable,enumerable
访问器属性:get,set
注:不能同时设置value和writable,这两对属性是互斥的

拦截对象的两种情况

let obj = {name:'',age:'',sex:''  },
    defaultName = ["这是姓名默认值1","这是年龄默认值1","这是性别默认值1"];
  Object.keys(obj).forEach(key => {
    Object.defineProperty(obj, key, {
      get() {
        return defaultName;
      },
      set(value) {
        defaultName = value;
      }
    });
  });

  console.log(obj.name);
  console.log(obj.age);
  console.log(obj.sex);
  obj.name = "这是改变值1";
  console.log(obj.name);
  console.log(obj.age);
  console.log(obj.sex);

  let objOne={},defaultNameOne="这是默认值2";
  Object.defineProperty(obj, 'name', {
      get() {
        return defaultNameOne;
      },
      set(value) {
        defaultNameOne = value;
      }
  });
  console.log(objOne.name);
  objOne.name = "这是改变值2";
  console.log(objOne.name);

拦截数组变化的情况

let a={};
bValue=1;
Object.defineProperty(a,"b",{
    set:function(value){
        bValue=value;
        console.log("setted");
    },
    get:function(){
        return bValue;
    }
});
a.b;//1
a.b=[];//setted
a.b=[1,2,3];//setted
a.b[1]=10;//无输出
a.b.push(4);//无输出
a.b.length=5;//无输出
a.b;//[1,10,3,4,undefined];


//结论:defineProperty无法检测数组索引赋值,改变数组长度的变化; 但是通过数组方法来操作可以检测到

拦截数组变化的情况多级嵌套对象监听

  let info = {};
  function observe(obj) {
    if (!obj || typeof obj !== "object") {
      return;
    }
    for (var i in obj) {
      definePro(obj, i, obj[i]);
    }
  }

  function definePro(obj, key, value) {
    observe(value);
    Object.defineProperty(obj, key, {
      get: function() {
        return value;
      },
      set: function(newval) {
        console.log("检测变化", newval);
        value = newval;
      }
    });
  }
  definePro(info, "friends", { name: "张三" });
  info.friends.name = "李四";

缺点:
1、只能监听属性,而不是监听对象本身,需要对对象的每个属性进行遍历(提取高度嵌套的对象里的属性可以采取递归的方法),对于原本不在对象中的属性难以监听。
2、无法监听数组的变化: 数组的这些方法是无法触发set的:push, pop, shift, unshift,splice, sort, reverse,vue中能监听是因为对这些方法进行了重写。

2.ES6:Proxy()

new Proxy(target,handler)。target,用Proxy包装的目标对象(可以是任何类型的对象)。handler,一个对象,其属性是当执行一个操作时定义代理的行为的函数。

实例:
let handler = {
    get: function(target, key){
        console.log('get:',key)
        return key in target ? target[key] : 'error'
    },
    set: function(target,key,val){
        console.log('set:',val)
        target[key] = val
    }
};

let p = new Proxy(test, handler);

p.a = 5
//set: 5
//5

p.b = 'qwe'
//set: qwe
//'qwe'

console.log(p.a, p.b);   
//get: a
//get: b
//5 'qwe'

p.c.abc = 'chiji'
//get: c
//'chiji'

console.log('d' in p, p.d);    
//get: d
//false 'error'

test 输出为
{
   a:5,
   b:"qwe",
   c:{
       abc:"chiji"
   }
}

涉及到多级对象或者多级数组

  //传递两个参数,一个是object, 一个是proxy的handler
//如果是不是嵌套的object,直接加上proxy返回,如果是嵌套的object,那么进入addSubProxy进行递归。 
function toDeepProxy(object, handler) {
    if (!isPureObject(object)) addSubProxy(object, handler); 
    return new Proxy(object, handler);

//这是一个递归函数,目的是遍历object的所有属性,如果不是pure object,那么就继续遍历object的属性的属性,如果是pure object那么就加上proxy
    function addSubProxy(object, handler) {
        for (let prop in object) {
            if ( typeof object[prop] == 'object') {
                if (!isPureObject(object[prop])) addSubProxy(object[prop], handler);
                object[prop] = new Proxy(object[prop], handler);
            }
        }
        object = new Proxy(object, handler)
    }

//是不是一个pure object,意思就是object里面没有再嵌套object了
    function isPureObject(object) {
        if (typeof object!== 'object') {
            return false;
        } else {
            for (let prop in object) {
                if (typeof object[prop] == 'object') {
                    return false;
                }
            }
        }
        return true;
    }
}
let object = {
    name: {
        first: {
            four: 5,
            second: {
                third: 'ssss'
            }
        }
    },
    class: 5,
    arr: [1, 2, {arr1:10}],
    age: {
        age1: 10
    }
}
//这是一个嵌套了对象和数组的数组
let objectArr = [{name:{first:'ss'}, arr1:[1,2]}, 2, 3, 4, 5, 6]

//这是proxy的handler
let handler = {
    get(target, property) {
        console.log('get:' + property)
        return Reflect.get(target, property);
    },
    set(target, property, value) {
        console.log('set:' + property + '=' + value);
        return Reflect.set(target, property, value);
    }
}
//变成监听对象
object = toDeepProxy(object, handler);
objectArr = toDeepProxy(objectArr, handler);

//进行一系列操作
console.time('pro')
objectArr.length
objectArr[3];
objectArr[2]=10
objectArr[0].name.first = 'ss'
objectArr[0].arr1[0]
object.name.first.second.third = 'yyyyy'
object.class = 6;
object.name.first.four
object.arr[2].arr1
object.age.age1 = 20;
console.timeEnd('pro')

//问题和优点:reflect对象没有构造函数 可以监听数组索引赋值,改变数组长度的变化, 是直接监听对象的变化,不用深层遍历
defineProterty和proxy的对比
  1. defineProterty是es5的标准,proxy是es6的标准
  2. proxy可以监听到数组索引赋值,改变数组长度的变化
  3. proxy是监听对象,不用深层遍历,defineProterty是监听属性
  4. 利用defineProterty实现双向数据绑定(vue2.x采用的核心) ,利用proxy实现双向数据绑定(vue3.x会采用)

2.数据双向绑定

在这里插入图片描述

实现数据双向绑定步骤:

  1. 实现目标类(dep.js),包含添加观察者方法、通知方法。
  2. 实现数据劫持类(observer.js),完成对传入数据的响应式处理,如对数据对象所有属性进行监听,并在get方法里调用目标类的添加观察者方法以及在set中触发通知方法,更新视图。
  3. 实现观察者类(watcher.js),观察者类包含更新视图方法以及初始化时通过vue连接数据劫持类(observer.js)触发get方法、获取新值、将观察者类储存在目标类中,来实现收到每个属性变动的通知及执行相应函数来更新视图。
  4. 实现模板编译类(Compiler.js),通过传入的参数,主要对node节点以及指令进行遍历,若涉及文本变量则创建新的观察者类,如果是元素节点,则遍历处理指令如v-text、v-model,并执行相应操作,指令方法内都需要创建新的观察者类。
  5. 实现Vue.js类,整合上述类,首先获取数据、元素节点,再处理数据,比如将数据与vue实例绑定,以及创建数据劫持类、模板编译类。
1.Dep.js(发布者类)

每个响应式属性都会创建这么一个Dep对象 ,在使用响应式数据的时候,负责收集Watcher对象。当我们对响应式属性在setter中进行更新的时候,会调用Depnotify方法发送更新通知,然后去调用 Watcher中的update实现视图的更新操作,该类主要需做以下三件事:

  1. 提供添加观察者的方法,让其在实例化时后往目标类(Dep.js)里面添加自己。
  2. 观察者类(watcher.js)必须有一个update()方法,因为在数据劫持类(observer.js)会将观察者类添加到subs中。
  3. 待属性变动dep.notify()通知时,能调用自身的update()方法,并触发Compile中绑定的回调。
class Dep {
  constructor() {
    // 存储观察者
    this.subs = []
  }
  // 添加观察者
  addSub(sub) {
    // 判断观察者是否存在 和 是否拥有update方法
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 通知方法
  notify() {
    // 触发每个观察者的更新方法
    this.subs.forEach((sub) => {
      sub.update()
    })
  }
}
2.Observer.js(数据劫持类)

数据劫持类主要实现以下逻辑:

  1. 使用 observerdata 中的属性转为 响应式 添加到 自身身上
  2. 使用 observer 方法监听 data 的所有属性变化来通过观察者模式,更新视图
  3. 当对象的属性值也是对象时,也要对其值进行劫持-----递归
  4. 当对象赋值与旧值一样,则不需要后续操作------防止重复渲染
  5. 当模板渲染获取对象属性会调用get添加target,对象属性改动通知订阅者更新----数据变化,视图更新

Tip:Vue通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性,一般通过$set(obj,index,value)或者 $delete(obj,index,value)进行处理,常见的有,不支持数组的长度变化(arr.length-1);修改数组中的指定元素,也无法侦测数组的变化(arr[0] =1)

class Observer {
  constructor(data) {
    // 用来遍历 data
    this.walk(data)
  }
  // 遍历 data 转为响应式
  walk(data) {
    // 判断 data是否为空 和 对象
    if (!data || typeof data !== 'object') return
    // 遍历 data
    Object.keys(data).forEach((key) => {
      // 转为响应式
      this.defineReactive(data, key, data[key])
    })
  }
  // 转为响应式
  // 要注意的 和vue.js 写的不同的是
  // vue.js中是将属性给了Vue转为 getter setter
  // 这里是将data中的属性转为getter setter
  defineReactive(obj, key, value) {
    // 如果是对象类型的 也调用walk 变成响应式,不是对象类型的直接在walk会被return
    this.walk(value)
    // 保存一下 this
    const self = this
    // 创建 Dep 对象
    let dep = new Dep()
    Object.defineProperty(obj, key, {
      // 设置可枚举
      enumerable: true,
      // 设置可配置
      configurable: true,

      // 获取值
      get() {
        // 在这里添加观察者对象 Dep.target 表示观察者
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      // 设置值
      set(newValue) {
        // 判断旧值和新值是否相等
        if (newValue === value) return
        // 设置新值
        value = newValue
        // 赋值的话如果是newValue是对象,对象里面的属性也应该设置为响应式的
        self.walk(newValue)
        // 触发通知 更新视图
        dep.notify()
      },
    })
  }
}
3.Watcher.js(订阅者类)

watcher的作用是数据更新后收到通知之后调用update 进行更新,当我们改变响应式属性的时候,就会触发Observer中的set()方法 ,然后调用notify 方法,拿到了所有的观察者watcher 实例去执行 update方法调用了回调函数 cb(newValue) 方法并把新值传递到了cb(),而cb方法的具体逻辑则在Watcher里(可以参考Compiler.js中的new Watcher),去更新视图。

class Watcher {
  constructor(vm, key, cb) {
    // vm 是 Vue 实例
    this.vm = vm
    // key 是 data 中的属性
    this.key = key
    // cb 回调函数 更新视图的具体方法
    this.cb = cb
    // 把观察者的存放在 Dep.target
    Dep.target = this
    // 在执行完下面这一条语句后,会触发observer中 get 方法,watcher通过vue连接到ovserver中
    // observer中的get 把 观察者 通过dep.addSub(Dep.target) 添加到了 dep.subs中
    this.oldValue = vm[key]
    // Dep.target 就不用存在了 因为上面的操作已经存好了
    Dep.target = null
  }
  // 观察者中的必备方法 用来更新视图
  update() {
    // 获取新值
    let newValue = this.vm[this.key]
    // 比较旧值和新值
    if (newValue === this.oldValue) return
    // 调用具体的更新方法
    this.cb(newValue)
  }
}
4.Compiler.js(模板编译类)

使用compiler编译元素节点上面指令和文本节点差值表达式

Comilper.js在这个文件里实现对文本节点和元素节点指令编译,比如v-text、v-model,他做了以下三件事:

  • 将当前根节点所有子节点遍历放到内存中
  • 编译文档碎片,替换模板(元素、文本)节点中属性的数据
  • 将编译的内容回写到真实DOM上

【重点】:

  1. 先把真实的 dom 移入到内存中操作 — 文档碎片
  2. 编译元素节点和文本节点
  3. 给模板中的表达式和属性添加观察者(Watcher.js)
class Compiler {
  // vm 指 Vue 实例
  constructor(vm) {
    // 拿到 vm
    this.vm = vm
    // 拿到 el
    this.el = vm.$el
    // 编译模板
    this.compile(this.el)
  }
  // 编译模板
  compile(el) {
    // 获取子节点 如果使用 forEach遍历就把伪数组转为真的数组
    let childNodes = [...el.childNodes]
    childNodes.forEach((node) => {
      // 根据不同的节点类型进行编译
      // 文本类型的节点
      if (this.isTextNode(node)) {
        // 编译文本节点
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        //元素节点
        this.compileElement(node)
      }
      // 判断是否还存在子节点考虑递归
      if (node.childNodes && node.childNodes.length) {
        // 继续递归编译模板
        this.compile(node)
      }
    })
  }
  // 判断是否是 文本 节点
  isTextNode(node) {
    return node.nodeType === 3
  }
  // 编译文本节点(简单的实现)
  compileText(node) {
    // 核心思想利用把正则表达式把{{}}去掉找到里面的变量
    // 再去Vue找这个变量赋值给node.textContent
    let reg = /\{\{(.+?)\}\}/
    // 获取节点的文本内容
    let val = node.textContent
    // 判断是否有 {{}}
    if (reg.test(val)) {
      // 获取分组一  也就是 {{}} 里面的内容 去除前后空格
      let key = RegExp.$1.trim()
      // 进行替换再赋值给node
      node.textContent = val.replace(reg, this.vm[key])
      // 创建观察者
      new Watcher(this.vm, key, (newValue) => {
        node.textContent = newValue
      })
    }
  }
  // 判断是否是元素节点
  isElementNode(node) {
    return node.nodeType === 1
  }
  // 编译元素节点这里只处理指令
  compileElement(node) {
    // 获取到元素节点上面的所有属性进行遍历
    // 问题:这里为什么要加一个!
    ![...node.attributes].forEach((attr) => {
      // 获取属性名
      let attrName = attr.name
      // 判断是否是 v- 开头的指令
      if (this.isDirective(attrName)) {
        // 除去 v- 方便操作
        attrName = attrName.substr(2)
        // 获取 指令的值就是  v-text = "msg"  中msg
        // msg 作为 key 去Vue 找这个变量
        let key = attr.value
        // 指令操作 执行指令方法
        // vue指令很多为了避免大量个 if判断这里就写个 uapdate 方法
        //问题:这一步是干嘛的
        this.update(node, key, attrName)
      }
    })
  }
  // 判断元素的属性是否是 vue 指令
  isDirective(attr) {
    return attr.startsWith('v-')
  }
  // 添加指令方法 并且执行
  update(node, key, attrName) {
    // 比如添加 textUpdater 就是用来处理 v-text 方法
    // 我们应该就内置一个 textUpdater 方法进行调用
    // 加个后缀加什么无所谓但是要定义相应的方法
    let updateFn = this[attrName + 'Updater']
    // 如果存在这个内置方法 就可以调用了
    updateFn && updateFn.call(this, node, key, this.vm[key])
  }

  // 实例
  // 提前写好 相应的指定方法比如这个 v-text
  // 使用的时候 和 Vue 的一样
  textUpdater(node, key, value) {
    node.textContent = value
    // 创建观察者
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue
    })
  }
  // v-model
  modelUpdater(node, key, value) {
    node.value = value
    // 创建观察者
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue
    })
    // 这里实现双向绑定
    node.addEventListener('input', () => {
      this.vm[key] = node.value
    })
  }
}
5.Vue.js

vue.js以及observer.js都对数据进行劫持的原因是,obsever.js中是把data的所有属性加到data自身,变为响应式转成gettersetter方式;在vue.js 中 也把data的所有属性加到Vue上,是为了以后方面操作可以用Vue的实例直接访问到或者在 Vue 中使用 this 访问,这样在Vue$data 中都存在了 所有的data属性了并且是响应式的。

class Vue {
  constructor(options) {
    // 获取到传入的对象 没有默认为空对象
    this.$options = options || {};
    this.$el = typeof options.el === 'string' ?
      document.querySelector(options.el) :
      options.el;
    //获取data
    this.$data = options.data || {};
    // 调用 _proxyData 处理 data中的属性,这里的目的是让用户可以直接通过vue.js去拿到值
    this._proxyData(this.$data); //走到这里直接去看_proxyData方法
    // 使用 Obsever 把data中的数据转为响应式,走到这里直接去处理Observer类
    new Observer(this.$data);
    // 编译模板
    new Compiler(this)
  }
  _proxyData(data) {
    Object.keys(data).forEach(key => {
      // 进行数据劫持
      // 把每个data的属性 到添加到 Vue 转化为 getter setter方法
      Object.defineProperty(this, key, {
        // 设置可以枚举
        enumerable: true,
        // 设置可以配置
        configurable: true,
        // 获取数据
        get() {
          return data[key];
        },
        // 设置数据
        set(newValue) {
          // 判断新值和旧值是否相等
          if (newValue === data[key]) return;
          // 设置新值
          data[key] = newValue;
        },
      });
    });
  }
}
6.HTML
<body>
  <div id="app">
    {{msg}} <br />
    {{age}} <br />
    <div v-text="msg"></div>
    <input v-model="msg" type="text" />
  </div>
  <script src="./js/dep.js"></script>
  <script src="./js/watcher.js"></script>
  <script src="./js/compiler.js"></script>
  <script src="./js/observer.js"></script>
  <script src="./js/vue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app',
      data: {
        msg: '123',
        age: 21,
      },
    })
  </script>
</body>

最后

走过路过,不要错过,点赞、收藏、评论三连~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值