vue2响应式实现原理「源码级」

为了搞清楚 Vue2 的响应式原理,我们通过一个 Vue 使用的简单例子,从 vue.js 文件加载及 Vue 实例化开始,一步步查看源码(v2.6.1),来一探究竟。

Vue应用举例

定义一个 count 状态,显示在页面中,点击 button 触发 greet 函数使得 count += 1。

<head>
   <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"> </script>
 </head>
<body>
  <div id="app">
    {{ count }}
    <button v-on:click="greet">Greet</button>
  </div>

  <script>
    debugger
    var vm = new Vue({
      el: '#app',
      data () {
        count: 0
      },
      methods: {
        greet () {
          this.count += 1
        }
      },
    })
  </script>
</body>
html应用示例

Vue 初始化及实例化

当页面引入 vue.js 文件时,首先会创建一个完善的 Vue 构造函数。

// src/core/instance/index.js

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options) // 划重点
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
Vue构造函数

执行 new Vue() 时,构造函数会执行 this._init(),在 _init 中会进行合并配置,并依次执行「初始化生命周期、初始化事件、初始化渲染函数、触发 beforeCreate、初始化 injections、初始化各种 State、初始化 provides、触发 created,最后执行 vm.$mount 将DOM及初始数据挂载到页面。

// src/core/instance/init.js

Vue.prototype._init = function (options?: Object) {
    // 合并配置
    // ...
    // mergeOptions
    // ...

    // 一系列初始化
    // ...
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm) // 划重点
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    // ...
    
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
}
_init方法

我们重点看 initState。这里会分别初始化 props、methods、data、computed、watch 内容。其中我们重点看 initData。

// src/core/instance/init.js

function initState (vm: Component) {
  vm._watchers = []

  if (opts.props) initProps(vm, opts.props) // 初始化 props
  if (opts.methods) initMethods(vm, opts.methods) // 初始化 methods
  if (opts.data) initData(vm) // 初始化 data  划重点
  if (opts.computed) initComputed(vm, opts.computed) // 初始化 computed
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch) // 初始化 watch
  }
}
initState方法

initData 中会执行 observe,这是使得 data 实现响应式的开端。

// src/core/instance/state.js

function initData (vm: Component) {
  // observe data
  observe(data, true /* asRootData */)
}
initData方法

Observer 「数据观察者」

observe 中执行实例化 Observer 动作。它会递归地把 data 对象和子对象添加 __ob__ 属性,同时通过 defindReactive 为属性定义 getter/setter

// src/core/observer/index.js

function observe (value, asRootData): Observer | void {
  // ...
  ob = new Observer(value)
  // ...
  return ob
}


/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 * Observer 类被添加在每个响应式对象上,添加后 observer 将目标对象的 
 * 每个属性转为 getter/setter,用来收集依赖和触发更新
 */
class Observer {
  value: any;
  dep: Dep;
  vmCount: number;

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0

    def(value, '__ob__', this) // 给响应式对象添加不可枚举属性 __ob__,值为 Observer 实例

    if (Array.isArray(value)) {
      this.observeArray(value) // 处理Array数据
    } else {
      this.walk(value) // 处理非Array数据
    }
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
       defineReactive(obj, keys[i])
    }
  }

  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }  
}
observe及Observer类

重中之重的是,执行 walk 函数,通过其中的  defineReactive  方法,将 data 中的每个属性进行 getter/setter 定义。但是对于 Array 类型属性则是通过 observeArray 方法来进行处理,本质是把每个数组项再传给 observe 进行处理。

重点看  defineReactive 方法。首先会将子对象递归传递给 observe 进行响应式改造。 然后看到 getter 里的 dep.depend()setter 里的 dep.notify(),这就是依赖收集和触发更新的起点。这里的 dep 是 Dep 的实例,且作为 defindReactive 内的一个常量,getter/setter 函数内持有对它的闭包引用。那 Dep 又是什么?

// src/core/observer/index.js

