class03:MVVM模型与响应式原理

news2024/10/6 4:05:48

目录

  • 一、MVVM模型
  • 二、内在
    • 1. 深入响应式原理
    • 2. Object.entries
    • 3. 底层搭建

一、MVVM模型

MVVM,即Model 、View、ViewModel。

Model => data数据

view => 视图(vue模板)

ViewModel => vm => vue 返回的实例 => 控制中心, 负责监听Model的数据,进行改变并且控制视图的更新

vue渲染流程
1. vue 拿到挂载中的所有节点
2. vue 取data中的数据,插入到带有vue指令,特殊的vue符号中
3. vue 将数据插入成功的元素放回到页面中

二、内在

1. 深入响应式原理

如何追踪变化: 当把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty把这些 property 全部转为 getter/setter。

例:监听对象的变化

<script>
// 方法:Object.defineProperty(监听对象,对象的属性名,{配置对象})
let data = {
    age:38,
}
// 定义一个变量
let _data = null;
Object.defineProperty(data,"age",{
    get(){
        console.log("get--data.age取值的时候触发");
        return _data
     },
     set(val){
        console.log("set--设置data.age的时候触发");
     	_data = val
    }
})
</script>

使用Object.defineProperty方法,函数内部把属性转化为get/set,get()函数在函数取值时触发,set()函数在设值时触发,通过在外部定义变量,在get()函数内部返回该变量,在set()内部将设的值赋值给该外部变量,从而实现监听。

对象之间的关联:原对象与代理对象

<script>
    // 关联:代理    
    let data = {
        age: 38,
    }
    // 定义一个变量
    let _data = {};
    Object.defineProperty(_data, "age", {
        get() {
            return data.age
        },
        set(val) {
            data.age = val
        }
    })
</script>

如上述代码,监听的对象是外部定义的对象,监听的属性名是另一个对象中的属性。在get()执行时,将data.age的值返回给监听对象_data,那么 _data中就会生成一个属性值age: 38,同理在set()执行时,也会将设置的值返回给监听对象 _data,从而修改 _data中的属性值。两个对象data与 _data之间是代理的关系。

vue2底层:数据代理 => 通过一个对象代理另一个对象的中的属性操作(读/写)

2. Object.entries

Object.entries将一级对象处理成键值对的形式。

let data = {
    name:"Evan You",
    age:"36",
    sex:"man"
}

然后通过循环遍历,使用Object.defineProperty方法,把属性转化为get/set,最后代理给_data。

// 原对象
let data = {
    name:"Evan You",
    age:36,
    sex:"man"
}
// 代理对象
let _data = {}
// 处理键值对
Object.entries(data).forEach(([key, val]) => {
    // 获取data对象的键值对,交给_data代理            
    createObj(_data, key, val)
})
// 代理
function createObj(_data, key, val){
    console.log(_data, key, val);
    Object.defineProperty(_data, key, {
        // 对data的每一个属性key,get获取值,set将值赋给属性,最后将属性及其对应值赋给监听对象_data
        get(){
            return val;
        },
        set(value){
            val = value;
        }
    })
}

修改_data的值,但data的值不会受影响。

3. 底层搭建

创建一个class类Test,返回一个constructor对象,实例化Test。在constructor输出arguments可获得节点和数据。

class Test{
    constructor(){
        console.log(arguments);
    }
}
let vm = new Test({
    el:"#root",
    data(){
        return {
            num:"32",
            name:"Jordan",
			country:"Ame",
			work:"basketball"
        }
    }
})    

操作

<div id="root">
    {{name}
    </br><input type="text" v-model="name"> </br>
    {{work}}
</div>

vue底层:

  1. vue 拿到挂载中的所有节点;
  2. vue 拿data中的数据, 插入到带有vue指令,特殊的vue符合中。Object.defineProperty监听data数据;
  3. vue 将数据插入成功的元素放回到页面中。
class Test {
    constructor({el, data}) { // 解构赋值
        // 获取节点
        this.el = document.querySelector(el);
        this._data = data;
        // 调用方法
        getDom(this.el, this._data)
    }
}
// 获取节点
function getDom(node, _data){
    console.log(node, _data);
}

可以通过firstChild和nextSibling获取每一个节点:

getDom函数

function getDom(node, _data){
    // console.log(node, _data);
    // 文本代码拼凑,创建一个新的空白的文档片段
    let frame = document.createDocumentFragment();
    
    let child;
    console.log(node.firstChild);
    // 空循环,将赋值给child
    while(child = node.firstChild){
        // 插入到frame
        frame.append(child);
        console.log(child);// child获得节点
    }
    return frame;
}

执行结果:页面上的节点被剪切。

循环过程:每执行一次append操作,root的第一个节点就会被剪切掉,当所有节点被剪切掉时为null循环结束。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ftsYk8AU-1678452135319)(C:\Users\Mamba\AppData\Roaming\Typora\typora-user-images\image-20230224222047687.png)]

