首页 前端知识 (十二)v-model双向绑定原理、vue 的数据双向绑定原理

(十二)v-model双向绑定原理、vue 的数据双向绑定原理

2024-04-13 09:04:28 前端知识 前端哥 174 992 我要收藏

一、v-model双向绑定原理

1、v-model是v-on和v-bind的语法糖
v-on用于事件绑定,给当前组件绑定一个input事件。
v-bind用于属性绑定,给当前组件绑定一个value属性。
2、v-model双向绑定的实现:
(1)在父组件内给子组件添加v-model,相当于给组件添加了个value 属性,传递的值就是绑定的值。和绑定了一个此绑定值的input事件。
(2)子组件中使用prop属性接受这个value值,名字必须是value。
(3)子组件内部更改value值的时候,必须通过$emit()事件派发个input事件,并携带最新的值。
(4)v-model绑定的input事件会自动监听这个input事件,把接收到的最近新值赋值给v-model绑定的变量。

二、vue 的双向绑定原理

1、vue数据双向绑定的框架:mvvm

  • 数据层(Model):应用的数据以及业务逻辑,为开发者编写的业务代码;
  • 视图层(View):应用的展示效果,各类UI组件,由template和css组成的代码;
  • 业务逻辑层(ViewModel):负责将view和model连接起来。使View和Model层的同步工作完全是自动的。

2、vue2数据双向绑定原理——MVVM的核心

vue2数据双向绑定原理图
vue2数据双向绑定原理图

1、监听器、发布者(Observer):

主要应用Object.defineProperty()方法,对数据对象的属性进行遍历,修改属性的setter、getter方法。在getter方法中属性添加到订阅器中,在setter方法中通知消息订阅器发出通知。使用 defineRecative 函数对 data 做 Object.defineProperty 处理,使得可以拦截 data 中的每个数据的get/set。
代码:

class Observer {
  constructor(data) {
    this.observer(data);
  }
  observer(obj) {
    if (obj && typeof obj === 'object') {
      // 遍历取出传入对象的所有属性, 给遍历到的属性都增加get/set方法
      for (let key in obj) {
        this.defineRecative(obj, key, obj[key])
      }
    }
  }
  // obj: 需要操作的对象
  // attr: 需要新增get/set方法的属性
  // value: 需要新增get/set方法属性的取值
  defineRecative(obj, attr, value) {
  this.observer(value); // 递归遍历所有子属性
  // 1,创建了属于当前属性的依赖收集对象
  let dep = new Dep();
  Object.defineProperty(obj, attr, {
    get() {
      if (Dep.target) { // 判断是否需要添加订阅者
        dep.addSub(Dep.target); // 在这里添加一个订阅者
      }
      return value;
    },
    set: (newValue) => {
      if (value !== newValue) {
        // 如果给属性赋值的新值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
        this.observer(newValue);
        value = newValue;
        // 通知到视图更新
        dep.notify();
        console.log('监听到数据的变化');
      }
    }
  })
}
}

将订阅器Dep添加一个订阅者设计在getter里面,这是为了让Watcher初始化进行触发,因此需要判断是否要添加订阅者。在setter函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。

拓展:object.defineProperty()方法介绍:
(1)Object.defineproperty 的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性
(2)接收是三个参数Object.defineproperty(obj, key, desc):
obj:要操作的对象
key:添加或修改的属性名
desc:配置项,一般是一个对象 :
配置对象中一般有6个可配置修改的属性

  • writable:是否可重写
  • value:当前属性对应的值
  • get:读取时内部自动调用的函数
  • set:写入时 内部自动调用的函数
  • enumerable:是否可遍历
  • configurable:是否可再次修改配置项

注意:当使用了getter或setter方法,不允许使用writable和value这两个属性(如果使用,会直接报错滴)
  get 是获取值的时候的方法,类型为 function ,获取值的时候会被调用,不设置时为undefined
  set 是设置值的时候的方法,类型为 function ,设置值的时候会被调用,undefined
  get或set不是必须成对出现,任写其一就可以

Object.defineProperty缺点:

  • 无法监听数组的变化
  • 需要深度遍历,浪费内存
  • 无法监听新增属性/删除属性(Vue.set Vue.delete,未在 data 中定义的属性会报 undefined)

2、消息订阅器(Dep):

订阅器Dep主要负责收集订阅者,然后在属性变化的是,然后在属性变化的时候通知订阅这执行更新函数。

