写在前面
相信大家都知道,vue实现数据双向绑定无非就是采用数据劫持的方式,结合发布订阅模式,通过Object.defineProperty()来劫持各个属性的setter,getter以监听属性的变动,在数据变动时发布消息给订阅者,以上是vue2.x的实现原理,3.0的话就是proxy替换Object.defineProperty(),其他流程没有改变。总结一下数据相应原理就发布订阅模式(观察者模式)+ 数据劫持(defineProperty or proxy)
什么是观察者模式?
通常又被称为 发布-订阅者模式 或 消息机制,它定义了对象间的一种一对多的依赖关系,只要当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,解决了主体对象与观察者之间功能的耦合,即一个对象状态改变给其他对象通知的问题。
文字繁琐,看名字高大尚,我们举个生活中栗子 平时想了解某个服装品牌的信息,但是又不想老是去官网查看,于是偷偷关注了公众号(订阅者),公众号有最新产品产品上市就推送消息给你(发布者),没错这个就是了
我们工作中也有经常看到,比如:
document.querySelector('#btn').addEventListener('click',function () { alert('You click this btn'); },false)复制代码
简单代码实现
const EventHub = { _messager: {}, on (type, fn) { if (this._messager[type] === undefined) { this._messager[type] = [fn] } else { this._messager[type].push(fn) } }, emit (type, arv) { const params = { type, arv: arv || {} } const len = this._messager[type] for (let i = 0; i < len.length; i++) { const fn = len[i]; fn.call(this, params) } } } EventHub.on('say', function (data) { console.log(data.arv.text) }) EventHub.on('say', function (data) { console.log('hhhhh') }) EventHub.on('hi', function () { console.log('hi,小田田') }) setTimeout(() => { // 发布 EventHub.emit('say', {text: 'ys大佬们,我要发布信息了'}) EventHub.emit('hi') }, 3000)复制代码
观察者模式使用
最熟悉的莫过于我们每天使用vue框架了,其中数据的双向绑定就是利用 Object.defineProperty() 和观察者模式实现的。
看看简单实现代码:
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <div id="demo"></div> <input type="text" id="inp"> <script> var obj = {}; var demo = document.querySelector('#demo') var inp = document.querySelector('#inp') Object.defineProperty(obj, 'name', { get: function() { return val; }, set: function (newVal) {//当该属性被赋值的时候触发 inp.value = newVal; demo.innerHTML = newVal; } }) inp.addEventListener('input', function(e) { // 给obj的name属性赋值,进而触发该属性的set方法 obj.name = e.target.value; }); obj.name = 'fei';//在给obj设置name属性的时候,触发了set这个方法 </script> </body> </html复制代码
当然真正的实现过程是复杂的,是这样子的
vue2.x vs vue3 响应式原理对比
vue2.x
Vue.prototype.observe= function(obj){ var value; var that = this; for(var key in obj){ value = obj[key]; if(typeof value === 'object'){ this.observe(value); }else{ Object.defineProperty(this.$data,key,{ get:function(){ return value }, set:function(newValue){ value = newValue; self.render(); } }) } } }复制代码
vue3.0
Vue.prototype.observe= function(obj){ var value; var that = this; // 用proxy就不需要for in了,即监听的颗粒度减小了,拿到的信息更多了。 this.$data = new Proxy(obj,{ get:function(target,key,reveive){ return target[key];// 不需要中间变量了 }, set: function (target, key, newValue,reveive){ target[key] = newValue; } }) }复制代码
Vue 3.0与Vue 2.0的区别仅是数据劫持的方式由Object.defineProperty更改为Proxy代理,其他代码不变。
Proxy 可以做哪些有意思的事情?
等不及可选链:深层取值(get)
平时取数据的时候,经常会遇到深层数据结构,如果不做任何处理,很容易造成 JS 报错。 为了避免这个问题,也许你会用多个 && 进行处理:
const country = { name: 'china', province: { name: 'guangdong', city: { name: 'shenzhen' } } } const cityName = country.province && country.province.city && country.province.city.name;复制代码
最新的 ES 提案中提供了可选链的语法糖,支持我们用下面的语法来深层取值。
country?.province?.city?.name复制代码
proxy的实现
function noop() {} function get (obj) { // 注意这里拦截的是 noop 函数 return new Proxy(noop, { // 这里支持返回执行的时候传入的参数 apply(target, context, [arg]) { return obj; }, get(target, prop) { return get(obj[prop]); } }) } const obj = { person: {} } get(obj)() === obj; // true get(obj).person.name(); // undefined复制代码
管道
在最新的 ECMA 提案中,出现了原生的管道操作符 |>,在 RxJS 和 NodeJS 中都有类似的 pipe 概念。使用 Proxy 也可以实现 pipe 功能,只要使用 get 对属性访问进行拦截就能轻易实现,将访问的方法都放到 stack 数组里面,一旦最后访问了 execute 就返回结果。
const pipe = (value) => { const stack = []; const proxy = new Proxy({}, { get(target, prop) { if (prop === 'execute') { return stack.reduce(function (val, fn) { return fn(val); }, value); } stack.push(window[prop]); return proxy; } }) return proxy; } var double = n => n * 2; var pow = n => n * n; pipe(3).double.pow.execute复制代码