关于vue.js:Vue2-响应式源码解读

要理解 Vue2 响应式零碎原理,咱们要思考两个问题:

  1. 当咱们扭转组件的状态时,零碎会产生什么变动?
  2. 零碎是如何晓得哪些局部依赖于这个状态?

实际上,组件的渲染、计算属性、组件watch对象和Vue.&watch()办法,它们之所以能响应组件propsdata的变动,都是围绕着Watcher类来实现的。

本文只截取局部外围代码,重在解说响应式原理,尽量减少其它代码的烦扰,但会正文代码起源,联合源码观看风味更佳。另外,本文源码版本:

"version": "2.7.14",

定义响应式属性

首先,看看组件的propsdata中的属性是如何定义为响应式的:

// src/core/instance/init.ts
Vue.prototype._init = function (options?: Record<string, any>) {
  const vm: Component = this
  initState(vm) // 初始化状态
}

// src/core/instance/state.ts
export function initState(vm: Component) {
  const opts = vm.$options
  initProps(vm, opts.props) // 初始化Props
  initData(vm) // 初始化Data
  initComputed(vm, opts.computed)
  initWatch(vm, opts.watch)
}
function initProps(vm: Component, propsOptions: Object) {
  const props = (vm._props = shallowReactive({}))
  for (const key in propsOptions) {
    defineReactive(props, key, value) // 定义响应式属性
  }
}
function initData(vm: Component) {
  let data: any = vm.$options.data
  data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
  observe(data)
}

// src/core/observer/index.ts
export function observe(value: any, shallow?: boolean, ssrMockReactivity?: boolean) {
  return new Observer(value, shallow, ssrMockReactivity)
}
export class Observer {
  constructor(public value: any, public shallow = false, public mock = false) {
    const keys = Object.keys(value)
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      // 定义响应式属性
      defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
    }
  }
}

从下面代码能够看出,在组件初始化阶段,无论是props还是data属性,最终都通过函数defineReactive定义为响应式属性。所以咱们要重点关注这个办法:

// src/core/observer/index.ts
export function defineReactive(obj: object, key: string, val?: any, customSetter?: Function | null, shallow?: boolean, mock?: boolean) {
  const dep = new Dep() // 创立一个dep实例
  const property = Object.getOwnPropertyDescriptor(obj, key)
  const getter = property && property.get
  const setter = property && property.set

  Object.defineProperty(obj, key, {
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      dep.depend() // 增加依赖关系Watcher
      return isRef(value) && !shallow ? value.value : value
    },
    set: function reactiveSetter(newVal) {
      setter.call(obj, newVal)
      dep.notify() // 赋值时,公布告诉
    }
  }
}

Object.defineProperty从新定义了属性的getset。当读取属性时,会主动触发get,当设置属性值时,会主动触发set,记住这一机制。从下面代码能够发现,每个属性都有一个dep实例,它的作用就是记录依赖这个属性watcher列表,并在属性赋值时,告诉列表中的watcher更新,这些更新包含:扭转计算属性值、执行组件watch对象中定义的办法、从新渲染等。

收集依赖关系

在进一步理解dep.depend()是之前,先看一下Vue.$watch如何办法创立watcher,有利于前面的了解:

Vue.prototype.$watch = function (
  expOrFn: string | (() => any), // 重点关注这个参数
  cb: any,
  options?: Record<string, any>
) {
  const vm: Component = this
  const watcher = new Watcher(vm, expOrFn, cb, options) // 创立watcher
}

expOrFn类型是一个字符串或函数,如果是字符串,会转化成函数,赋值给watcher.getter。接下来看dep.depend()是如何收集依赖的,重点关注DepWatcher两个类:

// src/core/observer/dep.ts
export default class Dep {
  static target?: DepTarget | null // Watcher正是DepTarget类的实现
  subs: Array<DepTarget | null> // 依赖列表

  addSub(sub: DepTarget) {
    this.subs.push(sub)
  }

  depend(info?: DebuggerEventExtraInfo) {
    if (Dep.target) {
      Dep.target.addDep(this) // 向watcher中增加dep实例
    }
  }
}
const targetStack: Array<DepTarget | null | undefined> = []
// 入栈watcher,并将target指向这个watcher
export function pushTarget(target?: DepTarget | null) {
  targetStack.push(target)
  Dep.target = target
}
// 出栈watcher,并将target指向最初的watcher
export function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

