重学Vue【Vue.set 原理分析】

简介: 重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。

网络异常,图片无法展示
|

重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈)github 上。


正文


在以往的开发过程中,可能会遇到操作了数据,但是视图没有更新的情况,然后使用官网提供的 Vue.set 方法就可以生效了,那为什么没有生效呢?下面我们用一个例子来把这种情况过一遍:

<template>
<div class="xx">
  <p>{{obj}}</p>
  <button @click="change">change</button>
</div>
</template>
<script>
export default {
  data(){
    return {
      obj: {
        a: 1
      }
    }
  },
  methods:{
    change(){
      this.obj.b = 2;
    }
  }
};
</script>

点击按钮之后,obj 是变化了的,但是视图上显示的还是 {a: 1},从前面分析响应式对象的逻辑来看,如果一个对象被定义成响应式的话,在它走 setter 的时候,会走派发更新的逻辑,视图就会改变,就算试图更新是在下一个 tick 才执行,它也应该改变才对呀?这种情况其实新增的属性 b 不是一个响应式对象的属性,也就是没有走响应式对象的 setter,此时官网告诉我们:

网络异常,图片无法展示
|

Vue.set 方法可以向响应式对象上添加一个属性,并且属性也是响应式的,这样就解决了视图更新的问题,修改数据就会触发 渲染 watcher 的重新渲染,那我们来看下它是怎么实现的。


对象

Vue.set 的代码在 src/core/global-api/index.js 中:

// ...
Vue.set = set
// ...

引用了一个名为 set 的方法,那它真正的逻辑是在 src/core/observer/index.js 中:

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

可以看到 set 方法接收三个参数,target 可以是数组或者对象,key 代表数组的下标或者对象的值,val 就是新增的值。首先判断如果 target 是一个数组并且 key 是一个合法的下标,就通过 splice 添加进新数组中然后返回,这里的 splice 注意不是原生的 splice 方法了,后面会说到。

接着又判断如果 key 已经在 target 里,就直接赋值返回,因为这样的变化是可以监测到的。接着再获取到 target.__ob__ 属性,并赋值给 __ob__,这个之前分析响应式对象的时候提到过,在 Observer 的构造函数执行的时候会初始化一个 __ob__,然后会实例化一个 Observer 对象,如果它不存在,就说明 target 不是一个响应式对象,就直接赋值返回,最后通过 defineReactive 把新加的属性变成响应式对象,然后通过 ob.dep.notify() 去手动触发依赖的更新,这里回顾一下响应式对象里的 getter

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // ...
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    // ...
  })
}

getter 过程中判断了一个 childOb,并调用了 childOb.dep.notify() 去收集依赖,这也就是为什么通过 Vue.set 的方式可以拿到改变后的值,因为 ob.dep.notify 手动去通知了 watcher,从而将新的属性的对象也可以检测到它的变化,并且后面也加了数组的判断,如果 value 是一个数组,就执行 dependArray 去做依赖收集。那这里解释了上面例子的 obj 对象,如果是数组呢?


数组

数组的变动,Vue也是不会检测到的,和对象类似,如果想利用下标去改变一个数组的话,可以:vm.items[index] = newValue,那如果要修改数组的长度的话,可以:vm.items.splice(newLength)splice 上面说到是不是原生的了,它是被重写过的。

这里再回顾一下 Observer 构造函数:

export class Observer {
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      // ...
    }
  }
}

如果 value 是一个数组的话,就获取 argument,这里的 hasProto 其实就是判断对象中有没有 __proto__ 属性,如果存在就把 argumet 指向 protoAugment,否则就指向 copyAugment,来看下这两个函数的定义:

/**
 * Augment an target Object or Array by intercepting
 * the prototype chain using __proto__
 */
function protoAugment (target, src: Object, keys: any) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}
/**
 * Augment an target Object or Array by defining
 * hidden properties.
 */
/* istanbul ignore next */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

protoAugment 函数直接把 target.__proto__ 原型改成了 src,而 copyAugment 函数遍历了 keys,然后循环调用 def,也就是 Object.defineProperty 来定义自己的属性,一般浏览器都有 __proto__,所以都会走到 protoAugment,这样就把 value 的原型指向了 arrayMethods,它的定义在 src/core/observer/array.js

import { def } from '../util/index'
const arrayProto = Array.prototype
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]
  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':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

arrayMethods 继承了 Array,然后把数组中可以改变自身的方法都重写了一遍,重写之后会先执行原有方法的逻辑,然后针对 pushunshiftsplice 作了其他判断,目的是获取到插入的值,然后把新值变成了一个响应式对象,再次手动调用 ob.dep.notify() 来触发依赖的通知,所以上面的 vm.items.splice(newLength) 就可以检测到数组的变化了。

那对于最上面的例子,如果想监听到 obj 的变化,得改成这样:

<template>
<div class="xx">
  <p>{{obj}}</p>
  <button @click="change">change</button>
</div>
</template>
<script>
import Vue from "vue";
export default {
  data(){
    return {
      obj: {
        a: 1
      }
    }
  },
  methods:{
      Vue.set(this.obj, "b", 2);
    }
  }
};
</script>
目录
相关文章
|
12天前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
30 3
|
3月前
|
JavaScript 算法 编译器
vue3 原理 实现方案
【8月更文挑战第15天】vue3 原理 实现方案
41 1
|
2天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
20 9
|
2月前
|
缓存 JavaScript 前端开发
「offer来了」从基础到进阶原理,从vue2到vue3,48个知识点保姆级带你巩固vuejs知识体系
该文章全面覆盖了Vue.js从基础知识到进阶原理的48个核心知识点,包括Vue CLI项目结构、组件生命周期、响应式原理、Composition API的使用等内容,并针对Vue 2与Vue 3的不同特性进行了详细对比与讲解。
「offer来了」从基础到进阶原理,从vue2到vue3,48个知识点保姆级带你巩固vuejs知识体系
|
18天前
|
JavaScript UED
Vue双向数据绑定的原理
【10月更文挑战第7天】
|
3月前
|
JavaScript 前端开发 索引
vue之$set
vue之$set
|
5天前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
11 0
|
2月前
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
该文章对比了Vue2与Vue3在响应式原理上的不同,重点介绍了Vue3如何利用Proxy替代Object.defineProperty来实现更高效的数据响应机制,并探讨了这种方式带来的优势与挑战。
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
|
2月前
|
开发框架 JavaScript 前端开发
手把手教你剖析vue响应式原理,监听数据不再迷茫
该文章深入剖析了Vue.js的响应式原理,特别是如何利用`Object.defineProperty()`来实现数据变化的监听,并探讨了其在异步接口数据处理中的应用。
|
2月前
|
缓存 JavaScript 容器
vue动态组件化原理
【9月更文挑战第2天】vue动态组件化原理
41 2