前面三个小节我们根据下图分析了Vue整个响应式系统的闭环,这一节我们直接来看Vue源码。
回顾一下我们自己的响应式实现过程:
这些是我们简化后的过程,Vue的响应式设计远比我们自己做的复杂的多,简化是为了帮助我们更好的理解响应式原理,接下来我们从Vue的源码中一步一步来看整个响应式系统的设计。
在此之前我们先来了解一下响应式系统的初始化过程。
从Vue实例化到响应式系统初始完成的过程是:
新创建一个实例后,Vue调用compile将el转换成vnode。
调用initState, 创建props, data等钩子以及其对象成员的Observer(添加getter和setter)。
执行mount挂载操作,在挂载时建立一个直接对应render的Watcher,并且编译模板生成render函数,执行vm._update来更新DOM。
每当有数据改变,都将通知对应的Watcher执行回调函数,更新视图。
在这个过程中:
有了以上的梳理之后,现在我们可以结合源码来进行验证了。(演示源码版本为vue 2.6.12)
真正创建compile的函数是createCompiler,这个函数在源码中直接就传入了默认参数(baseOptions)进行了调用以生成vnode。
var createCompiler = createCompilerCreator(function baseCompile( template, options ) { var ast = parse(template.trim(), options); if (options.optimize !== false) { optimize(ast, options); } var code = generate(ast, options); return { ast: ast, render: code.render, staticRenderFns: code.staticRenderFns, }; }); /* */ var ref$1 = createCompiler(baseOptions);
compile并不是本节重点
接着找到以下代码位置:
function Vue(options) { if (!(this instanceof Vue)) { warn("Vue is a constructor and should be called with the `new` keyword"); } this._init(options); }
可以看到在初始化Vue实例时,只调用了_init方法,而_init方法是在initMixin函数中设置的,去掉其它的代码,我们找到了initState调用和vm.$mount方法调用(用于挂载模板)
function initMixin(Vue) { Vue.prototype._init = function (options) { var vm = this; // 省略部分代码.... initState(vm); // 省略部分代码.... if (vm.$options.el) { vm.$mount(vm.$options.el); } }; }
initState的源码如下,它的作用初始化props、methods、data、computed、watch,而对数据进行observer在initData中
function initState(vm) { vm._watchers = []; var opts = vm.$options; if (opts.props) { initProps(vm, opts.props); } if (opts.methods) { initMethods(vm, opts.methods); } // 如果vm.$options.data存在则调用initData // 否则给vm添加_data,通过observe处理成响应式数据 if (opts.data) { initData(vm); } else { observe((vm._data = {}), true /* asRootData */); } if (opts.computed) { initComputed(vm, opts.computed); } if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch); } }
initData源码如下:前面的代码可以略过不看(为什么组件data中需要传函数并return一个对象,你可以在这里找到答案),最后是调用了observe函数
function initData(vm) { var data = vm.$options.data; // 对data进行函数验证 data = vm._data = typeof data === "function" ? getData(data, vm) : data || {}; // 如果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; // 判断props和methods中是否有跟data中同名的属性 // 进行相应的警告处理 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进行响应式处理 observe(data, true /* asRootData */); }
这个observe函数是什么呢?去掉一些影响阅读的部分,可以看到它是Observer类的实例
function observe(value, asRootData) { // 省略部分代码... var ob; ob = new Observer(value); return ob; }
Observer类源码如下,前面一大堆参数验证巴拉巴拉后,最后调用了this.walk方法
var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; // 给value添加__ob__属性,值就是本Observer对象,value.__ob__ = this; // Vue.$data 中每个对象都 __ob__ 属性,包括 Vue.$data对象本身 def(value, '__ob__', this); // 判断是否为数组,不是的话调用walk()添加getter和setter // 如果是数组,调用observeArray()遍历数组,为数组内每个对象添加getter和setter if (Array.isArray(value)) { var augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); this.observeArray(value); } else { this.walk(value); } };
walk方法源码如下:主要作用是遍历所有属性并调用defineReactive$$1函数处理数据
Observer.prototype.walk = function walk(obj) { var keys = Object.keys(obj); // 遍历每个属性并调用defineReactive$$1将数据转化成getter和setter for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i]); } };
defineReactive$$1源码如下:在第一小节的时候我们就提过这个函数的作用,主要是通过Object.defineProperty来将普通的数据处理成响应式数据
function defineReactive$$1(obj, key, val, customSetter, shallow) { // 闭包dep实例 var dep = new Dep(); // 省略部分代码... var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { var value = getter ? getter.call(obj) : val; // Dep.target 全局变量指向的就是当前正在解析指令的Complie生成的 Watcher // 会执行到 dep.addSub(Dep.target), 将 Watcher 添加到 Dep 对象的 Watcher 列表中 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 方法, 通知所有的 Watcher dep.notify(); }, }); }
关于defineReactive$$1的源码解析我们就不再详细展开了,本节主要是了解整个Vue响应式源码设计,有兴趣可以自己再去搜索引擎一下。
接着我们来看Dep类源码。
Dep类的作用就是把一个数据用到的地方收集起来,在这个数据发生改变的时候,统一去通知各个地方做对应的操作,是典型的发布订阅模式体现。
var Dep = function Dep() { // 每个Dep都有唯一的ID this.id = uid++; // subs用于存放依赖 this.subs = []; }; // 向subs数组添加依赖 Dep.prototype.addSub = function addSub(sub) { this.subs.push(sub); }; // 移除依赖 Dep.prototype.removeSub = function removeSub(sub) { remove(this.subs, sub); }; // 设置某个Watcher的依赖 // 这里添加了Dep.target是否存在的判断,目的是判断是不是Watcher的构造函数调用 // 也就是说判断他是Watcher的this.get调用的,而不是普通调用 Dep.prototype.depend = function depend() { if (Dep.target) { Dep.target.addDep(this); } }; // 通知依赖更新 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; }); } // 通知所有绑定 Watcher。调用watcher的update() for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } }; // The current target watcher being evaluated. // This is globally unique because only one watcher // can be evaluated at a time. Dep.target = null;
前面我们一直在说Watcher,那么Watcher是在什么时候调用的呢?
根据前面的分析和源码,我们看到了在initMixin()调用时分别调用了initState()和vm.KaTeX parse error: Expected 'EOF', got ',' at position 8: mount(),̲mount方法的作用就是挂载模板,源码如下:
Vue.prototype.$mount = function ( el, hydrating ) { el = el && inBrowser ? query(el) : undefined; return mountComponent(this, el, hydrating) };
可以看到最后$mount方法中是调用了mountComponent,mountComponent源码如下,去掉影响阅读的代码,可以看到Watcher类在mountComponent函数中进行了实例化。
function mountComponent(vm, el, hydrating) { // 省略部分代码... // 获取更新组件 var updateComponent; /* istanbul ignore if */ if (config.performance && mark) { updateComponent = function () { var name = vm._name; var id = vm._uid; var startTag = "vue-perf-start:" + id; var endTag = "vue-perf-end:" + id; mark(startTag); var vnode = vm._render(); mark(endTag); measure("vue " + name + " render", startTag, endTag); mark(startTag); vm._update(vnode, hydrating); mark(endTag); measure("vue " + name + " patch", startTag, endTag); }; } else { updateComponent = function () { vm._update(vm._render(), hydrating); }; } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined new Watcher( vm, updateComponent, noop, { before: function before() { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, "beforeUpdate"); } }, }, true /* isRenderWatcher */ ); return vm; }
首先会new一个watcher实例对象(主要是将模板与数据建立联系),在watcher对象创建后,会运行传入的方法 vm._update(vm._render(), hydrating) 。其中的vm._render()主要作用就是运行前面compiler生成的render方法,并返回一个vNode对象。vm.update() 则会对比新的 vdom 和当前 vdom,并把差异的部分渲染到真正的 DOM 树上。
Watcher类源码比较多,简化后的模型如下:
class Watcher { constructor(vm, expOrFn, cb, options) { //传进来的对象 例如Vue this.vm = vm; // 在Vue中cb是更新视图的核心,调用diff并更新视图的过程 this.cb = cb; this.id = ++uid$2; // 生成一个唯一id this.sync = !!options.sync; // 默认一般为false // 收集Deps,用于移除监听 this.newDeps = []; this.getter = expOrFn; // 设置Dep.target的值,依赖收集时的watcher对象 this.value = this.get(); } get() { // 设置Dep.target值,用以依赖收集 pushTarget(this); const vm = this.vm; let value = this.getter.call(vm, vm); return value; } //添加依赖 addDep(dep) { // 这里简单处理,在Vue中做了重复筛选,即依赖只收集一次,不重复收集依赖 this.newDeps.push(dep); dep.addSub(this); } // 更新 update() { if (this.lazy) { this.dirty = true; } else if (this.sync) { //如果是同步那就立刻执行回调 this.run(); } else { // 否则把这次更新缓存起来 // 但是就像上面说的,异步更新往往是同一事件循环中多次修改同一个值, // 那么一个wather就会被缓存多次。因为需要一个id来判断一下, queueWatcher(this); } } // 更新视图 run() { // 这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图 console.log(`这里会去执行Vue的diff相关方法,进而更新数据`); } }
在第二小节的时候提过收到通知后Vue会开启一个异步更新队列,至于原因也说的很清楚了,所谓的同步更新是指当观察的主体改变时立刻触发更新,而实际开发中这种需求并不多,同一事件循环中可能需要改变好几次state状态,但视图view只需要根据最后一次计算结果同步渲染就行(react中的setState就是典型)。如果一直做同步更新无疑是个很大的性能损耗。
这就要求watcher在接收到更新通知时不能立刻执行callback,而是将本次更新缓存起来,等到事件循环的下一次Tick时才执行。
在Vue中,一共有四种情况会产生Watcher:
这里的一个要注意的地方是,考虑到极限情况,如果正在更新队列中wather时,又塞入进来该怎么处理?因此,必须有一个Schedule来进行Watcher的调度。
在Vue中,这个负责调度的函数是flushSchedulerQueue,源码如下:
/** * 清空两个队列并运行watcher */ function flushSchedulerQueue() { currentFlushTimestamp = getNow(); flushing = true; // 加入一个flushing来表示队列的更新状态 var watcher, id; // 在刷新之前对队列进行排序。 // 这将确保: // 1. 组件从父组件更新到子组件。(因为父组件总是在子组件之前创建) // 2. 组件的user watchers在它的render watchers之前运行(因为user watchers在render watchers之前创建) // 3. 如果在父组件的监视程序运行期间组件被销毁,则可以跳过它的监视程序。 queue.sort(function (a, b) { return a.id - b.id; }); // do not cache length because more watchers might be pushed // as we run existing watchers for (index = 0; index < queue.length; index++) { watcher = queue[index]; if (watcher.before) { watcher.before(); } id = watcher.id; has[id] = null; watcher.run(); // in dev build, check and stop circular updates. if (has[id] != null) { circular[id] = (circular[id] || 0) + 1; if (circular[id] > MAX_UPDATE_COUNT) { warn( "You may have an infinite update loop " + (watcher.user ? 'in watcher with expression "' + watcher.expression + '"' : "in a component render function."), watcher.vm ); break; } } } }
Schedule 调度的作用(管理Watcher):
去重,每个Watcher有一个唯一的id,如果id已经在队列里了就没必要重复执行,如果id不在队列里,要看队列是否正在执行中。如果不在执行中,则在下一个时间片执行队列,因此队列永远是异步执行的。
排序,按解析渲染的先后顺序执行,即Watcher小的先执行。Watcher里面的id是自增的,先创建的id比后创建的id小。所以会有如下规律:
组件是允许嵌套的,而且解析必然是先解析了父组件再到子组件。所以父组件的id比子组件小。
用户创建的Watcher会比render时候创建的先解析。所以用户创建的Watcher的id比render时候创建的小。
删除Watcher,如果一个组件的Watcher在队列中,而他的父组件被删除了,这个时候也要删掉这个Watcher。
队列执行过程中,存一个对象circular,里面有每个watcher的执行次数,如果哪个watcher执行超过MAX_UPDATE_COUNT定义的次数就认为是死循环,不再执行,默认是100次。
data() { return { a: 1, }; }, computed: { b: function () { this.a + 1; }, }, methods: { editA: function () { this.a = 2; this.a = 3; this.a = 1; }, },
在editA方法中,对a属性进行了三次更新,最后一次与初始值相同,理想情况是a没变,b也不重新计算。这就要求b的watcher执行update时要拿到a最新的值来计算,这里a最新的值是1,如果队列中a的watcher已经更新过,那么就应该把后面的a的watcher放到当前的watcher后面并立即更新,这样可以保证后面的watcher可以拿到a最新的值。
同理,如果a的watcher还没有更新,那么新的a的watcher放在之前的a的watcher的下一位,也是为了保证后面的watcher可以拿到a最新的值。
以上便是Vue响应式系统源码的功能主体,核心部分是Observer、Dep、Watcher,当然还有很多细节部分,相信看了这么多你也能自己上高速了。
vue响应式原理是,我们通过递归遍历,把vue实例中data里面定义的数据,通过Observer类调用defineReactive(Object.defineProperty)重新定义。每个数据内新建一个Dep实例,闭包中包含了这个 Dep 类的实例,用来收集 Watcher 对象。在对象被「读」的时候,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。之后如果当该对象被「写」的时候,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。
由于Object.defineProperty这个API不能检测数组和对象的变化,例如:
对于对象,如果你需要让某个属性是响应式的,必须在data中显示声明它,如:
var vm = new Vue({ data: { a: 1, }, }); // `vm.a` 是响应式的 vm.b = 2; // `vm.b` 是非响应式的
当然,需求是千变万化的,有时候我们就是需要打破这个规则,比如你请求了后端接口,接口中的数据你是不确定的,但是这些数据又必须是响应式的,因此Vue也给我们提供了set方法用于向嵌套对象添加响应式属性,如:
// 全局 Vue.set(vm.someObject, 'b', 2) // 组件内 this.$set(this.someObject,'b',2)
对于数组,直接修改下标项的值或者操作length都是非响应式的,如:
var vm = new Vue({ data: { items: ['a', 'b', 'c'] } }) vm.items[1] = 'x' // 不是响应性的 vm.items.length = 2 // 不是响应性的
同样Vue也提供了解决办法,修改数组下标项的值可以使用如下方式:
// 全局 Vue.set(vm.items, indexOfItem, newValue) // 组件内 vm.$set(vm.items, indexOfItem, newValue)
而操作length属性不响应可以使用如下方式解决:
vm.items.splice(newLength)
Vue对数组原型链上的方法做了重写,使用以下方法操作数组都可以触发响应式。
到这里不得不提一种设计模式——装饰者模式。
什么是装饰者模式?
百度百科解释如下:
装饰模式指的是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
简单理解就是不用改变原对象,通过创建一个包装层来扩展原对象的功能。
当你发现一个方法它的原功能不适用、要扩展,但你又不能直接去修改原方法的时候,装饰者模式就可以派上用场了。
需求1:有一个他人写好的a模块,内部有一个方法b,不能修改他人模块的情况下,扩展b方法。
var a = { b() {}, };
这个需求非常简单,按照装饰者模式的基本结构三步走,我们新建一个自己的方法,在其内部调用b方法,然后加入要扩展的功能,这样就可以在不修改原对象的情况下扩展b方法的功能了,代码示例:
function myb() { a.b(); // 加入扩展操作 }
需求2:假设你进入一家新公司,接手了前同事的代码,他在dom上绑定了很多事件,比如删除按钮绑定了点击事件,点击就进行删除操作,你接手之后产品跟你说觉得之前这种点击就删除没有提示的方式不太友好,需要你在点击确定或者删除的同时给出一个提示,这时候你会怎么做?
给两个例子(自己对号入座【手动滑稽】)
这两种方式都是错的,如果采用第一种方案,势必要把他之前的删除功能代码再写一遍,非常麻烦,如果采用第二种方案,去找老代码这个找的过程也很麻烦,所以我们用装饰者模式来做这个事情,考虑到要做这个事情的按钮可能有很多,我们可以采用工厂模式的思维,直接封装一个装饰工厂,使用时告诉我你要装饰哪个dom,要扩展什么操作就可以了,代码示例:
const decoractor = function(dom, fn) { if(typeof dom.onclick === 'function') { const _old = dom.onclick; // 装饰者模式三步走 // 1.封装新方法 dom.onclick = function() { // 2.调用老方法 _old.apply(this, arguments); // 3.在新方法中加入扩展功能 fn.apply(this, arguments); } } }
在使用的时候比如说要装饰删除按钮,然后要扩展提示功能:
decoractor(deleteDom, function() { alert('删除成功'); })
这样既不用去找老代码也不用去重新写整个事件绑定,只需要调用装饰工厂就好了,扩展起来的速度就快多了。
现在我们需要扩展数组的功能,但是又不能修改数组的原型方法,正好是装饰者模式的应用场景,Vue源码是不是这么做的呢?
源码如下:
// 获取内置对象Array的prototype var arrayProto = Array.prototype; // 继承Array.prototype var arrayMethods = Object.create(arrayProto); // 将需要扩展的方法做成配置数组 var methodsToPatch = [ "push", "pop", "shift", "unshift", "splice", "sort", "reverse", ]; methodsToPatch.forEach(function (method) { // 缓存老的方法 var original = arrayProto[method]; // 1.封装新方法 def(arrayMethods, method, function mutator() { var args = [], len = arguments.length; while (len--) args[len] = arguments[len]; // 2.调用老方法 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 // 3.在新方法中加入扩展功能,通过notify方法通知依赖更新 ob.dep.notify(); return result; }); }); // 数组原型上有很多属性,这一步是去除多余属性 var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
以上源码的流程是:
如果你好奇第4步是在哪里完成的,可以在Observer类中找到它,前面我们说过,在Vue实例化时,首先通过Observer类将data处理成响应式数据,而数组方法的重写也是在Observer类中完成的。
var Observer = function Observer(value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, "__ob__", this); // 判断传入的数据是不是数组 if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods); } else { // 将重写的方法放到data中的数组原型链上 copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); } };
copyAugment函数源码如下:
function copyAugment(target, src, keys) { for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i]; // 取到重写方法名,如push、splice等 // 调用def方法对target重新处理 def(target, key, src[key]); } }
def函数源码如下:只是使用了Object.defineProperty修改对象的现有属性
function def(obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true, }); }
加油,打工人!