DDR爱好者之家 Design By 杰米

前言

对于使用Vue的前端而言,watch、computed和methods三个属性相信是不陌生的,是日常开发中经常使用的属性。但是对于它们的区别及使用场景,又是否清楚,本文我将跟大家一起通过源码来分析这三者的背后实现原理,更进一步地理解它们所代表的含义。 在继续阅读本文之前,希望你已经具备了一定的Vue使用经验,如果想学习Vue相关知识,请移步至官网。

Watch

我们先来找到watch的初始化的代码,/src/core/instance/state.js

export function initState (vm: Component) {
 vm._watchers = []
 const opts = vm.$options
 if (opts.props) initProps(vm, opts.props) // 初始化props
 if (opts.methods) initMethods(vm, opts.methods) // 初始化方法
 if (opts.data) {
 initData(vm) // 先初始化data 重点
 } else {
 observe(vm._data = {}, true /* asRootData */)
 }
 if (opts.computed) initComputed(vm, opts.computed) // 初始化computed
 if (opts.watch && opts.watch !== nativeWatch) {
 initWatch(vm, opts.watch) // 初始化watch
 }
}

接下来我们深入分析一下initWatch的作用,不过在接下去之前,这里有一点是data的初始化是在computed和watch初始化之前,这是为什么呢?大家可以停在这里想一下这个问题。想不通也没关系,继续接下来的源码分析,这个问题也会迎刃而解。

initWatch

function initWatch (vm: Component, watch: Object) {
 for (const key in watch) {
 const handler = watch[key]
 if (Array.isArray(handler)) { // 如果handler是一个数组
  for (let i = 0; i < handler.length; i++) { // 遍历watch的每一项,执行createWatcher
  createWatcher(vm, key, handler[i])
  }
 } else {
  createWatcher(vm, key, handler) 
 }
 }
}

createWatcher

function createWatcher (
 vm: Component,
 expOrFn: string | Function,
 handler: any,
 options"htmlcode">
 Vue.prototype.$watch = function (
 expOrFn: string | Function,
 cb: any,
 options"${watcher.expression}"`)
  }
 }
 return function unwatchFn () {
  watcher.teardown()
 }
 }
}

上面几个函数调用的逻辑都比较简单,所以就在代码上写了注释。我们重点关注一下这个userWatcher生成的时候做了什么。

Watcher

又来到了我们比较常见的Watcher类的阶段了,这次我们重点关注生成userWatch的过程。

export default class Watcher {
 vm: Component;
 expression: string;
 cb: Function;
 id: number;
 deep: boolean;
 user: boolean;
 lazy: boolean;
 sync: boolean;
 dirty: boolean;
 active: boolean;
 deps: Array<Dep>;
 newDeps: Array<Dep>;
 depIds: SimpleSet;
 newDepIds: SimpleSet;
 before: "${expOrFn}" ` +
   'Watcher only accepts simple dot-delimited paths. ' +
   'For full control, use a function instead.',
   vm
  )
  }
 }
 this.value = this.lazy
  "htmlcode">
/**
 * Parse simple path.
 */
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
 if (bailRE.test(path)) {
 return
 }
 const segments = path.split('.')
 return function (obj) {
 for (let i = 0; i < segments.length; i++) { // 遍历数组
  if (!obj) return
  obj = obj[segments[i]] // 每次把当前的key值对应的值重新赋值obj
 }
 return obj
 }
}

首先会判断传入的path是否符合预期,如果不符合则直接return,接着讲path根据'.'字符串进行拆分,因为我们传入的watch可能有如下几种形式:

watch: {
	a: () {}
 'formData.a': () {}
}

所以需要对path进行拆分,接下来遍历拆分后的数组,这里返回的函数的参数obj其实就是vm实例,通过vm[segments[i]],就可以最终找到这个watch所对应的属性,最后将obj返回。

