文章目录
前言
有过面试的人应该知道,vue 的经典问法:computed与watch的区别:)这时候你就应该找一下官网:
- 计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。
- 当需要在数据变化时执行异步或开销较大的操作时。
- 巴拉巴拉…但是却不知道为啥: )
提示:以下是本篇文章正文内容,下面案例可供参考
一、官网例子
在我们index.html的加上我们的官网案例修改一下
<div id="root">
<p>{{ fullName }}</p>
<p>
Ask a yes/no question:
<input v-model="question">
</p>
<p>答案{{ answer }}</p>
</div>
<script>
new Vue({
el: '#root',
data: {
firstName: 'Foo',
lastName: 'Bar',
question: '',
answer: 'I cannot give you an answer until you ask a question!'
},
computed: {
fullName: function() {
return this.firstName + ' ' + this.lastName
}
// fullName: {
// get: function() {
// return this.firstName + ' ' + this.lastName
// },
// // setter
// set: function(newValue) {
// var names = newValue.split(' ')
// this.firstName = names[0]
// this.lastName = names[names.length - 1]
// }
// }
},
watch: {
question: function(newQuestion, oldQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.getAnswer()
}
},
methods: {
getAnswer() {
var vm = this
setTimeout(() => {
vm.answer = 233
}, 5000)
}
}
})
好了现在进入源码:其实就是三种watcher 的区别,渲染watcher(上一篇),计算watcher,用户watcher。
二、watch
1.还是initSates
src\core\instance\state.js
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)//进入
}
}
2.进入initWatch
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]//拿到方法 如上面的question
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)//不是数组直接使用watcher
}
}
}
function createWatcher (
vm: Component,
expOrFn: string | Function, //"question"
handler: any,//function question(...){...}
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
2.vm.$watch(“question”, question(…){…}, options) 核心
Vue.prototype.$watch = function(
expOrFn: string | Function,//'question'
cb: any,//question(){}
options ? : Object
): Function {
const vm: Component = this
options.user = true,//用户watcher
const watcher = new Watcher(vm, expOrFn, cb, options)//回到了Watcher
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)//立马执行,// 以当前值立即执行一次回调函数
popTarget()
}
return function unwatchFn() {
watcher.teardown()
}
}
注意到了吗,new Watcher(vm, expOrFn, cb, options)
与渲染watcher 的区别
new Watcher(vm, updateComponent, noop, {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}
- 渲染watcher 第二个函数是updateComponent函数,第二个 callback函数是空。
- 用户watcher 第一个是属性名,第二个函数回调question(){}
3. 再进Watcher
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
user: boolean;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
vm._watchers.push(this)
// options
if (options) {
this.user = !!options.user
}
this.cb = cb
// parse expression for getter
this.getter = parsePath(expOrFn)//得是对象路径形式,如user.name 返回一个闭包
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)//执行那个闭包
}
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
}
4. parsePath(expOrFn)
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')//比如watch "user.nam2" segments=['user','name']
return function (obj) {//obj =vm
for (let i = 0; i < segments.length; i++) { //
if (!obj) return
obj = obj[segments[i]]//vm.user ==>obj = user
//第二次循环 user.name obj = name 其实就是读取这个watch 的对象 触发dep.depend
}
return obj
}
}
dep 收集的是这个user -watcher 当触发更新的时候,执行的是cb 罢了
update () {
else if (this.sync) {
this.run()
}
}
run () {
if (this.active) {
const value = this.get() //再次get()执行那个闭包 得到的是新值
if ( ) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
- 首先必须是一个响应式数据,
- new 一个user-watcher 回调放在了第三个参数上,此时会访问到该响应式数据的dep,并把该watcher 加入该响应式数据的dep,当该响应式数据更新的时候会触发更新,watcher.update拿到新值进行回调的执行。
三、computed
class Watcher{
constructor(vm, expOrFn, cb, options) {
this.vm = vm
this._watchers.push(this)
if(options) {
this.lazy = !!options.lazy // 表示是computed
}
this.dirty = this.lazy // dirty为标记位,表示是否对computed计算
this.getter = expOrFn // computed的回调函数
this.value = undefined
}
}
function initComputed(vm, computed) {
const watchers = vm._computedWatchers = Object.create(null) // 创建一个纯净对象
for(const key in computed) {
const getter = computed[key] // computed每项对应的回调函数
watchers[key] = new Watcher(vm, getter, noop, {lazy: true}) // 实例化computed-watcher
for(const key in computed) {
const getter = computed[key] // // computed每项对应的回调函数
...
if (!(key in vm)) {
defineComputed(vm, key, getter)//把computed 的key 成为一个响应式数据,但也没订阅的时候 作为一个dep 而它的回调是computed每项对应的回调函数
}
}
}
}
function defineComputed(target, key) {
...
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: createComputedGetter(key),
set: noop
})
}
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
}
}
}
class Watcher {
...
evaluate () {
this.value = this.get() // 计算属性求值 此时会触发computed内可以访问到的响应式数据的get 把当前watcher 加入响应式数据的dep
this.dirty = false // 表示计算属性已经计算,不需要再计算
}
depend () {
let i = this.deps.length // deps内是计算属性内能访问到的响应式数据的dep的数组集合
while (i--) {
this.deps[i].depend() // 让每个dep收集当前的render-watcher
}
}
}
更新:
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
}
}
简单点说就是:用一个c=a+b 的计算属性举例。也可以是c{get()}
- 拿到computed每项对应的回调函数 也就是
c(){return a+b}
, - new 一个 Watcher,
c(){}
回调在第二个参数,并且带一个lazy computed 的标记位, - 把 computed属性
c
进行响应式, - 并在computed对象属性
c
的get 做处理,当有人访问这个计算属性,执行计算的回调函数, - 因为计算属性里面有响应式数据,触发该响应式数据,把该计算watcher 收集到该响应式数据的dep,进行计算后得到第一次的值,并把dirty 位进行标记,还记得newDeps,也会收集那个计算属性里面的响应式数据dep。
(前两篇忘说了,watcher 与dep 是双向收集的)
并让这些dep 去收集该计算watcher 的订阅者,比如{{c}}
,渲染watcher 肯定也订阅了 - 当有人访问计算watcher 的时候由于dirty 位 已经计算过,直接返回缓存。
- 当计算属性其中的响应式数据更新了,订阅了该计算watcher,触发计算watcher 的update,标记为为true,表明需要下次计算需要重新计算。
- 计算属性其中的响应式数据更新,这个响应试数据的dep由于第5步,也会去触发render (如果视图用上了),更新。
简化:先new 一个计算watcher,把计算属性进行响应式,当有人访问这个计算属性,比如渲染watcher,触发了get,进行计算,由于访问里面的响应式数据,比如说a,a的dep就收集了该watcher,也收集了这个渲染watcher,计算到结果后把dirty为变true
当有人访问计算属性,由于dirty为直接返回缓存,当a改变,先通知计算watcher把dirty改变,再通知渲染watcher,同时触发computed 的get,dirty为改了,重新求值。
总结
区别就是vue 中有三种watcher : render-watcher,计算watcher,user-watcher.
有空再画个图