当前位置: 首页 > article >正文

Vue2 常见知识点(二)

使用简单的代码逻辑,理一理实现逻辑
为了方便理解,案例中,没有使用虚拟dom和抽象语法树,是通过直接操作dom来实现的

1.模板语法

先看一个简单的实现:

this.compile( this.$el );

  1. 执行模板编译,
  2. 如果是文本节点,且有{{}}模板语法,则取$data中的值进行数据替换
  3. 如果是元素节点 ,继续递归判断

大概就是以此来实现modal中的数据,渲染到View中

html:

<body>
<div id='app'>
	<h1> {{   str  }} </h1>
	{{   str  }}
	<p>{{b}}</p>
</div>
<script type="text/javascript" src='vue.js'></script>
<script type="text/javascript">
new Vue({
	el:'#app',
	data : {
		str:'你好',
		b:'这也是data的数据'
	}
})
</script>
</body>

vue.js

class Vue{
	constructor( options ){
	    // 获取到#app
		this.$el = document.querySelector(options.el);
		// 获取到data数据
		this.$data = options.data;
		// 执行模板解析
		this.compile(  this.$el );
	}

	compile( node ){
		node.childNodes.forEach((item,index)=>{
			// 如果是元素节点 说明还有子,继续递归
			if( item.nodeType == 1 ){
				this.compile(  item );
			}
			// 如果是文本节点,如果有{{}}就替换成数据
			if( item.nodeType == 3 ){
				//正则匹配{{}}
				let reg = /\{\{(.*?)\}\}/g;
				let text = item.textContent;
				//给节点赋值
				item.textContent = text.replace(reg,(match,vmKey)=>{
					// 排除空格 拿到属性名称
					vmKey = vmKey.trim();
					//属性名称 在$data中取值
					return this.$data[vmKey];
				})
			}
		})
	} 
}

页面显示如下 ,替换了{{}}中的数据在这里插入图片描述

2.生命周期执行顺序:

在编写代码时,不管外面怎么写顺序,内部生命周期执行顺序是固定的,不受顺序影响

class Vue{
	constructor( options ){
	
		// 1.执行beforeCreate  并绑定this
		if(  typeof options.beforeCreate == 'function' ){
			options.beforeCreate.bind(this)();
		}
		// 挂载data 从这里之后可以获取数据 this.$data值
		this.$data = options.data;
		
		// 2.执行created 并绑定this
		if(  typeof options.created == 'function' ){
			options.created.bind(this)();
		}
		
		// 3.执行beforeMount并绑定this
		if(  typeof options.beforeMount == 'function' ){
			options.beforeMount.bind(this)();
		}
		
		//挂载 节点  从这里之后可以获取dom this.$el值
		this.$el = document.querySelector(options.el);
		
		// 4.执行mounted 并绑定this
		if(  typeof options.mounted == 'function' ){
			options.mounted.bind(this)();
		}
		// 这里之后可与获取  this.$data值 和 this.$el值
	}
}

3.添加事件

模板编译过程中,判断元素节点是否有@click,@change…事件属性,有则addEventListener添加对应事件,当触发addEventListener的时候,执行绑定方法,一般方法在methods中会定义。

class Vue{
	constructor( options ){
		this.$options = options;
		if(  typeof options.beforeCreate == 'function' ){
			options.beforeCreate.bind(this)();
		}
		this.$data = options.data;
		if(  typeof options.created == 'function' ){
			options.created.bind(this)();
		}
		if(  typeof options.beforeMount == 'function' ){
			options.beforeMount.bind(this)();
		}
		this.$el = document.querySelector(options.el);
		
		//模版解析
		this.compile(  this.$el );
		
		if(  typeof options.mounted == 'function' ){
			options.mounted.bind(this)();
		}
	}

	compile( node ){
		node.childNodes.forEach((item,index)=>{
			//元素节点
			if( item.nodeType == 1 ){
				// 判断元素节点是否绑定了@click
				if( item.hasAttribute('@click')  ){
					// @click后绑定的属性名称
					let vmKey = item.getAttribute('@click').trim();
					item.addEventListener('click',( event )=>{
					    // 查找method里面的方法  并挂载事件
						this.eventFn = this.$options.methods[vmKey].bind(this);
						// 点击后 执行方法
						this.eventFn(event);
					})
				}
				if( item.childNodes.length > 0  ){
					this.compile(  item );
				}
			}
			//这是文本节点,如果有{{}}就替换成数据
			if( item.nodeType == 3 ){
				//正则匹配{{}}
				let reg = /\{\{(.*?)\}\}/g;
				let text = item.textContent;
				//给节点赋值
				item.textContent = text.replace(reg,(match,vmKey)=>{
					vmKey = vmKey.trim();
					return this.$data[vmKey];
				})
			}
		})
	} 
}

