前端|Vue双向绑定实现原理(一) 数据劫持


1.1 数据劫持 1.1.1 如何监控一个数据
vue可以直接通过v-model这个指令来实现双向绑定,这是react和小程序都没有,小程序是单向绑定,只能将data中的对象和基本数据类型展示在视图上,却没有办法通过视图来控制data中的数据,需要通过this.setData({})给出一个对象,重新设置数据,达到视图更新。
前端|Vue双向绑定实现原理(一) 数据劫持
文章图片

要达到如图1-1的效果,就要对数据进行监控,只有监控了数据的变化,在数据变化之后,通知视图去自主更新,这就是双向绑定的思路。这个思路很明显涉及到“监控” “更新”两个关键词,就可以联想到观察者模式。
观察一个数据,一旦数据变化,就通知视图执行更新操作。
思路一下子就明了,数据变化还好说,就是拿出一个变量存储旧值,一旦获取到新值,新值与旧值不同时,数据就发生了变化。可问题在于,不可能随时随地对数据进行监控,每分每秒都在取得数据的值去与旧值做对比。
只有当这个数据在被使用时,我们才监控他,拿旧值与新值做对比。
这个过程叫做让数据变为可观察,是通过Object.defineProperty() 来实现。
1.1.2 如何使用Object的静态方法定义属性

Object.defineProperty(obj, prop, descriptor)

  • obj 要在其上定义属性的对象。
  • prop 要定义或修改的属性的名称。
  • descriptor 将被定义或修改的属性描述符。
被这样定义的属性,所有的数据描述符默认为false,也就是不可删除,不可写,不可枚举
属性描述符
MDN文档上有提到
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。

let obj = { name:1 } Object.defineProperty(obj,'school',{ configurable:true,//表示configurable可以被删除 writable:true,//为true之后,便可以修改 // enumerable:true,//修改之后才可以被枚举,在遍历时被访问到 value:'zfpx' }) // delete obj.school; obj.school = "修改值" console.log(obj) for(var key in obj){ console.log(key) }

只有开启数据描述符为true之后,属性才可被删除,被写入,被枚举打印
getter-setter
这就是数据可监控的关键,使用Object.defineProperty(obj, prop, descriptor)定义的属性,一旦属性被使用,就会被读取,就会调用get函数,一旦属性被写入,就会调用set函数,即可以知道数据一旦发生写入,变化,就可以在set函数中通知视图更新。
由于,
存储描述符get set参数和数据描述符的writeable value存在冲突,二选其一
Object.defineProperty(obj, "school", { configurable: true, //表示configurable可以被删除 // writable: true, enumerable: true, //修改之后才可以被枚举,在遍历时被访问到 // value: "zfpx", get() { console.log("调用了get方法"); return value; }, set(newVal) { console.log("调用了set方法"); value = https://www.it610.com/article/newVal; } });

前端|Vue双向绑定实现原理(一) 数据劫持
文章图片

如图 1-2
1.1.3 数据劫持
知道了get和set的妙用,就可以对数据进行劫持了。
劫持的概念
说白了,就是拿到某数据,持有这个数据,可以操作增删改,也可以不操作,重点在持有他
监听
一旦数据被传入Vue实例就需要对data整个对象实行监听,
【前端|Vue双向绑定实现原理(一) 数据劫持】这里需要对data中的数据类型进行判断
如果是data中的属性是基本数据类型,只需要监控就好了
如果data中的属性是对象,则需要遍历对象下的所有属性,进行监控
可又有一个疑问,data的属性是对象A,A的属性还包含对象B,B有对象C,所以不能是遍历,而是递归,递归整个对象属性树
? 姓名是{{ name.firstName }}
年龄是{{ age }} {{ name }} ?

数据绑定(传入{ 对象的data挂载在vue实例上)
/** *Vue入口 *@{options} 限定为一个对象,接受这个{}对象 * */ function Vue(options = {}) { this.$options = options; // 将所有属性挂载在vue实例$options上 var data = https://www.it610.com/article/(this._data = this.$options.data); //将{}对象的data挂载vue实例上 observe(data); }

/** *观察对象变化,如果最开始传入的data是基本数据类型,已经被劫持了,不需要递归再去对属性进行监控 *@{data} 被观察的对象或属性 */ function observe(data) { if (typeof data !== "object") return null; return new Observe(data); }

class Observe { constructor(data) { this.start(data); }start(data) { for (let key in data) { let val = data[key]; // 如果data中包含属性是对象,则需要递归对象的中属性,进行数据劫持 // 如果data中的属性就是普通数据类型,递归退出 -- 递归出口 Object.defineProperty(data, key, { enumrable: true, get() { console.log("调用get方法"); return val; }, // 会在数据改变的时候直接设置 set(newVal) { console.log("调用set方法"); //数据并没有改变 if (newVal === val) { return; } val = newVal; } }); } } }

上述代码实现了数据劫持和监控数据的功能
接下来是
数据代理(this代理传入的{ }对象去调用data)
编译模板(读取文本节点中的字符串,抽成属性名,通过字面量的形式访问到属性值,去掉双大括号,显示到节点上)
到编译模板这一步,是实现了单向绑定,也就是data中的数据被显示在网页上,如同又通过视图,譬如input输入框,改变data的值。
往后看,谢谢。

    推荐阅读