vue3.0尝鲜 -- 摒弃Object.defineProperty,基于 Proxy 的观察者机制探索

写在前面: 11月16日早上,Vue.js的作者尤大大在 Vue Toronto 的主题演讲中预演了 Vue.js 3.0的一些新特性,其中一个很重要的改变就是Vue3 将使用 ES6的Proxy 作为其观察者机制,取代之前使用的Object.defineProperty。我相信许多同学深有体会,许多面试中Object.defineProperty是vue这个框架一个出现率很高的考察点,一开始大家对这个属性还有点陌生,慢慢的随着使用vue的人越来越多,这个属性经常被大家拿来研究,而就在大家渐渐熟悉了这个属性以后,vue的作者打算在下个vue版本中用 Proxy替换它,果然一入前端坑就爬不出来了哈哈。虽然vue3正式发布要等到明年下半年了,但我们下面可以来探索下基于 Proxy 的观察者机制,预测下vue3关于Proxy这部分的代码(虽然看上去并没有什么用哈哈)。

一.为什么要取代Object.defineProperty

既然要取代Object.defineProperty,那它肯定是有一些明显的缺点,总结起来大概是下面两个:

  • Object.defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。 为了解决这个问题,经过vue内部处理后可以使用以下几种方法来监听数组
push()
pop()
shift()
unshift()
splice()
sort()
reverse()

由于只针对了以上八种方法进行了hack处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。

  • Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。

而要取代它的Proxy有以下两个优点;

  • 可以劫持整个对象,并返回一个新对象
  • 有13种劫持操作

看到这可能有同学要问了,既然Proxy能解决以上两个问题,而且Proxy作为es6的新属性在vue2.x之前就有了,为什么vue2.x不使用Proxy呢?一个很重要的原因就是:

  • Proxy是es6提供的新特性,兼容性不好,最主要的是这个属性无法用polyfill来兼容

相信尤大大在vue3.0的版本中会有效的提供兼容解决方案。

关于Object.defineProperty来实现观察者机制,可以参照剖析Vue原理&实现双向绑定MVVM这篇文章,下面的内容主要介绍如何基于 Proxy来实现vue观察者机制。

二.什么是Proxy

1.含义:

  • Proxy是 ES6 中新增的一个特性,翻译过来意思是"代理",用在这里表示由它来“代理”某些操作。
    Proxy 让我们能够以简洁易懂的方式控制外部对对象的访问。其功能非常类似于设计模式中的代理模式。
  • Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
  • 使用 Proxy 的核心优点是可以交由它来处理一些非核心逻辑(如:读取或设置对象的某些属性前记录日志;设置对象的某些属性值前,需要验证;某些属性的访问控制等)。 从而可以让对象只需关注于核心逻辑,达到关注点分离,降低对象复杂度等目的。

2.基本用法:

let p = new Proxy(target, handler);

参数:

target 是用Proxy包装的被代理对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

handler 是一个对象,其声明了代理target 的一些操作,其属性是当执行一个操作时定义代理的行为的函数。

p 是代理后的对象。当外界每次对 p 进行操作时,就会执行 handler 对象上的一些方法。Proxy共有13种劫持操作,handler代理的一些常用的方法有如下几个:

get:读取
set:修改
has:判断对象是否有该属性
construct:构造函数

3.示例:

下面就用Proxy来定义一个对象的get和set,作为一个基础demo

 let obj = {};
 let handler = {
   get(target, property) {
    console.log(`${property} 被读取`);
    return property in target ? target[property] : 3;
   },
   set(target, property, value) {
    console.log(`${property} 被设置为 ${value}`);
    target[property] = value;
   }
 }
 
 let p = new Proxy(obj, handler);
 p.name = 'tom' //name 被设置为 tom
 p.age; //age 被读取 3

p 读取属性的值时,实际上执行的是 handler.get() :在控制台输出信息,并且读取被代理对象 obj 的属性。

p 设置属性值时,实际上执行的是 handler.set() :在控制台输出信息,并且设置被代理对象 obj 的属性的值。

以上介绍了Proxy基本用法,实际上这个属性还有许多内容,具体可参考Proxy文档

三.基于Proxy来实现双向绑定

话不多说,接下来我们就来用Proxy来实现一个经典的双向绑定todolist,首先简单的写一点html结构:

 <div id="app">
      <input type="text" id="input" />
      <div>您输入的是: <span id="title"></span></div>
      <button type="button" name="button" id="btn">添加到todolist</button>
      <ul id="list"></ul>
 </div>

