Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

理解 Vue 数据响应原理 #1

Open
Lindysen opened this issue Jan 10, 2019 · 0 comments
Open

理解 Vue 数据响应原理 #1

Lindysen opened this issue Jan 10, 2019 · 0 comments

Comments

@Lindysen
Copy link
Owner

Vue 的数据响应是通过数据劫持结合发布者和订阅者实现的。其主要是通过Object.defineProperty()来实现数据劫持的。
本文的例子实现数据更新驱动视图更新是直接通过操作DOM,且是通过直接分析DOM来定位依赖的。其实Vue 内部机制不完全是这样。
下面我补充一些Vue 实现数据响应的一些大体细节。这块东西真的很多,我仔细说个大概,具体细节还需要大家自己去了解。

1 数据观察

实例化 Vue 实例的时候,Vue.prototype._init 方法被第一个执行,在 initState 函数内部使用 initData 函数初始化 data 选项。这是数据响应的开始。initData 函数的主要作用是保证 options 中的 data 选项是个函数且返回的对象。同时在 Vue 实例对象上添加代理访问数据对象的同名属性。最后 通过调用了 observe 函数观测数据,将 data 变成响应式的(data数据类型 是对象或数组时处理的 方式是不同的)。实现对象类型的 data 的观测主要是通过 defineReactive 函数

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  // 省略...

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) { //  watcher
        // 这里闭包引用了上面的 dep 常量
        dep.depend() // 收集依赖 Watcher
        // 省略...
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // 省略...
      if (setter) { // 如果属性原本存在set函数则调用
        setter.call(obj, newVal)
      } else {
        val = newVal
      }

      // 这里闭包引用了上面的 dep 常量
      dep.notify() // 触发依赖
    }
  })
}

在访问器属性的 getter/setter 中,通过闭包引用了前面定义的“筐”,即 dep 常量。每一个数据字段都通过闭包引用着属于自己的 dep 常量。
那么为数组类型该如何处理???
处理数组的方式与纯对象不同,数组是一个特殊的数据结构,它有很多实例方法,并且有些方法会改变数组自身的值,这些方法有:push、pop、shift、unshift、splice、sort 以及 reverse 等。其通过拦截数组变异方法的方式得知用户调用这些变异方法,从而触发依赖。数组本身也是一个对象,所以它实例的 proto 属性指向的就是数组构造函数的原型,即 arr.proto === Array.prototype 为真。其是通过设置 proto 属性的值为一个新的对象,且该新对象的原型是数组构造函数原来的原型对象。在新对象的变异方法里收集依赖

2 收集依赖

正是因为 watcher 对表达式的求值,触发了数据属性的拦截器函数,从而收集到了依赖,当数据变化时能够触发响应。

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)
 updateComponent = () => {// 可以简单的认为把渲染函数生成的虚拟DOM渲染成真正的DOM
    // vm._render 函数的作用是调用 vm.$options.render 函数并返回生成的虚拟节点(vnode)
    // vm._update 函数的作用是把 vm._render 函数生成的虚拟节点渲染成真正的 DOM
    vm._update(vm._render(), hydrating)
  }

在上面的代码中 Watcher 观察者实例将对 updateComponent() 函数求值, updateComponent 函数的执行会间接触发渲染函数(vm.$options.render)的执行,而渲染函数的执行则会触发数据属性的 get 拦截器函数,从而将依赖(观察者)收集,当数据变化时将重新执行 updateComponent 函数,这就完成了重新渲染。
Watcher constructor 的最后

if (this.computed) {
  this.value = undefined
  this.dep = new Dep()
} else {
  this.value = this.get() // 此时调用 Watcher 的实例方法 get
}

get () {  // get 为 Watcher 的实例方法
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm) // 此时 this.getter 指的就是 updateComponent
  } 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
}

3 触发依赖

修改属性值时会触发属性的 set 拦截器函数,这样就会调用 Dep 实例对象的 noitfy 方法,

set: function reactiveSetter (newVal) {
  // 省略...
  dep.notify()
}
export default class Dep {
  // 省略...

  constructor () {
    this.id = uid++
    this.subs = []
  }

  // 省略...

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

notify 方法只做了一件事,就是遍历当前 Dep 实例对象的 subs属性所保存的所有观察者对象,并逐个调 用观察者对象的 update 方法,这就是触发响应的实现机制,那么大家应该也猜到了,重新求值的操作应该是在 update 方法中进行的,那我们就找到观察者对象的update方法真正的更新变化操作都是通过调用 观察者实例对象的 run 方法完成的,run 方法内判断次 Watcher 是否是激活状态,若激活则调用实例方法getAndInvoke。

getAndInvoke (cb: Function) {
// 重新求值其实等价于重新执行渲染函数,最终结果就是重新生成了虚拟DOM并更新真实DOM,这样就完成了重新渲染的过程
  const value = this.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
    this.dirty = false
    if (this.user) {
      try {
        cb.call(this.vm, value, oldValue)
      } catch (e) {
        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
      }
    } else {
      cb.call(this.vm, value, oldValue)
    }
  }
}

好啦,谢谢大噶!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant