结合源码去理解Vue响应式原理

491 阅读7分钟

1. 前言

本文主要是梳理一下vue中响应式的处理与实现,部分代码是经过删减或是自己理解的,并不等于vue中的源码

2. 了解一下MVVM

2.1 什么是MVVM?

MVVM是Model-View-ViewModel的简写

  1. M代表model(数据,模型)
  2. V代表view(视图,模板)
  3. VM代表ViewModel(连接model和view,从而实现视图和数据分离) View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的,View可以通过事件绑定改变数据,Model通过数据绑定驱动视图渲染更新

2.2 Vue是如何体现MVVM思想?

Vue 就是一个典型的 MVVM 模型的框架,充当了 MVVM 开发模式中的 ViewModel 层,负责 View 和 Model 之间通信的桥梁,其中最主要是体现在以下三点:

  1. 数据响应式(vue如何监听到data每个属性的变化)
  2. 模板解析引擎:vue模板如何被解析,怎么和数据进行关联
  3. 渲染:vue模板如何被渲染成?数据更新后又是什么重新渲染dom

3. Vue初始化过程

从一个简单的Vue实例的代码来分析Vue的响应式原理

// html部分
<div id="app"> {{ msg }} </div>

// script部分
<script>
    new Vue({ 
        el: '#app',
        data: { msg: 'hello vue' }
    })
</script>

这段代码很简单,最终会在页面上显示 hello vue,它是如何实现的呢?

我们从源头:new Vue 的地方开始分析

3.1 new Vue

在使用new Vue时,是对 /src/core/instance/index.js 中的Vue进行了实例化

import { initMixin } from './init'

// Vue 构造函数
function Vue (options) {
  // 调用 Vue.prototype._init 方法,该方法是在 initMixin 中定义的
  this._init(options)
}

// 定义 Vue.prototype._init 方法
initMixin(Vue)

export default Vue

3.2 Vue.prototype._init

/src/core/instance/init.js,可以看到 initState(vm)  是用来初始化data,props,methods,computed以及watch

/**
 * 定义 Vue.prototype._init 方法 
 */
export function initMixin (Vue) {
  // 这主要是Vue 的初始化过程
  Vue.prototype._init = function (options) {
    // vue 实例
    const vm = this
    
    // 注意:这里是删减了一些代码
    
    // 初始化组件实例关系属性,比如 $parent、$children、$root、$refs 等
    initLifecycle(vm)
    
    // 初始化自定义事件
    initEvents(vm)
    
    // 解析组件的插槽信息
    initRender(vm)
    
    // 调用 beforeCreate 钩子函数
    callHook(vm, 'beforeCreate')
    
    // 初始化组件的 inject 配置项,得到 result[key] = val 形式的配置对象
    // 然后对结果数据进行响应式处理,并代理每个 key 到 vm 实例
    initInjections(vm)
    
    // !!!注意:这里的initState是数据响应式的重点
    // 处理 data、props、methods、computed、watch
    initState(vm)
    
    // 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
    initProvide(vm)
    
    // 调用 created 钩子函数
    // 所以在vue的生命周期中,最早能访问data里定义的数据,是在 created
    callHook(vm, 'created')

    // 如果发现配置项上有 el 选项,则自动调用 $mount 方法
    // 也就是说有了 el 选项,就不需要再手动调用 $mount
    // 反之,没有 el 则必须手动调用 $mount
    if (vm.$options.el) {
      // 调用 $mount 方法,进入挂载阶段
      vm.$mount(vm.$options.el)
    }
  }
}

3.3 initState(vm):数据响应式的处理

initState方法是在/src/core/instance/state.js,本篇主要是梳理一下初始化data的过程

export function initState (vm) {
  vm._watchers = []
  const opts = vm.$options
  // 初始化props
  if (opts.props) initProps(vm, opts.props)
  
  // 初始化方法
  if (opts.methods) initMethods(vm, opts.methods)
  
  // 初始化data
  if (opts.data) {
    initData(vm)
  } else {
    // 该组件没有data的时候绑定一个空对象
    observe(vm._data = {}, true /* asRootData */)
  }
  
  // 初始化computed
  if (opts.computed) initComputed(vm, opts.computed)
  
  初始化watchers
  if (opts.watch) initWatch(vm, opts.watch)
}

3.4 initData(vm)

在初始化 data 的过程中,主要做了三件事:

  1. 在 data 中的属性进行判断,data 上的属性不能和 props、methods 对象上的属性相同
  2. 把 data 代码在 vm(vue的实例) 上
  3. Vue 将遍历 data 中的所有 属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setterObject.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因
