【Vue2源码】响应式原理

news2024/11/15 10:29:34

【Vue2源码】响应式原理

文章目录

  • 【Vue2源码】响应式原理
    • `Vue响应式`的核心设计思路
    • 整体流程
    • 响应式中的关键角色
    • 检测变化注意事项
    • 响应式原理
        • 数据观测
        • 重写数组7个变异方法
        • 增加__ob__属性
          • __ob__有两大用处:

Vue.js 基本上遵循 MVVM(Model–View–ViewModel)架构模式,数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。 本文讲解一下 Vue 响应式系统的底层细节。

Vue响应式的核心设计思路

当创建Vue实例时,vue会遍历data选项的属性,利用Object.defineProperty为属性添加gettersetter对数据的读取进行劫持(getter用来依赖收集,setter用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。

每个组件实例会有相应的watcher实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有computed watcher,user watcher实例),之后依赖项被改动时,setter方法会通知依赖与此datawatcher实例重新计算(派发更新),从而使它关联的组件重新渲染。

整体流程

作为一个前端的MVVM框架,Vue的基本思路和AngularReact并无二致,其核心就在于: 当数据变化时,自动去刷新页面DOM,这使得我们能从繁琐的DOM操作中解放出来,从而专心地去处理业务逻辑。

这就是Vue的数据双向绑定(又称响应式原理)。数据双向绑定是Vue最独特的特性之一。此处我们用官方的一张流程图来简要地说明一下Vue响应式系统的整个流程:

在这里插入图片描述

Vue中,每个组件实例都有相应的watcher实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。

这是一个典型的观察者模式。

响应式中的关键角色

在 Vue 数据双向绑定的实现逻辑里,有这样三个关键角色:

  • Observer: 它的作用是给对象的属性添加gettersetter,用于依赖收集和派发更新
  • Dep: 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个Dep实例(里面subsWatcher实例数组),当数据有变更时,会通过dep.notify()通知各个watcher
  • Watcher: 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种

检测变化注意事项

Vue 2.0中,是基于·Object.defineProperty 实现的响应式系统 (这个方法是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因) vue3 中,是基于Proxy/Reflect来实现的

1、由于 JavaScript 的限制,这个 Object.defineProperty() api 没办法监听数组长度的变化,也不能检测数组和对象的新增变化。

2、Vue 无法检测通过数组索引直接改变数组项的操作,这不是 Object.defineProperty() api 的原因,而是尤大认为性能消耗与带来的用户体验不成正比。对数组进行响应式检测会带来很大的性能消耗,因为数组项可能会大,比如1000条、10000条。

响应式原理

响应式基本原理就是,在 Vue 的构造函数中,对 options 的 data 进行处理。即在初始化vue实例的时候,对data、props等对象的每一个属性都通过 Object.defineProperty 定义一次,在数据被set的时候,做一些操作,改变相应的视图。

数据观测

基于 Object.defineProperty 来实现一下对数组和对象的劫持。

\src\observe\index.js

import { newArrayProto } from "./array"

class Observe {
    constructor (data) {
        //Object.defineReactive只能劫持已经存在的属性(vue俩民回为此单独写一些api)
        //data.__ob__ = this  //这里的this指的是Observe,把这个实例附到了ob上,还可以用于检测是否被劫持过
        Object.defineProperty(data,'__ob__',{
            value:this,
            enumerable:false //将__ob__编程不可枚举,这样循环的时候就无法获取到,不会进入死循环
        })
        if(Array.isArray(data)) {
            data.__proto__ = newArrayProto
            this.observeArray(data)  //如果数组中放置的是对象,也可以被监控到
            //这里我们可以重写数组中的方法,7个变异方法,是可以修改到数组本身的
        }else {
            this.walk(data)
        }
        
    }
    walk (data) { //循环对象,对属性依次劫持
        //“重新定义”属性  性能差
        Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
    }
    observeArray(data) {
        data.forEach(item=> observe(item))
    }
}

