您现在的位置是:亿华云 > IT科技

深入理解vue响应式原理

亿华云2025-10-02 21:10:13【IT科技】4人已围观

简介【.com原创稿件】前言Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工

【.com原创稿件】前言

Vue 最独特的深入特性之一,是理解其非侵入性的响应式系统。数据模型仅仅是应式原理普通的 JavaScript 对象。而当你修改它们时,深入视图会进行更新。理解这使得状态管理非常简单直接,应式原理不过理解其工作原理同样重要,深入这样你可以避开一些常见的理解问题。----官方文档 本文将针对响应式原理做一个详细介绍,应式原理并且带你实现一个基础版的深入响应式系统。本文的理解代码请猛戳Github博客

什么是响应式

我们先来看个例子:

<div id="app">     <div>Price :¥{ {  price }}</div>     <div>Total:¥{ {  price * quantity }}</div>     <div>Taxes: ¥{ {  totalPriceWithTax }}</div>     <button @click="changePrice">改变价格</button> </div>  var app = new Vue({    el: #app,   data() {      return {        price: 5.0,       quantity: 2     };   },   computed: {      totalPriceWithTax() {        return this.price * this.quantity * 1.03;     }   },   methods: {      changePrice() {        this.price = 10;     }   } }) 

上例中当price 发生变化的时候,Vue就知道自己需要做三件事情:

更新页面上price的应式原理值 计算表达式 price*quantity 的值,更新页面 调用totalPriceWithTax 函数,深入更新页面

发生变化后,理解会重新对页面渲染,应式原理这就是Vue响应式,那么这一切是怎么做到的呢?

想完成这个过程,我们需要:

侦测数据的变化 收集视图依赖了哪些数据 数据变化时,服务器托管自动“通知”需要更新的视图部分,并进行更新

对应专业俗语分别是:

数据劫持 / 数据代理 依赖收集 发布订阅模式

如何侦测数据的变化

首先有个问题,在Javascript中,如何侦测一个对象的变化? 其实有两种办法可以侦测到变化:使用Object.defineProperty和ES6的Proxy,这就是进行数据劫持或数据代理。这部分代码主要参考珠峰架构课。

方法1.Object.defineProperty实现

Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。

function render () {  console.log(模拟视图渲染) } let data = {  name: 浪里行舟, location: {  x: 100, y: 100 } } observe(data) function observe (obj) {  // 判断类型 if (!obj || typeof obj !== object) {  return } Object.keys(obj).forEach(key => {  defineReactive(obj, key, obj[key]) }) function defineReactive (obj, key, value) {  // 递归子属性 observe(value) Object.defineProperty(obj, key, {  enumerable: true, //可枚举(可以遍历) configurable: true, //可配置(比如可以删除) get: function reactiveGetter () {  console.log(get, value) // 监听 return value }, set: function reactiveSetter (newVal) {   observe(newVal) //如果赋值是一个对象,也要递归子属性  if (newVal !== value) {  console.log(set, newVal) // 监听 render()  value = newVal  } }  })  }  }  data.location = {   x: 1000,  y: 1000  } //set { x: 1000,y: 1000} 模拟视图渲染  data.name // get 浪里行舟 

几个注意点补充说明:

这种方式无法检测到对象属性的添加或删除(如data.location.a=1)。

这是因为 Vue 通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。如果是删除属性,亿华云我们可以用vm.$delete实现,那如果是新增属性,该怎么办呢? 1)可以使用 Vue.set(location, a, 1) 方法向嵌套对象添加响应式属性; 2)也可以给这个对象重新赋值,比如data.location = { ...data.location,a:1}

Object.defineProperty 不能监听数组的变化,需要进行数组方法的重写function render() {  console.log(模拟视图渲染) } let obj = [1, 2, 3] let methods = [pop, shift, unshift, sort, reverse, splice, push] // 先获取到原来的原型上的方法 let arrayProto = Array.prototype // 创建一个自己的原型 并且重写methods这些方法 let proto = Object.create(arrayProto)  methods.forEach(method => {   proto[method] = function() {   // AOP  arrayProto[method].call(this, ...arguments) render()  }  }) function observer(obj) {   // 把所有的属性定义成set/get的方式  if (Array.isArray(obj)) {   obj.__proto__ = proto  return  }  if (typeof obj == object) {   for (let key in obj) {   defineReactive(obj, key, obj[key])  }  }  }  function defineReactive(data, key, value) {   observer(value)  Object.defineProperty(data, key, {   get() {   return value  },  set(newValue) {  observer(newValue) if (newValue !== value) {   render()  value = newValue  } }  })  }  observer(obj)  function $set(data, key, value) {  defineReactive(data, key, value)  }  obj.push(123, 55)  console.log(obj) //[1, 2, 3, 123, 55] 

这种方法将数组的常用方法进行重写,进而覆盖掉原生的数组方法,重写之后的数组方法需要能够被拦截。但有些数组操作Vue时拦截不到的,当然也就没办法响应,比如:

obj.length-- // 不支持数组的长度变化 obj[0]=1 // 修改数组中***个元素,也无法侦测数组的变化 

ES6提供了元编程的能力,所以有能力拦截,Vue3.0可能会用ES6中Proxy 作为实现数据代理的主要方式。

方法2.Proxy实现

Proxy 是 JavaScript 2015 的一个新特性。Proxy 的高防服务器代理是针对整个对象的,而不是对象的某个属性,因此不同于 Object.defineProperty 的必须遍历对象每个属性,Proxy 只需要做一层代理就可以监听同级结构下的所有属性变化,当然对于深层结构,递归还是需要进行的。此外**Proxy支持代理数组的变化。

**function render() {   console.log(模拟视图的更新)  }  let obj = {   name: 前端工匠,  age: {  age: 100 },  arr: [1, 2, 3]  }  let handler = {  get(target, key) {  // 如果取的值是对象就在对这个对象进行数据劫持  if (typeof target[key] == object && target[key] !== null) {  return new Proxy(target[key], handler)  } return Reflect.get(target, key)  },  set(target, key, value) {   if (key === length) return true  render()  return Reflect.set(target, key, value)  }  }  let proxy = new Proxy(obj, handler)  proxy.age.name = 浪里行舟 // 支持新增属性  console.log(proxy.age.name) // 模拟视图的更新 浪里行舟  proxy.arr[0] = 浪里行舟 //支持数组的内容发生变化 console.log(proxy.arr) // 模拟视图的更新 [浪里行舟, 2, 3 ] proxy.arr.length-- // 无效 

以上代码不仅精简,而且还是实现一套代码对对象和数组的侦测都适用。不过Proxy兼容性不太好!

我们之所以要观察数据,其目的在于当数据的属性发生变化时,可以通知那些曾经使用了该数据的地方。比如***例子中,模板中使用了price 数据,当它发生变化时,要向使用了它的地方发送通知。那如何收集依赖呢?

收集依赖与发布订阅模式

如何收集依赖,总结起来就一句话,在getter中收集依赖,在setter中触发依赖 我们先来实现一个 Dep 类,用于解耦属性的依赖收集和派发更新操作。

// 通过 Dep 解耦属性的依赖和更新操作 class Dep {  constructor() {  this.subs = [] } // 添加依赖  addSub(sub) {   this.subs.push(sub)  }  // 更新  notify() {   this.subs.forEach(sub => {   sub.update()  })  }  }  // 全局属性,通过该属性配置 Watcher  Dep.target = null 

当需要依赖收集的时候调用 addSub,当需要派发更新的时候调用 notify。具体如何调用呢?

let dp = new Dep()  dp.addSub(() => {   console.log(emit here)  })  dp.notify() 

这就是一个简单实现的“事件发布订阅模式”,当然代码只是启发思路,真实应用还比较“粗糙”,没有进行事件名设置,APIs 也并不丰富,但完全能够说明问题了。

接下来我们先来简单的了解下 Vue 组件挂载时添加响应式的过程。在组件挂载时,会先对所有需要的属性调用 Object.defineProperty(),然后实例化 Watcher,传入组件更新的回调。在实例化过程中,会对模板中的属性进行求值,触发依赖收集。我们可以把Watcher理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。

***需要对 defineReactive 函数进行改造,在自定义函数中添加依赖收集和派发更新相关的代码。

function render () {    console.log(模拟视图渲染) } let data = {    name: 浪里行舟,   location: {  x: 100, y: 100 } } observe(data)   let dp = new Dep() function observe (obj) {    // 判断类型   if (!obj || typeof obj !== object) {      return   }   Object.keys(obj).forEach(key => {      defineReactive(obj, key, obj[key])   })   function defineReactive (obj, key, value) {      // 递归子属性     observe(value)     Object.defineProperty(obj, key, {        enumerable: true, //可枚举(可以遍历)       configurable: true, //可配置(比如可以删除)       get: function reactiveGetter () {          console.log(get, value) // 监听     // 将 Watcher 添加到订阅        if (Dep.target) {           dp.addSub(Dep.target)        }         return value       },       set: function reactiveSetter (newVal) {          observe(newVal) //如果赋值是一个对象,也要递归子属性         if (newVal !== value) {            console.log(set, newVal) // 监听           render()           value = newVal      // 执行 watcher 的 update 方法           dp.notify()         }       }     })   } } 

以上所有代码实现了一个简易的数据响应式,核心思路就是手动触发一次属性的 getter 来实现依赖收集。

总结

我们再来回顾下整个过程:

在 Vue 中模板编译过程中的指令或者数据绑定都会实例化一个 Watcher 实例,实例化过程中会触发 get() 将自身指向 Dep.target; data在 Observer 时执行 getter 会触发 dep.depend() 进行依赖收集;依赖收集的结果: data在 Observer 时闭包的dep实例的subs添加观察它的 Watcher 实例; Watcher 的deps中添加观察对象 Observer 时的闭包dep; 当data中被 Observer 的某个对象值变化后,触发subs中观察它的watcher执行 update() 方法,***实际上是调用watcher的回调函数cb,进而更新视图。

参考文章和书籍

珠峰架构课(强烈推荐) 剖析 Vue.js 内部运行机制 深入浅出Vue.js Vue官方文档 前端面试之道 前端开发核心知识进阶 Javascript响应式的最通俗易懂的解释(译)

作者介绍

浪里行舟:硕士研究生,专注于前端。个人公众号:「前端工匠」,致力于打造适合初中级工程师能够快速吸收的一系列优质文章!

【原创稿件,合作站点转载请注明原文作者和出处为.com】

很赞哦!(89)