​Vue2【双向数据绑定/响应式原理】

news2024/12/24 11:46:34

目录

初始化

initProps():父组件传的 props 列表,proxy() 把属性代理到当前实例上

vm._props.xx 变成 vm.xx

initData():判断data和props、methods是否重名,proxy() 把属性代理到当前实例上

this.xx

observe():给数据加上监听器(除了vnode/非引用类型(object/array))

Observe:标记响应式、分类(defineReactive()递归监听、observe()监听)

defineReactive():定义响应式对象

依赖收集

1.挂载前 生成一个组件渲染watcher

2. Dep.target 赋值为当前渲染 watcher 并压入栈targetStack(为了恢复用)

3.vm._render() 生成并渲染 vnode

4.访问数据,触发getter,dep收集watcher

5.更新数据,触发setter,遍历通知所有watcher更新

Dep类:管理Watcher

subs: Array

static target: ?Watcher:全局的 Watcher,同一时间只能存在一个全局的 Watcher

Watcher类:依赖/观察者/订阅者

watcher.run() :执行回调,传旧值新值

新旧Dep实例数组

派发更新

queueWatcher()优化:watcher队列,nextTick后执行

依赖的渲染结果:父watcher在子前,user watcher在渲染watcher前

defineProperty 缺陷处理

属性的增删无法触发 setter :Vue.set() 增属性

不能检测到数组元素的变化:重写数组方法,把原本的 push 保存起来,再做响应式处理

初始化

在 new Vue 初始化的时候,会对组件的数据 props 和 data 进行初始化

export function initMixin (Vue: Class<Component>) {
  // 在原型上添加 _init 方法
  Vue.prototype._init = function (options?: Object) {
    ...
    vm._self = vm
    initLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等
    initEvents(vm) // 初始化事件:$on, $off, $emit, $once
    initRender(vm) // 初始化渲染: render, mixin(混入,data,methods...)

    callHook(vm, 'beforeCreate') // 调用生命周期钩子函数

    initInjections(vm) // 初始化 inject(子孙传值)
    initState(vm) // 初始化组件数据:props, data, methods, watch, computed
    initProvide(vm) // 初始化 provide

    callHook(vm, 'created') // 调用生命周期钩子函数
    ...
  }
}

命名前缀

$:公共属性

_:私有属性

响应式数据相关: initProps()initData()observe()

initProps():父组件传的 props 列表,proxy() 把属性代理到当前实例

vm._props.xx 变成 vm.xx

  • 遍历父组件传进来的 props 列表
  • 校验每个属性的命名、类型、default 属性等,都没有问题就调用 defineReactive 设置成响应式
  • 然后用 proxy() 把属性代理到当前实例上,如把 vm._props.xx 变成 vm.xx,就可以访问
// 把不在默认 vm 上的属性,代理到实例上
// 可以让 vm._props.xx 通过 vm.xx 访问
if (!(key in vm)) {
proxy(vm, _props, key)
}
//vue自定义的proxy 函数,将 key 代理到组件实例上。这意味着你可以直接通过 vm.key 访问这个属性,而不必使用 vm._props.key

区别于js中的new Proxy(target, handler)

initData():判断data和props、methods是否重名,proxy() 把属性代理到当前实例

this.xx

  • 初始化一个 data,并拿到 keys 集合
  • 遍历 keys 集合,来判断有没有和 props 里的属性名或者 methods 里的方法名重名的
  • 没有问题就通过 proxy() 把 data 里的每一个属性都代理到当前实例上,就可以通过 this.xx 访问了
  • 最后再调用 observe 监听整个 data