接下来,我们在进入循环之后先调用另一个函数getDom2,判断节点的类型并把原对象的数据取出来赋值给这些节点。

function getDom(node, _data){
    let frame = document.createDocumentFragment();
    while(child = node.firstChild){
        getDom2(child, _data)
        frame.append(child);    
    }
    // 返回操作后的节点
    return frame;
}

function getDom2(node, _data){
    console.log(node, node.nodeType);
    //正则  匹配插值符号{{}}
    let reg = /\{\{(.*)\}\}/

    // 节点
    // if(noede.nodeType == 1){ // 元素节点,nodeType 属性返回 1 

    // }
    if(node.nodeType == 3){ //文本节点,nodeType 属性返回 3
        // 如果该文本节点匹配到{{}},返回true
        if(reg.test(node.nodeValue)){  // 取出节点值,匹配{{}}
            console.log(reg.test(node.nodeValue)); // 文本节点,返回true
            console.dir(RegExp);
            // $1为RegExp的属性,获取{{}}里面的值-> name,work
            let arg = RegExp.$1.trim();
            // 获取{{}}里面的值-> name,work
            console.log(arg);
            // 将原对象的name,work及其值赋给页面中的{{name}},{{work}}
            node.nodeValue = _data[arg];
            // 节点分别为:Jordan、basketball
            console.log(node);
            
            // 将数据(节点 data)存储下来 
			new watcher(_data, node, arg)
        }
    }
}

class Test {
    constructor({el, data}) {
        this.el = document.querySelector(el);
        this._data=data;
        // 获取节点
        this.dom = getDom(this.el, this._data)
        // 将返回的节点插入页面
        this.el.append(this.dom)
    }
}

输出说明:

**说明:**为了方便,html的div中将用于换行的两个删去。

对于元素节点,进行下述处理。

if(node.nodeType == 1){ // 元素节点,nodeType 属性返回 1 
    console.log(node, node.nodeType);
    // 获取元素节点上的属性节点
    console.log(node.attributes);
    [...node.attributes].forEach((item) => { // 遍历属性节点
        if(item.nodeName == "v-model"){
            console.log(item.nodeName);
            // 获取v-model的属性值-> name
            let arg = item.nodeValue;
            // 双向数据绑定,通过页面修改原对象
            node.addEventListener("input",(ev)=>{
            	_data[arg] = ev.target.value
            })
            console.log(arg);
            // 将原对象data中的name赋值(代理)到v-model的name
            node.value = _data[arg];
            console.log(node.value);
            console.log(node);
            
            // 将数据(节点 data)存储下来 
			new watcher(_data, node, arg)
        }
    })
}

从vue底层原理可知,在获取节点以及渲染之前,应该先进行数据监听。

数据监听第一步是启动订阅(Dep类),然后调用Object.defineProperty所有属性转为get/set,在get中调用Dep类的addSub方法存储数据,在set中调用Dep类的notify方法修改数据。此时还未获取节点,属于在后端修改数据。

在获取节点的分类节点并插入页面后,调用watcher类,该类先保存数据,并传送数据给Dep类,也进行数据的获取和修改。此时以获取节点,属于在页面修改数据。

以上两部分即是双向数据绑定

// 订阅发布:在数据变动时,发给订阅者,触发对应的函数
class Dep {
    constructor() {
        // 保留数据
        this.sub = []
    }
    addSub(val) {
        this.sub.push(val)
    }
    notify() {
        this.sub.map((item) => {
            item.update()
        })
    }
}

//观察者  =>  保存数据,以便后期进行修改
class watcher {
    constructor(_data, node, arg) {
        // 数据也给Dep一份,以便Dep订阅数据是否变化
        Dep.target = this
        // 保存数据
        this._data = _data;
        this.node = node;
        this.arg = arg;
        this.init()
    }
    init() {
        // 用于后期进行数据修改
        this.update()
        // 修改数据后,清空Dep留存的数据
        Dep.target = null
    }
    update() {
        // 用于获取数据
        this.get()
        // 修改数据
        this.node.value = this.node.nodeValue = this.value
    }
    get() {
        // 获取数据,数据代理
        this.value = this._data[this.arg]
    }
}

