Vue 最独特的个性之一,是非侵入式的响应零碎。数据模型仅仅是一般的 JavaScript 对象。而当你批改它们时,视图会进行更新。聊到 Vue 响应式实现原理,泛滥开发者都晓得实现的关键在于利用 Object.defineProperty , 但具体又是如何实现的呢,明天咱们来一探到底。
为了通俗易懂,咱们还是从一个小的示例开始:
<body>
<div id="app">
{{message}}
</div>
<script>
var app = new Vue({
el: '#app',
data: {message: 'Hello Vue!'}
})
</script>
</body>
咱们曾经胜利创立了第一个 Vue 利用!看起来这跟渲染一个字符串模板十分相似,然而 Vue 在背地做了大量工作。当初数据和 DOM 曾经被建设了关联,所有货色都是响应式的。咱们要怎么确认呢?关上你的浏览器的 JavaScript 控制台 (就在这个页面关上),并批改 app.message 的值,你将看到上例相应地更新。批改数据便会自动更新,Vue 是如何做到的呢?
通过 Vue 构造函数创立一个实例时,会有执行一个初始化的操作:
function Vue (options) {this._init(options);
}
这个 _init 初始化函数外部会初始化生命周期、事件、渲染函数、状态等等:
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm);
initState(vm);
initProvide(vm);
callHook(vm, 'created');
因为本文的主题是响应式原理,因而咱们只关注 initState(vm) 即可。它的要害调用步骤如下:
function initState (vm) {initData(vm);
}
function initData(vm) {// data 就是咱们创立 Vue 实例传入的 {message: 'Hello Vue!'}
observe(data, true /* asRootData */);
}
function observe (value, asRootData) {ob = new Observer(value);
}
var Observer = function Observer (value) {this.walk(value);
}
Observer.prototype.walk = function walk (obj) {var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
// 实现响应式要害函数
defineReactive$$1(obj, keys[i]);
}
};
}
咱们来总结一下下面 initState(vm) 流程。初始化状态的时候会对利用的数据进行检测,即创立一个 Observer 实例,其构造函数外部会执行原型上的 walk 办法。walk 办法的次要作用便是 遍历数据的所有属性,并把每个属性转换成响应式,而这转换的工作次要由 defineReactive$$1 函数实现。
function defineReactive$$1(obj, key, val) {var dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {var value = getter ? getter.call(obj) : val;
if (Dep.target) {dep.depend();
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);
dep.notify();}
});
}
defineReactive$$1 函数外部应用 Object.defineProperty 来监测数据的变动。每当从 obj 的 key 中读取数据时,get 函数被触发;每当往 obj 的 key 中设置数据时,set 函数被触发。咱们说批改数据触发 set 函数,那么 set 函数是如何更新视图的呢?拿本文结尾示例剖析:
<div id="app">
{{message}}
</div>
该模板应用了数据 message, 当 message 的值产生扭转的时候,利用中所有应用到 message 的视图都能触发更新。在 Vue 的外部实现中,先是收集依赖,即把用到数据 message 的中央收集起来,而后等数据产生扭转的时候,把之前收集的依赖全副触发一遍就能够了。也就是说咱们在上述的 get 函数中收集依赖,在 set 函数中触发视图更新。那接下来的重点就是剖析 get 函数和 set 函数了。先看 get 函数,其要害调用如下:
get: function reactiveGetter () {if (Dep.target) {dep.depend();
}
}
Dep.prototype.depend = function depend () {if (Dep.target) {Dep.target.addDep(this);
}
};
Watcher.prototype.addDep = function addDep (dep) {dep.addSub(this);
}
Dep.prototype.addSub = function addSub (sub) {this.subs.push(sub);
};
其中 Dep 构造函数如下:var Dep = function Dep () {
this.id = uid++;
this.subs = [];};
上述代码中 Dep.target 的值是一个 Watcher 实例,稍后咱们再剖析它是何时被赋值的。咱们用一句话总结 get 函数所做的工作:把以后 Watcher 实例(也就是 Dep.target)增加到 Dep 实例的 subs 数组中。在持续剖析 get 函数前,咱们须要弄清楚 Dep.target 的值何时被赋值为 Watcher 实例,这里咱们须要从 mountComponent 这个函数开始剖析:
function mountComponent (vm, el, hydrating) {updateComponent = function () {vm._update(vm._render(), hydrating);
};
new Watcher(vm, updateComponent, noop, xxx);
}
// Wather 构造函数下
var Watcher = function Watcher (vm, expOrFn, cb) {if (typeof expOrFn === 'function') {this.getter = expOrFn;} else {this.getter = parsePath(expOrFn);
}
this.value = this.get();}
Watcher.prototype.get = function get () {pushTarget(this);
value = this.getter.call(vm, vm);
}
function pushTarget (target) {targetStack.push(target);
Dep.target = target;
}
由上述代码咱们晓得 mountComponent 函数会创立一个 Watcher 实例,在其构造函数中最终会调用 pushTarget 函数,把以后 Watcher 实例赋值给 Dep.target。另外咱们留神到,创立 Watcher 实例这个动作是产生在函数 mountComponent 外部,也就是说 Watcher 实例是组件级别的粒度,而不是说任何用到数据的中央都新建一个 Watcher 实例。当初咱们再来看看 set 函数的次要调用过程:
set: function reactiveSetter (newVal) {dep.notify();
}
Dep.prototype.notify = function notify () {var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {subs[i].update();}
}
Watcher.prototype.update = function update () {queueWatcher(this);
}
Watcher.prototype.update = function update () {
// queue 是一个全局数组
queue.push(watcher);
nextTick(flushSchedulerQueue);
}
// flushSchedulerQueue 是一个全局函数
function flushSchedulerQueue () {for (index = 0; index < queue.length; index++) {watcher = queue[index];
watcher.run();}
}
Watcher.prototype.run = function run () {var value = this.get();
}
set 函数内容有点长,但上述代码都是精简过的,应该不难理解。当扭转利用数据的时候,触发 set 函数执行。它会调用 Dep 实例的 notify() 办法,而 notify 办法又会把以后 Dep 实例收集的所有 Watcher 实例的 update 办法调用一遍,以达到更新所有用到该数据的视图局部。咱们持续看 Watcher 实例的 update 办法做了什么。update 办法会把以后的 watcher 增加到数组 queue 中,而后把 queue 中每个 watcher 的 run 办法执行一遍。run 办法外部会执行 Wather 原型上的 get 办法,后续的调用在前文剖析 mountComponent 函数中都有形容,在此就不再赘述。总结来说,最终 update 办法会触发 updateComponent 函数:
updateComponent = function () {vm._update(vm._render(), hydrating);
};
Vue.prototype._update = function (vnode, hydrating) {vm.$el = vm.__patch__(prevVnode, vnode);
}
这里咱们留神到 _update 函数的第一个参数是 vnode。vnode 顾名思义是虚构节点的意思,它是一个一般对象,该对象的属性上保留了生成 DOM 节点所须要数据。说到虚构节点你是不是很容易就联想到虚构 DOM 了呢,没错 Vue 中也应用了虚构 DOM。前文说到 Wather 是和组件相干的,组件外部的更新就用虚构 DOM 进行比照和渲染。_update 函数外部调用了 patch 函数,通过该函数比照新旧两个 vnode 之间的不同,而后依据比照后果找出须要更新的节点进行更新。
注:本文剖析示例基于 Vue v2.6.14 版本。