if (!isReserved(key)) {
      // 都不重名的情况下,代理到 vm 上
      // 可以让 vm._data.xx 通过 vm.xx 访问
      proxy(vm, `_data`, key)

observe():给数据加上监听器(除了vnode/非引用类型(object/array))

  • 如果是 vnode 的对象类型或者不是引用类型,就直接跳出
  • 否则就给没有添加 Observer 的数据添加一个 Observer,也就是监听者

Virtual DOM节点(vnode)vnode对象通常用于表示虚拟DOM树的节点,而不是真实的数据对象。这些节点描述了组件的结构,而不是数据的值。

Vue的响应式系统是建立在对象的引用类型(如Object、Array)

基本数据类型(如Number、String、Boolean)或null等,它们是不可变的,无法被Vue追踪到变化

Observe:标记响应式、分类(defineReactive()递归监听、observe()监听)

  • 给当前 value 打上已经是响应式属性的标记,避免重复操作
  • 然后判断数据类型
    • 如果是对象,就遍历对象,调用 defineReactive()创建响应式对象
    • 如果是数组,就遍历数组,调用 observe()对每一个元素进行监听

用 this.msg = 'xxx' 能触发 setter 派发更新,但是我们修改数组并不是用 this.arr = xxx ,而是用 this.arr.push(xxx) 等修改数组的方法

defineReactive():定义响应式对象

  • 先初始化一个 dep 实例
  • 如果是对象就调用 observe,递归监听,以保证不管结构嵌套多深,都能变成响应式对象
  • 然后调用 Object.defineProperty() 劫持对象属性的 getter 和 getter
  • 如果获取时,触发 getter 会调用 dep.depend()观察者 push 到依赖的数组 subs 里去,也就是依赖收集
  • 如果更新时,触发 setter 会做以下操作
    • 新值没有变化或者没有 setter 属性的直接跳出
    • 如果新值是对象就调用 observe() 递归监听
    • 通过对应的所有依赖(Watcher),然后调用 dep.notify() 派发更新

依赖收集

1.挂载前 生成一个组件渲染watcher

2. Dep.target 赋值为当前渲染 watcher 并压入栈targetStack(为了恢复用)

3.vm._render() 生成并渲染 vnode

4.访问数据,触发getter,dep收集watcher

5.更新数据,触发setter,遍历通知所有watcher更新

 每个响应式数据都有一个Dep来管理它的一个/多个依赖

Dep类:管理Watcher

subs: Array<Watcher>

static target: ?Watcher:全局的 Watcher,同一时间只能存在一个全局的 Watcher

dep.target 的作用是建立依赖关系和追踪数据的Watcher

因为更新异步的特性,如果同时有多个全局 Watcher 在同一时间被触发,可能导致不可预测的结果,甚至可能引发性能问题。

通过在全局只维护一个 dep.target,Vue 确保在任何时刻只有一个 Watcher 在执行更新操作,避免了潜在的竞争条件和性能问题。

  1. Watcher 对象被创建:当你创建一个 Watcher 对象,它会将自身设置为当前的 dep.target。这是因为该 Watcher 正在计算或依赖于响应式数据,因此需要建立依赖关系。

  2. 在计算属性的求值过程中:如果你有一个计算属性(computed),当该计算属性的值被求值时,Vue 会将当前的 dep.target 设置为该计算属性的 Watcher,以建立依赖关系。

  3. 在渲染过程中:当组件渲染时,Vue 会创建一个渲染组件的 Watcher,该 Watcher 负责渲染组件的模板。在渲染过程中,当前的 dep.target 会被设置为渲染 Watcher,以确保建立正确的依赖关系。

let uid = 0
export default class Dep {
  static target: ?Watcher;//可选属性可以不存在或者是 null 或 undefined
  subs: Array<Watcher>;
  id: number;
  constructor () {
    this.id = uid++//确保每个 Dep 实例具有唯一的标识符
    this.subs = []
  }
  ...
  depend () {
    if (Dep.target) {
      // 调用 Watcher 的 addDep 函数
      Dep.target.addDep(this)
    }
  }
  // 派发更新
  notify () {
    ...
  }
}
// 同一时间只有一个观察者使用,赋值观察者
Dep.target = null
const targetStack = []//管理当前活动的观察者的栈

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

Watcher类:依赖/观察者/订阅者

watcher.run() :执行回调,传旧值新值

新旧Dep实例数组

let uid = 0
export default class Watcher {
  ...
  constructor (
    vm: Component,
    ...
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // Watcher 实例持有的 Dep 实例的数组
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    ...
  }
  get () 
    // 该函数用于缓存 Watcher
    // 因为在组件含有嵌套组件的情况下,需要恢复父组件的 Watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 调用回调函数,也就是upcateComponent,对需要双向绑定的对象求值,从而触发依赖收集
      value = this.getter.call(vm, vm)
    } catch (e) {
      ...
    } finally {
      // 深度监听
      if (this.deep) {
        traverse(value)
      }
      // 恢复Watcher
      popTarget()
      // 清理不需要了的依赖
      this.cleanupDeps()
    }
    return value
  }
  // 依赖收集时调用
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        // 把当前 Watcher push 进数组
        dep.addSub(this)
      }
    }
  }
  // 清理不需要的依赖(下面有)
  cleanupDeps () {
    ...
  }
  // 派发更新时调用(下面有)
  update () {
    ...
  }
  // 执行 watcher 的回调
  run () {
    ...
  }
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

