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 版本。
到此这篇关于Vue响应式原理的示例详解的文章就介绍到这了,更多相关Vue响应式原理内容请搜索阿兔在线工具以前的文章或继续浏览下面的相关文章希望大家以后多多支持阿兔在线工具!