长夜梦中惊坐起,Vue的双向绑定到底是个什么东西?

某天夜晚,我在沉沉的睡梦中突然惊醒0.0,想到做了这么久Vue了,到底它是肿么帮助我实现双向绑定的呢?当我陷入了沉思之中时,困意再次来袭。。。
今天想到了这个问题,我必须对的起这个噩梦!

对于这种我不懂的问题,第一时间就是去翻看官方的api。哦吼,果然有介绍,但是呢,这些字我都认识,但是为什么就是看不懂呢。
vue官方介绍
归根到底,是因为Object.defineProperty到底是个啥我不知道。

一、Object.defineProperty是什么玩意

object.defineProperty的介绍
object.defineProperty的介绍

obj和prop都很好理解,descriptor:属性描述符就有点难理解了。其实它是一个JavaScript对象隐藏的一些属性。其中包括

configurable:(Boolean类型 )该属性描述符是否可以改变。默认为 false。

enumerable:(Boolean类型 )该属性描述符是否可以枚举。默认为 false。

value:(数值,对象,函数等)任何有效的 JavaScript 值,默认为 undefined。

writable:(Boolean类型 )是否可以赋值运算,默认为 false。

get:(函数类型 )方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象),默认为 undefined。

set:(函数类型 )当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值,默认为 undefined。

说白了,这个object.defineProperty的方法就是一个定义和修改对象的操作,而vue主要是用到这个方法中的setget
我们来看一下,object.defineProperty里面的set和get都做了什么。

//正常情况下定义一个对象
let testObject={
	id='1',
	name='9527'
}
console.log(testObject.name) //'9527'


//如果我们用object.defineProperty
let testObject = {};
let name = '';
Object.defineProperty(testObject, 'name', {
    set(val) {
        name = val;
        console.log("从此你就叫", name)
    },
    get() {
        console.log('yes madam!');
        return '我是' + name + '!';
    }
});
//这里触发了对象的set方法,因此打印出来 ‘从此你就叫 9527’
testObject.name = '9527';
//这里触发了对象的get方法,因此打印出 
//‘yes madam!
//我是9527!’
console.log(testObject.name);

执行结果
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更。

二、vue如何根据setter追踪数据的变化

官方的追踪路径
网上找到的9

通过vue追踪的路径图可以看出,双向绑定在三个步骤:
1、组件渲染VMTree (Compile)
2、object.defineProperty中的getter/setter,来进行数据劫持 (Observer)
3、组件渲染的过程中把“接触”过的数据属性getter记录为依赖。之后当依赖项的 setter 触发时,会通知**(watcher)**,从而使它关联的组件重新渲染。

大致的思路摸清楚了,一个MVVM的框架核心点在于三个:Compile编译模板,Observer数据监听,watcher数据订阅。

  • Observer监听器,监听数据,一旦数据发生变化,通知Watcher
  • Watcher观察者,监测属性的变化,收到变化通知后,修改页面视图
  • Compile扫描和解析每个节点的相关指令,初始化页面的数据从而初始化Watcher。
    有了 总体的思路,我们一步步来实现一个MVVM的框架。

三、动手实现一个MVVM

1、创造一个Observer监听器
//创建一个监听器
function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    //轮询出对象内的属性名及属性值
    Object.keys(data).forEach(function (key) {
        defineReactive(data, key, data[key]);
    });
}
//定义响应
function defineReactive(data, key, val) {
    //如果属性内的值还是对象,继续往下轮询
    observe(val);
    //对象的属性添加getter和setter
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            return val;
        },
        set: function (newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal;
            console.log(key + '属性已经被监听,值为:' + newVal.toString());
        }
    });
}

//初始化一个对象
let policeman = {
    dept: {
        deptName: ''
    },
    id: '',
    name: ''
};
observe(policeman);
policeman.dept.deptId = 'fh';//此处由于初始化的对象里没有,因此不被监听
policeman.dept.deptName = '飞虎队';//deptName属性已经被监听,值为:飞虎队
policeman.id = '9527';//id属性已经被监听,值为:9527
policeman.name = '周星星';//name属性已经被监听,值为:周星星

这样就创建了一个能够监听数据变化的监听器,下一步引入Watcher观察者,这位的作用在于,收集数据的依赖,并更新数据同时提醒数据发生变化。