// src/core/observer/watcher.ts
export default class Watcher implements DepTarget {
  constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {
    this.cb = cb // 回调函数
    if (isFunction(expOrFn)) {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn) // 转化为函数
    }
    this.value = this.get() // 获取值
  }
  // 获取值,并收集依赖关系
  get() {
    pushTarget(this) // 入栈,Dep.target指向以后watcher
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm) // 执行getter期间只有读取了响应式属性,会触发属性的get,而后调用dep.depend(),再调用Dep.target(以后watcher)的addDep办法,将watcher增加到dep.subs
    popTarget() // 出栈
    return value
  }

  addDep(dep: Dep) {
    dep.addSub(this) // 将watcher增加到dep.subs
  }
}

执行getter期间只有读取了响应式属性,会触发改属性重写的get,而后调用dep.depend(),再调用Dep.target(以后watcher)的addDep办法,将watcher增加到dep.subs。于是,属性的dep就晓得了哪些watcher用到了这个属性,它们都保留在了dep.subs列表中。

赋值响应式属性

接着,看扭转props或state后,会产生什么状况:

  1. 扭转响应式属性值
  2. 触发重写的set,调用dep.notify()
  3. dep.notify()告诉dep.subs所有的watcher.update()
  4. watcher.update()中将watcher本人退出更新队列
  5. nextTick后执行更新,调用队列中所有watcher.run()
  6. watcher.run()中调用watcher.get()取得新值,并从新收集依赖
  7. 调用回调函数watcher.cb,传入新旧值
// 1. 扭转响应式属性值 examples/composition/todomvc.html
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="state.allDone"/>

// 2. 触发重写的set,调用dep.notify() src/core/observer/index.ts
export function defineReactive() {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    set: function reactiveSetter(newVal) {
      dep.notify()
    }
  }
}

// 3. dep.notify()告诉dep.subs所有的watcher.update() src/core/observer/dep.ts
notify(info?: DebuggerEventExtraInfo) {
  const subs = this.subs.filter(s => s) as DepTarget[]
  for (let i = 0, l = subs.length; i < l; i++) {
    const sub = subs[i]
    sub.update()
  }
}

// 4. watcher.update()中将watcher本人退出队列 src/core/observer/watcher.ts
update() {
  queueWatcher(this)
}

// 5. nextTick后执行更新,调用队列中所有watcher.run() src/core/observer/seheduler.ts
const queue: Array<Watcher> = []
export function queueWatcher(watcher: Watcher) {
  queue.push(watcher)
  nextTick(flushSchedulerQueue)
}
function flushSchedulerQueue() {
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    watcher.run()
  }
}

// 6. watcher.run()中调用watcher.get()取得新值,并从新收集依赖 src/core/observer/watcher.ts
run() {
  const value = this.get()
  const oldValue = this.value
  this.value = value
  this.cb.call(this.vm, value, oldValue) // 7. 调用回调函数watcher.cb,传入新旧值
}

渲染函数响应式

渲染函数_render用于生成虚构DOM,也就是VNode。当组件的propsdata发生变化时,会触发_render从新渲染组件:

// src/types/component.ts
class Component {
  _render: () => VNode
}

触发重绘机制也是通过watcher来实现的,不过这个watcher会比拟非凡,它没有回调函数,创立于组件mount阶段:

// src/platforms/web/runtime/index.ts
Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

// src/core/instance/lifecycle.ts
export function mountComponent(vm: Component, el: Element | null | undefined, hydrating?: boolean)  {
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  new Watcher(vm, updateComponent, noop, watcherOptions, true /* isRenderWatcher */)
}

updateComponent作为第二参数,也就成为了watcher.getter。和一般的watcher一样,getter执行时,也就是updateComponent执行期间,或者说_update_render执行期间,读取响应式属性时,会触发它们的get,将渲染watcher增加到属性对应的dep.subs中。当响应式属性发生变化时,触发从新渲染,这个流程与之前略有不同:

  1. 扭转响应式属性值
  2. 触发重写的set,调用dep.notify()
  3. dep.notify()告诉dep.subs所有的watcher.update()
  4. watcher.update()中将watcher本人退出更新队列
  5. nextTick后执行更新,调用队列中所有watcher.run()
  6. watcher.run()中调用watcher.get()取得新值,并从新收集依赖
  7. watcher.get()中会调用wacher.getter.call()
  8. 等于调用updateComponent,从新渲染组件(渲染watcher回调函数等于noop,相当于不执行回调)

以官网例子来看以上流程:

// 1. 扭转响应式属性值 examples/composition/todomvc.html
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="state.allDone"/>

// 2. 触发重写的set,调用dep.notify() src/core/observer/index.ts
export function defineReactive() {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    set: function reactiveSetter(newVal) {
      dep.notify()
    }
  }
}

// 3. dep.notify()告诉dep.subs所有的watcher.update() src/core/observer/dep.ts
notify(info?: DebuggerEventExtraInfo) {
  const subs = this.subs.filter(s => s) as DepTarget[]
  for (let i = 0, l = subs.length; i < l; i++) {
    const sub = subs[i]
    sub.update()
  }
}

