详解Vue响应式原理

news/2024/7/19 13:43:20 标签: js, vue, typescript, 前端, es6

什么是响应式

我们先来看个例子:

<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响应式,那么这一切是怎么做到的呢?

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

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

如何侦测数据的变化

方法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 浪里行舟

上面这段代码的主要作用在于:observe这个函数传入一个 obj(需要被追踪变化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理,以此来达到实现侦测对象变化。值得注意的是,observe 会进行递归调用。

那我们如何侦测Vue中data 中的数据,其实也很简单:

class Vue {
    /* Vue构造类 */
    constructor(options) {
        this._data = options.data;
        observer(this._data);
    }
}

这样我们只要 new 一个 Vue 对象,就会将 data 中的数据进行追踪变化。 不过这种方式有几个注意点需补充说明:

无法检测到对象属性的添加或删除(如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]

方法2. 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-- // 无效

订阅者 Dep

class Dep {
    constructor () {
        /* 用来存放Watcher对象的数组 */
        this.subs = [];
    }
    /* 在subs中添加一个Watcher对象 */
    addSub (sub) {
        this.subs.push(sub);
    }
    /* 通知所有Watcher对象更新视图 */
    notify () {
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}

以上代码主要做两件事情:
用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。

观察者 Watcher

依赖收集的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。

.Watcher的简单实现

class Watcher {
  constructor(obj, key, cb) {
    // 将 Dep.target 指向自己
    // 然后触发属性的 getter 添加监听
    // 最后将 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 获得新值
    this.value = this.obj[this.key]
   // 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
    this.cb(this.value)
  }
}

收集依赖

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)  // 递归子属性
    let dp = new Dep() //新增
    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() //新增
        }
      }
    })
  }
}
​
class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
        /* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
        new Watcher();
        console.log('模拟视图渲染');
    }
}

http://www.niftyadmin.cn/n/1542365.html

相关文章

六、Flash Media Server3.5流传输实时视频

初学者系列教程的第六篇。此文告诉你如何组合使用Adobe Flash cs4 Professional和Adobe Flash Media Live Encoder3和Adobe Flash Media Server3.5创建来自网路摄影等的实施视频流。 作为一个乐于花费大量的时间在Flash上的Flash教师&#xff0c;我喜欢告诉我的学生如何在Flash…

5月22日上课笔记-js属性选择器、过滤选择器、鼠标事件

一、属性选择器[attr] 包含属性[attrvalue] 属性值[attr!value] 属性值不等于value[attr^value] 属性值以value开头[attr$value] 属性值以value结尾[attr*value] 属性值包含value值 二、过滤选择器1.基本过滤选择器2.可见性过滤选择器display:nonetype:hidd…

大白话带你理解防抖和节流

防抖和节流 防抖和节流都是阻止短时间内多次连续出发的函数 防抖 定义&#xff1a;触发事件后在规定时间内回调函数只能执行一次&#xff0c;如果在规定时间内又触发了该事件&#xff0c;则重新开始计算规定时间。 函数防抖如坐公交车&#xff0c;最后一个人上车&#xff0c;…

Oracle的substr函数简单用法(转)

转&#xff1a;http://www.cnblogs.com/nicholas_f/articles/1526063.html substr(字符串,截取开始位置,截取长度) //返回截取的字 substr(Hello World,0,1) //返回结果为 H *从字符串第一个字符开始截取长度为1的字符串 substr(Hello World,1,1) //返回结果为 H *0和1都是表…

[原创]网吧收银守护神(PUBWIN版),完全杜绝破解上网!

功能简介: 通过Pubwin后台&#xff08;网页形式&#xff09;的客户机列表得到当前未上机客户机信息&#xff0c;安全;通过WinPcap底层发包的方式获取客户机开机状态&#xff0c;准确度高&#xff0c;速度快;默认3分钟扫描一次&#xff0c;默认连续检测到3次为开机状态报警。如果…

this、new、call、apply、bind

this、call、apply、bind this的指向问题 在ES5中&#xff0c;this的指向始终坚持一个原理&#xff1a;this永远指向最后调用它的那个对象&#xff0c;注意是在ES5中。 Case1: var name "windowsName";function a() {var name "Cherry";console.log(thi…

js 所有数组方法

序 方法名称 使用说明 1 concat(数组1,数组2,...,数组N) 将多个数组结合成一个新的数组 2 join(分隔字符) 将数组结合成一个字符串&#xff0c;用特定字符来分开 3 pop() 将数组内最后一个组件删除&#xff0c;并返回该组件内容 4 push(组件1,组件2,...,组件N) 将一个或多个…

Android Camera解析(上) 调用系统相机拍摄照片

开发中我们常须要通过相机获取照片&#xff08;拍照上传等&#xff09;。一般通过调用系统提供的相机应用就可以满足需求&#xff1b;有一些复杂需求还须要我们自己定义相机相关属性&#xff0c;下篇我们会涉及到。首先我们来研究怎样简单调用系统相机应用来获取照片 GitHub地址…