4. 数据劫持

Object.defineProperty是 JavaScript 中的一个方法,用于在一个对象上定义一个新属性,或者修改一个现有属性的配置。
它接受三个主要参数:要定义属性的对象、属性名称(作为字符串)和一个包含属性描述符的对象。

let obj = {};
Object.defineProperty(obj, 'name', {
    value: 'John',
    writable: true,
    enumerable: true,
    configurable: true
});
console.log(obj.name); // 输出: John

先看一个简单的实现:

class Vue{
	constructor( options ){
		this.$options = options;
		
		if(  typeof options.beforeCreate == 'function' ){
			options.beforeCreate.bind(this)();
		}
		// 这是data
		this.$data = options.data;
		// 处理数据
		this.proxyData();
		
		if(  typeof options.created == 'function' ){
			options.created.bind(this)();
		}
		if(  typeof options.beforeMount == 'function' ){
			options.beforeMount.bind(this)();
		}
		this.$el = document.querySelector(options.el);
		if(  typeof options.mounted == 'function' ){
			options.mounted.bind(this)();
		}
		
	}
	//1、给Vue大对象赋属性,来自于data中
	//2、data中的属性值和Vue大对象的属性保持双向(劫持)
	proxyData(){
		for( let key in this.$data ){
			Object.defineProperty(this,key,{
				get(){
				   // 取值劫持
					return this.$data[key];
				},
				set( val ){
					// 设置值劫持
					this.$data[key] = val;
				}
			})
		}
	}
}

5. 依赖收集

Dep:依赖收集器类的简单结构示例,用于依赖收集和通知更新

class Dep {
    constructor() {
        this.subs = [];
    }
    addSub(watcher) {
        this.subs.push(watcher);
    }
    notify() {
        this.subs.forEach(watcher => {
            watcher.update();
        });
    }
}
function defineReactive (obj, key, val) {
    var dep = new Dep();
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            if (Dep.target) {
            // 依赖收集
                dep.addSub(Dep.target);
            }
            return val;
        },
        set: function (newVal) {
            if (newVal === val) return;
            val = newVal;
            dep.notify();
        }
    });
}

上溯代码中, if (Dep.target) 为ture ,才会依赖收集,那什么时候 if (Dep.target) 为ture?

只有当 模板渲染场景 计算属性场景computed 监听器场景watch 情况下才会创建一个watcher ,调用watcher.get获取数据,把watcher实例赋值给Dep.target,触发依赖收集。

模板渲染场景:
 1.插值表达式  {{}}
 2.指令绑定条件 v-bind:class="activeClass"
 3.循环指令 v-if  v-for

例如:下面是一个模板渲染场景的插值表达式情况,生成watcher,第三个参数是是监听到更改值的时候,调用的函数。

function generateRenderFunction(ast) {
    // 遍历节点
    ast.nodes.forEach(node => {
        if (node.type === 'Interpolation') {
            let propertyName = node.content;
            let watcher = new Watcher(vm, propertyName, () => {
                // 当数据变化时更新节点内容
                updateNode(node, vm[propertyName]);
            });
        }
    });
}
Watcher.prototype.get = function () {
	// Dep.target 表是当前是有监听的
    Dep.target = this;
    
    // 然后去取值 走到defineProperty中的get方法中,判断 Dep.target不为空,依赖收集
    var value = this.getter.call(this.vm);
    
    // 依赖收集后,清空 Dep.target
    Dep.target = null;
    
    // 返回value值
    return value;
};

6. 视图更新

1.模板编译基础
在 Vue 2 中,模板编译主要分为三个阶段: 解析(parse)、优化(optimize)和代码生成(codegen)。
在解析阶段,会将模板字符串转换为抽象语法树(AST),这个 AST 包含了模板中的各种元素、指令和插值等信息。

