vue响应式原理学习(一)
栏目: JavaScript · 发布时间: 5年前
内容简介:原理:因为vue内部做了代理。假如我们用this去访问某个属性,vue会自动去data,props,methods等参数对象里面去查找。所以我们开发时会发现,props里面定义过的属性,data不能再定义了,会抛出警告。methods也一样。用过Vue都知道,Vue本身是一个构造函数,所以我们的用法是直接new Vue()。下面我们用代码模拟一下Vue内部的代理
原理:
因为vue内部做了代理。假如我们用this去访问某个属性,vue会自动去data,props,methods等参数对象里面去查找。所以我们开发时会发现,props里面定义过的属性,data不能再定义了,会抛出警告。methods也一样。
用过Vue都知道,Vue本身是一个构造函数,所以我们的用法是直接new Vue()。下面我们用代码模拟一下Vue内部的代理
(部分代码来源:vue项目下 src/core/instance/state.js)
// 定义一个空函数 function noop() {} // 定义一个公用的属性描述对象 const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop } /** * 定义代理函数 * @target 当前对象 * @sourceKey 传入的是来源,也就是代理对象的名称 * @key 要访问的属性 */ function proxy(target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter() { // 示例:如果你在data中访问this.name,那么此时返回的是 this['_data']['name'] // target[key] => target[source][key] return target[sourceKey][key]; } sharedPropertyDefinition.set = function proxySetter(val) { target[sourceKey][key] = val; } Object.defineProperty(target, key, sharedPropertyDefinition); } // 构造函数 function MyVue(options) { this._data = options.data || {}; this._props = options.props || {}; this._methods = options.methods || {}; this.init(options); } MyVue.prototype.init = function(options) { initData(this, options.data); initProps(this, options.props); iniMethods(this, options.methods); } // 相关方法 function initData(vm, dataObj) { Object.keys(dataObj).forEach(key => proxy(vm, '_data', key)); } function initProps(vm, propsObj) { Object.keys(propsObj).forEach(key => proxy(vm, '_props', key)); } function iniMethods(vm, methodsObj) { Object.keys(methodsObj).forEach(key => proxy(vm, '_methods', key)); } 复制代码
这里的代码主要是示例,并没有判断属性是否重复。
测试代码:
let myVm = new MyVue({ data: { name: 'JK', age: 25 }, props: { sex: 'man' }, methods: { about() { console.log(`my Name is ${this.name}, age is ${this.age}, sex is ${this.sex}`); } } }); myVm.name // 'JK' myVm.age // 25 myVm.sex // 'man' myVm.about() // my Name is JK, age is 25, sex is man myVm.age = 24; 复制代码
具体Vue内部的处理是比较复杂的,会判断很多边界情况。例如data返回一个函数时需要单独处理,例如props传入具有default和type属性的对象等等。
2. 如何实现一个简易的数据响应式系统
Vue的数据响应式实现是依赖 Object.defineProperty
这个api的,这也是它不支持IE8且无法hack的原因。
据说Vue3.0改用了ES6 的 ```Proxy``,并使用TypeScript编写。很是期待。
vue改变data之后做了什么? 如果要说完整的一套流程,那是很多的,涉及到 watcher,render 渲染函数,VNode,Dom diff 等等。
响应式系统本身是基于观察者模式的,也可以说是发布/订阅模式。 发布/订阅模式,就好比是你去找中介租房子。而观察者模式呢,就好比你直接去城中村找房东租房子。 发布/订阅模式比观察者模式多了个调度中心(中介)。
我这里只是先说一下怎么收集依赖,修改了值是怎么通知的思路。
(部分代码来源:vue项目下 src/core/observer/)
抛出任何其他的因素,我们先实现一个响应式的雏形
// 假如有一个对象是 data let data = { x: 1, y: 2 } // 我们把这个对象变成响应式的 for(const key in data) { Object.defineProperty(data, key, { get() { console.log(`我获取了data的${key}`); return data[key] }, set(val) { console.log(`我设置了data的${key}为${val}`); data[key] = val; } }) } 复制代码
把这个代码扔到浏览器里,然后获取一下 data.x
,会发现,啊哦,怎么浏览器一直在输出,为什么?
因为我在 get
中 return data[key]
,相当于又访问了一次 data[key]
, 会一直触发 get
方法的,造成死循环。所以我们等会把代码优化下。
接下来,我们在 get
里收集依赖, set
里触发响应
怎么收集依赖,怎么触发响应? 熟悉观察者模式的同学应该能马上想到,维护一个数组,每次触发 get 都把对应的函数push到这个数组,每次 set
时将对应的函数触发。是不是很像我们自定义一个事件系统,当然Vue内部肯定不会这么简单。
// 定义一个 watch 函数,作用是拿到改变某个值时对应的处理函数 // Target 是全局变量, 用于存储对应的函数 let Target = null function $watch (exp, fn) { // 将 Target 的值设置为 fn Target = fn; // 读取字段值,触发 get 函数 data[exp]; } // dep 在 get 和 set 被闭包引用,不会被回收 // 每一个 key 都有一个属于自己的 dep for(const key in data) { const dep = []; // 优化死循环 let val = data[key]; Object.defineProperty(data, key, { get() { console.log(`我获取了data的${key}`); // 收集依赖 dep.push(Target); return val; }, set(newVal) { console.log(`我设置了data的${key}为${newVal}`); if (val === newVal) { return ; } val = newVal; // 触发依赖 dep.forEach(fn => fn()); } }) } // 监听数据变化 $watch('x', () => console.log('x被修改')); // 输出 '我获取了data的x' data.x = 3; // 输出 '我设置了data的x为3', x被修改 复制代码
响应式是做好了,但眼尖的同学可能会发现,$watch 函数里,竟然写了一个固定的 data[exp]
,这里的 data
是我们上一段代码定义的变量,在开发中,肯定不可能是固定的呀。所以再优化下, 传入一个渲染函数,渲染函数内部触发属性的 get
。
全部代码:
let data = { x: 1, y: 2 } // Target 是全局变量, 用于存储对应的函数 let Target = null function $watch (exp, fn) { // 将 Target 的值设置为 fn Target = fn; // 如果 exp 是函数,直接执行该函数 if (typeof exp === 'function') { exp(); return; } // 读取字段值,触发 get 函数 data[exp]; } // dep 在 get 和 set 被闭包引用,不会被回收 // 每一个 key 都有一个属于自己的 dep for(const key in data) { const dep = []; // 优化死循环 let val = data[key]; Object.defineProperty(data, key, { get() { console.log(`我获取了data的${key}`); // 收集依赖 dep.push(Target); return val; }, set(newVal) { console.log(`我设置了data的${key}为${newVal}`); if (val === newVal) { return ; } val = newVal; // 触发依赖 dep.forEach(fn => fn()); } }) } // 测试代码 function render () { return document.write(`x:${data.x}; y:${data.y}`) } $watch(render, render); 复制代码
实际上Vue内部的处理是不会这么简单的,例如对数组和对象的区别处理,对象的深度遍历等,我们这里都还没考虑。
还有好多问题要学习:
如何避免重复收集依赖,如何根据template模板的解析并生成渲染函数,AST的实现,v-on,v-bind,v-for等指令的内部解析。
用vue时,push,slice等api改变data时可以触发数据响应,而直接改数据的下标或length却不会触发呢, Vue.$set 内部做了什么操作,
修改完数据后,内部怎么触发渲染对应的dom节点。
参考
Vue技术内幕 。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。