美文网首页饥人谷技术博客
JavaScript 之实现响应式数据

JavaScript 之实现响应式数据

作者: 临安linan | 来源:发表于2019-11-08 20:03 被阅读0次

更多个人博客:(https://github.com/zenglinan/blog)

如果对你有帮助,欢迎star。

数据响应式:

顾名思义,数据响应式就是当我们修改数据时,可以监听到这个修改,并且作出相应的响应。

一. 监测 Object 对象

需求:当我们修改 obj 对象时,触发 update 方法。

思路:使用 Object.defineProperty 对数据进行劫持,每次修改的时候都会执行 set 方法,在 set 内部可以进行响应更新

编写第一版代码:

function isObject(obj){
  return obj.constructor === Object
}

function update(){  // 更新响应
  console.log('updated!')
}

function observer(obj){ // 监测对象
  if(!isObject(obj)) return
  for(let key in obj){  // 对每个属性进行 Object.defineProperty 定义
    defineReactive(obj, key, obj[key])
  }
}

function defineReactive(obj, key, value){ // 数据劫持
  Object.defineProperty(obj, key, {
    get(){
      return value
    },
    set(newValue){  // 修改时,触发 update 方法
      update()
      value = newValue
    }
  })
}

let obj = {a: 1}
observer(obj)
obj.a = 3 // updated! 

当我们修改 obj 中通过 Object.defineProperty 定义的属性时,会触发 set 方法,触发更新。

第一版编写完成,已经实现了基础功能,但是有两个问题:

  1. 对于形如 {a: {b: 1}} 嵌套的对象,无法进行任意深度的监测,因为无法知道对象嵌套了几层,只能用递归进行监测。

  2. 修改的后值如果是一个对象,需要对这个对象也进行监测

obj.a = {c: 1}
obj.a.c = 3 // expected: updated!

我们对 defineReactive 进行一点修改即可:

function defineReactive(obj, key, value){
  observer(value) // 利用递归深度劫持:如果 value 还是对象,继续定义,直到 isObject 返回 false
  Object.defineProperty(obj, key, {
    get(){
      return value
    },
    set(newValue){
      if(isObject(newValue)){ // 如果新值为对象,对新值进行进行数据监测
        observer(newValue)
      }
      update()
      value = newValue
    }
  })
}

至此,我们实现了对对象数据的监测,当修改对象上的属性时,可以触发响应,并且这个对象可以是任意嵌套深度的,修改的新值也可以是任意深度嵌套的对象。

不足之处:给对象新增一个不存在的属性时,无法触发响应。

二. 监测数组

需求:当我们使用 push pop shift unshift reverse sort splice 方法修改数组时,会触发更新。

数组不能像对象那样用 Object.defineProperty 劫持修改,所以我们只能在上面说的这些方法上面下手,我们可以对这些方法进行重写。

但是要注意的是:重写不可以对使用这些 api 的其他地方产生影响

这里我们创建一个新的 Array 原型,然后改变需要监测的数组的原型,指向新的原型 ResponsiveArray

const ResponsiveArray = Object.create(Array.prototype);  // 创建新的 Array 原型
['pop', 'push', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
  // 对每个方法进行重写,挂载到 ResponsiveArray 上 
  ResponsiveArray[method] = function() {
    update()
    Array.prototype[method].apply(this, arguments)
  }
})

function observer(obj){
  if(Array.isArray(obj)){
    return Object.setPrototypeOf(obj, ResponsiveArray) // 改变原型
  }
}

function update(){
  console.log('updated!')
}

let arr = [1,2,3,4]
observer(arr)
arr.push(1,2,3) // updated!

以上,就实现了对普通对象和数组的监测。完整代码如下:

// 创建新的 Array 原型
const ResponsiveArray = Object.create(Array.prototype);

// 在新原型上重写数组方法
['pop', 'push', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
  ResponsiveArray[method] = function() {
    update()
    Array.prototype[method].apply(this, arguments)
  }
})

function update(){
  console.log('updated!')
}

function isObject(obj){
  return obj.constructor === Object
}