2.解析阶段添加Watcher的线索
当解析到模板中的插值表达式(如{{ message }})或指令(如v - bind、v - model等)时,编译器会识别出对数据属性的使用。
编译器会为插值表达式创建一个对应的 AST 节点,并且在这个节点中记录下需要获取的数据属性。

例如

{
    type: 'Interpolation',
    content: 'message'
}

3.从 AST 到Watcher的创建
在代码生成阶段,编译器会根据 AST 生成渲染函数(render函数),
在这个过程中,对于每个与数据属性相关的 AST 节点,会创建一个Watcher实例来监听对应的数据变化。

function generateRenderFunction(ast) {
    // 遍历AST节点
    ast.nodes.forEach(node => {
    //发现是{{}} 插值表达式
        if (node.type === 'Interpolation') {
            let propertyName = node.content;
            // 生成watcher
            let watcher = new Watcher(vm, propertyName, () => {
                // 当数据变化时更新节点内容
                updateNode(node, vm[propertyName]);
            });
        }
    });
    // 根据AST和创建的Watcher等生成完整的渲染函数
}
  • 当发现类型为Interpolation的节点(插值表达式)时,会提取出相关的数据属性名(propertyName),然后创建一个Watcher实例。
  • 这个Watcher的getter函数会获取对应的vm[propertyName]的值,并且在数据变化时,会执行一个回调函数来更新对应的节点内容(updateNode函数,这里假设它用于更新节点)。

Watcher 内容:

function Watcher (vm, expOrFn, cb, options) {
    this.vm = vm;
    this.getter = parsePath(expOrFn);
    this.value = this.get();
}
Watcher.prototype.get = function () {
	// Dep.target 表是当前是有监听的
    Dep.target = this;
    var value = this.getter.call(this.vm);
    Dep.target = null;
    return value;
};
Watcher.prototype.update = function () {
    // 处理更新逻辑,可能是异步或同步更新
    if (this.lazy) {
        this.dirty = true;
    } else if (this.sync) {
        this.run();
    } else {
        queueWatcher(this);
    }
};
Watcher.prototype.run = function () {
    var value = this.get();
    var oldValue = this.value;
    this.value = value;
    if (this.cb) {
        this.cb.call(this.vm, value, oldValue);
    }
};

7、虚拟 DOM 和 真实DOM(概念、作用)

1.1 概念

真实 DOM(Document Object Model):是浏览器中用于表示文档结构的树形结构。

<h2>你好</h2>

虚拟DOM:用 JavaScript 对象来模拟真实 DOM 的结构

{
  children: undefined
  data: {}
  elm: h1
  key: undefined
  sel: "h1"
  text: "你好h1"
}

步骤
1.用JS对象表示真实的DOM结构,生成一个虚拟DOM,再用虚拟DOM构建一个真实DOM树,渲染到页面
2.状态改变生成新的虚拟DOM,与旧的虚拟DOM进行比对,比对的过程就是DIFF算法,利用patch记录差异
3.把记录的差异用在第一个虚拟DOM生成的真实DOM上,视图就更新了。

(Vue.js 在早期开发过程中借鉴了 Snabbdom 的设计理念来构建自己的虚拟 DOM 系统)

1.2 作用

性能优化方面

真实DOM

  • 当直接操作真实 DOM 时,比如频繁地添加、删除或修改节点,会引起浏览器的重排(reflow)和重绘(repaint)。
  • 重排: DOM 结构的改变导致浏览器重新计算元素的几何属性,如位置、大小等;
  • 重绘:元素的外观发生改变,如颜色、背景等变化,只是重新绘制外观而不涉及布局调整。

虚拟DOM

  • 通过一种高效的 Diff 算法比较新旧虚拟 DOM 树的差异,可以快速地找出需要更新的部分,而不是每次都对整个 DOM 进行重新渲染。
  • 虚拟 DOM 的操作在 JavaScript 层面进行,比直接操作真实 DOM 快得多
  • 当组件的数据发生变化时,Vue.js 会收集一段时间内的数据变化,然后统一进行虚拟 DOM 的更新和差异比较,并根据差异更新真实 DOM,避免大量的无谓计算。

8、Diff 算法

(源码地址)

它的主要作用是比较新数据与旧数据虚拟 DOM 树的差异,从而找出需要更新的部分,以便将这些最小化的变更应用到真实 DOM上,减少不必要的 DOM 操作,提高性能。
在这里插入图片描述

  1. 首先sameVNode 比较一下新旧节点是不是同一个节点(同级对比,不跨级)

