Vue3 双向绑定——Proxy

上一期我用一个山寨的Vue class演示了vue响应式开发中双向绑定的实现。小结留了个尾巴——vue3将会用新的方式实现双向绑定。这一期就来介绍一下新的实现方式——Proxy

前情回顾

回忆一下vue2响应式设计的实现:

Object.keys(data).forEach((prop) => {
    const dep = new Dep();

    Object.defineProperty(data, prop, {
        get () {
            dep.depend();
            return Reflect.get(data, prop);
        },
        set (newVal) {
            Reflect.set(data, prop, newVal);
            dep.notify();
        }
    });
});

vue2利用Object.defineProperty来劫持data数据的getter和setter操作。这使得data在被访问或赋值时,动态更新绑定的template模块。不过,Object.defineProperty有一些天然的缺陷,而这些缺陷是es2015中Proxy可以解决的。我们下来慢慢介绍Proxy的解决之道。

代替循环遍历

在使用Object.defineProperty时,我们必须循环遍历所有的域值才能劫持每一个属性,说实在这就是个hack。

Object.keys(data).forEach((prop) => { ... }

而Proxy的劫持手段则是官宣标准——直接监听data的所有域值。

//data is our source object being observed
const observer = new Proxy(data, {
    get(obj, prop) { ... },
    set(obj, prop, newVal) { ... },
    deleteProperty() {
        //invoked when property from source data object is deleted
    }
})

Proxy构造函数的第一个参数是原始数据data;第二个参数是一个叫handler的处理器对象。Handler是一系列的代理方法集合,它的作用是拦截所有发生在data数据上的操作。这里的get()和set()是最常用的两个方法,分别代理访问赋值两个操作。在Observer里,它们的作用是分别调用dep.depend()dep.notify()实现订阅和发布。直接反映在Vue里的好处就是:我们不再需要使用Vue.$set()这类响应式操作了。除此之外,handler共有十三种劫持方式,比如deleteProperty就是用于劫持域删除。

Proxy具体实现

尽管Proxy API还没Merge到Vue项目里,但是我们可以大概猜测一下它的实现。改写一下上一版本的Observer

class Observer {

    static defineReactive(data) {

        let deps = new Map();

        function depReflect(prop, func) {
            if( !deps.has(prop) )
                deps.set(prop, new Dep());
            const dep = deps.get(prop);

            return func.call(dep);
        }

        return new Proxy(data, {
            get(obj, prop) {
                depReflect(prop, Dep.prototype.depend);
                return Reflect.get(obj, prop);
            },
            set(obj, prop, newVal) {
                Reflect.set(obj, prop, newVal);
                depReflect(prop, Dep.prototype.notify);
            }
        })
    }
}

  • Dep类还是和以前一模一样。我写了一个Dep Map来存储data各个域的依赖。

  • depReflect(prop, func)用于实现订阅发布功能。

  • get(obj, prop)用于代理原始对象data[prop]的getter(),并调用Dep的depend()实现订阅者watcher注册。

  • set(obj, prop, newVal)用于代理data[prop]的setter(),在域赋值时触发(如:observer[prop] = newVal);并把消息发布给订阅者watcher。

Observer Proxy

升级改造

上一版本山寨VUE是这么构造的:

class Vue {
    constructor({data}) {
        this.$data = data();
        new Observer(this.$data);
        Object.keys(this.$data).forEach( this.proxy.bind(this) );
    }
    // omit others
}

用Proxy改写后,稍微精简了一点,this.$data由Observer作为工厂创建。

class Vue {
    constructor({data}) {
        this.$data = Observer.defineReactive(data());
        Object.keys(this.$data).forEach( this.proxy.bind(this) );
    }
    // omit others
}

再跑一次上期的测试:

let watcher = function () {
  const total = this.price * this.quantity;
  console.log(`total = ${total}`)
};

vm.$mount( watcher );  // total = 10

vm.price = 100;     // total = 200
vm.quantity = 100; // total = 10000

输出依旧是10 200 10000,成功实现Proxy改造。

Object.defineProperty的缺陷

那么问题来了,为什么需要Proxy改造Object.defineProperty呢?

原因在于Object.defineProperty有先天缺陷——无法监听数组变化。而Vue文档提到它能检测如下八种数组操作操作;但很有趣的是:vm.items[0] = 1这种操作是无法检测的。原因还是在于作者使用了hack的方式实现了这八种操作,而vm.items[0] = 1实在是hack不了了。

push()
pop()
shift()
unshift()
splice()
sort()
reverse()

至于为什么当时非得使用Object.defineProperty而不是Proxy,原因还是浏览器兼容所限。至今IE仍不支持Proxy,polyfill也无法抹平。即便作者在重写vue3的时候,还是为原始浏览器保留了Object.defineProperty的实现。

Proxy优势

Proxy在ES2015规范中正式发布,它是浏览器底层实现的一种对象拦截器,原生支持JS数组操作(push、shift、splice等等)。

const list = [1, 2];

const observer = new Proxy(list, {
  set: function(obj, prop, value, receiver) {
    console.log(`prop: ${prop} is changed!`);
    return Reflect.set(...arguments);
  },
});

observer.push(3);
observer[3] = 4;

上面这个例子的打印结果是:

prop: 2 is changed!
prop: length is changed!
prop: 3 is changed!

很显然,得利于浏览器原生支持,Proxy不需要各种hack技术就可以无压力监听数组变化;甚至有比hack更强大的功能——自动检测length。除此之外,Proxy还有多达13种拦截方式,包括constructdeletePropertyapply等等操作;而且性能也远优于Object.defineProperty,这应该就是所谓的新标准红利吧。

小结

由于一些历史原因,vue只能使用Object.defineProperty实现双向绑定。这在当时是一种很前卫的设计,不过随着浏览器的不断迭代,这种技术在api和性能上愈发跟不上时代的步调。重写vue可以说是顺应历史潮流吧。

这是我写VUE源码设计的第三期,以后还会不定期更新。框架千变万化,但机理还是逃不过语言特性、数据结构和设计模式。我学习源码并没有什么功利的目的,想的还是拓展认知和巩固基础。当遇到特殊问题或是超过框架认知的需求时,其实最可靠的还是我们的基本功。

相关博客

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

推荐阅读更多精彩内容