export function defineReactive (target, key, value) {  //闭包
    observe(value) //对所有的对象都进行属性劫持  深度劫持
    Object.defineProperty(target, key, {
        get () { //取值的时候会执行get
            console.log(key,"key");
            return value
        },
        set (newValue) {  //修改的时候会执行set
            if (newValue === value) return
            observe(newValue)
            value = newValue
        }
    })
}


export function observe (data) {
    if (typeof data !== 'object' || data == null) {
        //只对对象进行劫持
        return
    }
    if(data.__ob__ instanceof Observe) { //如果存在data.__ob__就说明这个被代理过了
        return data.__ob__
    }
    //如果一个对象被劫持过了,那就不需要再被劫持了
    //要判断一个对象是否被劫持过,可以增添一个实例,用实例来判断是否被劫持过
    return new Observe(data)

}

重写数组7个变异方法

7个方法是指:push、pop、shift、unshift、sort、reverse、splice。(这七个都是会改变原数组的) 实现思路:面向切片编程!!!

不是直接粗暴重写 Array.prototype 上的方法,而是通过原型链继承与函数劫持进行的移花接木。

利用 Object.create(Array.prototype) 生成一个新的对象 newArrayProto,该对象的 proto 指向 Array.prototype,然后将我们数组的 proto 指向拥有重写方法的新对象 newArrayProto,这样就保证了 newArrayProto 和 Array.prototype 都在数组的原型链上。

arr.proto === newArrayProto;newArrayProto.proto === Array.prototype

然后在重写方法的内部使用 Array.prototype.push.call 调用原来的方法,并对新增数据进行劫持观测。
\src\observe\array.js


let oldArrayProto = Array.prototype  //获取数组的原型

export let newArrayProto = Object.create(oldArrayProto)

let methods = [ //通过遍历寻找到所有变异方法
    'push',
    'pop',
    'shift',
    'reverse',
    'sout',
    'splice'
] //concat slice不会改变原数组

methods.forEach(method => {
    newArrayProto[method] = function (...args) { //这里重写了数组的方法
        const result = oldArrayProto[method].call(this,...args)  //再内部调用原来的方法,函数的劫持,切片编程
        //我们需要对新增的数据进行劫持
        let inserted
        let ob = this.__ob__
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args
                break;
            case 'splice' : //arr.splice(0,1,{a:1},{b:2})
                inserted = args
                break

        }
        console.log("xinzeng ");
        if(inserted) {
            //对新增的内容再次进行观测
            ob.observeArray(inserted)

        }
        
        
        return result
    }
})

增加__ob__属性

这是一个恶心又巧妙的属性,我们在 Observer 类内部,把 this 实例添加到了响应式数据上。相当于给所有响应式数据增加了一个标识,并且可以在响应式数据上获取 Observer 实例上的方法

class Observe {
    constructor (data) {
        //Object.defineReactive只能劫持已经存在的属性(vue俩民回为此单独写一些api)
        //data.__ob__ = this  //这里的this指的是Observe,把这个实例附到了ob上,还可以用于检测是否被劫持过
        Object.defineProperty(data,'__ob__',{
            value:this,
            enumerable:false //将__ob__编程不可枚举,这样循环的时候就无法获取到,不会进入死循环
            
        })

        if(Array.isArray(data)) {
            data.__proto__ = newArrayProto

            this.observeArray(data)  //如果数组中放置的是对象,也可以被监控到
            //这里我们可以重写数组中的方法,7个变异方法,是可以修改到数组本身的
        }else {
            this.walk(data)
        }
        
    }
    walk (data) { //循环对象,对属性依次劫持
        //“重新定义”属性  性能差
        Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
    }
    observeArray(data) {
        data.forEach(item=> observe(item))
    }
}
__ob__有两大用处:

1、如果一个对象被劫持过了,那就不需要再被劫持了,要判断一个对象是否被劫持过,可以通过 ob 来判断


