vue mvvm数据响应实现

为什么实现数据响应式

当前vue、react等框架流行。无论是vue、还是react框架大家最初的设计思路都是类似的。都是以数据驱动视图,数据优先。希望能够通过框架减少开发人员直接操作节点,让开发人员能够把更多的精力放在业务上而不是过多的放在操作节点上。另一方面,框架会通过虚拟dom及diff算法提高页面性能。这其中需要数据优先最根本的思路就是实现数据响应式。so,本次来看下如何基于原生实现数据响应式。

vue中的数据响应

vue中会根据数据将数据通过大胡子语法及指令渲染到视图上,这里我们以大胡子语法为例。如下:

<div id="app">
    {{message}}
</div>
let vm = new Vue({
  el:"#app",
  data:{
    message:"测试数据"
  }
})
setTimeout(()=>{
  vm.message = "修改的数据";
},1000)

如上代码,很简单 。vue做了两件事情。一、把message数据初次渲染到视图。二、当message数据改变的时候视图上渲染的message数据同时也会做出响应。以最简单的案例。带着问题来看,通过原生js如何实现??这里为了简化操作便于理解,这里就不去使用虚拟dom。直接操作dom结构。

实现数据初次渲染

根据vue调用方式。定义Vue类来实现各种功能。将初次渲染过程定义成编译compile函数渲染视图。通过传入的配置以及操作dom来实现渲染。大概思路是通过正则查找html 里 #app 作用域内的表达式,然后查找数据做对应的替换即可。具体实现如下:

class Vue {
  constructor(options) {
    this.opts = options;
    this.compile();
  }
  compile() {
    let ele = document.querySelector(this.opts.el);
    // 获取所有子节点
    let childNodes = ele.childNodes;
    childNodes.forEach(node => {
      if (node.nodeType === 3) {
        // 找到所有的文本节点
        let nodeContent = node.textContent;
        // 匹配“{{}}”
        let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
        if (reg.test(nodeContent)) {
          let $1 = RegExp.$1;
          // 查找数据替换 “{{}}”
          node.textContent = node.textContent.replace(reg, this.opts.data[$1]);
        }
      }
    })
  }
}

如上完成了初次渲染,将message数据渲染到了视图上。但是会返现并没对深层次的dom结构做处理也就是如下情况:

 <div id="app">
    1{{ message }}2
    <div>
      hello , {{ message }}
    </div>
  </div>

渲染结果如上

发现结果并没有达到预期。so,需要改下代码,让节点可以深层次查找就可以了。代码如下:

  compile() {
    let ele = document.querySelector(this.opts.el);
    this.compileNodes(ele);
  }
  compileNodes(ele) {
    // 获取所有子节点
    let childNodes = ele.childNodes;
    childNodes.forEach(node => {
      if (node.nodeType === 3) {
        // 找到所有的文本节点
        let nodeContent = node.textContent;
        // 匹配“{{}}”
        let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
        if (reg.test(nodeContent)) {
          let $1 = RegExp.$1;
          // 查找数据替换 “{{}}”
          node.textContent = node.textContent.replace(reg, this.opts.data[$1]);
        }
      } else if (node.nodeType === 1) {
        if (node.childNodes.length > 0) {
          this.compileNodes(node);
        }
      }
    })
  }

上述代码通过递归查找节点 实现深层次节点的渲染工作。如此,就实现了视图的初次渲染。

数据劫持

回过头来看下上面说的第二个问题:当message数据改变的时候视图上渲染的message数据同时也会做出响应。如何实现数据响应式?简而言之就是数据变动影响视图变动?再将问题拆分下 1. 如何知道数据变动了? 2.如何根据数据变动来更改视图?

  • 如何知道数据变动了? 这里就需要用到数据拦截了,或者叫数据观察。把会变动的data数据观察起来。当他变动的时候我们可以做后续的渲染事情。如何拦截数据呢 ?vue2里采取的是definePrototype。
