VUE2.x和3.x的响应原理(未完待续 待我学成归来)

我们来学习关于vue的响应原理

vue对数据的处理是响应式的 他是一个MVVM的框架

什么是mvvm 所谓的MVVM其实就是Model、View 和ViewModel

Model:模型层(数据层),主要用于保存一些数据

View: 视图层,主要用于将后端数据借助各种元素呈现给用户,同时也可提供用户操作的入口

ViewModel:视图模型层:该层也是mvvm中的核心层,主要用于作为Model个View两个层的数据连接层,负责两个层之间的数据传递。该层主要包含两大功能点:

DOM监听(DOM Listener) 用于监听dom元素的一些事假,如果dom元素发生变化在需要的时候会改变对应的data

数据绑定(Data bindings)用于将model的改变反应在view上及时呈现给用户

上面简单介绍了什么是MVVM,那么接下来我们以Vue2为例再看看MVVM是如何实现的,它的实现原理又是什么?首先我们可以把MVVM的实现分为三步:数据劫持、模板编译和双向绑定。

数据劫持、模板编译和双向绑定。

我们先看第一个 数据劫持 什么叫做数据劫持

数据劫持是指,访问者或者修改对象某个属时,通过一段代码拦截这个行为,进行额外的操作或者返回结果

数据劫持 为什么要数据劫持?带着这个问题我们先来看下如何实现数据劫持,在vue2的源码中有个名为defineReactive$$1的方法,该方法就是用来实现数据劫持的

但是这个方法只是一个外在的方法实际上实现数据劫持的是js原生的Object.defineProperty方法

我们来了解一下这个原生的方法

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

Object.defineProperty(obj, prop, descriptor)

接收三个参数

第一个obj

要定义属性的对象。

第二个prop

要定义或修改的属性的名称或 Symbol

第三个descriptor

要定义或修改的属性描述符

返回值

被传递给函数的对象。

这里重点讲第三个参数

第三个参数是一个配置项对象(包括:value、enumerable、configurable、get和set等几个属性)

我们在做数据劫持时主要用到的就是get和set两个属性,通过该方法被劫持的对象属性,只要在外界获取或者修改属性值都会触发get或set方法,这样我们就可以在get或set中对属性做一些额外对操作了

了解了数据劫持是如何实现的,那么为什么一定要做数据劫持呢?

如上文中提到的 我们可以通过数据劫持对数据做一些额外的操作从而实现响应式数据。

以下是一个简单的数据劫持

function observe(data)
{
if(({})).toString.call(data)!=='[ojbect Object]') return
}
//定义方法 传入data data必须是一个对象 
//获取得到data中的所有属性
const keys = Object.keys(data);
//循环遍历keys为data中每一个属性做数据劫持
keys.foreach(key=>{
definReactive$$1(data,key,data[key])
})
}


function definReactive$$1(obj,key,val)
{
Object.defineProperty(obj,key,{
get(){
return val;
},
set(newV)
{
if(newV !== val)
{
val = newV;
}
}
})
}

板编译 第一步数据劫持已经实现了,接下来就是模板编译。同样还是先提出一个问题:为什么要模板编译?我们知道在vue中是通过一些指令来实现模板编译的

但是浏览器并不认识这些指令,因此在页面加载后需要将这些语法转换成真正的数据呈现给用户。下面我们input元素和v-model指令为例来实现一个简单的模板编译

遍历#app下所有的节点,然后根据节点的类型做相应的操作

如果是元素节点,获取该节点中所有的属性(attributes)并遍历看是否有v-model指令

如果有v-model指令,则根据该指令绑定的属性名(data中的属性名)获取到对应到值,并赋值给节点的value属性

如果是文本节点,则看该文本内容中是否包含小胡子语法

遍历完每个节点后再将该节点作为子节点添加到html到文档碎片中

最后再将整个文档碎片添加到dom中

需要说明到是:在vue中实现是借助虚拟dom实现的,而这里为来简单就借助文档碎片来模拟虚拟dom实现,另外为什么一定要用文档碎片,不能直接遍历节点吗?

直接遍历也是可以的但是这样一来由于不停的修改节点势必会造成大量的性能消耗,

而通过文档碎片在所有节点遍历完成后只需要一次消耗,这样就大大降低了回流重汇带来的性能损耗

这个我自己也不是很了解 涉及到浏览器的有关知识 后续需要重点补充

双向绑定

vue通过数据劫持和观察者模式来实现数据的双向绑定的,也就是我们经常使用到的v-model 那么是如何实现双向数据绑定的呢?

数据劫持的时候我们提到,数据劫持的目的就是为了在获取数据或给数据赋值之前对数据做一些额外的操作,

那么这些额外的操作其实就是利用发布订阅模式对数据属性进行监控,比如说data中的name属性,

对name属性进行监控,

首先需要知道这个name属性都在哪里用到了,以便后面如果name值发生改变时及时通知用到name的地方同步更新,从而实现响应式

这个在vue中叫做依赖收集。怎么才能知道name属性都在哪里用到了呢,这个时候数据劫持就派上用场了,

前面说过只要外界对name进行访问都会触发Object.defineProperty中的get函数,那么我们就可以利用这个特点在get函数中对name属性进行监听收集

首先需要定义一个有关类,用于对属性进行依赖收集以及通知属性到特定地方更新

然后再定义有关Watcher类,用于对属性进行监听,并实现属性值的同步更新,

在模板编译的时候,通过watcher来监听属性

在数据劫持的时,get函数进行依赖收集

在数据劫持的set函数通知watcher进行数据更新

class Dep{
    constructor(){
        this.subs = [];//事件池 存储watcher实例对象
    }
    addSub(sub){
        //sub就是watcher的实例
        this.subs.push(sub);
    }
    notify(){
        this.subs.forEach(item=>{
            item.update();//调用watcher的update
        })
    }
}

class Watcher{
    constructor(node, key, vm){
        Dep.target = this;//用于标识只用通过Watcher监听过的属性才会进行依赖收集
        this.node = node;
        this.key = key;
        this.vm = vm;
        this.getValue();
        Dep.target = null;
    }
    update(){
        this.getValue();//首先获取下最新值
        if(this.node.nodeType === 1){
            this.node.value = this.value;
        }else if(this.node.nodeType === 3){
            this.node.textContent = this.value;
        }
    }
    getValue(){
        this.value = this.vm.$data[this.key];
    }    

 

posted @   jeffmmo  阅读(40)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· Vue3状态管理终极指南:Pinia保姆级教程
点击右上角即可分享
微信分享提示