2、引入Watcher观察者

在引入观察者之前,先定义一个Dept用来接收Observer传递过来的数据,并且初始化和通知观察者

//给依赖数据开辟内存
function Dept() {
    this.watchers = []
}

//prototype方法用来给对象添加属性和方法,
//只作用在对象函数上
Dept.prototype = {
    //添加观察者
    addWatcher(watcher) {
        this.watchers.push(watcher)
    },
    //通知每一个观察者更新数据
    notify: function () {
        this.watchers.forEach(function (watcher) {
            watcher.update();        //通知每个观察者检查更新
        })
    }
};

有了存放观察者的地方了,开始创建观察者

function Watcher(vm, exp, cb) {
    this.vm = vm;    //指向vm的作用域
    this.exp = exp;  //绑定属性的key值
    this.cb = cb;    //闭包
    this.value = this.get();
}

Watcher.prototype = {
    //数据更新
    update: function () {
        this.run();
    },
    run: function () {
        let value = this.vm.data[this.exp];
        let oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    get: function () {
        Dep.target = this;                   // 缓存自己
        let value = this.vm.data[this.exp];  // 强制执行监听器里的get函数
        Dep.target = null;                   // 释放自己
        return value;
    }
};

修改之前Observer的get和set方法,在里面加入Dept

function defineReactive(data, key, val) {
    //如果属性内的值还是对象,继续往下轮询
    observe(val);
    //添加Dept
    let dept = new Dept();
    //对象的属性添加getter和setter
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            //判断是否需要添加观察者
            if (Dep.target) {
                dept.addWatcher(Dep.target);
            }
            return val;
        },
        set: function (newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal;
            console.log(key + '属性已经被监听,值为:' + newVal.toString());
            dept.notify(); // 如果数据变化,通知所有订阅者
        }
    });
};

这样整个数据层面的监听和观察就基本完成了,接下来研究如何实现页面数据的初始化已经编译解析。

3、dom解析器Compiler
function myMvvM(data,el,exp) {
    let self = this;
    this.data = data;
    //Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组
    Object.keys(data).forEach(function(key) {
        //绑定代理属性
        self.proxyKeys(key);     
    });
    //数据创建监听
    observe(data);
    // 初始化模板数据的值
    el.innerHTML = this.data[exp];  
    //数据创建观察者
    new Watcher(this,exp,function(value) {
    	//此处是一个回调函数,如何观察到数据变化时调用,如何返回到页面
        el.innerHTML = value;
    });
    return this;
}

myMvvM.prototype = {
    proxyKeys:function(key) {
        let self = this;
        Object.defineProperty(this,key,{
            enumerable:false,
            configurable:true,
            get:function proxyGetter() {
                return self.data[key];
            },
            set:function proxySetter(newVal) {
                self.data[key] = newVal;
            }
        });
    }
}

在html页面中使用myMvvM

<html>
<meta charset="utf-8">

<body>
<h1 id="name"></h1>
</body>

<script src="mvvm/observer.js"></script>
<script src="mvvm/Watcher.js"></script>
<script src="mvvm/index.js"></script>

<script>
	//获取页面的name标签
    var ele = document.querySelector('#name');//<h1 id="name"></h1>
    var myMvvM = new myMvvM({
        name: 'madam!,我是周星星'
    }, ele, 'name');
	
    window.setTimeout(function () {
        console.log('模拟2s后更新数据');
        myMvvM.name = '工号:9527';
    }, 2000);
</script>
</html>

好了这样一个简单的双向绑定就实现了,但是和vue中的似乎还差了点。上面的例子中并没有涉及到,DOM的解析,而是固定了一个节点作为模拟的数据,下面来实现Compiler解析器的解析和绑定。
解析过程比较复杂,可以理解为:

  • 解析模板并且替换掉模板的数据
  • 将模板指令对应的节点绑定对应的更新函数,初始化相应的观察者
    步骤为:
    1、先把页面中的 DOM 暂存在DocumentFragment中
    2、编译出元素节点(v-model、v-text…)和文本节点{{message}}
    3、将编译好的内容放回到页面中

Vue模板渲染是一个很大的点,虽然在使用中不太能意识到他的存在。由于这一个方面可研究的比较多,准备再单独写一篇文章记录一下。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值