Vue2 常见知识点(二)

使用简单的代码逻辑,理一理实现逻辑

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

1.模板语法

先看一个简单的实现:

this.compile( this.$el );

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

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

html:

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

javascript 复制代码
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.生命周期执行顺序:

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

javascript 复制代码
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中会定义。

javascript 复制代码
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 中的一个方法,用于在一个对象上定义一个新属性,或者修改一个现有属性的配置。

它接受三个主要参数:要定义属性的对象、属性名称(作为字符串)和一个包含属性描述符的对象。

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

先看一个简单的实现:

javascript 复制代码
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:依赖收集器类的简单结构示例,用于依赖收集和通知更新

javascript 复制代码
class Dep {
    constructor() {
        this.subs = [];
    }
    addSub(watcher) {
        this.subs.push(watcher);
    }
    notify() {
        this.subs.forEach(watcher => {
            watcher.update();
        });
    }
}
javascript 复制代码
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,触发依赖收集。

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

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

javascript 复制代码
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]);
            });
        }
    });
}
javascript 复制代码
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 节点,并且在这个节点中记录下需要获取的数据属性。

例如

javascript 复制代码
{
    type: 'Interpolation',
    content: 'message'
}

3.从 AST 到Watcher的创建

在代码生成阶段,编译器会根据 AST 生成渲染函数(render函数),

在这个过程中,对于每个与数据属性相关的 AST 节点,会创建一个Watcher实例来监听对应的数据变化。

javascript 复制代码
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 内容:

javascript 复制代码
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):是浏览器中用于表示文档结构的树形结构。

javascript 复制代码
<h2>你好</h2>

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

javascript 复制代码
{
  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.当节点类型,属性都相同,则比较是否存在子节点,

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,就前移一位。

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 前端框架

解决问题:可以把一个"特别大"页面,进行拆分(组件化),单个组件进行维护

相关推荐
中东大鹅13 分钟前
【JavaScript】下拉框的实现
前端·javascript·css·html
Domain-zhuo15 分钟前
什么是前端构建工具?比如(Vue2的webpack,Vue3的Vite)
前端·javascript·vue.js·webpack·node.js·vue·es6
yanmengying1 小时前
VUE脚手架练习
前端·javascript·vue.js
APItesterCris1 小时前
对于大规模的淘宝API接口数据,有什么高效的处理方法?
linux·服务器·前端·数据库·windows
突然暴富的我1 小时前
html button 按钮单选且 高亮
前端·javascript·html
用户49430538293801 小时前
一种简单粗暴的大屏自适应方案,原理及案例
前端
咿呀大河马1 小时前
vue中使用socket.io统计在线用户
vue.js·socket.io
fury_1232 小时前
怎么获取键值对的键的数值?
java·前端·数据库
午后书香2 小时前
看两道关于异步的字节面试题...
前端·javascript·面试
用户2404817096212 小时前
我来助你:Coze帮你零代码生成智能体
前端·人工智能·coze