先来一个Proxy,实现输入框的双向绑定显示:

    const obj = {};
    const input = document.getElementById("input");
    const title = document.getElementById("title");
    
    const newObj = new Proxy(obj, {
      get: function(target, key, receiver) {
        console.log(`getting ${key}!`);
        return Reflect.get(target, key, receiver);
      },
      set: function(target, key, value, receiver) {
        console.log(target, key, value, receiver);
        if (key === "text") {
          input.value = value;
          title.innerHTML = value;
        }
        return Reflect.set(target, key, value, receiver);
      }
    });

    input.addEventListener("keyup", function(e) {
      newObj.text = e.target.value;
    });

这里代码涉及到Reflect属性,这也是一个es6的新特性,还不太了解的同学可以参考Reflect文档.
接下来就是添加todolist列表,先把数组渲染到页面上去:


     // 渲染todolist列表
    const Render = {
      // 初始化
      init: function(arr) {
        const fragment = document.createDocumentFragment();
        for (let i = 0; i < arr.length; i++) {
          const li = document.createElement("li");
          li.textContent = arr[i];
          fragment.appendChild(li);
        }
        list.appendChild(fragment);
      },
      addList: function(val) {
        const li = document.createElement("li");
        li.textContent = val;
        list.appendChild(li);
      }
    };

再来一个Proxy,实现Todolist的添加:

    const arr = [];
    // 监听数组
    const newArr = new Proxy(arr, {
      get: function(target, key, receiver) {
        return Reflect.get(target, key, receiver);
      },
      set: function(target, key, value, receiver) {
        console.log(target, key, value, receiver);
        if (key !== "length") {
          Render.addList(value);
        }
        return Reflect.set(target, key, value, receiver);
      }
    });

    // 初始化
    window.onload = function() {
      Render.init(arr);
    };

    btn.addEventListener("click", function() {
      newArr.push(parseInt(newObj.text));
    });

这样就用 Proxy实现了一个简单的双向绑定Todolist,具体代码可参考proxy.html

四.基于Proxy来实现vue的观察者机制

1.Proxy实现observe

    observe(data) {
        const that = this;
        let handler = {
         get(target, property) {
            return target[property];
          },
          set(target, key, value) {
            let res = Reflect.set(target, key, value);
            that.subscribe[key].map(item => {
              item.update();
            });
            return res;
          }
        }
        this.$data = new Proxy(data, handler);
      }

这段代码里把代理器返回的对象代理到this.$data,即this.$data是代理后的对象,外部每次对this.$data进行操作时,实际上执行的是这段代码里handler对象上的方法。

2.compile和watcher

比较熟悉vue的同学都很清楚,vue2.x在 new Vue() 之后。 Vue 会调用 _init 函数进行初始化,它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。其中最重要的是通过 Object.defineProperty 设置 setter 与 getter 函数,用来实现「响应式」以及「依赖收集」。类似于下面这个内部流程图:

image

而我们上面已经用Proxy取代了Object.defineProperty这部分观察者机制,而要实现整个基本mvvm双向绑定流程,除了observe还需要compile和watche等一系列机制,我们这里像模板编译的工作就不展开描述了,为了实现基于Proxy的vue添加Totolist,这里只写了
compile和watcher来支持observe的工作,具体代码参考proxyVue,这个代码相当于一个基于Proxy的一个简化版vue,主要是实现双向绑定这个功能,为了方便这里把js放到了html页面中,大家本地运行后可以发现,现在的效果和第三章的效果达到一致了,等到明年vue3发布,它源码里基于 Proxy实现的的观察者机制可能和这里的实现会有很多不同,这篇文章主要是对 Proxy这个特性做了一些介绍以及它的一些应用,而作者本人也通过对Proxy 的观察者机制探索学到了不少东西,所以整合资源,总结出了这篇文章,希望能和大家共勉之,以上,我们下次有缘再见。

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

推荐阅读更多精彩内容

  • 面试官: 实现双向绑定Proxy比defineproperty优劣如何? 面试官系列(4): 实现双向绑定Prox...
    流动码文阅读 23,004评论 6 77
  • 来自深入理解ES6第十二章,由于最近业务中经常用到,记录一下 这里内容都太学术了,有一篇简单介绍Proxy作用的文...
    NowhereToRun阅读 1,004评论 0 1
  • defineProperty() 学习书籍《ECMAScript 6 入门 》 Proxy Proxy 用于修改某...
    Bui_vlee阅读 648评论 0 1
  • 投资果然是每天都可以精进的事。因为直接涉及到金钱利益,倒逼着自己必须认真观察市场的变化、思考投资决策的过程。其本质...
    唯思不破阅读 152评论 0 0
  • 第一次相遇 那一次别离 最后一次重逢 我都会安排在天桥上 在城市的缝隙里 爱来了 花开了 人散了 曲终了
    VVVVViliant阅读 167评论 0 0