vue 响应式解析

更新日期: 2019-12-03阅读: 2.1k标签: 响应式

一个vue的小demo

<template>
  <div>price: {{price}}</div>
  <div>total: {{price * quantity}}</div>
  <div>totalPriceWithSale: {{totalPriceWithSale}}</div>
</template>
<script>
var vue = new Vue({
  el: '#app',
  data: {
    price: 5,
    quantity: 10,
    sum: 0
  },
  computed: {
    totalPriceWithSale() {
      return this.price * 0.8
    }
  },
  watch: {
    price (val) {
      this.sum += val
    }
  }
})
</script>

在vue中当改变data中的price, quantity, sum中的值,其依赖这三个字段的地方就会触发更新,这就是响应式,那么vue具体是怎么实现的呢?

由于vue中的涉及的功能较多,所以我们先脱离vue,从最简单的开始了解

> let price = 5
> let quantity = 10
> let sum = 0
> let totalPriceWithSale = price * 0.8
> let total = price * quantity
> totalPriceWithSale
4
> total
50
> price = 10 // 修改 price 的值为10
10
> totalPriceWithSale // 未改变
4
> total // 未改变
50

将vue中的代码改写成上面最原始的样子,当我们修改price的值为10,打印其依赖price的变量totalPriceWithSale与total发现其值并没有改变,然而,在响应式中当price改变其依赖该属性的也会发生改变,那进一步优化一下吧, 将每次值更新需要重新执行的代码放在一个函数中,然后放在一个数组中保存起来,值更新后就重新执行函数

let price = 5
let quantity = 10
let sum = 0
let total = 0
let target = null // 当前需要执行的依赖函数
let storage = []
target = () => {
  total = price * quantity
}
function record() {
  storage.push(target)
}
record() // 保存值更新时需要执行的函数
target() // 初始化执行设置 total 值
// 遍历执行函数
function replay() {
  storage.forEach(run => run())
}
price = 20
console.log(total) // 50
replay() // 执行依赖函数
console.log(total) // 200:值更新

上面的代码中,price的值更新后执行replay函数,其依赖price的total变量的值就进行了更新。还可以将上面的代码中对依赖的收集以及执行使用一个类来进行优化

class Dep{
  constructor() {
    this.subs = []
  }
  depend() {
    if (target && !this.subs.includes(target)) {
      this.susb.push(target)
    }
  }
  notify() {
    this.subs.forEach(run => run())
  }
}

使用上面的类对以上的代码来进行优化

let price = 5
let quantity = 10
let sum = 10
let total = 0
const dep = new Dep()
let target = null
target = () => {
  total = price * quantity
}
dep.depend() // 收集依赖
target()
console.log(total) // 50
price = 10 // 修改 price 的值
dep.notify() // 更新其依赖 price 的值
console.log(total) // 100

在vue中对data中每一个属性都进行了Dep的实例,用来保存对其的依赖,在依赖的属性改变的时候更新值.现在对依赖收集进行了优化,那么能否对需要target进行优化使其复用呢

target = () => {
  total = price * quantity
}
dep.depend() // 收集依赖
target()

上面的这段代码,如果监听依赖price的另一个变量,则需要进行重写,不能友好的复用

watcher(() => {
  total = price * quantity
})
watcher(() => {
  sum = price + quantity
})
function wathcer(myFun) {
  target = myFun
  dep.depend()
  target()
  target = null
}
console.log(total, sum) // 50 15
price = 10
dep.notify()
console.log(total, sum) // 100 20

使用watcher能够很好的进行复用,在watcher中收集依赖函数,添加依赖。那么又有一个问题,每次修改值都需要手动的去执行dep.notify()函数,能不能当值改变的时候自动执行notify函数呢。在vue中,数据是存放在data属性中,该属性返回的是一个对象,可以对对象中的每一个属性进行监听,当获取或者设置值的时候调用相关的函数

let price = 5
let quantity = 10
let sum = 10
let total = 0
let salePrice = 0
// const dep = new Dep()
let target = null
let data = {
  price: 5,
  quantity: 10
}
Object.keys(data).forEach(ele => {
  const dep = new Dep()
  let interVal = data[ele]
  Object.defineProperty(data, ele, {
    get: () => {
      dep.depend()
      console.log(ele, dep.subs)
      // 其中的属性以及subs, price的subs中有3个函数,quantity只有两个
      //   price, Array(1) []
      //   quantity, Array(1) []
      //   price, Array(2) [, ]
      //   quantity, Array(2) [, ]
      //   price, Array(3) [, , ]
      //   price, Array(3) [, , ]
      //   quantity, Array(2) [, ]
      return interVal
    },
    set: (val) => {
      interVal = val
      dep.notify()
    }
  })
})
function watcher(myFun) {
  target = myFun
  target()
  target = null
}
watcher(() => {
  total = data.price * data.quantity
})
watcher(() => {
  sum = data.price + data.quantity
})
watcher(() => {
  salePrice = data.price * 0.7
})


