[Vue响应式原理]Object.defineProperty实现观察者机制的探索

下面这段话:Vue3.0版本中将基于Proxy来改造观察者模式。说明Vue3.0讲不再借助于ES5的Object.defineProperty,转而使用最新的Proxy语法实现Vue最根本的响应式原理(注又名:数据劫持,下文统称响应式原理)。
下文主要简述从Object.defineProperty到proxy的实现观察者机制探索,目前关于深入响应式原理的文章已经很多了,很多都写的很好,本文不做过深的vue里的源码解析,只是浅入探索和自己动手手写一个简易的Object.defineProperty实现观察者机制,以及手写一个简易的由Proxy实现观察者机制,当然最终以作者发布为准。主要有以下几个知识点带大家一起进入

1、Object.defineProperty实现观察者机制
2、Object.defineProperty的缺点
3、利用proxy实现简易的实现观察者机制
4、总结
一、Object.defineProperty实现观察者机制
这里我们照顾一下小白同学,首先我们来补充一下ES5中对Object.defineProperty() 方法的定义和一些基础知识,然后手写一个简易的响应式,最后再结合vue源码简析

1.1 Object.defineProperty基础知识
在developer.mozilla.org对Object.defineProperty()定义

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象
语法:
Object.defineProperty(obj, prop, descriptor)
参数说明:
1、obj要在其上定义属性的对象
2、要定义或修改的属性的名称
3、将被定义或修改的属性描述符
返回值:
被传递给函数的对象

这里我们重点关注一下语法中的第三个参数属性描述符:descriptor

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。这里descriptor有6个选键值configurable、enumerable、value、writableget、set这里他们分别的介绍可以移步

1.2 创建对象
通常我们创建对象来一步一步了解这个Object.defineProperty() 方法和属性描述符descriptor里面键值的用法

【1】正常我们创建一个对象,如下,然后控制台打印他们我们可以看到

let vm = {
   name: '简书'
 }
 console.log(vm)
image.png

【2】接下来我们通过Object.defineProperty创建一个对象,并设置这个对象要定义或者修改的属性“name”

let vm = Object.defineProperty({},"name",{
  get() {
    console.log("执行get");
    return name || "简书"
  },
  set(newVal) {
    console.log("执行set");
    console.log("新值:" + newVal);
    if (newVal !== name) {
      name = newVal;
    }
  }
})

其实这两种都创建一个对象的方式,通过Object.defineProperty创建的对象,我们可以看到,多了上面说的两个存取描述符键值方法get 和set 这样的对象,就变得可控被观察,也就是我们说的被劫持,当我们改变或者获取这对象的属性的时候,我们就可以控制到它

下面我们通过改变vm.name = "1",我们通过控制台可以看到
image.png

1.3 实现观察者机制,响应式对象

let vm = {
  id:"jianshu",
  name:"简书"
};
let keys = Object.keys(vm);
keys.forEach(key=>{
  let value = vm[key];
  Object.defineProperty(vm, key,{
    get() {
      console.log("执行get");
      return value
   },
   set(newVal){
      console.log("执行set");
      if(newVal !== value){
         value =  newVal;
       }
     }
  })
})
vm.id = "test";
console.log(vm)
image.png

这里主要是遍历对象中的每一个属性,每个属性都是赋予get和set,让对象中的每一个属性的改变都会被监测到,也就是实现了响应式

1.4 vue源码中的响应式原理简析
上面的例子我们试一下,用数组对象发现是不能生效的,那么在vue里数组是怎么实现响应式原理的呢,我们可以看到vue源码目录src/core/observer/index.js里,其实他是对对象进行了判断,如果是数组对象,就会走observeArray()方法,而且你会发现里面还有一个arrayMethods,里面是对数组的 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'进行了重写,重写过后的方法不仅能实现原有的功能,还能发布消息给订阅者。其他对象,都走walk()方法。
1.5 把数据渲染到页面上
当我们检测到对象更新了,如何同步更新到页面上呢?
1、首先,我们要找到作用域范围内(vue,里会有个 el:"#app")的节点,全部页面内容都会渲染到这个结点里面
2、然后遍历结点上所有含有使用该对象的地方,也就是Mustache语法 (双大括号) 的文本插值的地方,例如 {{ vm }}
3、绑定视图更新
二、Object.defineProperty的缺点
2.1 无法监听对象非已有的属性的添加和已有属性的删除
只会对对象原有的全部属性进行做数据劫持,也就是说Vue 不允许动态添加或者删除对象已有属性,它是不做数据劫持的,也就不能实现响应式

举例

<template>
<div>

<h1>{{ vm }}</h1>
<button @click="addAttribute">新增属性</button>
<button @click="delAttribute">删除属性</button>
</div>
</template>

<script>
export default {
data() {

return { 
  vm:{
    id:"juejin",
    name:"掘金"
  }
}
},
methods: {

addAttribute() {
  this.vm.use = "codercao"
  console.log(this.vm)
},
delAttribute() {
  for(let k in this.vm) {
   if(k=='id'){
     delete this.vm[k]
   }
  }
  console.log(this.vm)
}
},
}
</script>

新增,你会发现控制台打印的vm已经新增了use属性,而页面并没有响应式改变
删除,你会发现控制台打印的vm已经删除了id属性,而页面并没有响应式改变
2.2 数组变异
数组对象也不能通过属性或者索引控制数组,比如length,index实现响应式,通过1.4 里我们也看到vue源码只是对数组的 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'进行了重写,但是索引控制数组是没有办法实现响应式的

2.3 解决以上的办法。 使用Vue.set(object, propertyName, value) 方法
改进上面2.1里的新增属性方式,你会发现页面就实现了响应式,至于Vue.set方法介绍移步

addAttribute() {
 //this.vm.use = "codercao"
 this.$set(this.vm,'use','codercao')
  console.log(this.vm)
},

三、利用proxy实现简易的实现观察者机制
3.1proxy基础知识
Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)
语法
let p = new Proxy(target, handler);
参数说明
target用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
handler一个对象,其属性是当执行一个操作时定义代理的行为的函数

let vm = {

  id:"jianshu",
  name:"简书"
}
let newVm = new Proxy(vm,{
  get(target,key){
    console.log("执行get");
    return target[key];
   },
  set(target,key,newVal){
    console.log("执行set");
    if(target[key]!==newVal)
    target[key] = newVal;
  }
})
newVm.use = "codercao"
console.log(newVm)

你会发现用Proxy 也一样实现了一个简易的观察者机制,当然深入研究的话,你还可以实现双向绑定。

四、结尾
到这里我们这篇文章到此就结束了,至于最终作者会怎么用proxy来写这个观察者机制,待vue3.0发布可以一看究竟,文章主要是带大家实践探索一下Object.defineProperty实现观察者机制,顺便提了一下,这个Object.defineProperty缺陷和处理办法,然后引入proxy,属于比较初级的尝试,Vue发展到现在几年了,其实大部分人对其应用已经游刃有余了,关注源码和实践vue里功能的原理,或许对每个前端人都会有一些提升。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容