export function observe (data) {
    if (typeof data !== 'object' || data == null) {
        //只对对象进行劫持
        return
    }
    if (data.__ob__ instanceof Observe) { //如果存在data.__ob__就说明这个被代理过了
        return data.__ob__
    }
    //如果一个对象被劫持过了,那就不需要再被劫持了
    //要判断一个对象是否被劫持过,可以增添一个实例,用实例来判断是否被劫持过
    return new Observe(data)

}

2、我们重写了数组的7个变异方法,其中 push、unshift、splice 这三个方法会给数组新增成员。此时需要对新增的成员再次进行观测,可以通过 ob 调用 Observer 实例上的 observeArray 方法

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

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

相关文章

[oeasy]python0135_变量名与下划线_dunder_声明与赋值

变量定义 回忆上次内容 变量 就是 能变的量上次研究了 变量标识符的 规则 第一个字符 应该是 字母或下划线合法的标识符可以包括 大小写字母数字下划线 还研究了字符串(str)的函数 isidentifier查询字符串 是否为合法标识符 最后发现 这个isidentifier函数有时候不好使&…

实战iOS App 重签名

熟悉iOS开发的同学都知道,iOS应用的上架流程主要分为以下几步: 创建开发者账号借助辅助工具appuploader创建证书,描述文件iTunes connect创建App打包IPA上传App Store等待审核在签名的流程中,有一个App重签名的步骤,主要针对的是一些大公司有多个App的情况,多个App一个申…

Linux设备驱动开发 - S3C2440时钟分析