//处理数据监听
function setdefineProperty(data, _data) { // data===_data
    Object.entries(data).forEach(([key, val]) => {
        createObj(_data, key, val)
    });
}

//监听数据
function createObj(_data, key, val) {
    //启动 订阅发布
    let dep = new Dep()
    // 所有属性转为get/set
    Object.defineProperty(_data, key, {
        get() {
            // 如果Dep.target 有数据
            if (Dep.target) {  //没有数据不就要放进去 
                dep.addSub(Dep.target)
            }
            return val
        },
        set(value) {
            // 数据相同不作改变
            if (val == value) {
                return
            }
            // 更改数据
            val = value;
            // 设置的时候修改页面数据
            dep.notify()
        }
    })
}

双向数据绑定需在获取输入框节点的代码中加入:

// 双向数据绑定,通过页面修改原对象
node.addEventListener("input", (ev) => {
    _data[arg] = ev.target.value
})

最后,最好将多余的属性和样式删除。如删除v-model。

// 删除属性
node.removeAttribute("v-model")

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/404065.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

docker基本命令 - 数据卷

作用 ● 做数据持久化。防止容器一旦停止运行&#xff0c;该容器中运行产生的数据就没了 ● 不同容器之间的数据共享(大鲸鱼背上各个小集装箱之间可以共享数据) 交互式命令使用 docker run -it -v / 宿主机的绝对路径目录:/容器内绝对路径目录 镜像名 docker run -it -v / 宿…

SUBMIT的用法

SUBMIT的用法 一、简介 系统MB52/MB51/MB5B等类似的报表 &#xff0c;虽然数据很全面&#xff0c;执行效率也够快&#xff0c;但是经常会不满足用户需求&#xff08;增添字段、添加查询条件等&#xff09;&#xff0c;很多ABAP 会选择去COPY出标准程序&#xff0c;然后去做修改…

376. 摆动序列——【Leetcode每日刷题】

376. 摆动序列 如果连续数字之间的差严格地在正数和负数之间交替&#xff0c;则数字序列称为 摆动序列 。第一个差&#xff08;如果存在的话&#xff09;可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。 例如&#xff0c; [1, 7, 4, 9, 2, 5] 是一个…

WMS仓库管理系统解决方案,实现仓库管理一体化

仓库是企业的核心环节&#xff0c;若没有对库存的合理控制和送货&#xff0c;将会造成成本的上升&#xff0c;服务品质的难以得到保证&#xff0c;进而降低企业的竞争能力。WMS仓库管理系统包括基本信息&#xff0c;标签&#xff0c;入库&#xff0c;上架&#xff0c;领料&…

搭建Docker企业私有仓库

什么是仓库 仓库&#xff08;Repository&#xff09;是存储和分发 Docker 镜像的地方。镜像仓库类似于代码仓库&#xff0c;Docker Hub 的命名来自 GitHub&#xff0c;Github 是我们常用的代码存储和分发的地方。同样 Docker Hub 是用来提供 Docker 镜像存储和分发的地方。 谈…

flink入门-流处理

入门需要掌握&#xff1a;从入门demo理解、flink 系统架构&#xff08;看几个关键组件&#xff09;、安装、使用flink的命令跑jar包flink的webUI 界面的监控、常见错误、调优 一、入门demo&#xff1a;统计单词个数 0、单词txt 文本内容(words.txt)&#xff1a; hello world …

最详细的CentOS7安装Mysql数据库服务

1.查看是否安装mysql: rpm -qa | grep mysql如果有查出来东西&#xff0c;使用命令删除&#xff1a; rpm -e xxx2.检查是否有mysql用户组和mysql用户,没有就添加有就忽略&#xff1a; groups mysql 添加用户组和用户 groupadd mysql && useradd -r -g mysql mysql&a…

webstorm项目上传至gitee及遇到的问题

1.下载gitee及登录账户 File>>Settings Plugins>>搜索gitee 2.下载svn&#xff08;若已经下载过了可跳过此步&#xff09; 下载 SVN下载官网 将下载的文件解压在同一文件下 例如&#xff1a;我的两个解压文件文件放在Apache-Subversion-1.14.2这一文件下 找到Fi…

射频资料搜集--推荐几个网站和链接

https://picture.iczhiku.com/resource/eetop/wHKYFQlDTRRShCcc.pdfhttps://picture.iczhiku.com/resource/eetop/wHKYFQlDTRRShCcc.pdfVCO pulling的资料 模拟滤波器与电路设计手册 - 射频微波仿真 - RF技术社区 Practical RF Amplifier Design Using the Available Gain Pr…

