原理
响应式原理
三步骤
- 数据监听 observe
- 依赖收集 watcher
- 触发更新 patch
Vue2
Object.defineProperty
关于Object.defineProperty
的说法
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
javascript
/**
* [target] 要在其上定义属性的对象。
* [key]要定义或修改的属性的名称。
* [descriptor]将被定义或修改的属性描述符。
*/
// ES5语法,不支持IE8及以下版本
Object.defineProperty(target, key, {
writable: false, // 默认 是否可修改
configurable: false, // 默认 是否可被del
enumerable: false, // 默认 是否可遍历
set() {
// 默认:undefined
},
get() {
// 默认:undefined
}
})
/**
* [target] 要在其上定义属性的对象。
* [key]要定义或修改的属性的名称。
* [descriptor]将被定义或修改的属性描述符。
*/
// ES5语法,不支持IE8及以下版本
Object.defineProperty(target, key, {
writable: false, // 默认 是否可修改
configurable: false, // 默认 是否可被del
enumerable: false, // 默认 是否可遍历
set() {
// 默认:undefined
},
get() {
// 默认:undefined
}
})
- 基础使用
javascript
let obj = {
name: 'vue'
};
// 通过中间变量去get和set
let value = obj.name;
Object.defineProperty(obj, 'name', {
enumerable: true, // 可遍历
configurable: true, // 可删除
get() {
// 此处省略收集依赖
// 收集对应的变量再哪些地方用到了
// 响应式系统使用响应式数据的getter方法对观察者进行依赖收集(Collect as Dependency)
return value;
},
set(newValue) {
value = newValue;
// 省略了触发依赖,
// 读取视图模板,生产语法树
// 使用响应式数据的setter方法通知(notify)所有观察者进行更新,此时观察者 Watcher 会触发组件的渲染函数(Trigger re-render),
// 组件执行的 render 函数,生成一个新的 Virtual DOM Tree,此时 Vue 会对新老 Virtual DOM Tree 进行 Diff,查找出需要操作的真实 DOM 并对其进行更新。
that.render(newValue);
}
});
let obj = {
name: 'vue'
};
// 通过中间变量去get和set
let value = obj.name;
Object.defineProperty(obj, 'name', {
enumerable: true, // 可遍历
configurable: true, // 可删除
get() {
// 此处省略收集依赖
// 收集对应的变量再哪些地方用到了
// 响应式系统使用响应式数据的getter方法对观察者进行依赖收集(Collect as Dependency)
return value;
},
set(newValue) {
value = newValue;
// 省略了触发依赖,
// 读取视图模板,生产语法树
// 使用响应式数据的setter方法通知(notify)所有观察者进行更新,此时观察者 Watcher 会触发组件的渲染函数(Trigger re-render),
// 组件执行的 render 函数,生成一个新的 Virtual DOM Tree,此时 Vue 会对新老 Virtual DOM Tree 进行 Diff,查找出需要操作的真实 DOM 并对其进行更新。
that.render(newValue);
}
});
不足:只能监听一个属性,并且要通过中间变量
- 遍历对象的属性
javascript
// observe观察数据
Vue.prototype.observe = function (data) {
if (!data || typeof data !== 'object') {
return false;
}
// 遍历data,将原来所有属性改成set和get的形式
// 先获取到数据的key和value
Object.keys(data).forEach((key) => {
if (typeof data[key] === 'object') {
// 如果是对象,则继续去遍历他的属性
// data[key]充当一个中间变量
this.observe(data[key]);
} else {
this.defineReactive(data, key, data[key]);
}
});
};
// 添加数据监听
// 由于Object.defineProperty只能作用于Object,
// 所以数组的监听,使用了伪装者模式
Vue.prototype.defineReactive = function (target, key, value) {
let that = this;
// ES5语法,不支持IE8及以下版本
Object.defineProperty(target, key, {
enumerable: true, // 可遍历
configurable: true, // 可删除
get() {
// 此处省略收集依赖
// 收集对应的变量再哪些地方用到了
console.log('get', value);
return value;
},
set(newValue) {
console.log('set', newValue);
value = newValue;
// 数据改变,触发dom渲染
// 触发收集依赖后的更新
that.render(newValue);
}
});
};
// observe观察数据
Vue.prototype.observe = function (data) {
if (!data || typeof data !== 'object') {
return false;
}
// 遍历data,将原来所有属性改成set和get的形式
// 先获取到数据的key和value
Object.keys(data).forEach((key) => {
if (typeof data[key] === 'object') {
// 如果是对象,则继续去遍历他的属性
// data[key]充当一个中间变量
this.observe(data[key]);
} else {
this.defineReactive(data, key, data[key]);
}
});
};
// 添加数据监听
// 由于Object.defineProperty只能作用于Object,
// 所以数组的监听,使用了伪装者模式
Vue.prototype.defineReactive = function (target, key, value) {
let that = this;
// ES5语法,不支持IE8及以下版本
Object.defineProperty(target, key, {
enumerable: true, // 可遍历
configurable: true, // 可删除
get() {
// 此处省略收集依赖
// 收集对应的变量再哪些地方用到了
console.log('get', value);
return value;
},
set(newValue) {
console.log('set', newValue);
value = newValue;
// 数据改变,触发dom渲染
// 触发收集依赖后的更新
that.render(newValue);
}
});
};
- 数组的监听实现
使用装饰者模式
javascript
// 拿出数组原型链并拷贝
var arrayPro = Array.prototype;
var arrayOb = Object.create(arrayPro);
// 去重写以下的方法
var arrFun = ['push', 'pull', 'shift'];
arrFun.forEach((methods, index) => {
arrayOb[methods] = function() {
// 执行对应的数组操作,并执行视图的更新
var ret = arrayPro[method].apply(this, arguments);
// 触发视图更新
dep.notify();
return ret;
}
});
// 拿出数组原型链并拷贝
var arrayPro = Array.prototype;
var arrayOb = Object.create(arrayPro);
// 去重写以下的方法
var arrFun = ['push', 'pull', 'shift'];
arrFun.forEach((methods, index) => {
arrayOb[methods] = function() {
// 执行对应的数组操作,并执行视图的更新
var ret = arrayPro[method].apply(this, arguments);
// 触发视图更新
dep.notify();
return ret;
}
});
Vue3
Proxy
- 基础使用
javascript
let obj = {
name: 'vue'
}
// 相对于vue2省去了一个for in循环
// 不用去污染源对象
// 写法更优雅了
obj = new Proxy(obj, {
get: function(target, key, receiver) {
// receiver:代理对象
console.log(arguments);
return target[key];
},
set: function(target, key, value, receiver) {
console.log(arguments);
// 触发视图更新
dep.notify();
return Reflect.set(target, key, value);
}
})
obj.name = 'proxy';
console.log(obj.name);
let obj = {
name: 'vue'
}
// 相对于vue2省去了一个for in循环
// 不用去污染源对象
// 写法更优雅了
obj = new Proxy(obj, {
get: function(target, key, receiver) {
// receiver:代理对象
console.log(arguments);
return target[key];
},
set: function(target, key, value, receiver) {
console.log(arguments);
// 触发视图更新
dep.notify();
return Reflect.set(target, key, value);
}
})
obj.name = 'proxy';
console.log(obj.name);
- Proxy的应用
- 类型校验
- 私有变量
- ...
observe
javascript
var data = {
key: 'value',
}
for (var key in data) {
Object.defineProperty(data, key, {
get: function () {
},
set: function () {
}
})
}
var data = {
key: 'value',
}
for (var key in data) {
Object.defineProperty(data, key, {
get: function () {
},
set: function () {
}
})
}
watcher
patch
Diff算法
模板编译
with语法
改变作用域
javascript
var obj = {
name: 'vue'
}
with(obj) {
// 'vue'
console.log(name);
}
var obj = {
name: 'vue'
}
with(obj) {
// 'vue'
console.log(name);
}
render函数
vue-template-compiler
返回vnode
javascript
组件渲染过程
- 模板编译
解析模板为render函数
触发响应式
执行render函数,生成vnode
触发 getter,收集依赖
双向数据绑定原理
vue
<input v-model="name" />
// 等价于
<input :value="name" @input="name = $event.target.value" />
<input v-model="name" />
// 等价于
<input :value="name" @input="name = $event.target.value" />
- input元素的value = this.name
- 绑定input事件,this.name = $event.target.value
- data更新,触发render函数,重新生成虚拟dom
nextTick原理
javascript
// 1. 优先使用Promise
// 2. MutationObserver
// 3. setImmediate
// 4. setTimeout
// 1. 优先使用Promise
// 2. MutationObserver
// 3. setImmediate
// 4. setTimeout