function observer(obj){
  if(Array.isArray(obj)){
    return Object.setPrototypeOf(obj, ResponsiveArray)  // 改变数组的原型
  }
  if(!isObject(obj)) return
  for(let key in obj){ // 对普通对象的每个属性进行监测
    defineReactive(obj, key, obj[key])
  }
}

function defineReactive(obj, key, value){// 数据劫持
  observer(value) // 递归调用,使得任意深度的对象可以被监测到
  Object.defineProperty(obj, key, {
    get(){
      return value
    },
    set(newValue){
      if(isObject(newValue)){ // 对修改后为对象的新值进行监测
        observer(newValue)
      }
      update()
      value = newValue
    }
  })
}

三. 利用 proxy 进行代理

function update(){
  console.log('updated')
}

let obj = [1,2,3]

const proxyObj = new Proxy(obj, {
  set(target, key, value){
    if(key === 'length') return true  // ①
    update()
    return Reflect.set(target, key, value)
  },
  get(target, key){
    return Reflect.get(target, key)
  }
})

proxyObj.push(12)
proxyObj[1] = 'xxx'

与 defineProperty 的区别:

  1. 可以对添加新属性进行代理
  2. 无需额外操作即可对数组进行代理,包括 push pop 等方法,以及修改指定索引的元素

需要注意的点是:修改数组元素时,除了插入元素之外,还会修改 length 属性,触发两次更新,如果想避免修改 length 触发更新,可以加上上面的①,对 length 的修改进行过滤。

但不足的是:此时不能实现任意嵌套深度的对象的代理。

因为对于形如 proxyObj.a.b = 1 的语句,首先会返回 proxyObj.a对返回值上的 b 进行修改,没有经过代理,所以也不会触发更新

所以我们只需要在返回的时候,返回经过 proxy 代理的值即可。

const handler = {
  set(target, key, value){
    if(key === 'length') return true
    update()
    return Reflect.set(target, key, value)
  },
  get(target, key){
    if(typeof target[key] === 'object'){
      return new Proxy(target[key], handler)  // 只要获取的是对象,就返回经过代理后的对象。
    }
    return Reflect.get(target, key)
  }
}

let proxyObj = new Proxy(obj, handler)
proxyObj.b.c = 'xxx'

感谢你看到了这里,更多个人博文戳这

本文正在参与“写编程博客瓜分千元现金”活动,关注公众号“饥人谷”回复“编程博客”参与活动。

相关文章

  • vue双向数据绑定的实现原理

    实现数据响应式 在Javascript里实现数据响应式一般有俩种方案,分别对应着vue2.x和 vue3.x使用的...

  • (二)Vue-模拟实现响应式

    数据驱动 数据响应式、双向绑定、数据驱动 数据响应式数据模型仅仅是普通的 JavaScript 对象,而当我们修改...

  • JavaScript 之实现响应式数据

    更多个人博客:(https://github.com/zenglinan/blog) 如果对你有帮助,欢迎star...

  • 学习笔记(十三)模拟 Vue.js 响应式原理

    模拟Vue.js响应式原理 数据驱动 数据响应式数据模型是普通的JavaScript对象,当我们修改数据时,视图会...

  • 双向绑定

    数据响应式原理 vue实现数据响应式的原理就是利用了Object.defineProperty(),重新定义了对象...

  • VUE笔记1

    渐进式声明式渲染 只需要声明在那里做什么,无需关心如何实现命令式 需要+如何实现响应的数据绑定 响应的数据绑定 ...

  • Vue的响应式浅析

    1 Vue如何实现响应式? Vue的响应式是建立在监听data中的数据. 2 在Vue2中响应式的实现 Vue通过...

  • Vue常见面试题

    8.6.1响应式数据的理解 概述:响应式数据的实现原理是使用ES5的Object.defineProperty,递...

  • 【vue3源码】一、认识副作用函数与响应式数据

    认识副作用函数与响应式数据 在分析vue3响应式的实现前,我们需要先搞清两个概念:副作用函数、响应式数据。 副作用...

  • Vue源码03-响应式原理

    这节将专门讲解vue MVVM响应式处理 Vue采用的是 实现数据的响应式 数据劫持 Observer类 Obse...

网友评论

    本文标题:JavaScript 之实现响应式数据

    本文链接:https://www.haomeiwen.com/subject/gliwbctx.html