function Dep () {
  this.subs = [];
}
Dep.prototype = {
  addSub: function(sub) {
    this.subs.push(sub);
  },
  notify: function() {
    this.subs.forEach(function(sub) {
      sub.update();
    });
  }
};

3、订阅者(Watcher):

订阅者Watcher在初始化的时候需要将自己添加到订阅器中。
首先我们知道监听器observer在get函数中执行了添加订阅者操作,所以我们需要在订阅者Watcher初始化的时候触发对应的get函数去执行 添加订阅者操作。触发get 函数的核心就是使用了Object.defineProperty( )进行数据监听。只要在订阅者Watcher初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在Dep.target上缓存下订阅者,添加成功后再将其去掉就可以了。
代码实现:

/**
 * 订阅者
 *
 */
function Watcher(vm, exp, cb) {
  this.cb = cb;
  this.vm = vm;
  this.exp = exp;
  this.value = this.get();  // 将自己添加到订阅器的操作
}

Watcher.prototype = {
  update: function() {
    this.run();
  },
  run: function() {
    var value = this.vm.data[this.exp];
    var oldVal = this.value;
    if (value !== oldVal) {
      this.value = value;
      this.cb.call(this.vm, value, oldVal);
    }
  },
  get: function() {
    Dep.target = this;  // 缓存自己
    var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
    Dep.target = null;  // 释放自己
    return value;
  }
};

4、模版解析器(Compiler):

解析器实现对vue各个指令模版的解析,生成抽象语法树,编译成Virtual DOM,渲染视图。
解析器的实现步骤:

  • 将模板元素提取到内存中,方便将数据渲染到模板后,再一次性挂载到页面中
  • 模板提取到内存后,使用 buildTemplate 函数遍历该模板元素.:
    (1)元素节点: 使用函数检查元素上是否有v-开头的属性
    (2)文本节点:检查文本中是否有{{}}内容
  • 创建 CompilerUtil 类,用于处理vue指令和 {{}},完成数据的渲染
  • 到此就完成了首次数据渲染,接下来需要实现:数据改变时,自动更新视图。
    代码:

Compiler :

class Compiler {
  constructor(vm) {
    this.vm = vm;
    // 1.将网页上的元素放到内存中
    let fragment = this.node2fragment(this.vm.$el);
    // 2.利用指定的数据编译内存中的元素
    this.buildTemplate(fragment);
    // 3.将编译好的内容重新渲染会网页上
    this.vm.$el.appendChild(fragment);
  }
  node2fragment(app) {
    // 1.创建一个空的文档碎片对象
    let fragment = document.createDocumentFragment();
    // 2.编译循环取到每一个元素
    let node = app.firstChild;
    while (node) {
      // 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
      fragment.appendChild(node);
      node = app.firstChild;
    }
    // 3.返回存储了所有元素的文档碎片对象
    return fragment;
  }
  buildTemplate(fragment) {
    let nodeList = [...fragment.childNodes];
    nodeList.forEach(node => {
      // 需要判断当前遍历到的节点是一个元素还是一个文本
      if (this.vm.isElement(node)) {
        // 元素节点
        this.buildElement(node);
        // 处理子元素
        this.buildTemplate(node);
      } else {
        // 文本节点
        this.buildText(node);
      }
    })
  }
  buildElement(node) {
    let attrs = [...node.attributes];
    attrs.forEach(attr => {
      // v-model="name" => {name:v-model  value:name}
      let { name, value } = attr;
      // v-model / v-html / v-text / v-xxx
      if (name.startsWith('v-')) {
        // v-model -> [v, model]
        let [_, directive] = name.split('-');
        CompilerUtil[directive](node, value, this.vm);
      }
    })
  }
  buildText(node) {
    let content = node.textContent;
    let reg = /\{\{.+?\}\}/gi;
    if (reg.test(content)) {
      CompilerUtil['content'](node, content, this.vm);
    }
  }
}

工具类CompilerUtil:

