vue2原理初探-数据代理和数据劫持
- 几个基础知识点
- 数据代理
- 数据劫持
- 完整demo
一、几个基础知识点
1.普通函数和箭头函数的区别
如上图所示,a函数定义在全局,其作用域链,只有GO对象,当其执行的时候会临时产生一个aAO对象,所以b函数的作用域链就是 aAO -> GO
函数每次执行都会产生一个新的AO对象挂在作用域链头部,函数被解释执行的时候,其内部标识符的检索都是在作用域链上检索的。
根据以上理论,我们来执行b函数。
控制台输出如下:
为什么b函数中看到的this是window呢?是因为其顺着作用域链找,bBO -> aAO -> GO,只有GO上有this,就是window。
当然对于普通函数,我们可以改变其this指向:
控制台输出如下:
于是我们得到一个结论:
函数执行生成的临时AO对象中,包含了arguments隐式变量来保存实参列表。
函数执行看到的this变量,可以修改,通过对象调用,call,apply来修改。
但是,但是,但是。。。。
箭头函数,它就不是这样的。。。
控制台输出:
箭头函数,没有arguments隐式变量了。而且,this它居然修改不了。。
那么说白了,this只能在其作用域链上找了,生成的临时AO对象上没有this,没有this。
是否有arguments隐式变量 | 是否能改变this指向 | |
普通函数 | 是 | 是 |
箭头函数 | 否 | 否 |
2.闭包
由于plus,minus,showCount三个函数的作用域链中有aaa的AO对象,所以当他们被返回后,形成了闭包。
3.defineProperty函数的使用
但是其实除了这样给对象加属性外,我们也可以通过defineProperty来给对象加属性。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>demo01-defineProperty的使用</title> </head> <body> <script type="application/javascript"> // defineProperty() let obj = { name: 'zhangsan', age: 33, showInfo() { console.log(this.name + "--" + this.age) } } obj.showInfo(); Object.defineProperty(obj, 'ccc', { value: 10, enumerable: true, // 是否能枚举 configurable: true, // 是否能删除 writable: true // 是否能写入 }) // 枚举 var keys = Object.keys(obj); console.log(keys); // 写入 obj.ccc = 100; console.log(obj); // 删除 delete obj.ccc; console.log(obj); </script> </body> </html>
上述代码控制台输入如下:
其实这样的话,定义属性和我们直接写属性没什么太大区别,关键是下面这样的写法:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>demo01-defineProperty的使用</title> </head> <body> <script type="application/javascript"> // defineProperty() let obj = { name: 'zhangsan', age: 33, showInfo() { console.log(this.name + "--" + this.age) } } let ccc = 10; Object.defineProperty(obj, 'ccc', { // value: 10, enumerable: true, // 是否能枚举 configurable: true, // 是否能删除 // writable: true, // 是否能写入 get: function proxyGet() { return ccc; }, set: function proxySet(value) { ccc = value; } }) console.log(obj.ccc); obj.ccc = 100; console.log(obj.ccc); console.log(obj); </script> </body> </html>
注意,如果我们要定义属性的get/set,那么就不能定义value和writable了,否则会报错。
此时我们对属性ccc的写入和读取将走get/set方法了。
控制台输出如下:
这个ccc属性的三个点,是不是特别想我们使用vue的时候点开的组件对象里面的一些属性。
在这里我插一句,我点开set/get给大家看看,其实能看见[[scopes]]作用域链了。
如下图:
当然,这里我们看见了,set/get函数定义的时候的作用域链[[scopes]],其实是SO -> GO,这个SO其实就是外层包裹的script标签。
可以理解成,script标签执行流程就像一个函数执行一样,也会产生作用域对象挂在[[scopes]]上。
二、数据代理
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>demo01-vue简单使用</title> <!--引入vue--> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script> </head> <body> <div id="app"> <h3>姓名:{{name}}</h3> <h3>年龄:{{age}}</h3> <button @click="agePlusOne">年龄+1</button> </div> <script type="application/javascript"> let vm = new Vue({ el: '#app', data(){ return { name: '张三', age: 33 } }, methods: { agePlusOne(){ this.age ++; console.log(this) } } }); </script> </body> </html>
点击按钮,我把Vue对象打印出来:
可以清晰的看到,我们配置的data对象中的属性都被定义在了Vue组件对象中。
起码,这里看到了,vue做了数据代理,我们在组件对象中对data中同名属性的set和get都走了其对应的代理方法。
三、数据劫持
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>demo01-数据劫持</title> </head> <body> <div id="app"> </div> <script type="application/javascript"> function setAppInnerText (value) { document.querySelector("#app").innerText = value; } let obj = { name: '张三', age: 100, showInfo() { return this.name + "---" + this.age; } }; setAppInnerText(obj.showInfo()) // 数据劫持 Object.keys(obj).forEach(key => { let value = obj[key]; Object.defineProperty(obj, key, { enumerable: true, configurable: true, set(newValue) { if(newValue === value) { return ; } else { value = newValue; setAppInnerText(obj.showInfo()); } }, get() { return value; } }) }) obj.age = 1000; </script> </body> </html>
上面的代码,就是数据劫持,每次属性设置的时候,都触发了setAppInnerText函数的调用。
上述代码执行完之后,只要我们对obj对象的属性进行修改,都会触发页面的变化。
四、完整demo
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>data_observer</title> </head> <body> <div id="root"> a = {{a}} <br> b = {{b}} </div> <script> function Vue(config) { this._data = config.data; // 数据代理 方便程序员操作 for (let key in config.data) { Object.defineProperty(this, key, { enumerable: true, get: function proxyGet() { return this._data[key]; }, set: function proxySet(value) { this._data[key] = value; } }) } this.mounted = false; if (config.el) { this.$mount(config.el); } } Vue.prototype.$mount = function (id) { if (!this.mounted) { this.originInnerHtml = document.getElementById(id).innerHTML; // 编译模板生成render let _self = this; function render() { let innerHtml = _self.originInnerHtml; for (let key in _self._data) { innerHtml = innerHtml.replaceAll('{{' + key + '}}', _self._data[key]); } document.getElementById(id).innerHTML = innerHtml; } // 数据劫持 for (let key in this._data) { let value = this._data[key]; Object.defineProperty(this._data, key, { enumerable: true, configurable: true, get: function getObserver() { return value; }, set: function setObserver(newValue) { if (value !== newValue) { value = newValue; render(); } } }) } // 执行render render(); this.mounted = true; } } let config = { el: 'root', data: { a: '牛逼的消息', b: '学习vue2底层实现' } }; let vm = new Vue(config); </script> </body> </html>
控制台打印如下: