深入理解vue响应式原理

【稿件】前言

创新互联建站不只是一家网站建设的网络公司;我们对营销、技术、服务都有自己独特见解,公司采取“创意+综合+营销”一体化的方式为您提供更专业的服务!我们经历的每一步也许不一定是最完美的,但每一步都有值得深思的意义。我们珍视每一份信任,关注我们的网站制作、成都网站制作质量和服务品质,在得到用户满意的同时,也能得到同行业的专业认可,能够为行业创新发展助力。未来将继续专注于技术创新,服务升级,满足企业一站式营销型网站需求,让再小的品牌网站制作也能产生价值!

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

什么是响应式

我们先来看个例子:

  
 
 
 
  1.  
  2.     
    Price :¥{{ price }}
     
  3.     
    Total:¥{{ price * quantity }}
     
  4.     
    Taxes: ¥{{ totalPriceWithTax }}
     
  5.     改变价格 
 
  
 
 
 
  1. var app = new Vue({ 
  2.   el: '#app', 
  3.   data() { 
  4.     return { 
  5.       price: 5.0, 
  6.       quantity: 2 
  7.     }; 
  8.   }, 
  9.   computed: { 
  10.     totalPriceWithTax() { 
  11.       return this.price * this.quantity * 1.03; 
  12.     } 
  13.   }, 
  14.   methods: { 
  15.     changePrice() { 
  16.       this.price = 10; 
  17.     } 
  18.   } 
  19. }) 

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

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

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

对应专业俗语分别是:

如何侦测数据的变化

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