let CompilerUtil = {
  getValue(vm, value) {
    // 解析this.data.aaa.bbb.ccc这种属性
    return value.split('.').reduce((data, currentKey) => {
      return data[currentKey.trim()];
    }, vm.$data);
  },
  getContent(vm, value) {
    // 解析{{}}中的变量
    let reg = /\{\{(.+?)\}\}/gi;
    let val = value.replace(reg, (...args) => {
      return this.getValue(vm, args[1]);
    });
    return val;
  },
  // 解析v-model指令
 model: function (node, value, vm) {
    new Watcher(vm, value, (newValue, oldValue)=>{
        node.value = newValue;
    });
    let val = this.getValue(vm, value);
    node.value = val;
	// 看这里
    node.addEventListener('input', (e)=>{
        let newValue = e.target.value;
        this.setValue(vm, value, newValue);
    })
},
  // 解析v-html指令
  html: function (node, value, vm) {
    // 在触发getter之前,为dom创建Wather,并为Watcher.target赋值
    new Watcher(vm, value, (newValue, oldValue) => {
      node.innerHTML = newValue;
    });
    let val = this.getValue(vm, value);
    node.innerHTML = val;
  },
  // 解析v-text指令
  text: function (node, value, vm) {
    // 在触发getter之前,为dom创建Wather,并为Watcher.target赋值
    new Watcher(vm, value, (newValue, oldValue) => {
      node.innerText = newValue;
    });
    let val = this.getValue(vm, value);
    node.innerText = val;
  },
  // 解析{{}}中的变量
  content: function (node, value, vm) {
    let reg = /\{\{(.+?)\}\}/gi;
    let val = value.replace(reg, (...args) => {
      // 在触发getter之前,为dom创建Wather,并为Watcher.target赋值
      new Watcher(vm, args[1], (newValue, oldValue) => {
        node.textContent = this.getContent(vm, value);
      });
      return this.getValue(vm, args[1]);
    });
    node.textContent = val;
  }
}

5、总结

vue接收一个模板和data参数。
vue解析模版的时候渲染视图,初始化对行的订阅者。
遍历data数据的时候初始化发布者,利用Object.defineProperty函数在get 中触发订阅器中的添加订阅者方法。当数据更新的时候再set 函数中触发订阅器中的通知函数,通过订阅器的update 方法更新视图。

6、observer都在什么时候执行

在Vue.js中,observer(观察者)的执行时机与Vue实例的生命周期紧密相关。具体来说,observer的执行主要发生在Vue实例初始化的过程中,特别是与数据响应式系统的建立有关。

以下是observer执行的关键时刻:

Vue实例初始化:当创建一个新的Vue实例时,Vue会遍历实例的data对象中的每个属性,并使用Object.defineProperty来使这些属性变得“响应式”。这个过程是由observer来完成的。它为每个属性创建一个getter和setter函数,从而能够在属性值改变时触发相应的更新。

数据属性访问与修改:每当Vue实例中的数据属性被访问或修改时,observer也会参与其中。通过之前设置的getter和setter函数,observer能够追踪数据的变化,并在必要时通知依赖这些数据的watcher进行更新。

需要注意的是,observer本身并不是在某个特定的“时间”执行,而是与Vue实例的数据访问和修改操作紧密相连。换句话说,observer的执行是数据驱动和事件触发的,它会在数据发生变化时自动执行相应的操作。

总的来说,observer在Vue.js中扮演着建立和维护数据响应式系统的关键角色,它使得Vue实例能够在数据发生变化时自动更新相关的视图或计算属性。

7、Watcher实例创建情况:

计算属性(Computed Properties):当一个组件定义了计算属性时,每个计算属性都会对应一个Watcher实例。这些Watcher实例在组件初始化时被创建,用于监控计算属性所依赖的数据,并在这些数据变化时重新计算属性的值。

侦听器(Watchers):开发者可以在Vue组件中使用watch选项来定义侦听器,这些侦听器会监控指定的数据属性。当这些属性变化时,侦听器的回调函数会被触发。每个这样的侦听器都会对应一个Watcher实例,这些实例在组件初始化时根据watch选项的内容被创建。

模板渲染:在Vue组件的模板中使用的数据属性,也会被Vue监控。对于每个在模板中使用的响应式数据,Vue都会创建一个与之关联的Watcher实例,以确保当数据变化时能够更新DOM。这个Watcher通常被称为“渲染Watcher”,它在组件挂载(mount)过程中被创建。

由开发者显式创建:除了Vue自动为计算属性、侦听器和模板渲染创建的Watcher实例外,开发者也可以显式地创建Watcher实例,以便更灵活地监控数据变化并执行自定义逻辑。

总的来说,Watcher实例的创建时机取决于它们的使用场景。在Vue组件的生命周期中,这些Watcher实例主要在组件初始化、挂载以及开发者显式调用时被创建。

