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

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

相关推荐
练习两年半的工程师1 小时前
使用React和google gemini api 打造一个google gemini应用
javascript·人工智能·react.js
勘察加熊人2 小时前
angular九宫格ui
javascript·ui·angular.js
姑苏洛言3 小时前
30天搭建消防安全培训小程序
前端
左钦杨4 小时前
Nuxt2 vue 给特定的页面 body 设置 background 不影响其他页面
前端·javascript·vue.js
yechaoa4 小时前
【揭秘大厂】技术专项落地全流程
android·前端·后端
MurphyChen4 小时前
🤯 一行代码,优雅的终结 React Context 嵌套地狱!
前端·react.js
逛逛GitHub4 小时前
推荐 10 个受欢迎的 OCR 开源项目
前端·后端·github
_xaboy5 小时前
开源 FormCreate 表单设计器配置组件的多语言
前端·vue.js·低代码·开源·可视化表单设计器
uglyduckling04125 小时前
小程序构建NPM失败
前端·小程序·npm
草原上唱山歌5 小时前
C/C++都有哪些开源的Web框架?
前端·c++·开源