方法1.Object.defineProperty实现

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

 
 
 
 
  1. function render () { 
  2. console.log('模拟视图渲染') 
  3. let data = { 
  4. name: '浪里行舟', 
  5. location: { x: 100, y: 100 } 
  6. observe(data) 
  7. function observe (obj) { 
  8. // 判断类型 
  9. if (!obj || typeof obj !== 'object') { 
  10. return 
  11. Object.keys(obj).forEach(key => { 
  12. defineReactive(obj, key, obj[key]) 
  13. }) 
  14. function defineReactive (obj, key, value) { 
  15. // 递归子属性 
  16. observe(value) 
  17. Object.defineProperty(obj, key, { 
  18. enumerable: true, //可枚举(可以遍历) 
  19. configurable: true, //可配置(比如可以删除) 
  20. get: function reactiveGetter () { 
  21. console.log('get', value) // 监听 
  22. return value 
  23. }, 
  24. set: function reactiveSetter (newVal) {  
  25. observe(newVal) //如果赋值是一个对象,也要递归子属性  
  26. if (newVal !== value) { 
  27. console.log('set', newVal) // 监听 
  28. render()  
  29. value = newVal  
  30. }  
  31. })  
  32. }  
  33. }  
  34. data.location = {  
  35. x: 1000,  
  36. y: 1000  
  37. } //set {x: 1000,y: 1000} 模拟视图渲染  
  38. data.name // get 浪里行舟 

几个注意点补充说明:

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

 
 
 
 
  1. function render() { 
  2. console.log('模拟视图渲染') 
  3. let obj = [1, 2, 3] 
  4. let methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push'] 
  5. // 先获取到原来的原型上的方法 
  6. let arrayProto = Array.prototype 
  7. // 创建一个自己的原型 并且重写methods这些方法 
  8. let proto = Object.create(arrayProto)  
  9. methods.forEach(method => {  
  10. proto[method] = function() {  
  11. // AOP  
  12. arrayProto[method].call(this, ...arguments) 
  13. render()  
  14. }  
  15. }) 
  16. function observer(obj) {  
  17. // 把所有的属性定义成set/get的方式  
  18. if (Array.isArray(obj)) {  
  19. obj.__proto__ = proto  
  20. return  
  21. }  
  22. if (typeof obj == 'object') {  
  23. for (let key in obj) {  
  24. defineReactive(obj, key, obj[key])  
  25. }  
  26. }  
  27. }  
  28. function defineReactive(data, key, value) {  
  29. observer(value)  
  30. Object.defineProperty(data, key, {  
  31. get() {  
  32. return value  
  33. },  
  34. set(newValue) { 
  35. observer(newValue) 
  36. if (newValue !== value) {  
  37. render()  
  38. value = newValue  
  39. }  
  40. })  
  41. }  
  42. observer(obj)  
  43. function $set(data, key, value) { 
  44. defineReactive(data, key, value)  
  45. }  
  46. obj.push(123, 55)  
  47. console.log(obj) //[1, 2, 3, 123, 55] 

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

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

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

方法2.Proxy实现

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

 
 
 
 
  1. function render() {  
  2. console.log('模拟视图的更新')  
  3. }  
  4. let obj = {  
  5. name: '前端工匠',  
  6. age: { age: 100 },  
  7. arr: [1, 2, 3]  
  8. }  
  9. let handler = { 
  10. get(target, key) { 
  11. // 如果取的值是对象就在对这个对象进行数据劫持  
  12. if (typeof target[key] == 'object' && target[key] !== null) { 
  13. return new Proxy(target[key], handler)  
  14. return Reflect.get(target, key)  
  15. },  
  16. set(target, key, value) {  
  17. if (key === 'length') return true  
  18. render()  
  19. return Reflect.set(target, key, value)  
  20. }  
  21. }  
  22. let proxy = new Proxy(obj, handler)  
  23. proxy.age.name = '浪里行舟' // 支持新增属性  
  24. console.log(proxy.age.name) // 模拟视图的更新 浪里行舟  
  25. proxy.arr[0] = '浪里行舟' //支持数组的内容发生变化 
  26. console.log(proxy.arr) // 模拟视图的更新 ['浪里行舟', 2, 3 ] 
  27. proxy.arr.length-- // 无效 

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

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

收集依赖与发布订阅模式

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

 
 
 
 
  1. // 通过 Dep 解耦属性的依赖和更新操作 
  2. class Dep { 
  3. constructor() { 
  4. this.subs = [] 
  5. // 添加依赖  
  6. addSub(sub) {  
  7. this.subs.push(sub)  
  8. }  
  9. // 更新  
  10. notify() {  
  11. this.subs.forEach(sub => {  
  12. sub.update()  
  13. })  
  14. }  
  15. }  
  16. // 全局属性,通过该属性配置 Watcher  
  17. Dep.target = null 

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

 
 
 
 
  1. let dp = new Dep()  
  2. dp.addSub(() => {  
  3. console.log('emit here')  
  4. })  
  5. dp.notify() 

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

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

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

 
 
 
 
  1. function render () { 
  2.   console.log('模拟视图渲染') 
  3. let data = { 
  4.   name: '浪里行舟', 
  5.   location: { x: 100, y: 100 } 
  6. observe(data) 
  7.   let dp = new Dep() 
  8. function observe (obj) { 
  9.   // 判断类型 
  10.   if (!obj || typeof obj !== 'object') { 
  11.     return 
  12.   } 
  13.   Object.keys(obj).forEach(key => { 
  14.     defineReactive(obj, key, obj[key]) 
  15.   }) 
  16.   function defineReactive (obj, key, value) { 
  17.     // 递归子属性 
  18.     observe(value) 
  19.     Object.defineProperty(obj, key, { 
  20.       enumerable: true, //可枚举(可以遍历) 
  21.       configurable: true, //可配置(比如可以删除) 
  22.       get: function reactiveGetter () { 
  23.         console.log('get', value) // 监听 
  24.     // 将 Watcher 添加到订阅 
  25.        if (Dep.target) { 
  26.          dp.addSub(Dep.target) 
  27.        } 
  28.         return value 
  29.       }, 
  30.       set: function reactiveSetter (newVal) { 
  31.         observe(newVal) //如果赋值是一个对象,也要递归子属性 
  32.         if (newVal !== value) { 
  33.           console.log('set', newVal) // 监听 
  34.           render() 
  35.           value = newVal 
  36.      // 执行 watcher 的 update 方法 
  37.           dp.notify() 
  38.         } 
  39.       } 
  40.     }) 
  41.   } 

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

总结

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

  1. data在 Observer 时闭包的dep实例的subs添加观察它的 Watcher 实例;
  2. Watcher 的deps中添加观察对象 Observer 时的闭包dep;

参考文章和书籍

作者介绍

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

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

网站题目:深入理解vue响应式原理
网页路径:http://www.shufengxianlan.com/qtweb/news48/137048.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联

猜你还喜欢下面的内容

App设计知识

同城分类信息