vue实战-深入响应式数据原理

news2024/11/17 21:50:12

本文将带大家快速过一遍Vue数据响应式原理,解析源码,学习设计思路,循序渐进。

数据初始化

_init

在我们执行new Vue创建实例时,会调用如下构造函数,在该函数内部调用this._init(options)

import { initMixin } from "./init.js";

// 先创建一个Vue类,Vue就是一个构造函数(类) 通过new关键字进行实例化
function Vue(options) {
  // 这里开始进行Vue初始化工作
  this._init(options);
}
// _init方法是挂载在Vue原型的方法,每一个new 实例可以调用, 由initMixin方法挂载

// 将不同的操作拆分成不同的模块,导入后对Vue类做一些处理,此做法更利于维护
initMixin(Vue); // 定义原型方法_init
stateMixin(Vue)  //定义 $set $get $delete $watch 等
eventsMixin(Vue) // 定义事件  $on  $once $off $emit
lifecycleMixin(Vue) // 定义 _update  $forceUpdate  $destroy
renderMixin(Vue) // 定义 _render 返回虚拟dom 

export default Vue;

initMixin函数里面定义了原型方法_init_init调用了initState(vm)等方法,_init里做了很多初始化工作,我们重点关注initState

import { initState } from "./state";

export function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    const vm = this; // 这里的this指向调用_init方法的对象(即 new的实例)
    //  this.$options就是用户new Vue的时候传入的属性
    vm.$options = options;
    ...
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, 'beforeCreate');
    initInjections(vm); // resolve injections before data/props
    // 初始化状态,在beforeCreate之前,created之后
    initState(vm);
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created');
    ...

  };
}

initState

initState函数按顺序初始化$options的数据,顺序为 prop>methods>data>computed>watch

import { observe } from "./observer/index.js";

function initState (vm) {
    vm._watchers = [];
    const opts = vm.$options;  
    // 按顺序初始化 prop>methods>data>computed>watch
    if (opts.props) { initProps(vm, opts.props); } 
    if (opts.methods) { initMethods(vm, opts.methods); }
    if (opts.data) { // 初始化data
      initData(vm);
    } else {
      observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
  }

initData

initData做了什么事?

  1. vm.$options.data 赋值给vm._data

    此处有个细节,vue组件data推荐使用函数,防止数据在组件之间共享,原因是如果你定义的data是个对象的话,那所有的组件实例的data都会引用这个对象,一个组件更改了data别的组件也会发生变化,他们的data指向同一个内存地址。

  2. 判断方法和属性是否重名,以及是否有保留属性

  3. 没有问题就通过 proxy() 把 data 里的每一个属性都代理到当前实例上,就可以通过 this.xx 访问了

  4. 最后再调用 observe 监听整个 data,observe方法用于创建监听器

import { observe } from "./observer/index.js";

function initState (vm) {
    ...
    initData(vm);
}

function initData (vm: Component) {
  // 获取当前实例的 data 
  let data = vm.$options.data
  // 判断 data 的类型
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(`数据函数应该返回一个对象`)
  }
  // 获取当前实例的 data 属性名集合
  const keys = Object.keys(data)
  // 获取当前实例的 props 
  const props = vm.$options.props
  // 获取当前实例的 methods 对象
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    // 非生产环境下判断 methods 里的方法是否存在于 props 中
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(`Method 方法不能重复声明`)
      }
    }
    // 非生产环境下判断 data 里的属性是否存在于 props 中
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(`属性不能重复声明`)
    } else if (!isReserved(key)) {
      // 都不重名的情况下,代理到 vm 上,可以让 vm._data.xx 通过 vm.xx 访问
      proxy(vm, `_data`, key)
    }
  }
  // 监听 data
  observe(data, true /* asRootData */)
}

proxy 数据代理

proxy函数中调用了Object.defineProperty_data中的每个property代理到了vm身上,作用就是,可以vm._data.xx 通过 vm.xx 访问,当你访问vm.a的时候实际上是访问的vm._data.a。