function initData (vm) {
  // 获取data数据
  let data = vm.$options.data
  
  ...
  
  //遍历data中的数据
  while (i--) {
    const key = keys[i]
    // 保证data中的key不与methods中的key重复,methods优先,如果有冲突会产生warning
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
  
    // 与上面的methods判断同理
    if (props && hasOwn(props, keys[i])) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${keys[i]}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(keys[i])) {
      // 判断是否是保留字段
      // 将data上面的属性代理到了vm实例上
      proxy(vm, `_data`, keys[i])
    }
  }
  // observe data
  // 响应式处理的真正入口
  // 这里通过observe实例化Observe对象,开始对数据进行绑定
  observe(data, true /* asRootData */)
}

3.5 Observe

我们都知道,Vue 通过 Object.defineProperty 将 data 中声明的属性转换成getter/setter形式,Observer类则是用于进行依赖收集以及调度更新

export class Observer {
  constructor (value) {
    this.value = value
    this.dep = new Dep()
    
    ...
     
    if (Array.isArray(value)) {
       // 如果是数组,覆盖数组默认的七个原型方法,以实现数组响应式
       ... 
    } else {
      // value 为对象,为对象的每个属性设置响应:递归遍历所有属性
      this.walk(value)
    }
  }
    
  walk (obj) {
    const keys = Object.keys(obj)
    // walk方法会遍历对象的每一个属性,调用defineReactive方法
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
  
  // 遍历数组,为数组的每一项设置观察,处理数组元素为对象的情况
  observeArray (items) { 
      for(let i = 0, l = items.length; i < l; i++) { 
          observe(items[i]) 
      }
  }
}

3.6 defineReactive

  1. defineReactive方法会遍历 data 中所有的属性,通过 Object.defineProperty 为每个 key 设置响应式
  2. 因为是递归遍历所有属性,性能较差。所以在定义 data 的时候,最好不要使用多层嵌套。data 的结构尽量扁平化
function defineReactive(obj, key, val) {
    // 实例化 dep,一个 key 一个 dep 
    const dep = new Dep()

    // 对象的子对象递归进行observe并返回子节点的Observer对象
    let childOb = observe(val)
    
    // 数据响应式的核心
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            if (Dep.target) {
                /*进行依赖收集*/
                dep.depend()
                if (childOb) {
                    // 子对象进行依赖收集
                    childOb.dep.depend()
                }
                
                ...
            }
             return val
         },
         set(newVal) {
             // 用户赋值的值可能是一个对象,所以也要进行数据劫持
             observe(newVal)
             
             val = newVal
         }
    })
}

3.7 数组的响应式处理

因为 Object.defineProperty存在缺陷性,无法原生监听数组的变化。所以对于数组需要进行特殊处理。用户在调用数组的push、shift、unshift、pop、reverse、sort、splice这七个可以修改原数组的方法时,需要调用重写后的方法

// 原始的数组的原型对象
const oldProto = Array.prototype
// 通过继承的方式创建新的 arrayMethods
export const arrayMethods = Object.create(arrayProto)

// 操作数组的七个方法,这七个方法可以改变数组自身
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(method => {
    arrayMethods[method] = function(...args) {
        console.log(数组发送变化)
        // 先执行原生方法,比如 push.call(this, ...args)
        oldProto[method].call(this,...args)
        
        let inserted
        // 如果是数组新增值,需要对数组新增的值进行响应式处理
        switch (method) {
          case 'push':
          case 'unshift':
            inserted = args
            break
          case 'splice':
            inserted = args.slice(2)
            break
        }
        // 对新插入的元素做响应式处理
        if (inserted) ob.observeArray(inserted)
        // 通知更新
        ob.dep.notify()
        return result
    }
})

3.8 Watcher

Watcher是一个观察者对象。依赖收集以后Watcher对象会被保存在Dep的subs中,数据变动的时候Dep会通知Watcher实例,然后由Watcher实例回调cb进行视图的更新

src/core/observer/watcher.js

export default class Watcher {
  constructor (vm, expOrFn, cb, option) {
    this.vm = vm
    // _watchers存放订阅者实例
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } 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
    // 把表达式expOrFn解析成getter
    this.getter = expOrFn
    
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  
  