constructor () { // 初始化的最后一段逻辑
	this.value = this.lazy // 因为this.lazy为false,所以会执行this.get方法
  "${this.expression}"`) // 如果报错了会这这一块逻辑
  } else {
  throw e
  }
 } finally {
  // "touch" every property so they are all tracked as
  // dependencies for deep watching
  if (this.deep) { // 如果deep为true,则执行深递归
  traverse(value)
  }
  popTarget() // 将当前watch出栈
  this.cleanupDeps() // 清空依赖收集 这个过程也是尤为重要的,后续我会单独写一篇文章分析。
 }
 return value
 }

对于UserWatcher的初始化过程,我们基本上就分析完了,traverse函数本质就是一个递归函数,逻辑并不复杂,大家可以自行查看。 初始化过程已经分析完,但现在我们好像并不知道watch到底是如何监听data的数据变化的。其实对于UserWatcher的依赖收集,就发生在watcher.get方法中,通过this.getter(parsePath)函数,我们就访问了vm实例上的属性。因为这个时候已经initData,所以会触发对应属性的getter函数,这也是为什么initData会放在initWatch和initComputed函数前面。所以当前的UserWatcher就会被存放进对应属性Dep实例下的subs数组中,如下:

Object.defineProperty(obj, key, {
 enumerable: true,
 configurable: true,
 get: function reactiveGetter () {
  const value = getter "text-align: center">详解Vue中的watch和computed

, 当我们对data上的数据进行修改时,就会触发对应属性的setter函数,进而触发dep.notify(),遍历subs中的每一个watcher,执行watcher.update()函数->watcher.run,renderWathcer的update方法我们就不深究了,不清楚的同学可以参考下我写的Vue数据驱动。 对于我们分析的UserWatcher而言,相关代码如下:

class Watcher {
 constructor () {} //..
 run () {
 if (this.active) { // 用于标示watcher实例有没有注销
  const value = this.get() // 执行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.
  isObject(value) ||
  this.deep
  ) {
  // set new value
  const oldValue = this.value
  this.value = value
  if (this.user) { // UserWatcher
   try {
   this.cb.call(this.vm, value, oldValue) // 执行回调cb,并传入新值和旧值作为参数
   } catch (e) {
   handleError(e, this.vm, `callback for watcher "${this.expression}"`)
   }
  } else {
   this.cb.call(this.vm, value, oldValue)
  }
  }
 }
 }
}

首先会判断这个watcher是否已经注销,如果没有则执行this.get方法,重新获取一次新值,接着比较新值和旧值,如果相同则不继续执行,若不同则执行在初始化时传入的cb回调函数,这里其实就是handler函数。至此,UserWatcher的工作原理就分析完了。接下来我们来继续分析ComputedWatcher,同样的我们找到初始代码

Computed

initComputed

const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
 // $flow-disable-line
 const watchers = vm._computedWatchers = Object.create(null) // 用来存放computedWatcher的map
 // computed properties are just getters during SSR
 const isSSR = isServerRendering()

 for (const key in computed) {
 const userDef = computed[key]
 const getter = typeof userDef === 'function' "${key}".`,
  vm
  )
 }

 if (!isSSR) { // 不是服务端渲染
  // create internal watcher for the computed property.
  watchers[key] = new Watcher( // 执行new Watcher
  vm,
  getter || noop,
  noop,
  computedWatcherOptions { lazy: true }
  )
 }

 // 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)) { 
 // 会在vm的原型上去查找computed对应的key值存不存在,如果不存在则执行defineComputed,存在的话则退出,
 // 这个地方其实是Vue精心设计的
 // 比如说一个组件在好几个文件中都引用了,如果不将computed
  defineComputed(vm, key, userDef)
 } else if (process.env.NODE_ENV !== 'production') {
  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

new Watcher的逻辑我们先放一边,我们先关注一下defineComputed这个函数到底做了什么

export function defineComputed (
 target: any,
 key: string,
 userDef: Object | Function
) {
 const shouldCache = !isServerRendering()
 if (typeof userDef === 'function') { // 分支1
 sharedPropertyDefinition.get = shouldCache
  "${key}" was assigned to but it has no setter.`,
  this
  )
 }
 }
 Object.defineProperty(target, key, sharedPropertyDefinition)
}

这个函数本质也是调用Object.defineProperty来改写computed的key值对应的getter函数和setter函数,当访问到key的时候,就会触发其对应的getter函数,对于大部分情况下,我们会走到分支1,对于不是服务端渲染而言,sharedPropertyDefinition.get会被createComputedGetter(key)赋值,set会被赋值为一个空函数。

createComputedGetter

function createComputedGetter (key) {
 return function computedGetter () {
 const watcher = this._computedWatchers && this._computedWatchers[key] // 就是上文中new Watcher()
 if (watcher) {
  if (watcher.dirty) {
  watcher.evaluate()
  }
  if (Dep.target) {
  watcher.depend()
  }
  return watcher.value
 }
 }
}

可以看到createComputedGetter(key)其实会返回一个computedGetter函数,也就是说在执行render函数时,访问到这个vm[key]对应的computed的时候会触发getter函数,而这个getter函数就是computedGetter。

<template>
	<div>{{ message }}</div>
</template>
export default {
	data () {
 	return {
  	a: 1,
   b: 2
  }
 },
 computed: {
 	message () { // 这里的函数名message就是所谓的key
  	return this.a + this.b
  }
 }
}

以上代码为例子,来一步步解析computedGetter函数。 首先我们需要先获取到key对应的watcher.

const watcher = this._computedWatchers && this._computedWatchers[key]

而这里的watcher就是在initComputed函数中所生成的。

 if (!isSSR) { // 不是服务端渲染
  // create internal watcher for the computed property.
  watchers[key] = new Watcher( // 执行new Watcher
  vm,
  getter || noop,
  noop,
  computedWatcherOptions { lazy: true }
  )
 }

我们来看看computedWatcher的初始化过程,我们还是接着来继续回顾一下Watcher类相关代码

export default class Watcher {
 vm: Component;
 expression: string;
 cb: Function;
 id: number;
 deep: boolean;
 user: boolean;
 lazy: boolean;
 sync: boolean;
 dirty: boolean;
 active: boolean;
 deps: Array<Dep>;
 newDeps: Array<Dep>;
 depIds: SimpleSet;
 newDepIds: SimpleSet;
 before: "htmlcode">
if (watcher) {
 if (watcher.dirty) {
 watcher.evaluate()
 }
 if (Dep.target) {
 watcher.depend()
 }
 return watcher.value
}

首先判断watcher是否存在,如果存在则执行以下操作

  • 判断watcher.dirty是否为true,如果为true,则执行watcher.evaluate
  • 判断当前Dep.target是否存在,存在则执行watcher.depend
  • 最后返回watcher.value

在computedWatcher初始化的时候,由于传入的options.lazy为true,所以相应的watcher.diry也为true,当我们在执行render函数的时候,访问到message,触发了computedGetter,所以会执行watcher.evaluate。

evaluate () {
 this.value = this.get() // 这里的get() 就是vm['message'] 返回就是this.a + this.b的和
 this.dirty = false // 将dirty置为false
}

同时这个时候由于访问vm上的a属性和b属性,所以会触发a和b的getter函数,这样就会把当前这个computedWatcher加入到了a和b对应的Dpe实例下的subs数组中了。如图:

详解Vue中的watch和computed

接着当前的Dep.target毫无疑问就是renderWatcher了,并且也是存在的,所以就执行了watcher.depend()

depend () {
 let i = this.deps.length 
 while (i--) {
 this.deps[i].depend()
 }
}

对于当前的message computedWatcher而言,this.deps其实就是a和b两个属性对应的Dep实例,接着遍历整个deps,对每一个dep就进行depend()操作,也就是每一个Dep实例把当前的Dep.target(renderWatcher都加入到各自的subs中,如图:

详解Vue中的watch和computed

所以这个时候,一旦你修改了a和b的其中一个值,都会触发setter函数->dep.notify()->watcher.update,代码如下:

update () {
 /* istanbul ignore else */
 if (this.lazy) {
 this.dirty = true
 } else if (this.sync) {
 this.run()
 } else {
 queueWatcher(this)
 }
}

总结

其实不管是watch还是computed本质上都是通过watcher来实现,只不过它们的依赖收集的时机会有所不同。就使用场景而言,computed多用于一个值依赖于其他响应式数据,而watch主要用于监听响应式数据,在进行所需的逻辑操作!大家可以通过单步调试的方法,一步步调试,能更好地加深理解。

以上就是详解Vue中的watch和computed的详细内容,更多关于Vue watch和computed的资料请关注其它相关文章!

DDR爱好者之家 Design By 杰米
广告合作:本站广告合作请联系QQ:858582 申请时备注:广告合作(否则不回)
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
DDR爱好者之家 Design By 杰米

稳了!魔兽国服回归的3条重磅消息!官宣时间再确认!

昨天有一位朋友在大神群里分享,自己亚服账号被封号之后居然弹出了国服的封号信息对话框。

这里面让他访问的是一个国服的战网网址,com.cn和后面的zh都非常明白地表明这就是国服战网。

而他在复制这个网址并且进行登录之后,确实是网易的网址,也就是我们熟悉的停服之后国服发布的暴雪游戏产品运营到期开放退款的说明。这是一件比较奇怪的事情,因为以前都没有出现这样的情况,现在突然提示跳转到国服战网的网址,是不是说明了简体中文客户端已经开始进行更新了呢?