vue双向数据绑定原理

一:什么是双向数据绑定?

1.1 Vue中MVVM模型

vue双向数据绑定原理_第1张图片

  • 模型(Model)表示应用程序的数据和业务逻辑。这可以是从后端API获取的数据,或者在前端应用程序内部定义的数据。
  • 视图(View)是用户界面的可见部分,通常以HTML模板的形式存在。它负责将数据呈现给用户,并处理用户的输入事件。
  • 视图模型(ViewModel)是模型和视图之间的中间层,它负责管理视图所需的数据,并处理视图中发生的事件。视图模型通过双向数据绑定将模型的状态与视图保持同步。

MVVM即是“Model-View-ViewModel”,它是一种设计模式,用于实现用户界面的分离和交互。

主要职责

View中视图变化,通过ViewModel中的监听器反馈给model进行数据的更新

Model中数据的变化,通过ViewModel中的解析器反馈给View进行视图的更新

1.2 双向数据绑定原理

vue双向数据绑定原理_第2张图片

vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图。

二:实现双向数据绑定

进行数据的准备,我们目的是为了实现双向数据绑定

  • 模板解析,姓名年龄渲染出来的内容是将括号内容替换成我们的数据
  • 数据绑定,文本框的内容和上方渲染的数据是一致的,通过修改文本框上方渲染的内容同步修改

vue双向数据绑定原理_第3张图片

DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Documenttitle>
head>
<body>
    <div id="app">
      <h3>姓名:{{name}}h3>
      <h3>年龄:{{more.age}}h3>
      输入姓名:<input type="text" v-model="name">
      <br>
      输入年龄:<input type="text" v-model="more.age">
    div>
    
    <script src="./vue.js">script>
    <script>
      const vm = new Vue({
        el: '#app',
        data: {
          name: '张三',
          more: {
            age: 18
          }
        }
      })
      console.log(vm);
    script>
body>
html>

数据初始化

//定义vue类
class Vue {
  //构造函数
  constructor(obj_instance) {
    //执行初始化
    this.$data = obj_instance.data
    console.log(this.$data);
}

vue双向数据绑定原理_第4张图片

我们创建的vm实例已经传给vue类,为了模拟vue中 d a t a ,也在构造函数利用 t h i s . data,也在构造函数利用this. data,也在构造函数利用this.data来存储我们创建的vm实例中的data数据

只是此时的数据都还不是响应式的

2.1 数据劫持

//数据劫持 - 监听实例中的数据
function Observer(data_instance) {
	//递归出口
	if (!data_instance || typeof data_instance !== 'object') return;
    //object.keys以数组形式返回对象中的属性
    //遍历属性属性,通过obj.defineProperty来进行数据监视
    Object.keys(data_instance).forEach((key) => {
        let value = data_instance[key];
        //递归将 子属性的值进行数据劫持
        Observer(value);
        //三个参数,(对象, 监视的属性, 回调)
        Object.defineProperty(data_instance, key, {
            //可以枚举 属性描述符可以改变
            enumerable: true,
            configurable: true,
            //通过getter 和 setter函数进行数据监视
            get() {
                //访问属性时候 调用getter函数 返回return 值
                console.log(`访问了属性:${key} -> 值为${value}`);
                // console.log(Dependency.temp);
                return value;
            },
            //修改的新属性值
            set(newValue) {
                console.log(`将属性:${key}的值${value} 修改为->${newValue}`);
                value = newValue;
                Observer(newValue);
            },
        });
    });
}

  • Vue.js是通过Object.defineProperty来实现对数据的监视
  • data_instance是一个对象,通过Object.keys来实现对对象以数组形式放回

vue双向数据绑定原理_第5张图片

