基于Object.defineProperty实现双向数据绑定

双向数据绑定可算是前端领域经久不衰的热词,不管是前端开发还是面试都会有所涉及。而且不同的框架也想尽一切办法去实现这一特性,比如:
Knockout / Backbone --- 发布-订阅模式
Angular --- ‘脏检查’
Vue --- 'Object.defineProperty'

那么 双向数据绑定到底是什么?没图说个卵,直接上图

基于Object.defineProperty实现双向数据绑定_第1张图片

简单的说就是在数据UI之间建立双向的通信通道,当用户通过Function改变了数据,那么这个改变也会立即反应到UI上;或者说用户通过UI的操作,那么这些操作也会随之引起对应的数据变动。emmmmmm...没毛病!

既然本文标题是讨论Object.defineProperty,那么笔者就把当前火热的到Boom的国产框架:Vue.js 请出来,然后在了解完她实现双向数据绑定的原理之后,我们着手实现一个抽象派的双向数据绑定。那么那位朋友就说了,什么叫 抽象派 ?我估计吧,可能就是马(Vue)和马骨架的区别吧,TAT...

在介绍Vue的双向数据绑定之前,笔者还想多叨叨几句,如果某一天有人问你:Vue是如何实现双向数据绑定的? 姑且先在这里停顿下,思考下这个问题的答案...................
或许有朋友会脱口而出“数据劫持”,说的没错!的确就是“数据劫持”,但是还不够充分和不够精确。笔者在这里也谈下自己的一点点所见所闻所想:

  1. 不够精确:与其说是 数据劫持,更应该说是对数据对象的SetterGetter实现的劫持。
  2. 不够充分:为什么说不够充分?是因为 Object.defineProperty 仅仅是实现了对数据的监控,后续实现对UI的重新渲染并不是它做的,所以这里还涉及到 发布-订阅模式(有兴趣的朋友戳这里);过程是,当监控的数据对象被更改后,这个变更会被广播给所有订阅该数据的watcher,然后由该 watcher实现对页面的重新渲染。

下面进入正题,一起了解下Vue实现双向数据绑定的原理,果断上图:

基于Object.defineProperty实现双向数据绑定_第2张图片

首先,Vue的Compile模块会对Vue的 template 代码进行编译解析并生成一系列的watcher,也可以称之为“更新函数”,它负责把变更后的相关数据重新渲染到指定的地方。举个栗子:

Compile会解析出 v-moel 这个指令并且生成 watcher 并连接数据中的 message 和当前这个Dom对象,一旦收到这个message被变更的通知,watcher就会根据变更对这个Dom进行重新渲染。

当然一个页面或者一个项目中肯定有很多watcher,因此Vue使用了Dep这个对象来存储每一个watcher,当数据发生变更,Observer会调用Dep的notify方法以通知所有订阅了该数据的watcher,让它们醒醒该干活了...

Vue的双向数据绑定也说得差不多了,下面就开始顺着这个思路着手写一个吧,毕竟说得多不如code来得好啊!!!具体的实现效果如下,Let‘s do it
基于Object.defineProperty实现双向数据绑定_第3张图片

不知道为什么GIF上传不了,所以只能将就用图片了,QAQ....

功能就用文字解释下:
第一个行的 title 0 直接显示的是数据,以便观察;我们可以在输入框中输入任何int, 然后点击 “加”可以实现对数值的 +1 操作,同时输入框的数值和 title 也会随之变化;当然,通过输入数值,title也会跟着变化。

首先把Html代码呈上来:





    
    
    
    Object.defineProperty实现双向绑定



    

然后开始一步一步在index.js里写代码吧
1) 首先我们先定义一个数据源

//数据源
let vm = {
    value: 0
}

2) 然后定义一个Dep,用于存储watcher

//用于管理watcher的Dep对象
let Dep = function () {
    this.list = [];
    this.add = function(watcher){
        this.list.push(watcher)
    },
    this.notify = function(newValue){
        this.list.forEach(function (fn) {
            fn(newValue)
        })
    }
};

3) 模拟Compile出来的watchers,该demo涉及到两个地方的重新render,一个是title,另一个是输入框。所以写两个watcher,然后存入Dep

// 模拟compile,通过对Html的解析生成一系列订阅者(watcher)
function renderInput(newValue) {
    let el = document.getElementById('inp');
    if (el) {
        el.value = newValue
    }
}

function renderTitle(newValue) {
    let el = document.getElementById('h1');
    if (el) {
        el.innerHTML = newValue
    }
}

//将解析出来的watcher存入Dep中待用
let dep = new Dep();
dep.add(renderInput);
dep.add(renderTitle)

4) 使用 Object.defineProperty 定义一个Observer

function observer(vm, key, value) {
    Object.defineProperty(vm, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            console.log('Get');
            return value
        },
        set: function (newValue) {
            if (value !== newValue) {
                value = newValue
                console.log('Update')

                //将变动通知给相关的订阅者
                dep.notify(newValue)
            }
        }
    })
}

5) 再将页面使用的两个方法写出来。(Vue使用的是指令对事件进行绑定,但是本文不涉及指令,所以用最原始的方法绑定事件)

//页面引用的方法
function inputChange(ev) {
    let value = Number.parseInt(ev.target.value);
    vm.value = (Number.isNaN(value)) ? 0 : value;
}

function btnAdd() {
    vm.value = vm.value + 1;
}

主要的代码都写好后,下面第一件事就是初始化

//数据初始化方法
function initMVVM(vm) {
    Object.keys(vm).forEach(function (key) {
        observer(vm, key, vm[key])
    })
}

//初始化数据源
initMVVM(vm)

//初始化页面,将数据源渲染到UI
dep.notify(vm.value);

这样一个简单的基于 Object.defineProperty 的双向数据绑定就完成了。看完的朋友有没有对双向数据绑定有了更多的理解了呢?如果没有理解的话,可以将代码复制到本地,然后循着代码再运行下,或许能容易理解。当然这里的代码并不高深,只是从浅层去谈论了双向数据绑定,所以有不足或者表达错误的地方,烦请各位朋友多多指正。

这里是源码,由于放不了动图,所以有兴趣的小伙伴可以拿下来

最后还是补充一句,Object.defineProperty虽然好用,但并不是无懈可击的,它对数组数据的处理并没有想象中的好甚至表现很差,因此Vue团队专门为Vue中的数组类型编写了额外的方法以实现对数组的正确监控
。因此,ES6中的Proxy挺身而出,拯救了ES5 中 Object.defineProperty对数组数据处理的不足。有兴趣的朋友请期待笔者的下一篇博客,讨论下用Proxy实现双向数据绑定。

咱们下期再见!!

你可能感兴趣的