function defineReactive (
    obj,
    key,
    val,
    customSetter,
    shallow
  ) {
    var dep = new Dep();
    // ...
    var childOb = !shallow && observe(val); // 递归地对子对象执行 observe
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
          // ...
          if (Dep.target) {
            dep.depend()
          }
          //...
          return value
      },
      set: function reactiveSetter (newVal) {
          // ...
          dep.notify()
      },
    }
  }
defineReactive

Dep「订阅者管理方」

通过 Dep 类的定义可以看到,它有实例属性 subs 数组,数组项类型是 Watcher 类实例;实例方法 addSub/removeSub 添加或删除数组中的 Watcher。由此可以确定 dep 实例并非订阅者本身而是订阅者的管理方,subs 数组即为订阅者列表,而真正的订阅者正是大名鼎鼎的 Watcher。

// src/core/observer/dep.js

let uid = 0

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

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

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

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) { // 此时target是Watcher,这是在Watcher实例化时执行get()中的pushTarget(this)方法设置的
      Dep.target.addDep(this) // Watcher中的addDep方法
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
class Dep

Watcher「订阅者」

前面我们看到,「数据观察者 Observer」和「订阅管理者 Dep」已经实例化了,还有两个重要问题:

1、那真正的订阅者 Watcher 是在什么时机被实例化呢?

2、并且 data 中各属性的 getter 是在什么时机被触发,从而执行 dep.depend() ,来真正实现依赖收集呢?

 回到 Vue 构造函数实例化时,最后执行 this._init(options),_init 中最后执行 vm.$mount(vm.$options.el)。它先校验了 el 有效性,把 template/el 转为了 render 函数,最后执行了 mount.call() 实际上是另一个 Vue.prototype.$mount。

// src/platforms/web/entry-runtime-with-compiler.js

const mount = Vue.prototype.$mount

Vue.prototype.$mount = function (el, hydrating): Component {
  el = el && query(el)
  // 挂载 el 校验
  if (el === document.body || el === document.documentElement) {
    warn(`Do not mount Vue to <html> or <body> - mount to normal elements instead.`)
    return this
  }

  const options = this.$options
  // 把 template/el 转为 render 函数
  if (!options.render) {
    let template = options.template
    if (el) template = getOuterHTML(el)
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(template, {
        // ...一些参数
      }, this)
      options.render = render // render 函数挂到 vm.$options 上
      options.staticRenderFns = staticRenderFns
    }
  }
  return mount.call(this, el, hydrating) // 这里执行另一个 Vue.prototype.$mount
}
第一个Vue.prototype.$mount

接下来执行的 Vue.prototype.$mount 方法,就执行了一个 mountComponent 函数。

// src/platforms/web/runtime/index.js

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
第二个Vue.prototype.$mount

mountComponent 字面意思是挂载组件,挂载前先触发 beforeMount 钩子,最重要的是实例化了 Watcher,第二个参数是 updateComponent 函数,最后一个参数表明这是一个 render watcher。除此之外还有两类 watcher:computed watcher,user watcher 。

// src/core/instance/lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  callHook(vm, 'beforeMount') // 先触发 beforeMount 钩子

  // 这会作为 Watcher 实例化的 expOrFn 参数,执行它会触发更新 _update
  let updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

  if (vm.$vnode == null) {
    vm._isMounted = true;
    callHook(vm, 'mounted');
  }
 
return vm
}
mountComponent

我们看下 Watcher 类的中发生了什么。

1、vm._watcher 和 vm._watchers 分别设置为/添加此 watcher。实例属性 getter 为 updateComponent 函数。执行实例方法 get()。

2、get方法中,pushTarget() 可查看前面的 Dep 类的定义。它设置 Dep.target 为此 watcher,全局属性 targetStack 数组添加此 watcher。

3、接下来 this.getter.call(vm, vm),就会执行 updateComponent,返回 value 值。后面我们重点看 updateComponent 里发了什么。

4、如果参数配置为 deep = true,还会执行 遍历操作 traverse(value)。

5、最后执行 popTarget() 和实例方法 cleanDeps()。

