深入理解vue响应式原理

栏目: 编程语言 · 发布时间: 5年前

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

【51CTO.com原创稿件】

前言

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

深入理解vue响应式原理

什么是响应式

我们先来看个例子:

<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; 
    } 
  } 
}) 

深入理解vue响应式原理

上例中当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响应式原理

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

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

参考文章和书籍

珠峰架构课(强烈推荐)

剖析 Vue.js 内部运行机制

深入浅出Vue.js

Vue官方文档

前端面试之道

前端开发核心知识进阶

深入理解Vue响应式原理

Javascript响应式的最通俗易懂的解释(译)

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

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Complexity and Approximation

Complexity and Approximation

G. Ausiello、P. Crescenzi、V. Kann、Marchetti-sp、Giorgio Gambosi、Alberto M. Spaccamela / Springer / 2003-02 / USD 74.95

This book is an up-to-date documentation of the state of the art in combinatorial optimization, presenting approximate solutions of virtually all relevant classes of NP-hard optimization problems. The......一起来看看 《Complexity and Approximation》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

MD5 加密
MD5 加密

MD5 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换