派发更新

queueWatcher()优化:watcher队列,nextTick后执行

优化:在每次数据改变的时候不会都触发 watcher 回调,而是把这些 watcher 都添加到一个队列里,然后在 nextTick 后才执行(下次 DOM 更新循环结束之后,执行延迟回调,就可以拿到更新后的 DOM 相关信息)

依赖的渲染结果:父watcher在子前,user watcher在渲染watcher前

defineProperty 缺陷处理

属性的增删无法触发 setter :Vue.set() 增属性

不能检测到数组元素的变化:重写数组方法,把原本的 push 保存起来,再做响应式处理

深入浅出 Vue 响应式原理源码剖析 - 掘金

纯干货!图解Vue响应式原理 - 掘金

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

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

相关文章

c++实现dijskstra算法

图 我一开始写成了最小生成树的代码&#xff0c;最小生成树一直在选最小的那条边&#xff0c;对每个节点来说&#xff0c;它到原点的距离不一定是最近的。 代码 #include<iostream> using namespace std; #include<list> #include<vector>class Node { pub…

RocketMQ事务消息 超时重发还是原来的消息吗?

以下面的一个demo例子来分析一下&#xff0c;探索RocketMQ事务消息原理。 public static final String PRODUCER_GROUP "tran-test";public static final String DEFAULT_NAMESRVADDR "127.0.0.1:9876";public static final String TOPIC "Test&qu…

如何理解Quadratic Weighted Kappa?

Motivation 假定我们现在有 N N N个作文样例&#xff0c;以及它们对应的人类评分和GPT评分。评分一共有 C C C个互斥类别&#xff0c;分别是{0,1,2,3}。现在我们要衡量人类评分和GPT评分的一致性。 一个很直观的想法是&#xff0c;画出混淆矩阵&#xff0c;然后将对角线上的值…

Linux Centos7安装后,无法查询到IP地址,无ens0,只有lo和ens33的解决方案

文章目录 前言1 查看network-scripts目录2 创建并配置 ifcfg-ens33 文件3 禁用NetworkManager4 重新启动网络服务总结 前言 在VMware中&#xff0c;安装Linux centos7操作系统后&#xff0c;想查询本机的IP地址&#xff0c;执行ifconfig命令 ifconfig结果如下&#xff1a; 结…

吴恩达《机器学习》1-5:模型描述

一、单变量线性回归 单变量线性回归是监督学习中的一种算法&#xff0c;通常用于解决回归问题。在单变量线性回归中&#xff0c;我们有一个训练数据集&#xff0c;其中包括一组输入特征&#xff08;通常表示为&#x1d465;&#xff09;和相应的输出目标&#xff08;通常表示为…

UVa140 Bandwidth(带宽)

1、题目 2、题意 给出一个 n &#xff08; n ≤ 8 &#xff09; n&#xff08;n≤8&#xff09; n&#xff08;n≤8&#xff09;个结点的图G和一个结点的排列&#xff0c;定义结点 i i i 的带宽 b ( i ) b(i) b(i) 为 i i i 和相邻结点在排列中的最远距离&#xff0c;而所…

Ansible上通过roles简化playbook演示介绍

目录 一.roles介绍 1.作用 2.role的目录结构 3.role和tasks的执行优先级顺序 二.自定义一个httpd的角色 1.完整目录结构展示 2.主要的各个目录配置 &#xff08;1&#xff09;vars目录和templates目录 &#xff08;2&#xff09;tasks目录和handlers目录 &#xff08…

操作系统中套接字和设备独立性软件的关系

网络编程就是编写程序让两台联网的计算机相互交换数据。在我们不需要考虑物理连接的情况下&#xff0c;我们只需要考虑如何编写传输软件。操作系统提供了名为“套接字”&#xff0c;套接字是网络传输传输用的软件设备。 这是对软件设备的解释&#xff1a; 在操作系统中&#…

Unity ScrollView最底展示

Unity ScrollView最底展示 问题方案逻辑 问题 比如在做聊天界面的时候我们肯定会使用到ScrollView来进行展示我们的聊天内容&#xff0c;那么这个时候来新消息的时候就需要最底展示&#xff0c;我认为这里有两种方案&#xff1b; 一种是通过算法每一条预制体的高度*一共多少…

轮转数组(Java)