下图比较第二层级的右侧,左边是P,右边是div, 那么会认为这两个节点完全不同,直接删除旧的p替换新的div。
在这里插入图片描述
因为 dom 节点做跨层级移动的情况还是比较少的,一般情况下都是同一层级的 dom 的增删改。
但是 diff 算法除了考虑本身的时间复杂度之外,还要考虑一个因素:dom 操作的次数。
如果是一个list数组,新旧节点只是前后顺序的改变,直接删除新增,dom渲染成本会增加。

2.当节点类型相同的时候,Diff 算法会比较节点的属性是否有变化。如果属性有变化,就更新真实 DOM 节点的属性。

例如input节点,旧虚拟 DOM 中的value属性为abc,新虚拟 DOM 中的value属性为def,Diff 算法会更新真实 DOM 中input节点的value属性。

3.当节点类型,属性都相同,则比较是否存在子节点,
3.1子节点是文本节点

4.如果新节点和老节点都有子节点,需要进一步比较(双端diff核心updateChildren)

  • diff 算法我们从一端逐个处理的,叫做简单 diff 算法。简单 diff 算法其实性能不是最好的,比如旧的 vnode 数组是 ABCD,新的 vnode 数组是 DABC,按照简单 diff 算法,A、B、C 都需要移动。

那怎么优化这个算法呢?

  • vue使用的是双端 diff 算法:是头尾指针向中间移动,分别判断头尾节点是否可以复用,如果没有找到可复用的节点再去遍历查找对应节点的下标,然后移动。全部处理完之后也要对剩下的节点进行批量的新增和删除。

开启一个循环,循环的条件就是 oldStart 不能大于oldEnd ,newStart不能大于newEnd,以下是循环的重要判断

  • 跳过undefined **if (isUndef(oldStartVnode))**