let obj = {
  myname:"张三"
}
Object.defineProperty(obj,'myname',{
  configurable:true,
  enumerable:true,
  get(){
    console.log("get.")
    return "张三";
  },
  set(newValue){
    console.log("set")
    console.log(newValue);
  }
})
console.log(obj);

上述代码会发现,通过defineProperty劫持的对象属性下都会有get及set方法。那么当我们获取或者设置数据的时候就能出发对应的get及set 。这样就能拦截数据做后续操作。

还有没有其他方式达到数据劫持的效果呢?ES6中出现了Proxy 代理对象同样也可以达到类似劫持数据的功能。如下代码:

let obj = {
  myname:"张三"
}
let newObj = new Proxy(obj,{
  get(target,key){
    console.log("get...")
    return "张三"
  },
  set(target,name,newValue){
    console.log("set...");
    return Reflect.set(target,name,newValue);
  }
})

两种方式都可以实现数据劫持。proxy功能更加强大,很多方法是defineProperty所不具备的。且proxy直接拦截的是对象而defineProperty拦截的是对象属性。so,可以利用上述方式将data数据做劫持,代码如下:

observe(data){
    let keys = Object.keys(data);
    keys.forEach(key=>{
      let value = data[key];
      Object.defineProperty(data,key,{
        configurable:true,
        enumerable:true,
        get(){
          return value;
        },
        set(newValue){
          value = newValue;
        }
      });
    })
 }

观察者模式实现数据响应

有了劫持数据方式后,接下来需要实现的就是当修改数据的时候将新数据渲染到视图。如何办到呢?会发现,需要在data设置的时候触发视图的compile编译。二者之间互相影响,此时可以想到利用观察者模式,通过观察者模式让二者产生关联,如下:

图略小,代码也贴上吧。

class Vue extends EventTarget {
  constructor(options) {
    super();
    this.opts = options;
    this.observe(this.opts.data);
    this.compile();
  }
  observe(data){
    let keys = Object.keys(data);
    let _this = this;
    keys.forEach(key=>{
      let value = data[key];
      Object.defineProperty(data,key,{
        configurable:true,
        enumerable:true,
        get(){
          return value;
        },
        set(newValue){
          _this.dispatchEvent(new CustomEvent(key,{
            detail:newValue
          }));
          value = newValue;
        }
      });
    })
  }
  compile() {
    let ele = document.querySelector(this.opts.el);
    this.compileNodes(ele);
  }
  compileNodes(ele) {
    // 获取所有子节点
    let childNodes = ele.childNodes;
    childNodes.forEach(node => {
      if (node.nodeType === 3) {
        // 找到所有的文本节点
        let nodeContent = node.textContent;
        // 匹配“{{}}”
        let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
        if (reg.test(nodeContent)) {
          let $1 = RegExp.$1;
          // 查找数据替换 “{{}}”
          node.textContent = node.textContent.replace(reg, this.opts.data[$1]);
          this.addEventListener($1,e=>{
            let oldValue = this.opts.data[$1];
            let newValue = e.detail;
            let reg = new RegExp(oldValue);
            node.textContent = node.textContent.replace(reg,newValue);
          })
        }
      } else if (node.nodeType === 1) {
        if (node.childNodes.length > 0) {
          this.compileNodes(node);
        }
      }
    })
  }
}

如上,成功的通过观察者模式实现了数据的响应。但是会发现data与compile之间需要通过键名来进行关联。如果data数据结构嵌套关系复杂后面会比较难处理。有没有一种方式让二者松解耦呢?这时候可以用发布订阅模式来进行改造。

发布订阅模式改造响应式

还是略小,也还是贴上代码:

