原理:javascript
由于vue内部作了代理。假如咱们用this去访问某个属性,vue会自动去data,props,methods等参数对象里面去查找。因此咱们开发时会发现,props里面定义过的属性,data不能再定义了,会抛出警告。methods也同样。vue
用过Vue都知道,Vue自己是一个构造函数,因此咱们的用法是直接new Vue()。下面咱们用代码模拟一下Vue内部的代理java
(部分代码来源:vue项目下 src/core/instance/state.js)api
// 定义一个空函数
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属性的对象等等。闭包
Vue的数据响应式实现是依赖 Object.defineProperty
这个api的,这也是它不支持IE8且没法hack的缘由。dom
听说Vue3.0改用了ES6 的 ```Proxy``,并使用TypeScript编写。非常期待。函数
vue改变data以后作了什么? 若是要说完整的一套流程,那是不少的,涉及到 watcher,render 渲染函数,VNode,Dom diff 等等。oop
响应式系统自己是基于观察者模式的,也能够说是发布/订阅模式。 发布/订阅模式,就比如是你去找中介租房子。而观察者模式呢,就比如你直接去城中村找房东租房子。 发布/订阅模式比观察者模式多了个调度中心(中介)。
我这里只是先说一下怎么收集依赖,修改了值是怎么通知的思路。
(部分代码来源: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节点。
参考