Vue源码分析之-响应式原理

目标:通过查看源码,解决下面几个问题:

  • 重新给属性赋值,是否是响应式的?vm.msg = {count: 0};
  • 给数组元素赋值,视图是否会更新?vm.arr[0] = 4;
  • 修改数组的length,视图是否会更新?vm.arr.length = 0;
  • 使用会改变原数组的数组方法对数组进行操作,视图是否会更新?vm.arr.push(4);

响应式处理入口

整个响应式处理的过程是比较复杂的,可以先从:

  • src/core/instance/init.js
    • initState(vm) // vm的状态初始化
    • 初始化了_data, _props, methods等
  • src/core/instance/state.js
    export function initState (vm: Component) {
      vm._watchers = []
      const opts = vm.$options
      if (opts.props) initProps(vm, opts.props)
      if (opts.methods) initMethods(vm, opts.methods)
      if (opts.data) {
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
      if (opts.computed) initComputed(vm, opts.computed)
      if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
      }
    }

    关键的一句去是: initData(vm)

  • function initData (vm: Component) {
      let data = vm.$options.data
      // 初始化_data,
      // 组件中的data是函数,调用函数获得返回结果
      // 否则直接使用data
      data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {}
      // 判断data是否是原始的Object对象
      if (!isPlainObject(data)) {
        data = {}
        process.env.NODE_ENV !== 'production' && warn(
          'data functions should return an object:\n' +
          'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
          vm
        )
      }
      // proxy data on instance
      const keys = Object.keys(data)
      const props = vm.$options.props
      const methods = vm.$options.methods
      let i = keys.length
      // 判断data上的成员是否和props/methods 存在重名
      while (i--) {
        const key = keys[i]
        // 开发环境下提示重名情况
        if (process.env.NODE_ENV !== 'production') {
          if (methods && hasOwn(methods, key)) {
            warn(
              `Method "${key}" has already been defined as a data property.`,
              vm
            )
          }
        }
        if (props && hasOwn(props, key)) {
          process.env.NODE_ENV !== 'production' && warn(
            `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
            vm
          )
        } else if (!isReserved(key)) {
          // 把data中的成员使用代理的模式注入到vm中
          proxy(vm, `_data`, key)
        }
      }
      // observe data
      // 响应式处理
      observe(data, true /* asRootData */)
    }

    获得data的object对象形式的数据后,又传递给了observe(data: object, asRootData: boolean)函数进行处理。

  • /**
     * Attempt to create an observer instance for a value,
     * returns the new observer if successfully observed,
     * or the existing observer if the value already has one.
     */
    export function observe (value: any, asRootData: ?boolean): Observer | void {
      // 判断value是否为对象或者是VNode类型实例
      // 如果不是对象或者是VNode的实例的话,不做任何处理直接返回
      if (!isObject(value) || value instanceof VNode) {
        return
      }
      let ob: Observer | void
      // 如果value有__ob__(表示是Observer实例)属性,结束
      if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__
      } else if ( // 判断是否需要和可以做响应式处理
        shouldObserve &&
        !isServerRendering() &&
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue
      ) {
        // 可以,则创建一个Observer对象,并赋值给ob
        ob = new Observer(value)
      }
      if (asRootData && ob) {
        ob.vmCount++
      }
      return ob
    }

  • observe函数中将data作为Observer的构造函数的参数,创建一个Observer对象返回;

  • 或者如果data已经是有__ob__属性并且是一个Observer对象(表示Observer对象的缓存),则将data中的__ob__(Observer对象)直接返回。

Observer

**
 * 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.
 */
export class Observer {
  // 观测对象
  value: any;
  // 依赖对象
  dep: Dep;
  // 把当前观测对象作为rootData的vms计数器
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    // 初始化实例的vmCount 为 0
    this.vmCount = 0
    // 将当前Observer实例挂载到观察对象value的__ob__属性
    def(value, '__ob__', this)
    // 数组的响应式处理
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 为数组中的每一个对象创建一个Observer实例
      this.observeArray(value)
    } else {
      // 遍历对象中的每一个属性,通过defineReactive(obj, keys[i]) 转换成getter/setter
      this.walk(value)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    // 获取观察对象的每一个属性
    const keys = Object.keys(obj)
    // 遍历每一个属性,设置为响应式数据
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

这里面关键的一句代码是:defineReactive(obj, key)

defineReactive

// 为对象定义一个响应式的属性
/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 创建依赖对象实例
  const dep = new Dep()
  // 获取obj的属性描述符对象
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 如果属性不可配置,则结束
  if (property && property.configurable === false) {
    return
  }

  // 提供预定义的存取器函数
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // 判断是否递归观察子对象,并将子对象的属性转换成 getter/setter,返回子观察对象
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 如果预定义的getter存在,则value等于getter调用的返回值
      // 否则直接赋值属性值
      const value = getter ? getter.call(obj) : val
      // 如果存在当前依赖目标,即watcher对象,则建立依赖
      if (Dep.target) {
        dep.depend()
        // 如果子观察目标存在,建立子对象的依赖关系
        if (childOb) {
          childOb.dep.depend()
          // 如果属性是数组,则特殊处理收集数组对象依赖
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // 如果预定义的getter存在,则value等于getter调用的返回值
      // 否则直接赋值属性值
      const value = getter ? getter.call(obj) : val
      // 如果新值等于旧值,或者新值旧值为NaN,则不执行
      /* 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()
      }
      // 如果没有setter,直接返回
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      // 如果预定义setter存在则调用,否则直接更新新值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 如果新值是对象,观察子对象并返回 子的observer对象
      childOb = !shallow && observe(newVal)
      // 派发更新(发布更改通知)
      dep.notify()
    }
  })
}

依赖收集

Dep.target的初始化时机

首先Dep.target是在pushTarget(target: Watcher)被调用的时候被赋值的

// Dep.target 用来存放目前正在使用的 watcher
// 全局唯一,并且一次只能有一个watcher被使用
// 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 = []
// 入栈,并将当前的watcher 赋值给 Dep.target
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  // 出栈操作
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

而pushTarget()函数是在new Watcher的时候被调用的

/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
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;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } 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
  }
// more ...
}

这时候我们检索new Watcher,查看源码,看到是在mountComponent的时候 new Watcher(vm, uodateComponent, noop, {before: ...})触发的。

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    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 */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

初始化Vue实例时设置$watch方法时也是用到了

// core/instance/state.js
  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) {
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}

其他一些SSR相关的virtual Component 和 Weex的平台相关的代码中也有,作用跟这里的web browser的类似,就不在这里讨论了。当然单元测试单中也大量存在。

 继续往下执行的话触发 dep.depend(),

// class Dep
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

这里相当于调用了当前依赖的watcher的addDep()方法

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

把当前依赖记录到watcher实例的newDeps数组中,如果当前watcher对象中的depIds中没有包含当前传入的Dep实例的id,就通过dep.addSub(this)添加到依赖的订阅者数组subs中。

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

也就是:当访问对象的属性的时候会进行收集依赖,即getter方法触发时re-collect dependencies,对同一个属性的依赖只添加一次。

下面我们来通过基本的案例演示来调试一下依赖收集的过程。

Watcher类

watcher分为三种:

  • Computed Watcher
  • 用户Watcher(侦听器)
  • 渲染Watcher

渲染Watcher的创建时机

  • /src/core/instance/lifecycle.js

调试响应式数据执行过程

数据响应式处理的核心过程和数组收集依赖的的过程

当数组的数据变化的时候watcher的执行过程

数据响应式原理总结

  • 响应式处理过程

    1. _init()
      1. initState()
        1. initData()
          1. observe()
            1. observe(value)
              1. 所在文件:/src/core/observer/index.js
              2. 功能
                • 判断value是否是对象,如果不是对象直接返回
                • 或者已经是observer(有__ob__属性)的,直接返回
                • 关键:如果没有,创建Observer对象
                • 返回创建的observer对象
            2. Observer
              1. 所在文件:/src/core/observer/observer.js
              2. 功能
                1. 给value对象定义不可枚举的__ob__属性,记录当前的observer对象。
                2. 数组的响应式处理
                3. 对象的响应式处理,调用walk方法
            3. defineReactive
              1. 所在文件:/src/core/observer/index.js
              2. 功能
                1. 为每一个属性创建dep对象
                2. 如果当前属性的值是对象,调用observe方法
                3. 定义getter
                  1. 收集依赖
                  2. 返回属性的值
                4. 定义setter
                  1. 保存新值
                  2. 如果新值是对象,调用observe方法
                  3. 派发更新(发送通知),调用dep.notify()
            4. 收集依赖
              1. 在watcher对象的get 方法中调用 pushTarget 记录 Dep.target 属性
              2. 访问data 中的成员的时候收集依赖,defineReactive 的getter 中收集依赖
              3. 把属性对应的 watcher 对象添加到 dep 的 subs 数组中
              4. 给 childOb 收集依赖,目的是子对象添加和删除成员时发送通知
            5. Watcher
              1. dep.notify() 在调用 watcher 对象的 update() 方法
              2. queueWatcher() 判断 watcher 是否被处理,如果没有的话添加到 queue 队列中,并调用 flushScheduleQueue()
              3. flushScheduleQueue()
                1. 触发 beforeUpdate 钩子函数
                2. 调用 watcher.run()
                  1. run()
                    1. get()
                      1. getter()
                        1. updateComponent
                3. 清空上一次的依赖
                4. 触发 actived 钩子函数
                5. 触发updated 钩子函数

  • 动态添加一个响应式属性

    • Vue.set(obj, propKey/index, value)
    • vm.$set(obj, propKey/index, value) -- 使用得更多的方式
    • 上面两个方法是同一个方法,也可以用于修改数组元素。
    • 修改的数据对象不能式Vue实例,或者 Vue 实例的根数据对象。

        Vue.set/vm.$set方法可以向响应式对象中添加一个property,并确保这个新 property 同样是响应式的,且触发视图更新。它必须用于响应式对象上添加新property,因为Vue无法探测普通的新增property。

 Vue.set 源码分析

  • Vue.set()
    • global-api/index.js
      // global-api/index.js
        Vue.set = set
        Vue.delete = del
        Vue.nextTick = nextTick
    • instance/index.js
      
      // 注册vm的$data/$props属性对象和$set/$delete/$watch方法
      stateMixin(Vue)
       
      • instance/state.js
        
          Vue.prototype.$set = set
          Vue.prototype.$delete = del
        

通过源码,可以看到这里的set和del都是对observer/index.js中的set/del的引用,是完全相同的。

vm.$delete

功能

        删除对象的属性,如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开Vue不能检测到属性被删除的限制,但是应该很少会使用它。

如: vm.$delete(vm.obj, 'msg')

定义位置

  • Vue.delete()
    • global-api/index.js
  • vm.$delete()
    • instance/index.js
    • instance/state.js

vm.$watch

vm.$watch(expOrFn, callback, [options])

功能

  •         观察Vue实例变化的一个表达式或计算性函数,回调函数得到的参数为新值和旧值。表达式只接受监督的健路径。
  •         对于更复杂的表达式,用一个函数取代。

参数

  • expOrFn: 要监视的$data中的属性,可以是表达式或函数
  • callback:数据变化后执行的函数
    • 函数:回调函数
    • 对象:具有handler 属性(字符串或者函数),如果该属性为字符串,则methods 中相应的定义
  • options:可选的选项
    • deep:布尔类型,深度监听
    • immediate:布尔类型,是否立即执行一次回调函数

示例:

 // compiler 编译器,其目的是:把template模版字符串 转换成 render函数
        const vm = new Vue({
            el: '#app',
            template: '<h1>{{msg}} from template</h1>', // 需要在compiler-included build,即含编译器的完整版本下才能正常工作
            render(h) { // 可以在runtime-only build,即运行时版本中正常工作
                // render函数中的参数h, h是一个函数,它是虚拟DOM中用来创建虚拟DOM的。
                // render函数中需要把创建出来的虚拟DOM返回出去,才能被正常挂载并渲染到页面上
                return h('h1', this.msg + 'from render function') 
            },
            data() {
                return {
                    msg: 'Welcome to Vue World',
                    user: {
                        firstname: 'deng',
                        lastname: 'huiquan',
                        fullname: ''
                    }
                }
            }
        })

        vm.$watch('user', (od, nw) => {
            console.log(od, nw, '<------vm.$watch------')
            vm.user.fullname = nw.firstname + ' ' + nw.lastname
        }, {
            immediate: true,
            deep: true
        })

三种类型的Watcher对象

        没有静态方法,因为$watch 方法中要使用Vue的实例

Watcher 分三种:计算属性 Watcher、用户 Watcher(侦听器)、渲染 Watcher

  • 创建顺序:计算属性 Watcher、用户 Watcher(侦听器)、渲染 Watcher

vm.$watch()

  • 定义位置:src/core/instance/state.js

vm.$nextTick() 异步更新队列

  • Vue更新DOM是异步执行的,批量的

    • 在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。
  • vm.$nextTick(function(){.../* 操作DOM */}) // === Vue.nextTick()

vm.$nextTick()代码演示

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">Hello World
        <h1 ref="h1">{{ msg }} from template</h1>
        <h2>{{ user.fullname }}</h2>
    </div>
    <script src="../../dist/vue.js"></script>
    <script>
        // compiler 编译器,其目的是:把template模版字符串 转换成 render函数
        const vm = new Vue({
            el: '#app',
            data() {
                return {
                    msg: 'Welcome to Vue World',
                    user: {
                        firstname: 'deng',
                        lastname: 'huiquan',
                        fullname: ''
                    }
                }
            },
            mounted() {
                this.msg = 'Welcome to Vue World vm.$nextTick()'
                console.log(this.$refs.h1.textContent, '<---- before vm.$nextTick() is called')
                this.$nextTick(()=> {
                    console.log(this.$refs.h1.textContent, '<---- after vm.$nextTick() is called')
                })
            },
        })

        vm.$watch('user', () => {
            vm.user.fullname = vm.user.firstname + ' ' + vm.user.lastname
        }, {
            immediate: true,
            deep: true
        })
    </script>
</body>
</html>

console控制台的输出如下

Welcome to Vue World from template <---- before vm.$nextTick() is called
Welcome to Vue World vm.$nextTick() from template <---- after vm.$nextTick() is called

 

vm.$nextTick源码解析

  • 定义位置:src/core/instance/render.js
  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

源码nextTick()函数定义

  • 手动调用vm.$nextTick()
  • 在Watcher的 queueWatcher 中执行 nextTick()
  • src/core/util/next-tick.js

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值