大家好我是苏麟 , 这篇文章是凑数的 ... 轮转数组 描述 : 给定一个整数数组 nums&#xff0c;将数组中的元素向右轮转 k 个位置&#xff0c;其中 k 是非负数。 题目 : 牛客 NC110 旋转数组: 这里牛客给出了数组长度我们直接用就可以了 . LeetCode 189.轮转数组 : 189. 轮…

Python---break关键字对for...else结构的影响

for循环中添加else结构 循环可以和else配合使用&#xff0c; else下方缩进的代码指的是当循环正常结束之后要执行的代码。 强调&#xff1a; 循环 正常结束&#xff0c;else之后要执行的代码。 非正常结束&#xff0c;其else中的代码是不会执行的。&#xff08;如遇到br…

类和对象(1):类,对象,this指针

面向过程和面向对象初步认识&#xff1a; C语言是面向过程的&#xff0c;关注的是过程&#xff0c;分析出问题求解的步骤&#xff0c;用函数调用逐步解决。C是基于面向对象的&#xff0c;关注的是对象&#xff0c;将一件事情拆分成不同的对象&#xff0c;靠对象之间的交互完成。…

【.NET Core】创建一个在后台运行的控制台程序(ConsoleApp)

文章目录 1. 添加Nuget包2. 修改Program.cs3. 添加TestService 借助.NET的通用主机&#xff08;IHostBuilder&#xff09;可以轻易创建一个可以执行后台任务的程序 1. 添加Nuget包 Microsoft.Extensions.Hosting 2. 修改Program.cs 通过Host获取IHostService&#xff0c;然…

SSD: Single Shot MultiBox Detector(2016.11)

文章目录 AbstractIntroduction此前本文贡献总结如下: The Single Shot Detector (SSD)SSD ModelMulti-scale feature maps for detectionConvolutional predictors for detectionDefault boxes and aspect ratiosTrainingMatching strategyTraining objectiveChoosing scales …

python---for循环结构中的else结构(是同级关系)

为什么需要在for循环中添加else结构 循环可以和else配合使用&#xff0c; else下方缩进的代码指的是当循环正常结束之后要执行的代码。 强调&#xff1a; 循环 正常结束&#xff0c;else之后要执行的代码。 非正常结束&#xff0c;其else中的代码是不会执行的。&#xf…

GienTech动态|入选软件和信息技术服务名牌企业;荣获城市数字化转型优秀案例;参加第四届深圳国际人工智能展

中电金信入选“2023第二届软件和信息技术服务名牌企业” 近日&#xff0c;中国电子信息行业联合会发布了“2023第二届软件和信息技术服务名牌企业”名单&#xff0c;中电金信入选。此名单发布原则&#xff0c;重点突出技术创新力。突出市场影响力&#xff0c;品牌建设良好&…

Leetcode刷题笔记--Hot81--90

1--打家劫舍III 主要思路&#xff1a; 基于从下到上的 dp 回溯法&#xff0c;每一个节点只有两种状态&#xff0c;dp[0]表示被打劫&#xff0c;dp[1]表示不被打劫&#xff1b; 当前节点被打劫时&#xff0c;其孩子只能都不被打劫&#xff1b;dp[0] left[1] right[1] cur->…

redis集群理论和搭建

目录 环境 一&#xff0c;安装和部署redis 1&#xff0c;安装 2&#xff0c;部署 ​编辑 3&#xff0c;允许非本机连接redis 二、主从模式 主从模式搭建&#xff1a; 三&#xff0c;哨兵模式 哨兵模式搭建 四&#xff0c;集群模式 架构细节: 心跳机制 集群模式搭建&#xff1a…

【NLP】word复制指定内容到新的word文档

目录 1.python代码 2.结果 需求&#xff1a; 复制word文档里的两个关键字&#xff08;例如“起始位置”到“结束位置”&#xff09;之间的内容到新的word文档。 前提&#xff1a;安装win32包&#xff0c;通过pip install pywin32命令直接安装。话不多说&#xff0c;直接上代码…

底层全部重构,小米澎湃OS完整系统架构公布

上周&#xff0c;雷军发文称小米全新操作系统澎湃 OS 正式版已完成封包&#xff0c;将逐步接替 MIUI。而后&#xff0c;又有网友曝光小米澎湃 OS 界面。 今日&#xff0c;雷军再度发表长文预热小米澎湃 OS&#xff0c;正式公布了完整系统架构。 据介绍&#xff0c;从架构设计之…