使用简单的代码逻辑,理一理实现逻辑
为了方便理解,案例中,没有使用虚拟dom和抽象语法树,是通过直接操作dom来实现的
1.模板语法
先看一个简单的实现:
this.compile( this.$el );
大概就是以此来实现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"
}
(Vue.js 在早期开发过程中借鉴了 Snabbdom 的设计理念来构建自己的虚拟 DOM 系统)
1.2 作用
性能优化方面
真实DOM
虚拟DOM
8、Diff 算法
(源码地址)
它的主要作用是比较新数据与旧数据虚拟 DOM 树的差异,从而找出需要更新的部分,以便将这些最小化的变更应用到真实 DOM上,减少不必要的 DOM 操作,提高性能。
- 首先sameVNode 比较一下新旧节点是不是同一个节点(同级对比,不跨级)
2.当节点类型相同的时候,Diff 算法会比较节点的属性是否有变化。如果属性有变化,就更新真实 DOM 节点的属性。
3.当节点类型,属性都相同,则比较是否存在子节点,
4.如果新节点和老节点都有子节点,需要进一步比较(双端diff核心updateChildren)
那怎么优化这个算法呢?
开启一个循环,循环的条件就是 oldStart 不能大于oldEnd ,newStart不能大于newEnd,以下是循环的重要判断
- 跳过undefined
**if (isUndef(oldStartVnode))**
- 快捷对比(https://www.jianshu.com/p/b9916979a740
)**4个 else if(sameVnode(xxx))**
- key值查找(2.快捷对比都不满足的情况下)
**}else {**
将旧节点数组剩余的vnode(oldStartIdx到oldEndIdx之间的节点)进行一次遍历,生成由vnode.key作为键,idx索引作为值的对象oldKeyToIdx,然后遍历新节点数组的剩余vnode(newStartIdx 到 newEndIdx 之间的节点),根据新的节点的key在oldKeyToIdx进行查找。
以上是while内部处理,以下是while外部处理
- 剩余元素处理(不满足循环条件后退出,循环外处理剩余元素)
循环外
以上便是vue2的diff的核心流程了,具体案例参考这里
什么是MVVM
1.概念
它主要目的是分离用户界面(View)和业务逻辑(Model),并通过一个中间层(ViewModel)来进行数据绑定和交互。
这种模式能够使代码更加清晰、易于维护和扩展。
- MVVM 模式的优势在于它能够很好地分离,这使得代码的维护和扩展变得更加容易。
- 开发人员专注于 Model 的业务逻辑,设计人员专注于 View 的界面设计, ViewModel 则负责两者之间的沟通和协调。
web1.0时代
web2.0时代
MVC、MVVM 前端框架