使用简单的代码逻辑,理一理实现逻辑
为了方便理解,案例中,没有使用虚拟dom和抽象语法树,是通过直接操作dom来实现的
1.模板语法
先看一个简单的实现:
this.compile( this.$el );
- 执行模板编译,
- 如果是文本节点,且有{{}}模板语法,则取$data中的值进行数据替换
- 如果是元素节点 ,继续递归判断
大概就是以此来实现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 操作,提高性能。
- 首先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,就前移一位。
- 快捷对比(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 前端框架
解决问题:可以把一个"特别大"页面,进行拆分(组件化),单个组件进行维护