class Vue {
  constructor(options) {
    this.opts = options;
    this.observe(this.opts.data);
    this.compile();
  }
  observe(data){
    let keys = Object.keys(data);
    let _this = this;
    keys.forEach(key=>{
      let value = data[key];
      let dep = new Dep();
      Object.defineProperty(data,key,{
        configurable:true,
        enumerable:true,
        get(){
          if(Dep.target){
            dep.addSub(Dep.target);
          }
          return value;
        },
        set(newValue){
          dep.notify(newValue);
          value = newValue;
        }
      });
    })
  }
  compile() {
    let ele = document.querySelector(this.opts.el);
    this.compileNodes(ele);
  }
  compileNodes(ele) {
    // 获取所有子节点
    let childNodes = ele.childNodes;
    childNodes.forEach(node => {
      if (node.nodeType === 3) {
        // 找到所有的文本节点
        let nodeContent = node.textContent;
        // 匹配“{{}}”
        let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
        if (reg.test(nodeContent)) {
          let $1 = RegExp.$1;
          // 查找数据替换 “{{}}”
          node.textContent = node.textContent.replace(reg, this.opts.data[$1]);
          new Watcher(this.opts.data,$1,(newValue)=>{
            let oldValue = this.opts.data[$1];
            let reg = new RegExp(oldValue);
            node.textContent = node.textContent.replace(reg,newValue);
          })
        }
      } else if (node.nodeType === 1) {
        if (node.childNodes.length > 0) {
          this.compileNodes(node);
        }
      }
    })
  }
}

class Dep{
  constructor(){
    this.subs = [];
  }
  addSub(sub){
    this.subs.push(sub);
  }
  notify(newValue){
    this.subs.forEach(sub=>{
      sub.update(newValue);
    })
  }
}

class Watcher{
  constructor(data,key,cb){
    Dep.target = this;
    data[key];
    this.cb = cb;
    Dep.target = null;
  }
  update(newValue){
    this.cb(newValue);
  }
}

如上代码思路是 针对每个数据会生成一个dep(依赖收集器)在数据get的时候收集watcher,将watcher 添加到dep里保存。数据一旦有改变触发notify发布消息从而影响compile编译更新视图。这个流程也可以参看下图:

如上就完成了视图响应。通过上述代码,我们可以看出实现数据响应两个核心点1.数据劫持。2.观察者和发布订阅。在这我们可以思考一个问题,2个设计模式都是可以实现的但是有什么区别呢?

观察者与发布订阅

这里需要从概念来看

  • 观察者模式:定义一个对象与其他对象之间的一种依赖关系,当对象发生某种变化的时候,依赖它的其它对象都会得到更新,一对多的关系。
  • 发布订阅模式:是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。

两者之间关系,发布订阅是三者之间关系。发布订阅会多了一个关系器来组织主题和观察者之间的关系。这样做的好处就是松解耦。看上面响应式例子可以看出观察者需要通过事件名称来进行关联。发布订阅定义dep管理器之后data和compile彻底解耦,让二者松散解耦。在处理多层数据结构上发布订阅会更清晰。松解耦能够应对更多变化,把模块之间依赖降到最低。发布订阅广义上是观察者模式。

好了 暂时先over 。 如果觉得有收获的话可以点个赞,赠人玫瑰,手有余香!!!!

以上就是vue mvvm数据响应实现的详细内容,更多关于vue mvvm数据响应的资料请关注我们其它相关文章!

(0)

