VueJS 响应式原理及简单实现

栏目: JavaScript · 发布时间: 7年前

内容简介:Vue 的响应式模型指的是:上面的三种方式追根揭底,都是通过回调的方式去更新视图或者通知观察者更新数据Vue的响应式原理是基于观察者模式和JS的API:

Vue 的响应式模型指的是:

vm.$watch()
watch

上面的三种方式追根揭底,都是通过回调的方式去更新视图或者通知观察者更新数据

Vue的响应式原理是基于观察者模式和JS的API: Object.defineProperty()Proxy 对象

主要对象

每一个被观察的对象对应一个Observer实例,一个Observer实例对应一个Dep实例,Dep和Watcher是多对多的关系,附上官方的图,有助于理解:

VueJS 响应式原理及简单实现

1. Observer

一个被观察的对象会对应一个Observer实例,包括 options.data

一个Observer实例会包含被观察的对象和一个Dep实例。

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number;
}
复制代码

2. Dep

Dep实例的作用是收集被观察对象(值)的订阅者。

一个Observer实例对应一个Dep实例,该Dep实例的作用会在 Vue.prototype.$setVue.prototype.$del 中体现——通知观察者。

一个Observer实例的每一个属性也会对应一个Dep实例,它们的getter都会用这个Dep实例收集依赖,然后在被观察的对象的属性发生变化的时候,通过Dep实例通知观察者。

options.data 就是一个被观察的对象,Vue会遍历 options.data 里的每一个属性,如果属性也是对象的话,它也会被设计成被观察的对象。

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
}
复制代码

3. Watcher

一个Watcher对应一个观察者,监听被观察对象(值)的变化。

Watcher会维护一个被观察者的旧值,并在被通知更新的时候,会调用自身的 this.getter() 去获取最新的值并作为要不要执行回调的依据。

Watcher分为两类:

  1. 视图更新回调,在数据更新(setter)的时候,watcher会执行 this.getter() ——这里Vue把 this.getter() 作为视图更新回调(也就是重新计算得到新的vnode)。

  2. 普通回调,在数据更新(setter)的时候,会通知Watcher再次调用 this.getter() 获取新值,如果新旧值对比后需要更新的话,会把新值和旧值传递给回调。

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: ?Function;
  getter: Function;
  value: any;
}
复制代码

使 options.data 成为响应式对象的过程

Vue使用 initData() 初始化 options.data ,并在其中调用了 observe 方法,接着:

  1. 源码中的 observe 方法是过滤掉不是对象或数组的其它数据类型,言外之意Vue仅支持对象或数组的响应式设计,当然了这也是语言的限制,因为Vue使用API: Object.defineProperty() 来设计响应式的。
  2. 通过 observe 方法过滤后,把传入的value再次传入 new Observer(value)
  3. 在Observer构造函数中,把Observer实例连接到value的属性 __ob__ ;如果value是数组的话,需要修改原型上的一些变异方法,比如 push、pop ,然后调用 observeArray 遍历每个元素并对它们再次使用 observe 方法;如果value是普通对象的话,对它使用 walk 方法,在 walk 方法里对每个可遍历属性使用 defineReactive 方法
  4. defineReactive 方法里,需要创建Dep的实例,作用是为了收集Watcher实例(观察者),然后判断该属性的 property.configurable 是不是false(该属性是不是不可以设置的),如果是的话返回,不是的话继续,对该属性再次使用 observe 方法,作用是深度遍历,最后调用 Object.defineProperty 重新设计该属性的descriptor
  5. 在descriptor里,属性的getter会使用之前创建的Dep实例收集Watcher实例(观察者)——也是它的静态属性 Dep.target ,如果该属性也是一个对象或数组的话,它的Dep实例也会收集同样的Watcher实例;属性的setter会在属性更新值的时候,新旧值对比判断需不需要更新,如果需要更新的话,更新新值并对新值使用 observe 方法,最后通知Dep实例收集的Watcher实例—— dep.notify() 。至此响应设计完毕
  6. 看一下观察者的构造函数—— constructor (vm, expOrFn, cb, options, isRenderWatcher) ,vm表示的是关联的Vue实例,expOrFn用于转化为Watcher实例的方法getter并且会在初始化Watcher的时候被调用,cb会在新旧值对比后需要更新的时候被调用,options是一些配置,isRenderWatcher表示这个Watcher实例是不是用于通知视图更新的
  7. Watcher构造函数中的 expOrFn 会在被调用之前执行Watcher实例的 get() 方法,该方法会把该Watcher实例设置为Dep.target,所以 expOrFn 里的依赖收集的目标将会是该Watcher实例
  8. Watcher实例的value属性是响应式设计的关键,它就是被观察对象的getter的调用者—— value = this.getter.call(vm, vm) ,它的作用是保留旧值,用以对比新值,然后确定是否需要调用回调