By: fulinux E-mail: fulinuxsina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 目录 系统框架图FLCK,HCLK,PCLK时钟源的选择 时钟体系流程流程分析驱动中的clk 系统框架图 2440是一个SOC(system on …

【网络编程】网络编程 和 Socket 套接字认识

✨个人主页:bit me👇 ✨当前专栏:Java EE初阶👇 目 录 🎧一. 网络编程基础🎺1. 为什么需要网络编程?🎷2. 什么是网络编程🎸3. 网络编程中的基础概念 🎿二. So…

继续学习Easyx

画圆角矩形函数:roundrect,前四个参数是和矩形一样的,表示先画一个矩形,然后就是要画它的圆角了,要怎么画它的圆角?这里使用四个一样的椭圆来将它的四个角画出来,而在这个里面的椭圆不再需要四个…

MySQL8.0.33主从复制配置记录

目录 1. 下载2. 解压3.重命名4.创建存储数据文件5. 设置用户组并赋权6. 初始化MySQL7.配置文件8. 启动MySQL9. 设置软连接,并重启MySQL10. 登录并更改密码11. 开放远程连接12. 连接工具测试连接MySQL13. 开机自启配置14.从服务器配置15. 主库配置16. 从库配置17. 测…

功率MOS管烧毁,有这些原因

功率MOS管烧毁的原因以及相应的预防措施。在本文中,我将会介绍功率MOS管的基本结构、工作原理,以及可能导致功率MOS管烧毁的原因,并提供相应的解决方案。 1. 功率MOS管的基本结构和工作原理 功率MOS管是一种常用的功率电子器件,…

SpringBoot集成MyBatis-plus

SpringBoot集成MyBatis-plus 一、Mybatis-Plus简介1.简介2.特性3.官网及文档地址 二、入门案例1.开发环境2.创建数据库及表3.创建Springboot项目导入依赖4.配置application.yml5.启动类6.实体类7.添加mapper8.添加UserController9.日志配置 三、CURD1.BaseMapper2.通用Service …

事件主循环

一、事件主循环 1、事件处理主流程 libevent的事件循环主要是通过event_base_loop函数来完成,其主要的操作如下: 1、根据timer-heap中的事件最小超时时间,计算系统I/O demultiplexer的最大等待时间。例如:当底层使用的是Linux提供…

Android系统启动流程--system_server进程的启动流程

紧接上一篇zygote进程的启动流程,上一篇的结尾提到zygote进程中会fock出一个system_server进程,用于启动和管理Android系统中大部分的系统服务,本篇就来分析system_server进程是如何创建并运行的以及它都做了哪些重要的工作。 //文件路径&am…

Show, Attend, and Tell | a PyTorch Tutorial to Image Captioning代码调试(跑通)

Show, Attend, and Tell | a PyTorch Tutorial to Image Captioning代码调试(跑通) 文章目录 Show, Attend, and Tell | a PyTorch Tutorial to Image Captioning代码调试(跑通)前言1. 创建、安装所用的包1.1 创建环境&#xff0c…

【深度学习】OCR文本识别

OCR文字识别定义 OCR(optical character recognition)文字识别是指电子设备(例如扫描仪或数码相机)检查纸上打印的字符,然后用字符识别方法将形状翻译成计算机文字的过程;即,对文本资料进行扫描…

【数据结构】二叉树经典oj题

🚀write in front🚀 📜所属专栏:初阶数据结构 🛰️博客主页:睿睿的博客主页 🛰️代码仓库:🎉VS2022_C语言仓库 🎡您的点赞、关注、收藏、评论,是对…

B. Make Them Equal(Codeforces Round 673 (Div. 1))

传送门 题意: 思路: 首先判断是否能够操作达到目的:即所有的数都相等。 不能达到有两种情况: 1:所有数之和对n取余不等于0 2: 每个ai都是小于i的,例如n5, a[]{0,1,2,3,4}。因为每个数都是小于 i 的&am…

idea中的 debug 中小功能按钮都代表的意思

1.step over 步过----->一行一行的往下走,如果这一行中有方法那么不会进入该方法,直接一行一行往下走,除非你在该方法中打入断点 2.step into 步入—>可以进入方法内部,但是只能进入自己写的方法内部,而不会进入方法的类库中 3.Force step into 强制步入---->可以步…

编译livox ros driver2(ROS2、livox、rviz、ubuntu22.04)

1. 编译Livox-SDK2 官方地址:https://github.com/Livox-SDK/Livox-SDK2 执行一下命令: git clone https://github.com/Livox-SDK/Livox-SDK2.git cd ./Livox-SDK2/ mkdir build cd build cmake .. && make sudo make install 如上就安装完成了…

嵌入式【CPLD】5M570ZT100C5N、5M1270ZF256C5N、5M2210ZF256C5N采用独特的非易失性架构,低成本应用设计。

英特尔MAX V CPLD 采用独特的非易失性架构,提供低功耗片上功能,适用于以边缘为中心的应用。MAX V CPLD系列能够在单位空间中提供大量 I/O 和逻辑。这些设备还使用了低成本绿色封装技术,封装大小只有 20 毫米。 MAX V系列的关键应用包括&…

PCL点云库(1) - 简介与数据类型

目录 1.1 简介 1.2 PCL安装 1.2.1 安装方法 1.2.2 测试程序 1.3 PCL数据类型 1.4 PCL中自定义point类型 1.4.1 增加自定义point的步骤 1.4.2 完整代码 1.1 简介 来源:PCL(点云库)_百度百科 PCL(Point Cloud Library&…

个推打造消息推送专项运营提升方案,数据驱动APP触达效果升级

“数智化运营”能力已成为企业的核心竞争力之一。借助数据和算法,构建完善的数智化运营体系,企业可增强用户洞察和科学决策能力,提高日常运营效率和投入产出比。近半年,个推精准把握行业客户的切实需求,将“数智化运营…

分析型数据库:MPP 数据库的概念、技术架构与未来发展方向

随着企业数据量的增多,为了配合企业的业务分析、商业智能等应用场景,从而驱动数据化的商业决策,分析型数据库诞生了。由于数据分析一般涉及的数据量大,计算复杂,分析型数据库一般都是采用大规模并行计算或者分布式计算…