Vue 响应式原理
Vue 具有一套非侵入式的响应式系统,这使得用户可以完全通过直接改变数据来驱动界面显示从而快速实现自己的业务逻辑,在以前,用户改变数据后,是需要自己编写dom操作逻辑来控制view显示的。响应式系统内在的处理逻辑对用户都是透明的,用户可以把大量的精力都用来维护好数据的状态。下图是vue官网提供的响应式的流程图五条箭头分别代表:1.组件render函数生成虚拟dom2. render过程变量...
Vue 具有一套非侵入式的响应式系统,这使得用户可以完全通过直接改变数据来驱动界面显示从而快速实现自己的业务逻辑,在以前,用户改变数据后,是需要自己编写dom操作逻辑来控制view显示的。响应式系统内在的处理逻辑对用户都是透明的,用户可以把大量的精力都用来维护好数据的状态。
下图是vue官网提供的响应式的流程图
五条箭头分别代表:
1.组件render函数生成虚拟dom
2. render过程变量获取触发getter
3.getter 执行的同时收集了当前订阅者
4.数据修改操作触发setter
5.setter触发getter的时候收集的订阅者组件们重新render
一:响应系统的实现步骤源码详解
代码中的中文注释进行了关键步骤的说明。
- 初始化入口
Vue.prototype._init = function (options) {
// 省略n行
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
// 这里开始处理数据,可以看出,响应式处理是在created钩子之前就要完成的
// 所以如果你要想让你的数据具有响应式特性,必须提前在data里定义好。
initState(vm); // 这里是处理入口,看下一段代码
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');
// 省略n行
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
}
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
if (opts.methods) { initMethods(vm, opts.methods); }
if (opts.data) {
initData(vm); // 调用初始化方法来初始化实例上的data属性里的值,看下一段代码
} else {
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
if (!isPlainObject(data)) {// 检测到是不是定义的一个对象,不是的话给出警告
data = {};
warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
);
}
// proxy data on instance
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
{
if (methods && hasOwn(methods, key)) {
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
}
}
if (props && hasOwn(props, key)) {
warn(
"The data property \"" + key + "\" is already declared as a prop. " +
"Use prop default value instead.",
vm
);
} else if (!isReserved(key)) {
proxy(vm, "_data", key);
}
}
// observe data
observe(data, true /* asRootData */); // 到此 data的数据校验完毕,然后调用observe 来绑定get set
}
- 绑定get和set方法
为data上每个属性定义get set 方法。使用Object.defineProperty来实现。这也就是vue无法支持低版本浏览器的原因之一,因为这个方法是es5的语法,IE9之前的版本不支持。
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // 检测是否已经是响应式数据
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value); // 执行响应式处理,调用下面的观察者构造函数
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
这是一个观察者构造函数
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);// 将观察者实例通过__ob__ 和当前值关联起来,类似原型_proto__的用法
// 判断是否是数组
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value); // 对数组型数据绑定get set方法
} else {
this.walk(value); // 非数组对象绑定get set 方法
}
};
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i]); // 每个key执行Object.defineProperty
}
};
// 响应系统的大功臣函数
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && observe(val);
// 最终在这里终于调用了原生js的Object.defineProperty
// 当vue渲染函数执行的时候,会去解析模版上的变量,获取变量的值。
// 在获取变量的值的时候会触发get方法
// 这个时候Dep.target 就是当前渲染函数对应的Watcher实例
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// watcher 模版在获取当前key的值得时候,Watcher会往该值上进行一个订阅。
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend(); // watcher订阅该值的变动
if (childOb) { // 如果值有子对象的观察者,继续深入下去订阅该观察者
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value); // 如果是值是数组,遍历处理订阅关系
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
// 发布更新,之前get里所有订阅的watcher接到通知,全部会执行更新
dep.notify();
}
});
}
// 下面的代码解释Dep.target 为啥是指向的当前Watcher
Watcher.prototype.get = function get () {
pushTarget(this); // Dep.target 指向的实际例子在此注入
// 省略N行
};
Dep.target = null;
var targetStack = [];
function pushTarget (target) {
targetStack.push(target);
Dep.target = target; // Dep.target 是全局的一个变量
}
// 更新操作,会把Dep底下所有的订阅的对象都遍历执行update方法
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if (!config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
二:响应式限制问题:
1.由于语言的限制, 数组通过下标操纵内部元素的时候,例如调用 arr[0]=123 上面的机制是无法被捕获到变动的,在vue中,vue重写了数组的一些操作元素的方法。所以要想让数组的数据操作也能够被响应,必须调用push pop shift unshift, reverse, sort, splice来操作。
var methodsToPatch = [ // 数组操作的方法名
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
var original = arrayProto[method];
def(arrayMethods, method, function mutator () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
break
}
// 如果是插入操作,会对插入的元素全部绑定上响应模式
if (inserted) { ob.observeArray(inserted); }
// notify change
ob.dep.notify();// 通知更新
return result
});
});
- 如果有一些数据的确在初始化的时候没有定义,这个时候如果我们在后面的业务里再定型定义,如果强制响应式系统能够在其上起作用呢?如下面的例子
data () {
return {
obj;{
key1: 'abc'
}
}
},
mounted () {
this.obj.key2='def' // 这时候key2是不具有响应属性的,因为在data里没有定义
this.obj.key1="abc1" // key1 是能够响应去更新dom上的绑定的
}
vue考虑到了这点,给我们提供了set方法
this.$set(this.obj,'key2',‘def’) // 这样写了后就起作用了
实现原理
set
Vue.prototype.$set = set;
function set (target, key, val) {
if (isUndef(target) || isPrimitive(target)
) {
warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val
}
var ob = (target).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
);
return val
}
if (!ob) {
target[key] = val;
return val
}
defineReactive$$1(ob.value, key, val);// 为新设置的这个key绑定上响应功能
ob.dep.notify();// 通知订阅者更新
return val
}
- 对象的delete删除属性操作,也是vue无法监控的一种情况,同样vue提供了delete方法来监控
delete this.obj.key1 // 无法监控到
this.$delete(this.obj, 'key1') // 可以监控到
实现原理
Vue.prototype.$delete = del;
function del (target, key) {
if (isUndef(target) || isPrimitive(target)
) {
warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target))));
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 如果是数组的delete 直接换成调用splice,在上面的介绍中,我们知道splice是被重写过的,所以能捕获响应
target.splice(key, 1);
return
}
var ob = (target).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
);
return
}
if (!hasOwn(target, key)) {
return
}
delete target[key];
if (!ob) {
return
}
ob.dep.notify(); // 强制主动通知订阅的watcher 更新
}
三:响应式流程总结
- 第一步:Vue会初始化组件内的data属性,通过Object.defineProperty 来绑定上get set。get包含了当前watcher的订阅操作,set包含了所有订阅的watcher的更新操作
- 第二步:每一个组件,在执行mount 方法的时候,vue最终都把它会生成一个观察者,watcher的表达式是组件的渲染函数,更新方法。详细参考源码中的Watcher构造函数。
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
- 第三步: Vue 组件watcher 在执行表达式获取组件更新内容的时候,比如获取下面表达式里的key1的值得时候
<a>{{key1}}</a>
会触发get,在get的钩子里,会把当前的watcher push到该get的值所对应的订阅者数组里,这样就实现了精准的将数据和观察者关联起来,当数据变动的时候就能精准的知道哪些组件是需要更新的。而不相关的就会被忽略更新。
- 第四步:当数据更新的时候,会触发之前预先绑定好的set方法,set方法会把观察者数组里的观察者都取出来,通知他们进行更新,更新其实是调用watcher对象的一个预先定义好的update函数。
以上是Vue 响应式的基本原理,本文只是窥探了它的主要流程,还有很多可以展开的内容,比如Watcher的详细使用,数据更新和view更新的时机NextTick相关等等。
更多推荐
所有评论(0)