1、vue.js响应式原理
参考:https://cn.vuejs.org/v2/guide/reactivity.html
https://github.com/answershuto/learnVue
注:learnVue讲解的vue版本是2.3.0,我粘贴的源码是2.6.10
Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data
选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty
把这些属性全部转为 getter/setter。Object.defineProperty
是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更。每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
受现代 JavaScript 的限制 (而且 Object.observe
也已经被废弃),Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data
对象上存在才能让 Vue 将它转换为响应式的。
var vm = new Vue({ data:{ a:1 // `vm.a` 是响应式的 } }) vm.b = 2 // `vm.b` 是非响应式的
对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value)
方法向嵌套对象添加响应式属性。由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值。
var vm = new Vue({ data: { message: ‘‘ // 声明 message 为一个空值字符串 }, template: ‘<div>{{ message }}</div>‘ }) vm.message = ‘Hello!‘ // 之后设置 `message`
如果你未在 data
选项中声明 message
,Vue 将警告你渲染函数正在试图访问不存在的属性。
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then
、MutationObserver
和 setImmediate
,如果执行环境不支持,则会采用 setTimeout(fn, 0)
代替。例如,当你设置 vm.someData = ‘new value‘
,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)
。这样回调函数将在 DOM 更新完成后被调用。
在initData中会调用observe这个函数将Vue的数据设置成observable的。当_data数据发生改变的时候就会触发set,对订阅者进行回调。那么问题来了,需要对app._data.text操作才会触发set。为了偷懒,我们需要一种方便的方法通过app.text直接设置就能触发set对视图进行重绘。那么就需要用到代理。
function initData (vm) { var data = vm.$options.data; data = vm._data = typeof data === ‘function‘ ? getData(data, vm) : data || {}; // proxy data on instance var keys = Object.keys(data); var i = keys.length; while (i--) { var key = keys[i]; proxy(vm, "_data", key); } // observe data observe(data, true /* asRootData */); }
let app = new Vue({ el: ‘#app‘, data: { text: ‘text‘ } })
那么问题来了,需要对app._data.text操作才会触发set。为了偷懒,我们需要一种方便的方法通过app.text直接设置就能触发set对视图进行重绘。那么就需要用到代理。这样我们就把data上面的属性代理到了vm实例上。
function proxy (target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] }; sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val; }; Object.defineProperty(target, key, sharedPropertyDefinition); }
2、vue.js依赖收集
new Vue({ template: `<div> <span>text1:</span> {{text1}} <span>text2:</span> {{text2}} <div>`, data: { text1: ‘text1‘, text2: ‘text2‘, text3: ‘text3‘ } });
按照之前《响应式原理》中的方法进行绑定则会出现一个问题——text3在实际模板中并没有被用到,然而当text3的数据被修改(this.text3 = ‘test‘)的时候,同样会触发text3的setter导致重新执行渲染,这显然不正确。
当对data上的对象进行修改值的时候会触发它的setter,那么取值的时候自然就会触发getter事件,所以我们只要在最开始进行一次render,那么所有被渲染所依赖的data中的数据就会被getter收集到Dep的subs中去。在对data中的数据进行修改的时候setter只会触发Dep的subs的函数。
Dep(依赖)
var Dep = function Dep () { this.id = uid++; this.subs = []; }; Dep.prototype.addSub = function addSub (sub) { this.subs.push(sub); }; /*依赖收集*/ Dep.prototype.depend = function depend () { if (Dep.target) { Dep.target.addDep(this); } };
Watcher(观察者)
/*添加一个依赖关系到Deps集合中*/ Watcher.prototype.addDep = function addDep (dep) { var id = dep.id; if (!this.newDepIds.has(id)) { this.newDepIds.add(id); this.newDeps.push(dep); if (!this.depIds.has(id)) { dep.addSub(this); } } };
将观察者Watcher实例赋值给全局的Dep.target,然后触发render操作只有被Dep.target标记过的才会进行依赖收集。有Dep.target的对象会将Watcher的实例push到subs中,在对象被修改触发setter操作的时候dep会调用subs中的Watcher实例的update方法进行渲染。
function defineReactive$$1 (obj, key, val, customSetter, shallow) { var dep = new Dep(); // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); } return value } }); }
3、vue.js数据绑定原理
这张图比较清晰地展示了整个流程,首先通过一次渲染操作触发Data的getter(这里保证只有视图中需要被用到的data才会触发getter)进行依赖收集,这时候其实Watcher与data可以看成一种被绑定的状态(实际上是data的闭包中有一个Deps订阅者,在修改的时候会通知所有的Watcher观察者),在data发生变化的时候会触发它的setter,setter通知Watcher,Watcher进行回调通知组件重新渲染的函数,之后根据diff算法来决定是否发生视图的更新。
initData主要是初始化data中的数据,将数据进行Observer,监听数据的变化。下面这段代码主要做了两件事,一是将_data上面的数据代理到vm上,另一件事通过observe将所有数据变成observable。
function initData (vm) { var data = vm.$options.data; data = vm._data = typeof data === ‘function‘ ? getData(data, vm) : data || {}; // proxy data on instance var keys = Object.keys(data); var i = keys.length; while (i--) { // 遍历data中的数据 var key = keys[i]; proxy(vm, "_data", key); // 将data上面的属性代理到了vm实例上 } // observe data /*从这里开始我们要observe了,开始对数据进行绑定,这里有尤大大的注释asRootData,这步作为根数据,下面会进行递归observe进行对深层对象的绑定。*/ observe(data, true /* asRootData */); }
observe
/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */ // 尝试创建一个Observer实例(__ob__),如果成功创建Observer实例则返回新的Observer实例,如果已有Observer实例则返回现有的Observer实例。 function observe (value, asRootData) { if (!isObject(value) || value instanceof VNode) { return } var ob; /*这里用__ob__这个属性来判断是否已经有Observer实例,如果没有Observer实例则会新建一个Observer实例并赋值给__ob__这个属性,如果已有Observer实例则直接返回该Observer实例*/ 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) { /*如果是根数据则计数,后面Observer中的observe的asRootData非true*/ ob.vmCount++; } return ob }
Vue的响应式数据都会有一个__ob__的属性作为标记,里面存放了该属性的观察器,也就是Observer的实例,防止重复绑定。
Observer的作用就是遍历对象的所有属性将其进行双向绑定。
/** * Observer class that is attached to each observed * object. Once attached, the observer converts the target * object‘s property keys into getter/setters that * collect dependencies and dispatch updates. */ var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, ‘__ob__‘, this); // 将Observer实例绑定到value的__ob__属性上面去 if (Array.isArray(value)) { /* 如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。 这里如果当前浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,如果不支持该属性,则直接覆盖数组对象的原型。 */ if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); // 如果是数组则需要遍历数组的每一个成员进行observe } else { this.walk(value); // 如果是对象则直接walk进行绑定 } };
/** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */ Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); // 遍历对象的每一个属性进行defineReactive绑定 for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i]); } }; /** * Observe a list of Array items. */ Observer.prototype.observeArray = function observeArray (items) { // 数组需要遍历每一个成员进行observe for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } };
Observer为数据加上响应式属性进行双向绑定。如果是对象则进行深度遍历,为每一个子对象都绑定上方法,如果是数组则为每一个成员都绑定上方法。如果是修改一个数组的成员,该成员是一个对象,那只需要递归对数组的成员进行双向绑定即可。但这时候出现了一个问题:如果我们进行pop、push等操作的时候,push进去的对象根本没有进行过双向绑定,更别说pop了,那么我们如何监听数组的这些变化呢? Vue.js提供的方法是重写push、pop、shift、unshift、splice、sort、reverse这七个数组方法。
参考:https://cn.vuejs.org/v2/guide/list.html#数组更新检测
变异方法,顾名思义,会改变调用了这些方法的原始数组。相比之下,也有非变异方法,例如 filter()
、concat()
和 slice()
。它们不会改变原始数组,而总是返回一个新数组。当使用非变异方法时,可以用新数组替换旧数组。你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的启发式方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。
var arrayProto = Array.prototype; var arrayMethods = Object.create(arrayProto); 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); } // 数组新插入的元素需要重新进行observe才能响应式 // notify change // dep通知所有注册的观察者进行响应式处理 ob.dep.notify(); return result }); });
从数组的原型新建一个Object.create(arrayProto)对象,通过修改此原型可以保证原生数组方法不被污染。在保证不污染不覆盖数组原生方法添加监听,主要做了两个操作,第一是通知所有注册的观察者进行响应式处理,第二是如果是添加成员的操作,需要对新成员进行observe。
Dep就是一个发布者,可以订阅多个观察者,依赖收集之后Deps中会存在一个或多个Watcher对象,在数据变更的时候通知所有的Watcher。
// 通知所有订阅者 Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice();for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } };
/** * Subscriber interface. * Will be called when a dependency changes. */ // 调度者接口,当依赖发生改变的时候进行回调 Watcher.prototype.update = function update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true; } else if (this.sync) { // 同步则执行run直接渲染视图 this.run(); } else { // 异步推送到观察者队列中,由调度者调用 queueWatcher(this); } };
/** * Scheduler job interface. * Will be called by the scheduler. */ // 调度者工作接口,将被调度者回调 Watcher.prototype.run = function run () { if (this.active) { var value = this.get(); if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. // 即便值相同,拥有Deep属性的观察者以及在对象/数组上的观察者应该被触发更新,因为它们的值可能发生改变。 isObject(value) || this.deep ) { // set new value var oldValue = this.value; this.value = value; if (this.user) { try { this.cb.call(this.vm, value, oldValue); } catch (e) { handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\"")); } } else { this.cb.call(this.vm, value, oldValue); } } } };
/** * Define a reactive property on an Object. */ function defineReactive$$1 (obj, key, val, customSetter, shallow) { var dep = new Dep();// cater for pre-defined getter/setters // 如果之前该对象已经预设了getter以及setter函数则将其取出来,新定义的getter/setter中会将其执行,保证不会覆盖之前已经定义的getter/setter。 var getter = property && property.get; var setter = property && property.set;// 对象的子对象递归进行observe并返回子节点的Observer对象 var childOb = !shallow && observe(val); 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) { } }); }
4、vue.js事件机制
为什么在HTML中监听事件(参考:https://cn.vuejs.org/v2/guide/events.html#为什么在-HTML-中监听事件)
你可能注意到这种事件监听的方式违背了关注点分离这个长期以来的优良传统。但不必担心,因为所有的 Vue.js 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,它不会导致任何维护上的困难。实际上,使用 v-on
有几个好处:扫一眼 HTML 模板便能轻松定位在 JavaScript 代码里对应的方法;因为你无须在 JavaScript 里手动绑定事件,你的 ViewModel 代码可以是非常纯粹的逻辑,和 DOM 完全解耦,更易于测试;当一个 ViewModel 被销毁时,所有的事件处理器都会自动被删除。你无须担心如何清理它们。
原文地址:https://www.cnblogs.com/colorful-coco/p/11719077.html