// src/core/observer/watcher.js

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function, // computed watcher时为函数,user watcher时为watch对象的key,render watcher时为updateComponent函数
    cb: Function, // watch对象的回调
    options?: ?Object,
    isRenderWatcher?: boolean // 是否renderWatcher
  ) {
    this.vm = vm
    if (isRenderWatcher) vm._watcher = this
    vm._watchers.push(this)

    // ...options
    this.expression = expOrFn.toString();
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn // 此处 getter 为 updateComponent 函数
    } else {
      this.getter = parsePath(expOrFn)
    }

    this.value = this.lazy
      ? undefined
      : this.get() // 执行 get
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   * 解析 getter,重新收集依赖
   */
  get () {
    pushTarget(this) // Dep.target 设置为此 watcher
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (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
  }

  /**
   * 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)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      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.cb.call(this.vm, value, oldValue)
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}
class Watcher

updateComponent 实际是执行  vm._update(vm._render(), hydrating) ,先执行 vm._render()。

_render 函数中,先取出 vm.$options.render,这是在前面 Vue.prototype.$mount 中挂载上去的,是由 compileToFunctions(template) 方法转出来的渲染函数。然后执行render.call()。

// src/core/instance/render.js

export function initRender (vm: Component) {
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}

export function renderMixin (Vue: Class<Component>) {
  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render } = vm.$options // Vue.prototype.$mount 中设置的 render

    // render self
    let vnode = render.call(vm._renderProxy, vm.$createElement)

    return vnode
  }
}
Vue.prototype._render

那这个渲染函数 render 内容是什么呢?render 函数实际上是个匿名函数,定义如下,其中涉及 _c、_v、_s 三个函数。其中执行到 _s(count) 时要取 count 的值,便会触发 _data.count 的 reactiveGetter,从而实现依赖收集。终于来到了我们想要的 reactiveGetter 触发时机!

function anonymous() {
  with(this){
    return _c(
      'div',
      { attrs:{"id":"app"}},
      [
        _v("\n    "+_s(count)+"\n    "),
        _c(
          'button',
          {on:{"click":greet}},
          [_v("Greet")]
        )
      ]
    )
  }
}
render匿名函数

最终来到 reactiveGetter 时,此时的执行栈为:

addDep (vue.js:4505)
depend (vue.js:727)
reactiveGetter (vue.js:1043)
proxyGetter (vue.js:4633)
(anonymous) (VM1073:3)
Vue._render (vue.js:3549)
updateComponent (vue.js:4067)
get (vue.js:4480)
Watcher (vue.js:4468)
mountComponent (vue.js:4074)
Vue.$mount (vue.js:9057)
Vue.$mount (vue.js:11943)
Vue._init (vue.js:5023)
Vue (vue.js:5090)
(anonymous) (index.html:18)
到达reactiveGetter时的JS执行栈

最后还需要把 vm._update() 执行完, 将 vnode 转为真实 html element 挂载到 el 上。

依赖收集步骤总结

以上通过梳理从 Vue 实例初始化到 mounted 生命周期结束,这中间的详细步骤,搞清楚了 Observer、Dep、Watcher 这三者的关系:

initData() 中通过 Observer 递归地把 data 对象和子对象添加 __ob__ 属性,同时通过 defindReactive 为每个属性定义 reactiveGetter/reactiveSetter,用来收集订阅者和触发订阅者更新。
众多订阅者的管理是通过 Dep 来完成的,其实例属性 subs 数组就是订阅者列表,每一项是一个订阅者 watcher,还有添加、删除订阅者 watcher 等功能。
订阅者 watcher 实例化是在 vm.$mount 中的 mountComponent,实例化时会执行 vm._update(vm._render()),执行 _render() 会获取 data 中的值,此时触发 reactiveGetter 完成订阅者收集。

 

 

 

参考:

Observer、Dep、Watcher 傻傻搞不清楚

posted on 2021-01-28 23:02  dawnxuuu  阅读(556)  评论(0编辑  收藏  举报

导航