// 4. watcher.update()中将watcher本人退出队列 src/core/observer/watcher.ts
update() {
  queueWatcher(this)
}

// 5. nextTick后执行更新,调用队列中所有watcher.run() src/core/observer/seheduler.ts
const queue: Array<Watcher> = []
export function queueWatcher(watcher: Watcher) {
  queue.push(watcher)
  nextTick(flushSchedulerQueue)
}
function flushSchedulerQueue() {
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    watcher.run()
  }
}

// 6. watcher.run()中调用watcher.get()取得新值,并从新收集依赖 src/core/observer/watcher.ts
run() {
  const value = this.get()
  const oldValue = this.value
  this.value = value
  this.cb.call(this.vm, value, oldValue)
}

// 7. watcher.get()中会调用wacher.getter.call() src/core/observer/watcher.ts
get() {
  pushTarget(this)
  let value
  const vm = this.vm
  value = this.getter.call(vm, vm) // 等于updateComponent()
  popTarget()
  return value
}

// 8. 等于调用updateComponent,从新渲染组件(渲染watcher回调函数等于noop,相当于不执行任何回调)src/core/instance/lifecycle.ts
export function mountComponent(vm: Component, el: Element | null | undefined, hydrating?: boolean)  {
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  new Watcher(vm, updateComponent, noop, watcherOptions, true /* isRenderWatcher */)
}

计算属性响应式

计算属性同样是通过watcher实现的。在实例初始化阶段initState时,调用initComputed为每个计算属性创立一个watcher,它同样没有回调函数:

// src/core/instance/state.ts
export function initState(vm: Component) {
  const opts = vm.$options
  if (opts.computed) initComputed(vm, opts.computed)
}
const computedWatcherOptions = { lazy: true }
function initComputed(vm: Component, computed: Object) {
  const watchers = (vm._computedWatchers = Object.create(null))

  for (const key in computed) {
    const userDef = computed[key]
    const getter = isFunction(userDef) ? userDef : userDef.get
    watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)

    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}

export function defineComputed(target: any, key: string, userDef: Record<string, any> | (() => any)) {
  sharedPropertyDefinition.get = createComputedGetter(key) // 重写属性的get
  sharedPropertyDefinition.set = noop // 不容许更改属性值
  Object.defineProperty(target, key, sharedPropertyDefinition) // 从新定义计算属性的set和get
}

function createComputedGetter(key) {
  return function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    return watcher.value // 返回watcher.value值
  }
}

以上代码能够看出,defineComputed从新定义了计算属性的setgetget永远返回对应watcher.value。计算属性的值是用户定义的函数,它也是watcher.getter,原理同上。函数中的响应式属性发生变化时:

  1. 扭转响应式属性值
  2. 触发重写的set,调用dep.notify()
  3. dep.notify()告诉dep.subs所有的watcher.update()
  4. watcher.update()中将watcher本人退出更新队列
  5. nextTick后执行更新,调用队列中所有watcher.run()
  6. watcher.run()中调用watcher.get()取得新值,并从新收集依赖
  7. 读取计算属性时,触发重写的get办法,返回watcher.value

组件的watch对象

它通过Vue.$watch来实现的,看代码即可,原理同上。

// src/core/instance/state.ts
function initWatch(vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    createWatcher(vm, key, handler)
  }
}

function createWatcher(
  vm: Component,
  expOrFn: string | (() => any),
  handler: any,
  options?: Object
) {
  return vm.$watch(expOrFn, handler, options)
}

异步更新和 Watcher执行程序

nextTick中的函数是异步执行的,也就是说随响应式属性变动的watcher会顺次退出更新队列中,直到这部分同步代码全副执行结束,之后才会执行异步代码,按顺序调用队列中watch.run,执行回调函数和从新渲染组件。

watcher.run执行是考究程序的,为了满足执行程序,必须在watcher.run之前从新按watcher.id大小排序,因为watcher.id是自增的,所以后创立的wacher.id要大于先创立的。排序能满足以下要求:

  1. 组件更新必须从父组件到子组件。(父组件永远先于子组件创立,因而父组件watcher.id小于子组件)
  2. 用户wachers必须在渲染watcher之前执行。(用户propsdatacomputedwacher创立于组件初始化阶段,watcher.id肯定小于mount阶段创立的渲染watcher
function flushSchedulerQueue() {
  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  queue.sort(sortCompareFn)
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    watcher.run()
  }
}

const sortCompareFn = (a: Watcher, b: Watcher): number => {
  if (a.post) {
    if (!b.post) return 1
  } else if (b.post) {
    return -1
  }
  return a.id - b.id
}

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理