值为对象时的处理

上面就是响应式的基本原理,现在data中的值都为基本的类型值,很容易处理,如果其属性值仍然为对象呢?

值仍然为对象时,那么需要进行递归监听,将Object.defineProperty的监听操作进行相应的封装,使用observe函数进行值额类型判断
如果其值为对象,则对其中的每个属性设置getter与setter, 并且在设置getter与setter之前判断其值的类型,值为对象则继续添加getter与setter

let data = {
  // price: 5,
  // quantity: 10,
  obj: {
    a: 1,
    b: 2,
    c: {
      d: 3
    }
  }
}
function isObject(val) {
  return Object.prototype.toString.call(val) === '[object Object]'
}
function observe(val) {
  if (isObject(val)) {
    const keys = Object.keys(val)
    keys.forEach(key => {
      defineProperty(val, key)
    })
  }
}
function defineProperty(obj, key) {
  const dep = new Dep()
  let internalVal = obj[key]
  observe(internalVal)
  Object.defineProperty(obj, key, {
    get: function () {
      // TODO 外层对象改变,内部对象也应该更新依赖
      dep.depend()
      // target 函数为 target = () => { console.log('render obj', data.obj, data.obj.a, data.obj.b, data.obj.c, data.obj.c.d) }
      // obj 被访问5次,向其中添加了5次 watcher 中的 target 函数
      // obj 中的 c 属性,被访问两次,则其对应的 subs 中有两个 watcher 中的 target 函数
      // 为了避免向 subs 中添加重复的 target 函数,需要判断是否存在 this.subs.includes(target)
      console.log(`get key: ${key} dep: ${dep} subs: ${dep.subs}`)
      return internalVal
    },
    set: function(val) {
      internalVal = val
      dep.notify()
    }
  })
}
observe(data)
function watcher(myFun) {
  target = myFun
  target()
  target = null
}
watcher(() => {
  total = data.obj.a + data.obj.b + data.obj.c.d
  // console.log('render obj', data.obj, data.obj.a, data.obj.b, data.obj.c, data.obj.c.d)
})
// 修改其中的每一个属性的值,total 的值
total // 6
data.obj.a = 2
total // 7
data.obj.b = 3
total // 8
data.obj.c.d = 4
total // 9

上面对值为对象时进行了循环递归添加,那么当值为数组时怎么处理呢?


值为数组时的处理

let data = {
  arr: [1, 2, 3, {a: 4}, [5, 6]]
}
function observer(val) {
  if (isObject(val)) {
    const keys = Object.keys(val)
    keys.forEach(key => defineProperty(val, key))
  } else if(Array.isArray(val)) {
    // 对数组中的每一项绑定
    val.forEach(ele => defineProperty(val,))
  }
}

上面的代码中对数组中的每一项都会进行监听,然后在数组的每一项改变的时候都会进行依赖更新,由于数组的数据量可能会很大,这样会比较影响性能(我暂时是这么认为的)

将上面的代码优化一下,当数组的中的值是基本类型时,不进行监听,引用类型是再进行监听

let data = {
  arr: [1, 2, 3, [4, 5], {a:  7}
}
function observer(val) {
  if (isObject(val) || Array.isArray(val)) {
    addObserver(val)
  }
}
function addObserver(val) {
  if(isObject(val)) {
    const keys = Object.keys(val)
    keys.forEach(key => defineProperty(val, key))
  } else if(Array.isArray(val)) {
    val.forEach(ele => {
      observer(ele)
    })
  }
}
watcher(() => {
  total = data.arr[0] + data.arr[1] + data.arr[2] + data.arr[3][0] + data.arr[3][1] + data.arr[4].a
})
total // 22
data.arr[0] = 2 // 修改其中某个依赖的值
total // 22 未更新
data.arr[3][0] = 5 // 修改数组中的数组中的值
total // 22 未更新
data.arr[4].a = 8 // 修改数组中的对象的某个属性值
total // 25 更新

上面的方法没有监听数组中值为基本类型的值,但出现了一个问题,数组内嵌套数组不会被监听到,这也就是在vue中直接修改数组内的值或者数组中嵌套的数组中的值不会触发视图更新的原因,但是vue给我们提供了一些改变数组值的方法可以触发更新或者$set方法

那么vue中是怎么做到可以使用数组的方法修改数组以此来触发视图更新的呢?


对数组中的方法进行处理

// 用Array原型中的方法创建一个新对象
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
// 需要进行重写的方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(ele => {
  const original = arrayProto[ele]
  Object.defineProperty(arrayMethods, ele, {
    value: function(...args) {
      const result = original.apply(this, args)
      console.log(`调用 ${ele} 方法`)
      return result
    },
    enumerable: false,
    writable: true,
    configurable: true
  })
})
function protoAugment(target, proto) {
  targte.__proto__ = proto
}
function addObserver(val) {
  if (isObject(val)) {
    const keys = Object.keys(val)
    keys.forEach(key => defineProperty(val, key))
  } else if(Array.isArray(val)) {
    protoAugment(val, arrayMethods)
    val.forEach(ele => observe(ele))
  }
}

