引言:express
在Vuejs中用watch来侦听数据变化,computed用来监听多个属性的变化并返回计算值,那么这两个特性是如何实现的呢?下面讲一下二者实现的具体方法以及一些使用经验。函数
结论:oop
首先给一下结论,由上一篇Vue核心原理的介绍的数据绑定能够了解到,若是想监听某个属性的数据变化,那么只须要 new 一个 Watcher 并在 watcher 执行的时候用到那个属性就够了,使用的过程当中该 watcher 会被加入到对应数据的依赖中,数据变化的时候就会获得通知该。那么若是想要实现 Vue 的 watch 和 computed,实际上只须要为对应的属性创建 watcher,并构造出执行时使用数据的函数便可,接下来展开讲一下。性能
1、watch实现原理:ui
借官网的例子用一下this
<div id="demo">{{ fullName }}</div>
var vm = new Vue({ el: '#demo', data: { firstName: 'Foo', lastName: 'Bar', fullName: 'Foo Bar' }, watch: { firstName: function (val) { this.fullName = val + ' ' + this.lastName }, lastName: function (val) { this.fullName = this.firstName + ' ' + val } } })
watch 这段的意思就是要监听 firstName 和 lastName的变化,当数据变化时执行对应的函数,给fullName赋值。lua
根据上面的结论,若是监听 firstName 的变化,那么只须要在初始化的时候 new 一个 watcher,watcher 的回调里面使用到 firstName 就能够了,关键是如何构造回调函数,按照这个思路咱们看一下 Vue 的实现。spa
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); } else { observe(vm._data = {}, true /* asRootData */); } if (opts.computed) { initComputed(vm, opts.computed); } if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch); } }
首先 initWatch(vm, opts.watch) ,注意这里的顺序,initwatch 是在 initData 以后执行的,由于 watch 也是在已有的响应式基础上进行监听,因此要先初始化数据。prototype
function initWatch (vm, watch) { for (var key in watch) { var handler = watch[key]; if (Array.isArray(handler)) { for (var i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]); } } else { createWatcher(vm, key, handler); //以firstName为例,此处为:createWatcher(vm, 'firstName', watch.firstName) } } } function createWatcher ( vm, expOrFn, handler, options ) { if (isPlainObject(handler)) { options = handler; handler = handler.handler; } if (typeof handler === 'string') { handler = vm[handler]; } return vm.$watch(expOrFn, handler, options) //以firstName为例,此处为:vm.$watch('firstName',watch.firstName, options)
}
以后调用 $watch 为 watch 中监听的每一个属性创建 warcher ,watch构造函数中会构造函数并执行。代理
Vue.prototype.$watch = function ( expOrFn, cb, options ) { var vm = this; if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {}; options.user = true; var watcher = new Watcher(vm, expOrFn, cb, options); //以firstName为例,此处为:new watcher('firstName',watch.firstName, undefined) if (options.immediate) { try { cb.call(vm, watcher.value); } catch (error) { handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\"")); } } return function unwatchFn () { watcher.teardown(); } }; }
watcher函数逻辑
var Watcher = function Watcher ( vm, expOrFn, // 'firstName' cb, //watch.firstName 函数 options, isRenderWatcher ) { this.vm = vm;if (isRenderWatcher) { vm._watcher = this; } vm._watchers.push(this); // options if (options) { this.deep = !!options.deep; this.user = !!options.user; this.lazy = !!options.lazy; this.sync = !!options.sync; this.before = options.before; } else { this.deep = this.user = this.lazy = this.sync = false; } this.cb = cb; this.id = ++uid$2; // uid for batching this.active = true; this.dirty = this.lazy; // for lazy watchers this.deps = []; this.newDeps = []; this.depIds = new _Set(); this.newDepIds = new _Set(); this.expression = expOrFn.toString(); // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn; } else { this.getter = parsePath(expOrFn); if (!this.getter) { this.getter = noop; warn( "Failed watching path: \"" + expOrFn + "\" " + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ); } } this.value = this.lazy ? undefined : this.get(); }; /** * Evaluate the getter, and re-collect dependencies. */ Watcher.prototype.get = function get () { pushTarget(this); var value; var vm = this.vm; try { value = this.getter.call(vm, vm); } catch (e) { if (this.user) { handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\"")); } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value); } popTarget(); this.cleanupDeps(); } return value };
能够看到,new Watcher 时传入的表达式是 ‘firstName’,非函数类型,Vue 调用 parsePath(expOrFn=‘firstName’) 构造使用 firstName 的一个 getter 函数,从而创建依赖,开启监听。
/** * Parse simple path. */ var bailRE = new RegExp(("[^" + unicodeLetters + ".$_\\d]")); function parsePath (path) { //path === 'firstName' if (bailRE.test(path)) { return } var segments = path.split('.'); return function (obj) { for (var i = 0; i < segments.length; i++) { if (!obj) { return } obj = obj[segments[i]]; //首次循环 obj === vm ,即 vm.fisrtName } return obj } }
能够看到 parsePath 返回的结果就是一个获取属性的函数,简化下能够写成
this.getter = function (){ return vm.firstName; }
执行次函数就能够将 ‘fisrtName ’ 与当前的 watcher 关联起来,此时的 this.cb 即为 watch 中传入的 firstName 函数。据变化时会通知此 watcher 执行 this.cb。
this.cb = function (val) { this.fullName = val + ' ' + this.lastName }
以上就是 Vue watch 的实现原理,其核心就是如何为侦听的属性构造一个 watcher 以及 watcher 的 getter 函数。
2、computed 实现原理:
一样借助官网的例子看下
<div id="example"> <p>Original message: "{{ message }}"</p> <p>Computed reversed message: "{{ reversedMessage }}"</p> </div> var vm = new Vue({ el: '#example', data: { message: 'Hello' }, computed: { // 计算属性的 getter reversedMessage: function () { // `this` 指向 vm 实例 return this.message.split('').reverse().join('') } } })
一样,想要监听数据( 这里是 'message' )的变化须要创建一个watcher,构造出使用 ' message' 的函数在watcher执行的过程当中进行使用。这里很显然新建watcher须要用到的就是 computed.reverseMessage 函数,不须要构造了。这里须要考虑一个问题,reversedMessage 是一个新增属性,vm上并未定义过响应式,因此此处确定须要借助 Object.defineProperty 将 reverMessage 定义到 vm 上,看一下实现。
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); } else { observe(vm._data = {}, true /* asRootData */); } if (opts.computed) { initComputed(vm, opts.computed); } if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch); } }
var computedWatcherOptions = { lazy: true };
function initComputed (vm, computed) { // $flow-disable-line var watchers = vm._computedWatchers = Object.create(null); // computed properties are just getters during SSR var isSSR = isServerRendering(); for (var key in computed) { var userDef = computed[key]; var getter = typeof userDef === 'function' ? userDef : userDef.get; if (getter == null) { warn( ("Getter is missing for computed property \"" + key + "\"."), vm ); } if (!isSSR) { // create internal watcher for the computed property. watchers[key] = new Watcher( vm, getter || noop, // computed.reverseMessage noop, computedWatcherOptions ); } // component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. if (!(key in vm)) { defineComputed(vm, key, userDef); } else { if (key in vm.$data) { warn(("The computed property \"" + key + "\" is already defined in data."), vm); } else if (vm.$options.props && key in vm.$options.props) { warn(("The computed property \"" + key + "\" is already defined as a prop."), vm); } } } }
初始化首先调用 initComputed(vm, opts.computed) ,在 initComputed 中会执行两个步骤:
第一,为每一个 key 创建一个 watcher ,watcher 的 getter 为对应 key 的函数。
var computedWatcherOptions = { lazy: true }; --- watchers[key] = new Watcher( vm, getter || noop, // computed.reverseMessage noop, // 回调 空 function(){} computedWatcherOptions // lazy: true );
这里须要注意的是 initComputed 中建立的 watcher 为 lazy 模式 。
简单说下,Vue 的 watcher 根据传参不一样能够分为两种,一种是常规(当即执行)模式 ,一种是 lazy (懒执行) 模式:常规模式的 watcher 在初始化时会直接调用 getter ,getter 会获取使用到的响应式数据,进而创建依赖关系;lazy 模式 watcher 中的 getter 不会当即执行,执行的时机是在获取计算属性时,稍后会讲。
第2、经过 defineComputed 调用 Object.defineProperty 将 key 定义到 vm 上。
function defineComputed ( target, key, userDef ) { var shouldCache = !isServerRendering(); if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef); sharedPropertyDefinition.set = noop; } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get) : noop; sharedPropertyDefinition.set = userDef.set || noop; } if (sharedPropertyDefinition.set === noop) { sharedPropertyDefinition.set = function () { warn( ("Computed property \"" + key + "\" was assigned to but it has no setter."), this ); }; } Object.defineProperty(target, key, sharedPropertyDefinition); //target === vm key == 'reverseMessage' }
能够看到,计算属性的get 是由 createComputedGetter 建立而成,那么咱们看下 createComputedGetter 的返回值:
ps:经过 sharedPropertyDefinition 的构造过程能够看到,若是传入的计算属性值为函数,那么至关于计算属性的 get ,此时不容许 set,若是须要对计算属性进行set,那么须要自定义传入 set、get 方法。
function createComputedGetter (key) { return function computedGetter () { var watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { watcher.evaluate(); } if (Dep.target) { watcher.depend(); } return watcher.value } } }
createComputedGetter 返回了一个 computedGetter 函数,这个函数就是获取计算属性(reveserMessage)时的 get 函数,当获取 reveserMessage 的时候会调用 watcher.evaluate() ,看一下watcher.evaluate 的逻辑:
/** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */ Watcher.prototype.evaluate = function evaluate () { this.value = this.get(); this.dirty = false; }; /** * Evaluate the getter, and re-collect dependencies. */ Watcher.prototype.get = function get () { pushTarget(this); var value; var vm = this.vm; try { value = this.getter.call(vm, vm); } catch (e) { if (this.user) { handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\"")); } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value); } popTarget(); this.cleanupDeps(); } return value };
能够看到 watcher 在 evaluate 中会直接调用 get 方法,get 方法会直接调用 getter 并返回获取到的值,而这里的 getter 就是前面新建 watcher 时早已传入的计算属性值,即 computed.reveseMessage 函数,执行getter过程当中就会创建起对数据的依赖。
这里啰嗦两个小问题:
一、计算属性使用了懒执行模式,使用时才会运算并创建依赖,多是考虑到两方面:一个是计算属性不必定会被使用,性能会被白白浪费;另外一个计算属性中可能会存在比较复杂的运算逻辑,放在相对靠后的生命周期中比较合适。
二、计算属性函数的传入是由开发者自行传入的,须要注意数据监听开启的条件是数据被使用过,在使用过程当中须要注意 if 条件语句的使用,最好把须要用到的数据都定义在最上层。
以上就是computed的实现原理。
总结:
本文主要讲了 Vuejs 中 watch 和 computed 的实现原理,核心就是要建立 watcher 并为 watcher 构造相应的 getter 函数,经过 getter 函数的执行进行绑定依赖。根据 getter 执行的时机不一样 watcher 能够分为当即执行以及懒执行两种模式,当即执行模式 getter 会在构造函数中直接执行,懒执行模式 getter 须要调用 evaluate 来执行。在使用的场景上 watch 适合直接监听单个属性,不涉及复杂的监听逻辑场景,computed 适合涉及多个监听变化的逻辑,另外 computed 比较适合作数据代理,当某些数据产生的过程比较复杂,不敢下手的时候直接一层 computed 代理就能够完美解决。