进程系统调用

进程系统调用 文章目录进程系统调用fork()进程创建&#xff1a;fock()fork函数fork用法僵尸进程孤儿进程vfork函数vfork与fork区别exec函数族exec函数族-何时使用&#xff1f;exec函数族语法exec函数族使用区别exit和_exit_exit和exit的区别wait和waitpidfork() 进程创建&…

记录一次WIN11开机在登录页面循环的问题

记录一次由于未进行win密码设置&#xff0c;导致开机后卡在登录界面无法登录进去的问题。最后完美解决了。 1. 背景 开机后&#xff0c;显示用户登录界面&#xff0c;但是和以往不同&#xff0c;没有了密码输入框&#xff0c;只有一个“登录”按钮孤零零地显示在屏幕中间&…

Flink从入门到精通系列(四)

5、DataStream API&#xff08;基础篇&#xff09; Flink 有非常灵活的分层 API 设计&#xff0c;其中的核心层就是 DataStream/DataSet API。由于新版本已经实现了流批一体&#xff0c;DataSet API 将被弃用&#xff0c;官方推荐统一使用 DataStream API 处理流数据和批数据。…

每天5分钟快速玩转机器学习:贝叶斯算法的局限性

本文重点 贝叶斯算法的应用很广泛,其中最经典的应用就是垃圾邮件的分类,本节课程通过垃圾邮件的例子来看一下贝叶斯算法存在的一些问题,我们应该如何解决它? 垃圾邮件分类 给定一封电子邮件,我们如何判断这封电子邮件是垃圾邮件还是正常邮件,这是机器学习中的二分类问…

corn表达式

简单理解corn表达式&#xff1a;在使用定时调度任务的时候&#xff0c;我们最常用的&#xff0c;就是cron表达式了。通过cron表达式来指定任务在某个时间点或者周期性的执行。cron表达式配置起来简洁方便&#xff0c;无论是Spring的Scheduled还是用Quartz框架&#xff0c;都支持…

JavaWeb14-线程池

目录 1.传统线程的缺点 2.线程池的定义 3.线程池的优点 4.线程池的创建/使用&#xff08;2类7种&#xff09; 4.1.通过Executors&#xff08;执行器&#xff09;自动创建&#xff08;6种&#xff09; ①Executors.newFixedThreadPool&#xff1a;创建⼀个固定⼤⼩的线程池…

哈希冲突

为什么会有哈希冲突&#xff1f;哈希表通过哈希函数来计算存放数据&#xff0c;在curd数据时不用多次比较&#xff0c;时间复杂度O&#xff08;1&#xff09;。但是凡事都有利弊&#xff0c;不同关键字通过相同哈希函数可能计算出来相同的存放地址&#xff0c;这种现象被称为哈…

JVM的内存回收及常见算法

什么样的对象应该被回收&#xff1f;某个对象不再被栈直接或间接地引用&#xff0c;此时就应该被回收了。o被指向null的时候&#xff0c;new Object()创建的对象就不在被栈引用了&#xff0c;可以被回收。p1和personList均不再指向第一个Person对象的时候&#xff0c;第一个Per…

【小墩墩学Android】开发常见问题FAQ之Gradle更新

文章目录1、简介1.1 Android简介1.2 Gradle简介1.3 Gradle的配置文件1.3.1 应用模块的 build.gradle1.3.2 项目的 settings.gradle1.3.3 gradle-wrapper.properties2、Gradle文件下载失败2.1 手动下载gradle2.2 配置本地gradle2.3 配置国内镜像3、repositories配置国内源3.1 单…

蓝桥杯三月刷题 第八天

文章目录&#x1f4a5;前言&#x1f609;解题报告&#x1f4a5;分数&#x1f914;一、思路:&#x1f60e;二、代码&#xff1a;&#x1f4a5;回文日期&#x1f914;一、思路:&#x1f60e;二、代码&#xff1a;&#x1f4a5;迷宫&#x1f914;一、思路:&#x1f60e;二、代码&a…

LVGL学习笔记18 - 表Table

目录 1. Parts 1.1 LV_PART_MAIN 1.2 LV_PART_ITEMS 2. 样式 2.1 设置行列数 2.2 设置单元格字符串 2.3 设置单元格宽度 2.4 设置表格高度和宽度 2.5 设置字符串颜色 2.6 设置边框颜色 2.7 设置背景颜色 3. 事件 4. CELL CTRL 表格是由包含文本的行、列和单元格构…