function proxy (target, sourceKey, key) {
    sharedPropertyDefinition.get = function proxyGetter () {
      return this[sourceKey][key]
    };
    sharedPropertyDefinition.set = function proxySetter (val) {
      this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
  }

observe 数据劫持

observe

该方法用于创建监听器实例

export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 如果不是'object'类型 或者是 vnode 的对象类型就直接返回
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // __ob__是监听器对象,如果存在的话说明已经被监听过,避免重复监听
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 创建监听器
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Observer

监听器类,将数据转换为响应式数据

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // 根对象上的 vm 数量
  constructor (value: any) {
    this.value = value
    this.dep = new Dep(); // 预先实例化一个dep,用于保存数组的依赖
    this.vmCount = 0
    // 给 value 添加 __ob__ 属性,值为为当前value 创建的 Observe 实例
    // 表示已经变成响应式了,目的是对象遍历时就直接跳过,避免重复监听
    def(value, '__ob__', this)
    // 类型判断
    if (Array.isArray(value)) {
      // 判断数组是否有__proto__
      if (hasProto) {
        // 如果有就把它的原型设置为arrayMethods,arrayMethods对象拥有变异后的七个数组方法并且原型是原生数组Array的原型
        protoAugment(value, arrayMethods); // 原型增强
      } else {
        // 没有就通过 def,也就是Object.defineProperty 去定义属性值
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  // 如果是对象类型
  walk (obj: Object) {
    const keys = Object.keys(obj)
    // 遍历对象所有属性,转为响应式对象,也是动态添加 getter 和 setter,实现双向绑定
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  // 监听数组
  observeArray (items: Array<any>) {
    // 遍历数组,对每一个元素进行监听
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

参考 前端进阶面试题详细解答

对于数组和对象有不同的处理,我们先来看处理对象响应式的方法,walk

walk

遍历对象所有属性,调用defineReactive方法转为响应式对象,

  walk (obj: Object) {
    const keys = Object.keys(obj)
    // 遍历对象所有属性,转为响应式对象,也是动态添加 getter 和 setter,实现双向绑定
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

defineReactive

定义响应式对象,getter时收集依赖,setter时触发依赖

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {

  // 创建 dep 实例,保存属性的依赖,getter时添加依赖,setter时触发依赖
  const dep = new Dep(); 这个是对象的依赖
  // 拿到对象的属性描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // 获取自定义的 getter 和 setter
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // 如果 val 是对象的话就递归监听
  // 递归监听子属性,如果value还是一个对象会继续走一遍defineReactive 层层遍历一直到value不是对象才停止,所以如果对象层级过深,对性能会有影响
  let childOb = !shallow && observe(val) // data = {a: {b: 3}, c: [1, 2]} 属性值如果是对象或数组会返回Observer实例
  // 截持对象属性的 getter 和 setter
  Object.defineProperty(obj, key, { // 例如监听data.a,那val就是{b: 3}
    enumerable: true,
    configurable: true,
    // 拦截 getter,当取值时会触发该函数
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 开始依赖收集 (在get中会收集属性的依赖,以及其属性值的依赖)
      // 初始化渲染 watcher 时访问到已经被添加响应式的对象,从而触发 get 函数
        if (Dep.target) { // 如果现在处于依赖收集阶段
          dep.depend(); // 添加当前属性的依赖
          if (childOb) { // 数组会在此收集依赖,在数组被push等操作时调用保存的Observer实例触发依赖;对象会收集两次依赖,但是对象的第二次收集不会被setter触发
            // childOb.dep 就是Observer 中 this.dep = new Dep()
            childOb.dep.depend(); // 父属性包含子属性,即访问了this.a,实际上也访问了this.a.b,this.a.b变了,this.a就变了,所以子属性也要收集依赖
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    // 拦截 setter,当值改变时会触发该函数
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      // 判断是否发生变化
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // 没有 setter 的访问器属性
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 如果新值是对象的话递归监听
      childOb = !shallow && observe(newVal)
      // 遍历通知储存在Dep实例中的所有依赖
      dep.notify()
    }
  })
}

Object.defineProperty定义响应式对象的缺点

  1. 监听嵌套层级过深的对象会影响性能
  2. 对象新增或者删除的属性无法被set 监听到 只有对象本身存在的属性修改才会被劫持,所以Vue设计了$set$delete方法,更新数据的同时手动触发通知依赖
  3. 如果用其来监听数组的话,无法监听数组长度动态变化,并且只能监听通过对已有元素下标的访问进行的修改,即arr[已有元素下标] = val

我们自己手写一个递归设置响应式的方法来试一下:

function defineProperty(obj, key, val){
  observer(val);
  Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 读取方法
        console.log('读取', key, '成功')
        return val
      },
      set(newval) {
        // 赋值监听方法
        if (newval === val) return
        observer(newval)
        console.log('监听赋值成功', newval)
        val = newval
      }
    })
}

function observer(obj) {
  if (typeof obj !== 'object' || obj == null) {
    return
  }
  for (const key of Object.keys(obj)) {
    // 给对象中的每一个方法都设置响应式
    defineProperty(obj, key, obj[key])
  }
}

const arr = [{a:3}, 66, [4,5]];
const obj = {a:1, b: [2]};

arr.length = 33; // 无法监听数组长度动态变化
arr[2].push(22) // 只能监听通过对已有元素下标的访问进行的修改
arr[2][0] = 5; // 访问已有元素的下标可以监听修改

obj.c = 6; // 无法监听新添加的属性
delete obj.b // 无法监听属性被删除
obj.b = 66; // 被删除后就失去响应式了

虽然defineProperty可以监听通过对已有元素下标访问的修改,但是出于性能考虑,vue并没有使用这一功能来使数组实现响应式,因为数组元素太多时耗费一定性能,要挨个遍历监听一遍数组的每一个属性,属性可能还会包含自己的嵌套属性,所以vue的做法是修改原生操作数组的方法,并且跟用户约定修改数组要用这些方法去操作。

尤大也做出了官方的解释:

在这里插入图片描述

数组的观测

数组元素添加或删除操作的观测通过创建一个以原生Array的原型为原型的新对象,为新对象添加数组的变异方法,将观察的对象的原型设置为这个新对象,被观察的对象调用数组方法时就会使用被重写后的方法。

记得我们在讲寄生式继承时说的么,寄生式继承的核心:使用原型式继承Object.create(parent)可以获得一份目标对象的浅拷贝,在这个浅拷贝对象上进行增强,添加一些方法属性。
vue对重写数组方法的设计与寄生式继承类似,都是面向切面编程的思想(AOP),即不破坏原有功能封装的前提下,动态的扩展功能

import { TriggerOpTypes } from '../../v3'
import { def } from '../util/index'

const arrayProto = Array.prototype // 用Array的原型创建一个新对象,arrayMethods.__proto__ === arrayProto,免得污染原生Array
export const arrayMethods = Object.create(arrayProto);

// 需要重写的方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/** * Intercept mutating methods and emit events */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  // 给arrayMethods对象定义上述方法,使该对象拥有原生方法能力的同时添加响应式行为
  def(arrayMethods, method, function mutator(...args) {
    const result = original.apply(this, args) // 先调用原生方法
    const ob = this.__ob__
    let inserted; //  新添加的元素
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice': // 可以监测数组长度变化
        //splice格式是splice(下标,数量,插入的新项)
        inserted = args.slice(2); // 获取插入的新项
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    if (__DEV__) {
      ob.dep.notify({
        type: TriggerOpTypes.ARRAY_MUTATION,
        target: this,
        key: method
      })
    } else {
      ob.dep.notify()
    }
    return result
  })
})

因为出于性能考虑,vue没有使用defineProperty劫持数组,所以要通过索引修改数组,我们需要使用$set

总结

以上就是Vue2的响应式数据原理,讲述了如何对数据进行响应式观测,核心就是通过Object.defineProperty对数据进行劫持,在getter中收集依赖,setter中派发依赖,完整的响应式原理,如修改数据后视图是如何更新视图的还需要结合Dep和Watcher来看,这段后续接着说,一点点地来消化。

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

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

相关文章

代码随想录算法训练营第一天| 704. 二分查找、27. 移除元素

Leetcode 704 二分查找题目链接&#xff1a;704二分查找介绍给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xff0c;写一个函数搜索 nums 中的 target&#xff0c;如果目标值存在返回下标&#xff0c;否则返回 -1。思路先看看一个…

MyBatis源码分析(三)SqlSession的执行主流程

文章目录一、熟悉主要接口二、SqlSession的获取1、通过数据源获取SqlSession三、Mapper的获取与代理1、从SqlSession获取Mapper2、执行Mapper方法前准备逻辑3、SqlCommand的创建4、构造MethodSignature四、执行Mapper的核心方法1、执行Mapper的方法逻辑五、简单SELECT处理过程1…

【蓝桥杯试题】 递归实现指数型枚举例题

&#x1f483;&#x1f3fc; 本人简介&#xff1a;男 &#x1f476;&#x1f3fc; 年龄&#xff1a;18 &#x1f91e; 作者&#xff1a;那就叫我亮亮叭 &#x1f4d5; 专栏&#xff1a;蓝桥杯试题 文章目录1. 题目描述2. 思路解释2.1 时间复杂度2.2 递归3. 代码展示最后&#x…

超级简单又功能强大还免费的电路仿真软件

设计电路的时候经常需要进行一些电路仿真。常用的仿真软件很多&#xff0c;由于大学里经常使用Multisim作为教学软件&#xff0c;所以基本上所有从事硬件开发的人都听过或者用过Multisim这个软件。这个软件最大的好处就是简单直观&#xff0c;可以在自己的PC上搭建电路并使用软…

gdb常用命令详解

gdb常用调试命令概览和说明 run命令 在默认情况下&#xff0c;gdbfilename只是attach到一个调试文件&#xff0c;并没有启动这个程序&#xff0c;我们需要输入run命令启动这个程序&#xff08;run命令被简写成r&#xff09;。如果程序已经启动&#xff0c;则再次输入 run 命令…

从面试官角度告诉你高级性能测试工程师面试必问的十大问题

目录 1、介绍下最近做过的项目&#xff0c;背景、预期指标、系统架构、场景设计及遇到的性能问题&#xff0c;定位分析及优化&#xff1b; 2、项目处于什么阶段适合性能测试介入&#xff0c;原因是什么&#xff1f; 3、性能测试场景设计要考虑哪些因素&#xff1f; 4、对于一…

SAP MM学习笔记4-在库类型都有哪些,在库类型有哪些控制点

SAP MM模块中的在库类型有3种&#xff1a; 1&#xff0c;利用可能在库 (非限制使用库存) 2&#xff0c;品质检查中在库 &#xff08;质检库存&#xff09; 3&#xff0c;保留在库&#xff08;已冻结库存&#xff09; 这3种在库标识该物料的状态&#xff0c;是否可用。 这3种…

bugku 安全加固1

js劫持 根据题目所给出的ip访问原本应该进入一个学院的二手交易网站 但是实际进入了一个博客 flag需要去除最后的斜杆 黑客首次webshell密码 利用所给的账户密码进行登录进入www目录并且进行备份 #我们对网站进行备份 cd /var/www && tar -czvf /tmp/html.tgz html …

Kubernetes之存储管理(上)

数据持久化的主要方式简介 pod是临时的&#xff0c;pod中的数据随着pod生命周期的结束也会被一起删除。 pod想实现数据持久化主要有以下几种方式&#xff1a; emptyDir&#xff1a;类似于docker run –v /xx&#xff0c;在物理机里随机产生一个目录(这个目录其实挂载的是物理…

墨天轮2022年度数据库获奖名单

2022年&#xff0c;国家相继从高位部署、省级试点布局、地市重点深入三个维度&#xff0c;颁布了多项中国数据库行业发展的利好政策。但是我们也能清晰地看到&#xff0c;中国数据库行业发展之路道阻且长&#xff0c;而道路上的“拦路虎”之一则是生态。中国数据库的发展需要多…

如何创建发布新品上市新闻稿

推出新产品对任何企业来说都是一个激动人心的时刻&#xff0c;但向潜在客户宣传并围绕您的新产品引起轰动也可能是一个挑战。最有效的方法之一就是通过发布新品上市新闻稿。精心制作的新闻稿可以帮助我们通过媒体报道、吸引并在目标受众中引起关注。下面&#xff0c;我们将讲述…

计算机组成原理4小时速成2:计算机运算方法,原码,反码,补码,移位,加法减法,乘除法

计算机组成原理4小时速成2&#xff1a;计算机运算方法&#xff0c;原码&#xff0c;反码&#xff0c;补码&#xff0c;移位&#xff0c;加法减法&#xff0c;乘除法 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多…

OpenCV入门(二)快速学会OpenCV1图像基本操作

OpenCV入门&#xff08;一&#xff09;快速学会OpenCV1图像基本操作 不讲大道理&#xff0c;直接上干货。操作起来。 众所周知&#xff0c;OpenCV 是一个跨平台的计算机视觉库, 支持多语言, 功能强大。今天就从读取图片&#xff0c;显示图片&#xff0c;输出图片信息和简单的…

记录自己遇到的关于Hashmap的面试题

一.麻烦讲述一下Hashmap的扩容原理 jdk1.8中的hashmap扩容原理 1.put流程图 首先贴一张图(图片来源于传送门&#xff09;&#xff0c;多谢大佬的美图&#xff0c;此图已经完美的描述了put的整个流程&#xff0c;我也就不想自己画了&#xff0c;嘿嘿: 2.hashmap中几个比较重…

hive临时目录清理

hive运行失败会导致临时目录无法自动清理&#xff0c;因此需要自己写脚本去进行清理 实际发现hive临时目录有两个&#xff1a; /tmp/hive/{user}/* /warehouse/tablespace//hive/**/.hive-staging_hive 分别由配置hive.exec.scratchdir和hive.exec.stagingdir决定: 要注意的…

requests---(4)发送post请求完成登录

前段时间写过一个通过cookies完成登录&#xff0c;今天我们写一篇通过post发送请求完成登录豆瓣网 模拟登录 1、首先找到豆瓣网的登录接口 打开豆瓣网站的登录接口&#xff0c;请求错误的账号密码&#xff0c;通过F12或者抓包工具找到登录接口 通过F12抓包获取到请求登录接口…

每日分享(微信社区小程序/h5/圈子论坛贴吧交友/博客/社交)

1.Java单元测试实战 高清PDF中文版 Java单元测试实战来自于作者多年来的单元测试实践&#xff0c;最初发表在阿里内网的ATA上&#xff0c;成为了很多阿里同学单元测试学习的必读文章。很多程序员认为单元测试会花费大量的时间&#xff0c;因此他们写单元测试的意愿比较低&…

【同步工具类:Semaphore】

同步工具类:Semaphore介绍源码分析构造函数acquire 获取信号量release 释放信号量业务场景代码测试结果总结介绍 官方说明: Semaphore用于限制可以访问某些资源&#xff08;物理或逻辑的&#xff09;的线程数目&#xff0c;他维护了一个许可证集合&#xff0c;有多少资源需要限…

vue2、vue3组件传值,引用类型,对象数组如何处理

vue2、vue3组件传值&#xff0c;引用类型&#xff0c;对象数组如何处理 Excerpt 所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定&#xff1a;父级 prop 的更新会向下流动到子组件中&#xff0c;但是反过来则不行。这样会防止从子组件意外变更父… 下述组件传值指引…

【Go|第1期】Go遍历目录的三种方法

日期&#xff1a;2023年3月1日 作者&#xff1a;Commas 签名&#xff1a;(ง •_•)ง 积跬步以致千里,积小流以成江海…… 注释&#xff1a;如果您觉得有所帮助&#xff0c;帮忙点个赞&#xff0c;也可以关注我&#xff0c;我们一起成长&#xff1b;如果有不对的地方&#xff…