  // 获得getter的值并且重新进行依赖收集
  get () {
    // 将自身watcher观察者实例设置给Dep.target,用以依赖收集
    pushTarget(this)
    let value
    const vm = this.vm
    /*执行了getter操作,看似执行了渲染操作,其实是执行了依赖收集。
      在将Dep.target设置为自生观察者实例以后,执行getter操作。
      譬如说现在的的data中可能有a、b、c三个数据,getter渲染需要依赖a跟c,
      那么在执行getter的时候就会触发a跟c两个数据的getter函数,
      在getter函数中即可判断Dep.target是否存在然后完成依赖收集,
      将该观察者对象放入闭包中的Dep的subs中去。*/
    if (this.user) {
      try {
        value = this.getter.call(vm, vm)
      } catch (e) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      }
    } else {
      value = this.getter.call(vm, vm)
    }
    // 如果存在deep,则触发每个深层对象的依赖,追踪其变化
    if (this.deep) {
      // 递归每一个对象或者数组,触发它们的getter
      // 使得对象或数组的每一个成员都被依赖收集,形成一个“深(deep)”依赖关系
      traverse(value)
    }
    /*将观察者实例从target栈中取出并设置给Dep.target*/
    popTarget()
    this.cleanupDeps()
    return value
  }
  
  // 添加一个依赖关系到Deps集合中
  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)
      }
    }
  }
  
  ...
  
  // 当数据更新时,dep 会通知 watcher 更新
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      /*同步则执行run直接渲染视图*/
      this.run()
    } else {
      /*异步推送到观察者队列中,下一个tick时调用。*/
      queueWatcher(this)
    }
  }
  
  run () {
    if (this.active) {
      /* get操作在获取value本身也会执行getter从而调用update更新视图 */
      const value = this.get()
      if (
        /*
            即便值相同,拥有Deep属性的观察者以及在对象/数组上的观察者应该被触发更新,因为它们的值可能发生改变。
        */
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        /*设置新的值*/
        this.value = value
        /*触发回调*/
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
 
  // 获取观察者的值
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
  
  // 收集该watcher的所有deps依赖
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
  
  ...
}

3.9 Dep

data 的属性被Observer后,在触发 getter 时,Dep 就会收集依赖的 Watcher ,其实 Dep 可以接受多个订阅者的订阅,当 data 中某个属性变动时,就会通过 Dep 给 Watcher 发通知进行更新

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

/**
 * 一个 dep 对应一个 obj.key
 * 在读取响应式数据时,负责收集依赖,每个 dep(或者说 obj.key)依赖的 watcher 有哪些
 * 在响应式数据更新时,负责通知 dep 中那些 watcher 去执行 update 方法
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

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

  // 在 dep 中添加 watcher
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

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

  // 像 watcher 中添加 dep
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  /**
   * 通知 dep 中的所有 watcher,执行 watcher.update() 方法
   */
  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)
    }
    // 遍历 dep 中存储的 watcher,执行 watcher.update()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

/**
 * 当前正在执行的 watcher,同一时间只会有一个 watcher 在执行
 * Dep.target = 当前正在执行的 watcher
 * 通过调用 pushTarget 方法完成赋值,调用 popTarget 方法完成重置(null)
 */
Dep.target = null
const targetStack = []

// 在需要进行依赖收集的时候调用,设置 Dep.target = watcher
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

// 依赖收集结束调用,设置 Dep.target = null
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

4. 总结

通过上面的代码和描述,结合vue官网的说明,大家应该带vue的初始化过程和响应式处理有了一定的了解,此时再看官网上的这张图,相信大家已经有了自己的理解。 vue.png

  1. Vue 的初始化过程(new Vue(options))都做了什么?
    • initLifecycle:初始化组件实例的关系
    • initMethods: 初始化自定义事件
    • initRender在:解析组件的插槽
    • callHook(vm, 'beforeCreate'):调用 beforeCreate
    • initInjections:初始化组件的 inject 配置项
    • initState:数据响应式的重点,处理 props、methods、data、computed、watch
    • initProvide: 解析组件配置项上的 provide 对象,将其挂载到vm._provided
    • callHook(vm, 'created'): 调用 created
  2. 数据响应式的处理与原理
    • 响应式的核心是通过 Object.defineProperty 拦截对数据的访问和设置
    • 从 new Vue 开始,设置 get、set 监听 Data 中的数据变化,同时创建 Dep 用来搜集使用该 Data 的 Watcher
    • 响应式的分类:
      • 对象,循环遍历对象的所有属性,为每个属性设置 getter、setter,以达到拦截访问和设置的目的,如果属性值依旧为对象,则递归为属性值上的每个 key 设置 getter、setter

        1. 访问数据时(obj.key)进行依赖收集,在 dep 中存储相关的 watcher
        2. 设置数据时由 dep 通知相关的 watcher 去更新
      • 数组,增强数组的那 7 个可以更改自身的原型方法,然后拦截对这些方法的操作

        1. 添加新数据时进行响应式处理,然后由 dep 通知 watcher 去更新
        2. 删除数据时,也要由 dep 通知 watcher 去更新