  • 将数组中的每一项通过Object.defineProperty进行数据监视,设置getter和setter,当访问数据和修改数据时调用
  • 还要通过递归去将每一项的子属性的值都进行数据监听

vue双向数据绑定原理_第6张图片

2.2 模板解析

对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

//创建vue类
class Vue {
	//执行初始化
	constructor(obj_instance) {
		this.$data = obj_instance.data;
		//调用Observe - 对data中的每个数据进行数据劫持
		//对data中的每一项进行响应式处理
		Observer(this.$data);

		//解析模板
		Compile(obj_instance.el, this);
	}
}
//HTML模板解析 - {{}}替换dom
function Compile(element, vm) {
	//获取id为app的dom元素 绑定到vm.$el上
	vm.$el = document.querySelector(element);
	// console.log(vm.$el);
	//创建文档碎片节点 临时存储数据的改变 避免过频繁地操作dom 文档片段存储在于内存中不在dom中,元素的改变不会引起页面的回流
	const fragment = document.createDocumentFragment();
	//循环将vm.$el中的dom元素 插入到fragment文档碎片中
	let child;
	while ((child = vm.$el.firstChild)) {
		//使用fragment.append会将原先dom删除
		fragment.append(child);
	}
	// console.log(fragment);
	// console.log(fragment.childNodes);
	//要将{{}}替换 所以节点类型为 1 和 3为h3
	fragment_compile(fragment);
	//替换文档碎片内容
	function fragment_compile(node) {
		//正则匹配 {{ 属性 }}
		const pattern = /\{\{\s*(\S+)s*}\}/;
		//如果节点为文本节点
		if (node.nodeType === 3) {
    		const temp = node.nodeValue
			//输出正则验证过后 去除换行符等一些不需要的元素 返回的数组 "{{ name }}" "name" 需要索引为1的值 不需要{{}}
			const result_regex = pattern.exec(node.nodeValue);
			if (result_regex) {
				// console.log(vm.$data[result_regex[1]]);
				const arr = result_regex[1].split('.');
				//reduce迭代累加器 遍历arr数组 total[current] 不断地迭代的链式获取最终的值 ,reduce两个参数 , 第一个参数是个回调函数,第二参数vm.$data是初始值,total的初始值
				const value = arr.reduce(
					(total, current) => total[current],
					vm.$data
				);
				//将 {{name}}  {{more.age}} 替换成value
				node.nodeValue = temp.replace(pattern, value);
			}
			return;
		}
	//将文档碎片 fragment渲染到el中
	vm.$el.appendChild(fragment);
}
  • 开辟一个内存空间,创建fragment文档碎片,不属于dom,属于内存区域,当所有数据更新完成时再渲染页面,避免过多操作dom
  • 将vm.$el中的dom元素 ,通过appendChild插入到fragment文档碎片中,原先dom中的元素会被移除,存放在fragment文档碎片之中

vue双向数据绑定原理_第7张图片

  • 替换文档碎片中 括号中的内容,首先得遍历fragment中的node节点也要通过递归遍历, 通过正则表达式来匹配 {{ name }}内容
		//递归遍历
		node.childNodes.forEach((child) => fragment_compile(child));

vue双向数据绑定原理_第8张图片

  • 要将name 和 more.age 替换成数据,通过reduce方法获取数据

  • reduce迭代累加器 遍历arr数组 total[current] 不断地迭代的链式获取最终的值 ,reduce两个参数 , 第一个参数是个回调函数,第二参数vm. d a t a 是初始值, t o t a l 的初始值,无法通过 v m . data是初始值,total的初始值,无法通过vm. data是初始值,total的初始值,无法通过vm.data[more.age]来获取数据

				const value = arr.reduce(
					(total, current) => total[current],
					vm.$data
				);
  • vm.$data[more.age]

image-20230727181812004

  • 将括号内内容替换成value
				node.nodeValue = temp.replace(pattern, value);
  • 最终将文档碎片 fragment渲染到el中
				vm.$el.appendChild(fragment);

vue双向数据绑定原理_第9张图片

2.3 订阅者-发布者模式

//依赖 --收集和通知订阅者
class Dependency {
	constructor() {
		//收集订阅者
		this.subscribers = [];
	}
	//添加订阅者
	addSub(sub) {
		this.subscribers.push(sub);
	}
	//通知订阅者
	notify() {
		//遍历订阅者 让订阅者触发自己的update函数
		this.subscribers.forEach((sub) => sub.update());
	}
}
  • 数组用于收集订阅者
  • 添加订阅者的方法
  • 当数据修改时需要通知订阅者,触发自己的update更新函数来更新视图
//订阅者
class Watcher {
	//三个参数
	constructor(vm, key, callback) {
		this.vm = vm;
		this.key = key;
		this.callback = callback;
		//临时属性 --触发getter
    //因为想要将watcher实例添加到依赖的数组中
		Dependency.temp = this;
    //触发getter时候 将订阅者实例添加到订阅者数组中
    key.split('.').reduce((total, current) => total[current], vm.$data  )
    //避免多次重复添加到订阅者数组中
    Dependency.temp = null
	}
	//更新函数
	update() {
    //获取属性值
    const value = this.key.split('.').reduce((total, current) => total[current], this.vm.$data  )
		this.callback(value);
	}
}

将watcher类实例添加到Dep数组中来实现数据视图的绑定

    //因为想要将watcher实例添加到依赖的数组中
	Dependency.temp = this;
    //触发getter时候 将订阅者实例添加到订阅者数组中
    key.split('.').reduce((total, current) => total[current], vm.$data  )
    //避免多次重复添加到订阅者数组中
    Dependency.temp = null
  • 创建Dependency.temp用于临时存储创建的watcher实例,触发getter

  • 在observer类中触发getter时,将临时存储的watcher实例添加到Dependency的存储订阅者的数组之中

			get() {
                //将订阅者实例添加到订阅者数组中
                Dependency.temp && dependency.addSub(Dependency.temp)
			},
  • 同时为了避免多次重复,添加watcher实例,在添加该实例过后,赋空值

2.4 v-model数据绑定

视图与数据的绑定

在fragment_compile()函数中

    //找v-model属性的元素 更改其nodeValue
    if(node.nodeType === 1 && node.nodeName === 'INPUT'){
      const attr = Array.from(node.attributes)
      attr.forEach(item => {
        if(item.nodeName === 'v-model'){
          // console.log(item.nodeValue);
          //修改nodeValue
          const value = item.nodeValue.split('.').reduce((total, current) => total[current], vm.$data)
          // console.log(value);
          node.value = value
          //创建watcher实例
          new Watcher(vm, item.nodeValue, newValue => {
            node.value = newValue
          })
          //触发input事件来通过视图修改数据
          node.addEventListener('input', e => {
            const arr1 = item.nodeValue.split('.')
            // console.log(arr1);
            const arr2 = arr1.slice(0, arr1.length - 1)
            const final = arr2.reduce((total, current) => total[current], vm.$data)
            // console.log(final);
            final[arr1[arr1.length - 1]] = e.target.value
          })
        } 
      })
    }
  • 在文档碎片fragment中遍历node,通过node.attributes方法来找到属性值为v-model的node节点

  • 遍历的节点 item.nodeValue,是name, more.age

vue双向数据绑定原理_第10张图片

  • 通过reduce方法来获取到vm.$data上对应属性的属性值
  • 将node.value 修改为属性值,此时将文本框中的内容和属性值相绑定

vue双向数据绑定原理_第11张图片

  • 然后需要通过,文本框修改数据同时修改上方的视图,那就需要用到addEvetListener方法添加input事件
  • 然后通过文本框视图来修改数据
  final[arr1[arr1.length - 1]] = e.target.value

三:完整代码

html

DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Documenttitle>
head>
<body>
    <div id="app">
      <h3>姓名:{{name}}h3>
      <h3>年龄:{{more.age}}h3>
      输入姓名:<input type="text" v-model="name">
      <br>
      输入年龄:<input type="text" v-model="more.age">
    div>
    
    <script src="./vue.js">script>
    <script>
      const vm = new Vue({
        el: '#app',
        data: {
          name: '张三',
          more: {
            age: 18
          }
        }
      })
      console.log(vm);
    script>
body>
html>

vue.js

//创建vue类
class Vue {
	//执行初始化
	constructor(obj_instance) {
		this.$data = obj_instance.data;
		//调用Observe - 对data中的每个数据进行数据劫持
		//对data中的每一项进行响应式处理
		Observer(this.$data);

		//解析模板
		Compile(obj_instance.el, this);
	}
}

//数据劫持 - 监听实例中的数据
function Observer(data_instance) {
	//递归出口
	if (!data_instance || typeof data_instance !== 'object') return;
  //创建订阅者实例
  const dependency = new Dependency()
	//object.keys以数组形式返回对象中的属性
  // console.log(Object.keys(data_instance));
	//遍历属性属性,通过obj.defineProperty来进行数据监视
	Object.keys(data_instance).forEach((key) => {
		let value = data_instance[key];
		//递归将 子属性的值进行数据劫持
		Observer(value);
		//三个参数,(对象, 监视的属性, 回调)
		Object.defineProperty(data_instance, key, {
			//可以枚举 属性描述符可以改变
			enumerable: true,
			configurable: true,
			//通过getter 和 setter函数进行数据监视
			get() {
				//访问属性时候 调用getter函数 返回return 值
				console.log(`访问了属性:${key} -> 值为${value}`);
        // console.log(Dependency.temp);
        //将订阅者实例添加到订阅者数组中
        Dependency.temp && dependency.addSub(Dependency.temp)
				return value;
			},
			//修改的新属性值
			set(newValue) {
				console.log(`将属性:${key}的值${value} 修改为->${newValue}`);
				value = newValue;
				Observer(newValue);
        dependency.notify()
			},
		});
	});
}

//HTML模板解析 - {{}}替换dom
function Compile(element, vm) {
	//获取id为app的dom元素 绑定到vm.$el上
	vm.$el = document.querySelector(element);
	// console.log(vm.$el);
	//创建文档碎片节点 临时存储数据的改变 避免过频繁地操作dom 文档片段存储在于内存中不在dom中,元素的改变不会引起页面的回流
	const fragment = document.createDocumentFragment();
	//循环将vm.$el中的dom元素 插入到fragment文档碎片中
	let child;
	while ((child = vm.$el.firstChild)) {
		//使用fragment.append会将原先dom删除
		fragment.append(child);
	}
	// console.log(fragment);
	// console.log(fragment.childNodes);
	//要将{{}}替换 所以节点类型为 1 和 3为h3
	fragment_compile(fragment);
	//替换文档碎片内容
	function fragment_compile(node) {
		//正则匹配 {{ 属性 }}
		const pattern = /\{\{\s*(\S+)s*}\}/;
		//如果节点为文本节点
		if (node.nodeType === 3) {
      const temp = node.nodeValue
			//输出正则验证过后 去除换行符等一些不需要的元素 返回的数组 "{{ name }}" "name" 需要索引为1的值 不需要{{}}
			const result_regex = pattern.exec(node.nodeValue);
      // console.log(result_regex);
			if (result_regex) {
				// console.log(vm.$data[result_regex[1]]);
				const arr = result_regex[1].split('.');
				//reduce迭代累加器 遍历arr数组 total[current] 不断地迭代的链式获取最终的值 ,reduce两个参数 , 第一个参数是个回调函数,第二参数vm.$data是初始值,total的初始值
				const value = arr.reduce(
					(total, current) => total[current],
					vm.$data
				);
				//将 {{name}}  {{more.age}} 替换成value
				node.nodeValue = temp.replace(pattern, value);
				//文档碎片替换的时候添加创建订阅者
				new Watcher(vm, result_regex[1], newValue => {
          //wacther的回调函数 会将文档碎片中的nodevalue更新为我们修改的newValue
					node.nodeValue = temp.replace(pattern, newValue);
				});
			}
			return;
		}
    //找v-model属性的元素 更改其nodeValue
    if(node.nodeType === 1 && node.nodeName === 'INPUT'){
      const attr = Array.from(node.attributes)
      attr.forEach(item => {
        if(item.nodeName === 'v-model'){
          console.log(item.nodeValue);
          //修改nodeValue
          const value = item.nodeValue.split('.').reduce((total, current) => total[current], vm.$data)
          // console.log(value);
          node.value = value
          //创建watcher实例
          new Watcher(vm, item.nodeValue, newValue => {
            node.value = newValue
          })
          //触发input事件来通过视图修改数据
          node.addEventListener('input', e => {
            const arr1 = item.nodeValue.split('.')
            // console.log(arr1);
            const arr2 = arr1.slice(0, arr1.length - 1)
            const final = arr2.reduce((total, current) => total[current], vm.$data)
            // console.log(final);
            final[arr1[arr1.length - 1]] = e.target.value
          })
        } 
      })
    }
		//递归遍历
		node.childNodes.forEach((child) => fragment_compile(child));
	}
	//将文档碎片 fragment渲染到el中
	vm.$el.appendChild(fragment);
}

//依赖 --收集和通知订阅者
class Dependency {
	constructor() {
		//收集订阅者
		this.subscribers = [];
	}
	//添加订阅者
	addSub(sub) {
		this.subscribers.push(sub);
	}
	//通知订阅者
	notify() {
		//遍历订阅者 让订阅者触发自己的update函数
		this.subscribers.forEach((sub) => sub.update());
	}
}

//订阅者
class Watcher {
	//三个参数
	constructor(vm, key, callback) {
		this.vm = vm;
		this.key = key;
		this.callback = callback;
		//临时属性 --触发getter
    //因为想要将watcher实例添加到依赖的数组中
		Dependency.temp = this;
    //触发getter时候 将订阅者实例添加到订阅者数组中
    key.split('.').reduce((total, current) => total[current], vm.$data  )
    //避免多次重复添加到订阅者数组中
    Dependency.temp = null
	}
	//更新函数
	update() {
    //获取属性值
    const value = this.key.split('.').reduce((total, current) => total[current], this.vm.$data  )
		this.callback(value);
	}
}

你可能感兴趣的:(javascript,vue,vue.js,前端,javascript)