8、Dep.target被赋值的过程:

具体来说,以下是Dep.target被赋值的过程:

**创建Watcher实例:**当Vue需要观察某个数据的变化时(例如,为了计算属性、侦听器或组件的重新渲染),它会创建一个Watcher实例。

开始依赖收集:在Watcher实例化的过程中,或者在其开始监控某个数据之前,它会调用一个方法来收集依赖。这个方法通常会涉及到访问数据对象的属性,从而触发属性的getter函数。

**设置Dep.target:**在调用getter函数之前,Watcher会将自己设置为当前全局唯一的Dep.target。这是通过全局变量来实现的,确保同一时间只有一个Watcher在进行依赖收集。

触发getter函数:当Watcher尝试访问数据对象的属性时,会触发该属性的getter函数。由于此时Dep.target已经被设置为当前的Watcher实例,因此在getter函数内部可以访问到这个Watcher。

**收集依赖:**在getter函数内部,Vue会检查Dep.target是否存在。如果存在,说明当前有一个Watcher正在进行依赖收集,于是Vue会将这个Watcher添加到该属性的依赖列表(即Dep实例的subs数组)中。

**清除Dep.target:**依赖收集完成后,Dep.target会被清除或重置,以确保不会错误地将后续的依赖收集与当前的Watcher关联起来。

这个过程确保了当数据对象的属性值发生变化时,Vue能够知道哪些Watcher实例依赖了这个属性,并通知它们进行相应的更新。通过这种方式,Vue实现了数据的响应式更新机制。

3、vue3的双向绑定原理

,vue3通过proxy进行双向数据绑定,proxy对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等),可以通过使用ref和reactive对数据实现响应式。

Proxy可以理解成,在目标对象之前架设一层 “拦截”,当外界对该对象访问的时候,都必须经过这层拦截,而Proxy就充当了这种机制,类似于代理的含义,它可以对外界访问对象之前进行过滤和改写该对象。

const obj = new Proxy(target, handler)
被代理之后返回的对象 = new Proxy(被代理对象,要代理对象的操作)

handler中常用的对象方法如下:

  1. get(target, propKey, receiver)
  2. set(target, propKey, value, receiver)
  3. has(target, propKey)
  4. construct(target, args):
  5. apply(target, object, args)

reflect 对象(详细描述可见es6中的 reflect)

// 创建响应式
function reactive(target {}) { 
   if (typeof target !== 'object' || typeof target == null) {
   	 return target // 不是对象或数组直接返回
   }
   // 代理配置
   const proxyConf = {
		get(targe, key, receiver) {
			// 只处理本身(非原型)的属性
			const ownKeys = Reflect.ownKeys(target)
			if(ownKeys.include(key)) {
				console.log('get', key) // 监听
			}
			const result = Reflect.get(target, key, receiver) // 返回不做处理
			return reactive(result) // 递归调用,这里所做的优化是只在调用到对象深层次的属性时才会触发递归
		}

   	set(target, key, val, receiver) {
   		// 重复的数组,不处理
   		if(val === target[key])  {
   			return true;
   		}
   		const ownKeys = Reflect.ownKeys(target)
		if(ownKeys.include(key)) {
			console.log('set 已有的属性', key) // 监听
		} else {
			console.log('新增的属性', key)
		}
   		const result = Reflect.set(target, key, val, receiver)
   		console.log('set', key, val)
   		return result  // 是否设置成功
   	}

   	deleteProperty(target, key) {
   		const result = Reflect.deleteProperty(target, key)
   		console.log('deleteProperty', key)
   		return result
   	}
    })
    
	// 生成代理 对象
	const observed = new Proxy(target, proxyConf)
	return observed;
}


// 测试数据 
const data = {
   name: 'zhangsan',
   age: 20,
   info: {
   	city: 'beijing',
   	a: {
   		b: {
   			c: {
   				d: e
   			}
   		}
   	}
   }
}

vue3的未完后续补充…

参考:
https://juejin.cn/post/6844903942254510087#heading-13
https://juejin.cn/post/7065967379095748638
https://blog.csdn.net/weixin_57677300/article/details/126278467

vue3参考
https://blog.csdn.net/weixin_38318244/article/details/123601856

转载请注明出处或者链接地址:https://www.qianduange.cn//article/4850.html
标签
评论
发布的文章

JQuery中的load()、$

2024-05-10 08:05:15

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!