vue系列---理解Vue中的computed,watch,methods的区别及源码实现(六)
阅读目录
一. 理解 Vue 中的 computed 用法
computed 是计算属性的; 它会根据所依赖的数据动态显示新的计算结果, 该计算结果会被缓存起来。computed 的值在 getter 执行后是会被缓存的。如果所依赖的数据发生改变时候, 就会重新调用 getter 来计算最新的结果。
下面我们根据官网中的 demo 来理解下 computed 的使用及何时使用 computed。
computed 设计的初衷是为了使模板中的逻辑运算更简单, 比如在 Vue 模板中有很多复杂的数据计算的话, 我们可以把该计算逻辑放入到 computed 中去计算。
下面我们看下官网中的一个 demo 如下:
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> {{msg.split('').reverse().join('') }} </div> <script type="text/javascript"> new Vue({ el: '#app', data: { msg: 'hello' } }); </script> </body> </html>
如上代码, 我们的 data 属性中的 msg 默认值为 'hello'; 然后我们在 vue 模板中会对该数据值进行反转操作后输出数据, 因此在页面上就会显示 'olleh'; 这样的数据。这是一个简单的运算, 但是如果页面中的运算比这个还更复杂的话, 这个时候我们可以使用 computed 来进行计算属性值, computed 的目的就是能使模板中的运算逻辑更简单。因此我们现在需要把上面的代码改写成下面如下代码:
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <p>原来的数据: {{msg}}</p> <p>反转后的数据为: {{reversedMsg}}</p> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'hello' }, computed: {reversedMsg() { // this 指向 vm 实例 return this.msg.split('').reverse().join('')}} }); </script> </body> </html>
如上代码, 我们在 computed 中声明了一个计算属性 reversedMsg。我们提供的 reversedMsg 函数, 将用作属性 vm.reversedMsg 的 getter 函数; 我们可以在上面实例化后代码中, 打印如下信息:
console.log(vm);
打印信息如下所示, 我们可以看到 vm.reversedMsg = 'olleh';
我们也可以打开控制台, 当我们修改 vm.msg 的值后, vm.reversedMsg 的值也会发生改变,如下控制台打印的信息可知:
如上打印的信息我们可以看得到, 我们的 vm.reversedMsg 的值依赖于 vm.msg 的值,当 vm.msg 的值发生改变时, vm.reversedMsg 的值也会得到更新。
computed 应用场景
1. 适用于一些重复使用数据或复杂及费时的运算。我们可以把它放入 computed 中进行计算, 然后会在 computed 中缓存起来, 下次就可以直接获取了。
2. 如果我们需要的数据依赖于其他的数据的话, 我们可以把该数据设计为 computed 中。
二:computed 和 methods 的区别?
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <p>原来的数据: {{msg}}</p> <p>反转后的数据为: {{reversedMsg() }}</p> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'hello' }, /* computed: {reversedMsg() { // this 指向 vm 实例 return this.msg.split('').reverse().join('') } } */ methods: {reversedMsg() { // this 指向 vm 实例 return this.msg.split('').reverse().join('')}} }); console.log(vm); </script> </body> </html>
如上代码, 我们反转后的数据在模板中调用的是方法 reversedMsg(); 该方法在 methods 中也定义了。那么也可以实现同样的效果, 那么他们之间到底有什么区别呢?
区别是:
1. computed 是基于响应性依赖来进行缓存的。只有在响应式依赖发生改变时它们才会重新求值, 也就是说, 当 msg 属性值没有发生改变时, 多次访问 reversedMsg 计算属性会立即返回之前缓存的计算结果, 而不会再次执行 computed 中的函数。但是 methods 方法中是每次调用, 都会执行函数的, methods 它不是响应式的。
2. computed 中的成员可以只定义一个函数作为只读属性, 也可以定义成 get/set 变成可读写属性, 但是 methods 中的成员没有这样的。
我们可以再看下如下 demo:
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <div>第一次调用 computed 属性: {{reversedMsg}}</div> <div>第二次调用 computed 属性: {{reversedMsg}}</div> <div>第三次调用 computed 属性: {{reversedMsg}}</div> <!-- 下面是 methods 调用 --> <div>第一次调用 methods 方法: {{reversedMsg1() }}</div> <div>第二次调用 methods 方法: {{reversedMsg1() }}</div> <div>第三次调用 methods 方法: {{reversedMsg1() }}</div> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'hello' }, computed: {reversedMsg() { console.log(1111); // this 指向 vm 实例 return this.msg.split('').reverse().join('')}}, methods: {reversedMsg1() { console.log(2222); // this 指向 vm 实例 return this.msg.split('').reverse().join('')}} }); console.log(vm); </script> </body> </html>
执行后的结果如下所示:
如上代码我们可以看到, 在 computed 中有属性 reversedMsg, 然后在该方法中会打印 1111; 信息出来, 在 methods 中的方法 reversedMsg1 也会打印 2222 信息出来, 但是在 computed 中, 我们除了第一次之后,再次获取 reversedMsg 值后拿得是缓存里面的数据, 因此就不会再执行该 reversedMsg 函数了。但是在 methods 中, 并没有缓存, 每次执行 reversedMsg1() 方法后,都会打印信息。
从上面截图信息我们就可以验证的。
那么我们现在再来理解下缓存的作用是什么呢? computed 为什么需要缓存呢? 我们都知道我们的 http 也有缓存, 对于一些静态资源, 我们 nginx 服务器会缓存我们的静态资源,如果静态资源没有发生任何改变的话, 会直接从缓存里面去读取, 这样就不会重新去请求服务器数据, 也就是避免了一些无畏的请求, 提高了访问速度, 优化了用户体验。
对于我们 computed 的也是一样的。如上面代码, 我们调用了 computed 中的 reversedMsg 方法一共有三次,如果我们也有上百次调用或上千次调用的话, 如果依赖的数据没有改变, 那么每次调用都要去计算一遍, 那么肯定会造成很大的浪费。因此 computed 就是来优化这件事的。
三:Vue 中的 watch 的用法
watch 它是一个对 data 的数据监听回调, 当依赖的 data 的数据变化时, 会执行回调。在回调中会传入 newVal 和 oldVal 两个参数。
Vue 实列将会在实例化时调用 $watch(), 他会遍历 watch 对象的每一个属性。
watch 的使用场景是:当在 data 中的某个数据发生变化时, 我们需要做一些操作, 或者当需要在数据变化时执行异步或开销较大的操作时. 我们就可以使用 watch 来进行监听。
watch 普通监听和深度监听
如下普通监听数据的基本测试代码如下:
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <p>空智个人信息情况: {{basicMsg}}</p> <p>空智今年的年龄: <input type="text" v-model="age" /></p> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { basicMsg: '', age: 31, single: '单身' }, watch: {age(newVal, oldVal) { this.basicMsg = '今年' + newVal + '岁' + ' ' + this.single; } } }); </script> </body> </html>
显示效果如下:
如上代码, 当我们在 input 输入框中输入年龄后, 比如 32, 那么 watch 就能对 'age' 这个属性进行监听,当值发生改变的时候, 就会把最新的计算结果赋值给 'basicMsg' 属性值, 因此最后在页面上就会显示 'basicMsg' 属性值了。
理解 handler 方法及 immediate 属性
如上 watch 有一个特点是: 第一次初始化页面的时候, 是不会去执行 age 这个属性监听的, 只有当 age 值发生改变的时候才会执行监听计算. 因此我们上面第一次初始化页面的时候, 'basicMsg' 属性值默认为空字符串。那么我们现在想要第一次初始化页面的时候也希望它能够执行 'age' 进行监听, 最后能把结果返回给 'basicMsg' 值来。因此我们需要修改下我们的 watch 的方法,需要引入 handler 方法和 immediate 属性, 代码如下所示:
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <p>空智个人信息情况: {{basicMsg}}</p> <p>空智今年的年龄: <input type="text" v-model="age" /></p> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { basicMsg: '', age: 31, single: '单身' }, watch: { age: {handler(newVal, oldVal) { this.basicMsg = '今年' + newVal + '岁' + ' ' + this.single; }, immediate: true } } }); </script> </body> </html>
如上代码, 我们给我们的 age 属性绑定了一个 handler 方法。其实我们之前的 watch 当中的方法默认就是这个 handler 方法。但是在这里我们使用了 immediate: true; 属性,含义是: 如果在 watch 里面声明了 age 的话, 就会立即执行里面的 handler 方法。如果 immediate 值为 false 的话, 那么效果就和之前的一样, 就不会立即执行 handler 这个方法的。因此设置了 immediate:true 的话, 第一次页面加载的时候也会执行该 handler 函数的。即第一次 basicMsg 有值。
因此第一次页面初始化效果如下:
理解 deep 属性
watch 里面有一个属性为 deep,含义是:是否深度监听某个对象的值, 该值默认为 false。
如下测试代码:
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <p>空智个人信息情况: {{basicMsg}}</p> <p>空智今年的年龄: <input type="text" v-model="obj.age" /></p> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { obj: { basicMsg: '', age: 31, single: '单身' } }, watch: { 'obj': {handler(newVal, oldVal) { this.basicMsg = '今年' + newVal.age + '岁' + ' ' + this.obj.single; }, immediate: true, deep: true // 需要添加 deep 为 true 即可对 obj 进行深度监听 } } }); </script> </body> </html>
如上测试代码, 如果我们不把 deep: true 添加的话, 当我们在输入框中输入值的时候,改变 obj.age 值后,obj 对象中的 handler 函数是不会被执行到的。受 JS 的限制, Vue 不能检测到对象属性的添加或删除的。它只能监听到 obj 这个对象的变化, 比如说对 obj 赋值操作会被监听到。比如在 mounted 事件钩子函数中对我们的 obj 进行重新赋值操作, 如下代码:
mounted() { this.obj = { age: 22, basicMsg: '', single: '单身' }; }
最后我们的页面会被渲染到 age 为 22; 因此这样我们的 handler 函数才会被执行到。如果我们需要监听对象中的某个属性值的话, 我们可以使用 deep 设置为 true 即可生效。deep 实现机制是: 监听器会一层层的往下遍历, 给对象的所有属性都加上这个监听器。当然性能开销会非常大的。
当然我们可以直接对对象中的某个属性进行监听的,比如就对 'obj.age' 来进行监听, 如下代码也是可以生效的。
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <p>空智个人信息情况: {{basicMsg}}</p> <p>空智今年的年龄: <input type="text" v-model="obj.age" /></p> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { obj: { basicMsg: '', age: 31, single: '单身' } }, watch: { 'obj.age': {handler(newVal, oldVal) { this.basicMsg = '今年' + newVal + '岁' + ' ' + this.obj.single; }, immediate: true, // deep: true // 需要添加 deep 为 true 即可对 obj 进行深度监听 } } }); </script> </body> </html>
watch 和 computed 的区别是:
相同点:他们两者都是观察页面数据变化的。
不同点:computed 只有当依赖的数据变化时才会计算, 当数据没有变化时, 它会读取缓存数据。
watch 每次都需要执行函数。watch 更适用于数据变化时的异步操作。
四:computed 的基本原理及源码实现
computed 上面我们也已经说过, 它设计的初衷是: 为了使模板中的逻辑运算更简单。它有两大优势:
1. 使模板中的逻辑更清晰, 方便代码管理。
2. 计算之后的值会被缓存起来, 依赖的 data 值改变后会重新计算。
因此我们要理解 computed 的话, 我们只需要理解如下几个问题:
1. computed 是如何初始化的, 初始化之后做了那些事情?
2. 为什么我们改变了 data 中的属性值后, computed 会重新计算, 它是如何实现的?
3. computed 它是如何缓存值的, 当我们下次访问该属性的时候, 是怎样读取缓存数据的?
理解 Vue 源码中 computed 实现流程
computed 初始化
在理解如何初始化之前, 我们来看如下简单的 demo, 然后一步步看看他们的源码是如何做的。
<!DOCTYPE html> <html> <head> <title>vue</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <p>原来的数据: {{msg}}</p> <p>反转后的数据为: {{reversedMsg}}</p> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'hello' }, computed: {reversedMsg() { // this 指向 vm 实例 return this.msg.split('').reverse().join('')}} }); </script> </body> </html>
如上代码, 我们看到代码入口就是 vue 的实例化, new Vue({}) 作为入口, 因此会调用 vue/src/core/instance/index.js 中的 init 函数代码, 如下所示:
......... 更多代码省略 /* @param {options} Object options = { el: '#app', data: {msg: 'hello'}, computed: {reversedMsg() { // this 指向 vm 实例 return this.msg.split('').reverse().join('') } } }; */ import {initMixin} from './init' function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } initMixin(Vue);..... 更多代码省略
export default Vue;
如上代码, 会执行 this._init(options); 方法内部,因此会调用 vue/src/core/instance/init.js 文件中的 _init 方法, 基本代码如下所示:
import {initState} from './state'; export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { .... 更多代码省略 initState(vm); .... 更多代码省略 } }
因此继续执行 initState(vm); 中的代码了, 因此会调用 vue/src/core/instance/state.js 中的文件代码, 基本代码如下:
import config from '../config' import Watcher from '../observer/watcher' import Dep, {pushTarget, popTarget} from '../observer/dep'..... 更多代码省略
/
@param {vm}
vm = {
$attrs: {},
$children: [],
$listeners: {},
$options: {
components: {},
computed: {
reversedMsg() {
// this 指向 vm 实例
return this.msg.split('').reverse().join('')
}
},
el: '#app',
..... 更多属性值
},
.... 更多属性
};
/
export function initState (vm: Component) {
vm._watchers = []
const 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)
}
}..... 更多代码省略
如上代码, 形参上的 vm 参数值基本值如上注释。代码内部先判断 vm.options.props是否有该属性,有的话,就调用initProps()方法进行初始化,接着会判断vm.options.methods; 是否有该方法, 有的话,调用 initMethods()方法进行初始化。这些所有的我们先不看, 我们这边最主要的是看 if (opts.computed) initComputed(vm, opts.computed) 这句代码; 判断 vm.$options.computed 是否有, 如果有的话, 就执行 initComputed(vm, opts.computed); 函数。因此我们找到 initComputed 函数代码如下:
/* @param {vm} 值如下: vm = {$attrs: {}, $children: [], $listeners: {}, $options: {components: {}, computed: {reversedMsg() { // this 指向 vm 实例 return this.msg.split('').reverse().join('') } }, el: '#app', ..... 更多属性值 }, .... 更多属性 }; @param {computed} Object computed = {reversedMsg() { // this 指向 vm 实例 return this.msg.split('').reverse().join('') } }; */const computedWatcherOptions = { lazy: true };
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null);
// computed properties are just getters during SSR
const isSSR = isServerRendering()for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(Getter is missing </span><span style="color: rgba(0, 0, 255, 1)">for</span> computed property "${key}"<span style="color: rgba(0, 0, 0, 1)">.
,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
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 (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(The computed property </span>"${key}" is already defined <span style="color: rgba(0, 0, 255, 1)">in</span><span style="color: rgba(0, 0, 0, 1)"> data.
, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(The computed property </span>"${key}"<span style="color: rgba(0, 0, 0, 1)"> is already defined as a prop.
, vm)
}
}
}
}
如上代码, 首先使用 Object.create(null); 创建一个空对象, 分别赋值给 watchers; 和 vm._computedWatchers; 接着执行代码:
const isSSR = isServerRendering(); 判断是否是服务器端渲染, 我们这边肯定不是服务器端渲染,因此 const isSSR = false;
接着使用 for in 循环遍历 computed; 代码:for (const key in computed) {const userDef = computed[key] };
接着判断 userDef 该值是否是一个函数, 或者也可以是一个对象, 因此我们可以推断我们的 computed 可以如下编写代码:
computed: {reversedMsg() { // this 指向 vm 实例 return this.msg.split('').reverse().join('')}}
或如下初始化代码也是可以的:
computed: { reversedMsg: {get() { // this 指向 vm 实例 return this.msg.split('').reverse().join('')}} }
当我们拿不到我们的 getter 的时候, vue 会报出一个警告信息。
接着代码, 如下所示:
if (!isSSR) { // create internal watcher for the computed property. watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) }
如上代码, 我们会根据 computed 中的 key 来实例化 watcher,因此我们可以理解为其实 computed 就是 watcher 的实现, 通过一个发布订阅模式来监听的。给 Watch 方法传递了四个参数, 分别为 VM 实列, 上面我们获取到的 getter 方法, noop 是一个回调函数。computedWatcherOptions 参数我们在源码初始化该值为:const computedWatcherOptions = {lazy: true}; 我们再来看下 Watcher 函数代码, 该函数代码在:
vue/src/core/observer/watcher.js 中; 基本源码如下:
/* vm = {$attrs: {}, $children: [], $listeners: {}, $options: {components: {}, computed: {reversedMsg() { // this 指向 vm 实例 return this.msg.split('').reverse().join('') } }, el: '#app', ..... 更多属性值 }, .... 更多属性 }; expOrFn = function reversedMsg(){}; expOrFn 是我们上面获取到的 getter 函数. cb 的值是一个回调函数。 options = {lazy: true}; isRenderWatcher = undefined; */ export default class Watcher { .... constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } /* 当前的 watcher 添加到 vue 的实列上, 因此: vm._watchers = [Watcher]; 即 vm._watchers[0].vm = {$attrs: {}, $children: [], $listeners: {}, $options: {components: {}, computed: {reversedMsg() {}} } } .... */ vm._watchers.push(this); // options /* options = {lazy: true}; 因此: // 如果 deep 为 true 的话,会对 getter 返回的对象再做一次深度的遍历 this.deep = !!options.deep; 即 this.deep = false; // user 是用于标记这个监听是否由用户通过 $watch 调用的 this.user = !!options.user; 即: this.user = false;// lazy用于标记watcher是否为懒执行,该属性是给 computed data 用的,当 data 中的值更改的时候,不会立即计算 getter // 获取新的数值,而是给该 watcher 标记为dirty,当该 computed data 被引用的时候才会执行从而返回新的 computed // data,从而减少计算量。 this.lazy = !!options.lazy; 即: this.lazy = true; // 表示当 data 中的值更改的时候,watcher 是否同步更新数据,如果是 true,就会立即更新数值,否则在 nextTick 中更新。 this.sync = !!options.sync; 即: this.sync = false; this.before = options.before; 即: this.before = undefined; </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (options) { </span><span style="color: rgba(0, 0, 255, 1)">this</span>.deep = !!<span style="color: rgba(0, 0, 0, 1)">options.deep </span><span style="color: rgba(0, 0, 255, 1)">this</span>.user = !!<span style="color: rgba(0, 0, 0, 1)">options.user </span><span style="color: rgba(0, 0, 255, 1)">this</span>.lazy = !!<span style="color: rgba(0, 0, 0, 1)">options.lazy </span><span style="color: rgba(0, 0, 255, 1)">this</span>.sync = !!<span style="color: rgba(0, 0, 0, 1)">options.sync </span><span style="color: rgba(0, 0, 255, 1)">this</span>.before =<span style="color: rgba(0, 0, 0, 1)"> options.before } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> { </span><span style="color: rgba(0, 0, 255, 1)">this</span>.deep = <span style="color: rgba(0, 0, 255, 1)">this</span>.user = <span style="color: rgba(0, 0, 255, 1)">this</span>.lazy = <span style="color: rgba(0, 0, 255, 1)">this</span>.sync = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)"> } </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> cb 为回调函数</span> <span style="color: rgba(0, 0, 255, 1)">this</span>.cb =<span style="color: rgba(0, 0, 0, 1)"> cb </span><span style="color: rgba(0, 0, 255, 1)">this</span>.id = ++uid <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> uid for batching </span> <span style="color: rgba(0, 0, 255, 1)">this</span>.active = <span style="color: rgba(0, 0, 255, 1)">true</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> this.dirty = true;</span> <span style="color: rgba(0, 0, 255, 1)">this</span>.dirty = <span style="color: rgba(0, 0, 255, 1)">this</span>.lazy <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> for lazy watchers</span> <span style="color: rgba(0, 0, 255, 1)">this</span>.deps =<span style="color: rgba(0, 0, 0, 1)"> [] </span><span style="color: rgba(0, 0, 255, 1)">this</span>.newDeps =<span style="color: rgba(0, 0, 0, 1)"> [] </span><span style="color: rgba(0, 0, 255, 1)">this</span>.depIds = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Set() </span><span style="color: rgba(0, 0, 255, 1)">this</span>.newDepIds = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Set(); </span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> 把函数转换成字符串的形式(不是正式环境下) this.expression = "reversedMsg() { return this.msg.split('').reverse().join('') }" </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">this</span>.expression = process.env.NODE_ENV !== 'production' ?<span style="color: rgba(0, 0, 0, 1)"> expOrFn.toString() : </span>'' <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> parse expression for getter</span> <span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> 判断expOrFn是否是一个函数, 如果是一个函数, 直接赋值给 this.getter; 否则的话, 它是一个表达式的话, 比如 'a.b.c' 这样的,因此调用 this.getter = parsePath(expOrFn); parsePath函数的代码在:vue/src/core/util/lang.js 中。 </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">typeof</span> expOrFn === 'function'<span style="color: rgba(0, 0, 0, 1)">) { </span><span style="color: rgba(0, 0, 255, 1)">this</span>.getter =<span style="color: rgba(0, 0, 0, 1)"> expOrFn } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> { </span><span style="color: rgba(0, 0, 255, 1)">this</span>.getter =<span style="color: rgba(0, 0, 0, 1)"> parsePath(expOrFn) </span><span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.getter) { </span><span style="color: rgba(0, 0, 255, 1)">this</span>.getter =<span style="color: rgba(0, 0, 0, 1)"> noop process.env.NODE_ENV </span>!== 'production' &&<span style="color: rgba(0, 0, 0, 1)"> warn( `Failed watching path: </span>"${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.'<span style="color: rgba(0, 0, 0, 1)">, vm ) } } </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 不是懒加载类型调用get</span> <span style="color: rgba(0, 0, 255, 1)">this</span>.value = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.lazy </span>?<span style="color: rgba(0, 0, 0, 1)"> undefined : </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.get()
}
}
因此如上代码执行完成后, 我们的 vue/src/core/instance/state.js 中的 initComputed() 函数中,如下这句代码执行后:
watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions );
watchers["reversedMsg"] 的值变为如下:
watchers["reversedMsg"] = { active: true, before: false, cb: f noop(a, b, c) {}, deep: false, depIds: Set, deps: [], dirty: true, expression: 'reversedMsg(){ return this.msg.split('').reverse().join('') }', getter: f reversedMsg() { return this.msg.split('').reverse().join('') }, id: 1, lazy: true, newDepIds: Set, newDeps: [], sync: false, user: false, value: undefined, vm: { // Vue 的实列对象 } };
如果 computed 中有更多的方法的话, 就会返回更多的 watchers['xxxx'] 这样的对象了。
现在我们再回到 vue/src/core/instance/state.js 中的 initComputed() 函数中,继续执行如下代码:
// component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. // 如果 computed 中的 key 没有在 vm中, 则通过 defineComputed 挂载上去。第一次执行的时候, vm 中没有该属性的 if (!(key in vm)) {defineComputed(vm, key, userDef) } else if (process.env.NODE_ENV !== 'production') { // 如果我们的 computed 中的 key 在 data 中或在 props 有同名的属性的话,则直接发出警告。 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)}}
现在我们继续查看 defineComputed 函数代码如下:
export function defineComputed ( target: any, key: string, userDef: Object | Function ) { const 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 (process.env.NODE_ENV !== 'production' && sharedPropertyDefinition.set === noop) { sharedPropertyDefinition.set = function () { warn( `Computed property "${key}" was assigned to but it has no setter.`, this )}} Object.defineProperty(target, key, sharedPropertyDefinition) }
如上代码, 首先执行 const shouldCache = !isServerRendering(); 判断是不是服务器端渲染, 我们这边肯定不是的, 因此 shouldCache 为 true, 该参数的作用是否需要被缓存数据, 为 true 是需要被缓存的。也就是说我们的这里的 computed 只要不是服务器端渲染的话, 默认会缓存数据的。
接着会判断 userDef 是否是一个函数, 如果是函数的话,说明是我们的 computed 的用法。因此 sharedPropertyDefinition.get = createComputedGetter(key); 的返回值。如果不是函数, 有可能就是表达式, 比如 watch 中的监听 'a.b.c' 这样的话, 就执行 else 语句代码了。
现在我们来看下 createComputedGetter 函数代码如下:
/* @param key = "reversedMsg" */ function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) {watcher.evaluate() } if (Dep.target) {watcher.depend() } return watcher.value } } }
因此 sharedPropertyDefinition.get,其实返回的是 computedGetter()函数的,即: function computedGetter() {};
最后我们再回到 export function defineComputed()函数代码中:执行代码:Object.defineProperty(target, key, sharedPropertyDefinition); 使用 Object.defineProperty 来监听对象属性值的变化;
/* @param {target} vm 实列对象 @param {key} "reversedMsg" @param {sharedPropertyDefinition} sharedPropertyDefinition = { configurable: true, enumerable: true, get: function computedGetter () {var watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) {if (watcher.dirty) {watcher.evaluate(); } if (Dep.target) {watcher.depend(); } return watcher.value } }, set: function noop(a, b, c) {}} */ Object.defineProperty(target, key, sharedPropertyDefinition);
如上代码我们可以看到, 我们会使用 Object.defineProperty 来监听 Vue 实列上的 reversedMsg 属性. 然后会执行 sharedPropertyDefinition 中的 get 或 set 函数的。因此只要我们的 data 对象中的某个属性发生改变的话, 我们的 reversedMsg 方法中依赖了该属性的话, 也会调用 sharedPropertyDefinition 方法中的 get/set 方法的。
但是在我们的页面第一次初始化的时候, 我们要如何初始化执行 computed 中的对应方法呢?
因此我们现在需要再回到 vue/src/core/instance/init.js 中的 _init() 方法中,接着需要看下面的代码; 如下代码:
Vue.prototype._init = function (options?: Object) { ...... 更多的代码已省略 /* vm = {$attrs: {}, $children: [], $listeners: {}, $options: {components: {}, computed: {reversedMsg: f reversedMsg(){}}, data: function mergedInstanceDataFn () { ..... }, el: '#app', ..... 更多参数 } }; */ if (vm.$options.el) {vm.$mount(vm.$options.el) } ...... 更多的代码已省略 }
因此执行 vm.mount(vm.options.el); 这句代码了; 该代码的作用是对我们的页面中的模板进行编译操作。
该代码在 vue/src/platforms/web/entry-runtime-with-compiler.js 中。具体的内部代码我们先不看, 在下一个章节中我们会有讲解该内部代码的。我们只需要看该 js 中的最后一句代码即可, 如下代码:
const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component{ ..... 省略很多很多代码 return mount.call(this, el, hydrating);}
最后一句代码, 会调用 mount.call(this, el, hydrating); 这句代码; 因此会找到 vue/src/platforms/web/runtime/index.js 中的代码:
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
接着执行代码 mountComponent(this, el, hydrating); 会找到 vue/src/core/instance/lifecycle.js 中代码
export function mountComponent() { ..... 省略很多代码new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true / isRenderWatcher /).... 省略很多代码
}
在这里我们就可以看到, 我们对 Watcher 进行实列化了, new Watcher(); 因此我们又回到了 vue/src/core/observer/watcher.js 中对代码进行初始化;
export default class Watcher { ..... 省略很多代码 constructor() { .... 省略很多代码 this.value = this.lazy ? undefined : this.get();} }
此时 this.lazy = false; 因此会执行 this.get() 函数, 该函数代码如下:
get () { pushTarget(this) let value const 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 }
也就是说执行了 this.getter.call(vm, vm) 方法; 最后就执行到 vue/src/core/instance/state.js 中如下代码:
function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) {watcher.evaluate() } if (Dep.target) {watcher.depend() } return watcher.value } } }
因此最后就返回 watcher.value 值了, 就是我们的 computed 的 reversedMsg 返回的值了。如上就是整个 computed 执行的过程,它最主要也是通过事件的发布 - 订阅模式来监听对象数据的变化实现的。如上只是简单的理解下源码如何做到的, 等稍后会有章节 讲解 new Vue({}) 实列话,到底做了那些事情, 我们会深入讲解到的。
对于 methods 及 watcher 也是一样的,后续会更深入的讲解到。