上面的代码执行后,调用data.arr数组的push, pop, shift, unshift, splice, sort, reverse都会打印出对应的提示,这样在调用相应的方法时都可以进行依赖分发。那么,问题又来了,怎么可以拿到依赖的dep呢?当我们调用这些方法时,是通过data.arr.push(3)这样的方式进行调用的,那么在push方法内部就可以通过this拿到data.arr这个数组,所以我们可以在这个数组中保存所需要的依赖dep,然后进行分发即可

let data = {
  arr: [1, 2, 3]
}
function observe(val) {
  if (isObject(val) || Array.isArray(val)) {
    return addObserver(val)
  }
}
function addObserver(val) {
  const dep = new Dep()
  Object.defineProperty(val, '__dep__', {
    value: dep,
    enumerable: false,
    writable: true,
    configurable: true
  })
  if (isObject(val)) {
    const keys = Object.keys(val)
    keys.forEach(key => defineProperty(val, key))
  } else if(Array.isArray(val)) {
    val.forEach(ele => observe(ele))
  }
  return val
}
function defineProperty(val, key) {
  const dep = new Dep()
  const internalVal = val[key]
  const childOb = observe(internalVal) // 获取到值为引用类型的 __dep__
  Object.defineProperty(val, key, {
    get: function() {
      dep.depend()
      if (childOb) {
        childOb.__dep__.depend() // 添加依赖,当调用 push 等方法时进行依赖分发
      }
    },
    set: function(newVal) {
      internalVal = newVal
      dep.notify()
    }
  })
}
methodsToPatch.forEach(ele => {
  const original = arrayProto[ele]
  Object.defineProperty(arrayMethods, ele, {
    value: function(...args) {
      const result = original.apply(this, args)
      const dep = this.__dep__
      console.log(`调用 ${ele} 方法`)
      dep.notity()
      return result
    },
    enumerable: false,
    writable: true,
    configurable: true
  })
})
total // 6
data.arr.push(4)
total // 10 值进行了更新

以上内容为本人理解,如有不对处,请指正

仓库地址 欢迎 star
参考:Build a Reactivity System - Advanced Components | Vue Mastery


链接: https://www.fly63.com/article/detial/7144

移动端web app要使用rem实现自适应布局:font-size的响应式

rem是相对于根元素html,这样就意味着,我们只需要在根元素确定一个px字号,则可以来算出元素的宽高。

使用现代CSS的响应式版面

通过模块化缩放,使用传统属性和calc()来动态缩放你的字体大小.为字体大小使用百分比.给文本内容和媒体查询使用em,针对不同视口尺寸使用不同缩放值.视口越小,缩放比例越小,使用媒体查询或者media()函数基于视口来改变比例和基础字号

web响应式图片的5种实现

在目前的前端开发中,我们经常需要进行响应式的网站开发。本文着重介绍一下弹性图片,也就是响应式图片的解决方案:js或服务端、srcset 、sizes 、picture标签、svg图片

HTML5+CSS3响应式垂直时间轴,高端,大气

HTML5+CSS3响应式垂直时间轴,使用了HTML5标签<section>,时间轴中所有的内容包括标题、简介、时间和图像都放在.cd-timeline-block的DIV中,多个DIV形成一个序列,并把这些DIV放在<section>中。

实现响应式_CSS变量

CSS 变量是 CSS 引入的一个新特性,目前绝大多数浏览器已经支持了,它可以帮助我们用更少的代码写出同样多的样式,大大提高了工作效率,本篇文章将教你如何使用 CSS 变量(css variable)。CSS中原生的变量定义语法是:--*,变量使用语法是:var(--*),其中*表示变量名称

vue响应式原理及依赖收集

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

vue响应式系统--observe、watcher、dep

Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的JavaScript 对象,而当你修改它们时,视图会进行更新,这使得状态管理非常简单直接,我们可以只关注数据本身

Responsive Web Design 响应式网页设计

常见的布局方案:固定布局:以像素作为页面的基本单位,不管设备屏幕及浏览器宽度,只设计一套尺寸;可切换的固定布局:同样以像素作为页面单位,参考主流设备尺寸

响应式布局的实现

响应式布局,即 Responsive design,在实现不同屏幕分辨率的终端上浏览网页的不同展示方式。通过响应式设计能使网站在手机和平板电脑上有更好的浏览阅读体验。响应式布局的关键不仅仅在于布局

深入响应式原理

说到响应式原理其实就是双向绑定的实现,说到 双向绑定 其实有两个操作,数据变化修改dom,input等文本框修改值的时候修改数据1. 数据变化 -> 修改dom;2. 通过表单修改value -> 修改数据

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!