总结:

__ob__

Vue提供的其它响应式API

Vue除了用于更新视图的观察者API,还有一些其它的API

1. Vue实例的computed属性

构造Vue实例时,传入的 options.computed 会被设计成既是观察者又是被观察对象,主要有下面的三个方法:initComputed、defineComputed、createComputedGetter

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 for computed property "${key}".`,
        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 "${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)
      }
    }
  }
}

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.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)
}

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      watcher.depend()
      return watcher.evaluate()
    }
  }
}
复制代码

2. Vue实例的watch属性

在实例化Vue的时候,会把 options.watch 里的属性都遍历了,然后对每一个属性调用 vm.$watch()

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}
复制代码

vm.$watch 被作为一个独立的API导出。

3. Vue.prototype.$watch

Vue.prototype.$watch 是Vue的公开API,可以用来观察 options.data 里的属性。

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  if (options.immediate) {
    cb.call(vm, watcher.value)
  }
  return function unwatchFn () {
    watcher.teardown()
  }
}
复制代码

4. Vue.prototype.$set

Vue.prototype.$set 用于在操作响应式对象和数组的时候通知观察者,也包括给对象新增属性、给数组新增元素。

Vue.prototype.$set = set

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
复制代码

ob.dep.notify() 之所以可以通知观察者,是因为在 defineReactive 里有如下代码:

let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
      dep.depend()
      if (childOb) {
        childOb.dep.depend()
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
    }
    return value
  },
  set: function reactiveSetter (newVal) {
    const value = getter ? getter.call(obj) : val
    /* eslint-disable no-self-compare */
    if (newVal === value || (newVal !== newVal && value !== value)) {
      return
    }
    /* eslint-enable no-self-compare */
    if (process.env.NODE_ENV !== 'production' && customSetter) {
      customSetter()
    }
    if (setter) {
      setter.call(obj, newVal)
    } else {
      val = newVal
    }
    childOb = !shallow && observe(newVal)
    dep.notify()
  }
})
复制代码

上面的 childOb.dep.depend() 也为响应式对象的 __ob__.dep 添加了同样的Watcher实例。所以 Vue.prototype.$setVue.prototype.$del 都可以在内部通知观察者。

5. Vue.prototype.$del

Vue.prototype.$del 用于删除响应式对象的属性或数组的元素时通知观察者。

Vue.prototype.$del = del

/**
 * Delete a property and trigger change if necessary.
 */
export function del (target: Array<any> | Object, key: any) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key]
  if (!ob) {
    return
  }
  ob.dep.notify()
}
复制代码

简单实现响应式设计

  1. 实现Watcher类和Dep类,Watcher作用是执行回调,Dep作用是收集Watcher
class Watcher {
  constructor(cb) {
    this.callback = cb
  }

  update(newValue) {
    this.callback && this.callback(newValue)
  }
}

class Dep {
  // static Target

  constructor() {
    this.subs = []
  }

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

  notify(newValue) {
    this.subs.forEach(sub => sub.update(newValue))
  }
}
复制代码
  1. 处理观察者和被观察者
// 对被观察者使用
function observe(obj) {
  let keys = Object.keys(obj)
  let observer = {}
  keys.forEach(key => {
    let dep = new Dep()
    Object.defineProperty(observer, key, {
      configurable: true,
      enumerable: true,
      get: function () {
        if (Dep.Target) dep.addSub(Dep.Target)
        return obj[key]
      },
      set: function (newValue) {
        dep.notify(newValue)
        obj[key] = newValue
      }
    })
  })
  return observer
}

// 对观察者使用
function watching(obj, key) {
  let cb = newValue => {
    obj[key] = newValue
  }
  Dep.Target = new Watcher(cb)
  return obj
}
复制代码
  1. 检验代码
let subscriber = watching({}, 'a')
let observed = observe({ a: '1' })
subscriber.a = observed.a
console.log(`subscriber.a: ${subscriber.a}, observed.a: ${observed.a}`)
observed.a = 2
console.log(`subscriber.a: ${subscriber.a}, observed.a: ${observed.a}`)
复制代码
  1. 结果:
subscriber.a: 1, observed.a: 1
subscriber.a: 2, observed.a: 2
复制代码

CodePen演示

参考

深入理解Vue响应式原理 vue.js源码 - 剖析observer,dep,watch三者关系 如何具体的实现数据双向绑定 50行代码的MVVM,感受闭包的艺术 Vue.js 技术揭秘


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Nine Algorithms That Changed the Future

Nine Algorithms That Changed the Future

John MacCormick / Princeton University Press / 2011-12-27 / GBP 19.95

Every day, we use our computers to perform remarkable feats. A simple web search picks out a handful of relevant needles from the world's biggest haystack: the billions of pages on the World Wide Web.......一起来看看 《Nine Algorithms That Changed the Future》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具