相关推荐

  • 在vue中对数组值变化的监听与重新响应渲染操作

    在我们项目开发过程中,实例中的数据类型可以是对象.数组等.在对象中,某个属性值发生更改时,我们可以通过对象的深度监听,以达到重新渲染页面的需求.或者查阅这篇文章 例如: <script> export default { data(){ return { objVal: { name: 'obj', type: 'obj' } } }, watch:{ objVal:{ handler(val,oldval){ }, deep: true, } }, methods:{ changeObj()

  • Vue数组响应式操作及高阶函数使用代码详解

    数组的响应式操作 //this.letters.push('123');//在末尾添加一个元素 //this.letters.pop();//从末尾删除一个元素 //this.letters.unshift('111');//在开端添加一个元素 //this.letters.shift();//从开端删除一个元素 //this.letters.splice(1,2);//从下标为1的元素开始删除两个元素 //this.letters.splice(1,2,'777','888');//从下标为1

  • 如何在Vue中使localStorage具有响应式(思想实验)

    响应式是Vue.js的最大特色之一.如果你不知道幕后情况,它也是最神秘的地方之一.例如,为什么它不能用于对象和数组,而不能用于诸如 localStorage 之类的其他东西? 让我们回答这个问题,在解决这个问题时,让Vue响应式与 localStorage 一起使用. 如果运行以下代码,则会看到计数器显示为静态值,并且不会像我们期望的那样发生变化,这是因为setInterval在 localStorage 中更改了该值. new Vue({ el: "#counter", data:

  • vue在响应头response中获取自定义headers操作

    日常开发,我们可能会为了安全问题,保证第三方无法通过伪造返回报文欺骗前端,需要在返回报文中添加自定义参数,用于验证身份,后端添加自定义参数,前端校验自定义参数通过后才会执行相应的操作. 系统为了安全会去掉自定义头,如果不做任何处理,前端无法通过javascript访问自定义头,所以需要在接口返回中添加这样的操作. response['Cookie'] ='13231231231' #自定义头 添加后接口返回信息如以下截图: 控制台打印headers信息如以下截图: 要正确打印需要在接口返回中设置

  • vue中的provide/inject的学习使用

    前言 最近在看element-ui的源码,发现了一个这样的属性:inject.遂查看官网provider/inject provider/inject:简单的来说就是在父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量. 需要注意的是这里不论子组件有多深,只要调用了inject那么就可以注入provider中的数据.而不是局限于只能从当前父组件的prop属性来获取数据. 下面我们来验证下猜想: first:定义一个parent component <template

  • vue 解决provide和inject响应的问题

    官网上说provide 和 inject 绑定并不是可响应的.这是刻意为之的.然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的. provide: Object | () => Object(一个对象或返回一个对象的函数) inject: Array | { [key: string]: string | Symbol | Object }(一个字符串数组,或一个对象,对象的 key 是本地的绑定名) 要实现父子组件响应,父组件传递的数据类型必须是对象Object,子组件接收的数

  • vue mvvm数据响应实现

    为什么实现数据响应式 当前vue.react等框架流行.无论是vue.还是react框架大家最初的设计思路都是类似的.都是以数据驱动视图,数据优先.希望能够通过框架减少开发人员直接操作节点,让开发人员能够把更多的精力放在业务上而不是过多的放在操作节点上.另一方面,框架会通过虚拟dom及diff算法提高页面性能.这其中需要数据优先最根本的思路就是实现数据响应式.so,本次来看下如何基于原生实现数据响应式. vue中的数据响应 vue中会根据数据将数据通过大胡子语法及指令渲染到视图上,这里我们以大胡

  • 稍微学一下Vue的数据响应式(Vue2及Vue3区别)

    什么是数据响应式 从一开始使用 Vue 时,对于之前的 jq 开发而言,一个很大的区别就是基本不用手动操作 dom,data 中声明的数据状态改变后会自动重新渲染相关的 dom. 换句话说就是 Vue 自己知道哪个数据状态发生了变化及哪里有用到这个数据需要随之修改. 因此实现数据响应式有两个重点问题: 如何知道数据发生了变化? 如何知道数据变化后哪里需要修改? 对于第一个问题,如何知道数据发生了变化,Vue3 之前使用了 ES5 的一个 API Object.defineProperty Vue

  • 详解实现vue的数据响应式原理

    这篇文章主要是给不了解或者没接触过 vue 响应式源码的小伙伴们看的,其主要目的在于能对 vue 的响应式原理有个基本的认识和了解,如果在面试中被问到此类问题,能够知道面试官想让你回答的是什么?「PS:文中如有不对的地方,欢迎小伙伴们指正」 响应式的理解 响应式顾名思义就是数据变化,会引起视图的更新.这篇文章主要分析 vue2.0 中对象和数组响应式原理的实现,依赖收集和视图更新我们留在下一篇文章分析. 在 vue 中,我们所说的响应式数据,一般指的是数组类型和对象类型的数据.vue 内部通过

  • 深入理解Vue的数据响应式

    1. ES语法的getter和setter 在开始了解 Vue 的数据响应式原理前应该先搞清楚 ES语法 中的 getter 和 setter 方法的具体用法. getter和setter 方法是以 get 和 set 关键字来为对象添加虚拟属性的一种方式.这种属性其实并不真实存在,而是以取值函数 getter 和存值函数 setter 来模拟的一种属性.目的是对某个属性设置存值函数和取值函数,拦截该属性的存取行为,以便于对该属性的存取做一些限定处理.如下所示(以下代码来源于 mdn) gett

  • vue.js数据响应式原理解析

    目录 Object.defineProperty() 定义 defineReactive 函数 递归侦测对象的全部属性 流程分析 observe 函数 Observer 类 完善 defineReactive 函数 One More Thing Object.defineProperty() 得力于 Object.defineProperty() 的特性,vue 的数据变化有别于 react 和小程序,是非侵入式的.详细介绍可以看 MDN 文档,这里特别说明几点: get / set 属性是函数

  • 浅谈Vue 数据响应式原理

    前言 Vue的数据响应主要是依赖了Object.defineProperty(),那么整个过程是怎么样的呢?以我们自己的想法来走Vue的道路,其实也就是以Vue的原理为终点,我们来逆推一下实现过程. 本文代码皆为低配版本,很多地方都不严谨,比如 if(typeof obj === 'object')这是在判断obj是否为为一个对象,虽然obj也有可能是数组等其他类型的数据,但是本文为了简便,就直接这样写来表示判断对象,对于数组使用Array.isArray(). 改造数据 我们先来尝试写一个函数

  • Vue源码解析之数据响应系统的使用

    接下来重点来看Vue的数据响应系统.我看很多文章在讲数据响应的时候先用一个简单的例子介绍了数据双向绑定的思路,然后再看源码.这里也借鉴了这种方式,感觉这样的确更有利于理解. 数据双向绑定的思路 1. 对象 先来看元素是对象的情况.假设我们有一个对象和一个监测方法: const data = { a: 1 }; /** * exp[String, Function]: 被观测的字段 * fn[Function]: 被观测对象改变后执行的方法 */ function watch (exp, fn)

  • vue3.x源码剖析之数据响应式的深入讲解

    目录 前言 什么是数据响应式 数据响应式的大体流程 vue2.x数据响应式和3.x响应式对比 大致流程图 实现依赖收集 代码仓库 结尾 前言 如果错过了秋枫和冬雪,那么春天的樱花一定会盛开吧.最近一直在准备自己的考试,考完试了,终于可以继续研究源码和写文章了,哈哈哈.学过vue的都知道,数据响应式在vue框架中极其重要,写代码也好,面试也罢,数据响应式都是核心的内容.在vue3的官网文档中,作者说如果想让数据更加响应式的话,可以把数据放在reactive里面,官方文档在讲述这里的时候一笔带过,笔

  • 浅谈Vue数据响应思路之数组

    之前梳理Vue数据响应思路时没有考虑数组的情况. js 中数组有很多实例方法,其中有一部分会改变数组本身的值,比如 push pop shift unshift 等,这些方法被称为变异方法,这些变异方法也是 Vue 开发中常用的数组操作方法.那么要实现对数组的观测,首先要考虑的就是如何截获这些变异方法的调用. 简单来说,Vue 是通过保持这些数组变异方法原有功能不变的前提下,对其功能进行扩展来实现拦截的.具体怎么操作,可以先看一下例子: function add10(num) { return

  • Vue.js实现数据响应的方法

    许多前端JavaScript框架(例如Angular,React和Vue)都有自己的数据相应引擎.通过了解相应性及其工作原理,您可以提高开发技能并更有效地使用JavaScript框架.在视频和下面的文章中,我们构建了您在Vue源代码中看到的相同类型的Reactivity. 如果您观看此视频而不是阅读文章,请观看系列中的下一个视频,与Vue的创建者Evan You讨论反应性和代理.

随机推荐