为什么会有undefined,老节点移动过程中,会产生undefined占位,之后的流程图会说明清楚。这里只要记住,如果旧开始节点为undefined,就后移一位;如果旧结束节点为undefined,就前移一位。

  • 快捷对比(https://www.jianshu.com/p/b9916979a740)**4个 else if(sameVnode(xxx))**

1 新开始和旧开始节点比对 如果匹配,表示它们位置都是对的,Dom不用改,就将新、旧节点开始的下标往后移一位即可。

2 旧结束和新结束节点比对 如果匹配,也表示它们位置是对的,Dom不用改,就将新、旧节点结束的下标前移一位即可。

3 旧开始和新结束节点比对 如果匹配,位置不对需要更新Dom视图,将旧开始节点对应的真实Dom插入到最后一位,旧开始节点下标后移一位,新结束节点下标前移一位。

4 旧结束和新开始节点比对 如果匹配,位置不对需要更新Dom视图,将旧结束节点对应的真实Dom插入到旧开始节点对应真实Dom的前面,旧结束节点下标前移一位,新开始节点下标后移一位。

  • key值查找(2.快捷对比都不满足的情况下) **}else {**

将旧节点数组剩余的vnode(oldStartIdx到oldEndIdx之间的节点)进行一次遍历,生成由vnode.key作为键,idx索引作为值的对象oldKeyToIdx,然后遍历新节点数组的剩余vnode(newStartIdx 到 newEndIdx 之间的节点),根据新的节点的key在oldKeyToIdx进行查找。

1 找到相同的key

  • 如果和已有key值匹配 那就说明是已有的节点,只是位置不对,则将找到的节点插入到 oldStartIdx 对应的 vnode 之前;并且,这里会将旧节点数组中 idxInOld 对应的元素设置为 undefined。
  • 如果和已有key值不匹配,那就说明是新的节点,那就创建一个对应的真实Dom节点,插入到旧开始节点对应的真实Dom前面即可

2 没有相同key

  • 没有找到对应的索引,则直接createElm创建新的dom节点并将新的vnode插入 oldStartIdx 对应的 vnode 之前。

以上是while内部处理,以下是while外部处理

  • 剩余元素处理(不满足循环条件后退出,循环外处理剩余元素)循环外
  • 旧节点数组遍历结束、新节点数组仍有剩余,经过两端对比查找都没有查找到,则说明新插入内容是处于 oldstartIdx与 oldEndIdx 之间的,所以可以直接在 newEndIdx 对应的 vnode 之前创建插入新节点即可。
  • 新节点数组遍历结束、旧节点数组仍有剩余,则遍历旧节点oldStartIdx 到 oldEndIdx 之间的剩余数据,进行移除
    因为旧节点oldStartIdx之前的数据和 oldEndIdx之后的数据都是对比确认之后的,且数量与新节点数组相同,则中间剩下的都是要删除的节点

以上便是vue2的diff的核心流程了,具体案例参考这里

什么是MVVM

1.概念
它主要目的是分离用户界面(View)和业务逻辑(Model),并通过一个中间层(ViewModel)来进行数据绑定和交互。
这种模式能够使代码更加清晰、易于维护和扩展。

  • M: Model 主要代表应用程序的数据和业务逻辑;这包括像数据对象,如用户信息、产品列表;
  • V:View 是用户直接看到和交互的界面部分;通常是指组件中的 template 标签内的 HTML 内容, style 标签内的 CSS
    样式也属于视图。
  • VM:ViewModel 是连接 Model 和 View 的桥梁。像data函数(它返回数据对象)、computed属性、methods以及生命周期钩子都属于 ViewModel(vue源码)
  • MVVM 模式的优势在于它能够很好地分离,这使得代码的维护和扩展变得更加容易。
  • 开发人员专注于 Model 的业务逻辑,设计人员专注于 View 的界面设计, ViewModel 则负责两者之间的沟通和协调。

例如,当业务逻辑发生变化,如待办事项的完成状态需要增加一个审核流程,我们只需要在 Model 部分修改相关的数据结构和处理函数,而不会影响到视图的展示逻辑。同样,如果要改变视图的外观,如将待办事项列表从无序列表改为表格形式,只要修改
View 部分的 HTML 和 CSS,而不需要大量改动业务逻辑部分。这种分离使得团队协作更加高效,也提高了代码的可复用性和可测试性。

web1.0时代

文件全在一起,也就是前端和后端的代码全在一起:
1、前端和后端都是一个人开发。(技术没有侧重点或者责任不够细分)
2、项目不好维护。
3、html、css、js页面的静态内容没有,后端是没办法工作的(没办法套数据)mvc…都是后端先出的

web2.0时代

ajax出现了,就可以:前端和后数据分离了 解决问题:
后端不用等前端页面弄完没,后端做后端的事情(写接口)、前布局、特效、发送请求问题:
1、html、c5s、js都在一个页面中,单个页面可能内容也是比较多的(也会出现不好维护的情况)

MVC、MVVM 前端框架

解决问题:可以把一个"特别大”页面,进行拆分(组件化),单个组件进行维护
在这里插入图片描述


http://www.kler.cn/a/420427.html

相关文章:

  • 1、C++ 介绍
  • rpm包转deb包或deb包转rpm包
  • 在21世纪的我用C语言探寻世界本质——字符函数和字符串函数(2)
  • python之Flask入门—路由参数
  • elementUI el-image的使用
  • Python 面向对象编程详解
  • unity3d———xml 存储数据例子
  • MySQL的Json类型数据操作方法
  • LeetCode 129.求根节点到叶节点数字之和
  • VBA数据库解决方案第十七讲:Recordset对象记录位置的定位方法
  • 你还没有将 Siri 接入GPT对话功能吗?
  • 在线影视播放网站PHP电影网站源码自动采集MKCMS升级版米酷模板含WAP手机版附三套模板
  • Y20030017php+mysql小型宠物服务平台的设计与实现 源码 文档 PPT
  • java单例设计
  • 在树莓派上使用自带的摄像头采集视频
  • 深度探索Spring Context:框架式的Bean访问与企业级功能
  • Day2 生信新手笔记: Linux基础
  • 鸿蒙与Linux内核的关系
  • java Stream 详解
  • 嵌入式Linux无窗口系统下搭建 Qt 开发环境
  • redis针对hash的命令 及 使用场景
  • 电机驱动MCU介绍
  • 理解 Python PIL库中的 convert(‘RGB‘) 方法:为何及如何将图像转换为RGB模式
  • 广东省计算机学会40周年暨2024年庆典活动 粤港澳数字人的生产与驱动技术论坛、第五届人工智能与信息系统国际学术会议
  • 【大数据学习 | 面经】Spark为什么比MR计算更快
  • 解析生成对抗网络(GAN):原理与应用