浅谈Vue响应式原理(下)

开始结合Vue源码来整理响应式的原理(此篇代码有部分简写),上篇可看浅谈vue的响应式(上)

VUE响应式原理

了解了Object.defineProperty观察者模式之后就可以开始解释Vue的响应式原理了,VUE响应式原理图如下:

响应式原理

Vue实现响应式分为3步:

1. init 阶段

对数据进行初始化

首先面对的就是vue的初始化,在执行new Vue(options) 的时候,会调用这个initMixin方法。

export function initMixin (Vue: Class) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    vm._uid = uid++
    // options参数的处理
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      // mergeOptions 对 mixin 选项和 new Vue 传入的 options 选项进行合并
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    vm._self = vm
    // vm的生命周期相关变量初始化
    initLifecycle(vm)
    // vm的事件监听初始化
    initEvents(vm)
    // vm的编译render初始化
    initRender(vm)
    // vm的beforeCreate生命钩子的回调
    callHook(vm, 'beforeCreate')
    // vm在data/props初始化之前要进行绑定
    initInjections(vm) 
    // 初始化数据
    initState(vm)
    // vm在data/props之后要进行提供
    initProvide(vm)
    // vm的created生命钩子的回调
    callHook(vm, 'created')
    if (vm.$options.el) {
      // 初始化渲染页面 挂载组件
      vm.$mount(vm.$options.el)
    }
  }
}

通过initState进行数据初始化,这里的initPropsinitMethodsinitDatainitComputedinitWatch,都是针对Vue不同的数据进行初始化。

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // 初始化props
  if (opts.props) initProps(vm, opts.props)
  // 初始化methods
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    // 初始化data
    initData(vm)
  } else {
    // 当data为空时observe 函数观测一个空对象:{}
    observe(vm._data = {}, true /* asRootData */)
  }
  // 初始化计算属性
  if (opts.computed) initComputed(vm, opts.computed)
  // 初始化watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

接下来只关注observe,上面通过observe(vm._data = {}, true /* asRootData */)调用函数

// 用observe来进行数据观测
export function observe(data) {
  // ../utils/index.js的函数用于判断data是否是对象
  if (!isObject(data)) {
    return
  }
  let ob;
  // 如果这个数据被观测过,就会添加__ob__属性,并且它的构造函数是Observe
  if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observe) {
    // 这里表示观察过了
    ob = data.__ob__
  } else {
    // 如果没有被观察过就创建新的Observe实例,传入data
    ob = new Observe(data)
  }
  return ob
}

observe函数中只对object类型的数据进行观测,并且还会判断这个观测对象是否之前被观测过,如果没有就会创建新的observe实例

class Observe {
  constructor(data) {
    // 调用./dep的dep类,生成dep实例,用于收集依赖
    this.dep = new Dep()
    // 为观察的对象添加 __ob__ 标识
    // def方法来自../utils/index.js,def(data, key, value),内部使用Object.defineProperty
    def(data, '__ob__', this)
    // 判断传入的是否是一个数组
    if (Array.isArray(data)) {
      // 重写数组方法
      protoAugment(data, arrayMethods)
      // 调用数组观测函数
      this.observeArray(data)
    } else {
      // 观察对象
      this.walk(data)
    }
  }
  // 普通数据观测方法
  walk(data) {
    // 遍历对每个元素添加观测
    Object.keys(data).forEach(key => {
      defineReactive(data, key, data[key])
    })
  }
  // 数组数据观测方法
  observeArray(data) {
    // 遍历对每个元素添加观测
    data.forEach(item => {
      // 这里如果是个对象数组,那就是对每个对象元素添加观测,而不是对数组对象添加观测
      // 可以data[0].name = 'taec'更改
      // 但是不可以data[0] = {name:'taec'}
      observe(item)
    })
  }
}

observe中会使用walkobserveArray分别对两种数据类型进行观测,通过defineReactive方法来设置数据劫持,给每个data中的数据添加getter/setter属性,在get内使用dep.depend()注册函数,在setter内使用dep.notify()当数据变化时通知被观察者

// defineReactive主要为数据添加get和set,即数据劫持
function defineReactive(obj, key, value) {
  // 考虑到有可能是对象形式,要使用递归来添加劫持
  let childOb = observe(value)
  // 新建Dep
  const dep = new Dep()
  //使用Object.defineProperty方法
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        dep.depend()
        // 子元素收集依赖
        // 主要应用于数组变动时会调用 ob.dep.notify()
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return value
    },
    set(newVal) {
      if (newVal === value) {
        return
      }
      value = newVal
      // 新值有可能是对象或者数组,childOb收集依赖
      childOb = observe(newVal)
      // 更新视图
      dep.notify()
      return value
    }
  })
}

defineReactive中new出来的dep主要就是用于收集依赖的,代码如下

// 设置dep类
class Dep{
  constructor() {
    // id是唯一的id,
    this.id = ++id
    // 用于存放watch
    this.subs = []
  }
  depend() {
    // watcher 添加当前 dep
    // 同时让 dep 添加 watcher
    Dep.target.addDep(this)
  }
  addSub(watcher) {
    this.subs.push(watcher)
  }
  // watcher 更新
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
  removeSub(watcher) {
    for (let i = 0, len = this.subs.length; i < len; i++) {
      if (this.subs[i] === watcher) {
        this.subs.splice(i, 1)
        return
      }
    }
  }
}
// target是一个watch实例
Dep.target = null
// 存储 watcher 的栈
let stack = []  
// 添加watch
export function pushTarget(watcher) {
  stack.push(watcher)
  Dep.target = watcher
} 
// 删除最后一个watch
export function popTarget(){
  stack.pop()
  Dep.target = stack[stack.length - 1]
}

defineReactive执行完毕之后,所有的数据都被劫持过了,此时init阶段完成。
init阶段:initState中调用initData,并且在initData中使用observe对数据进行观测,observe会区分对象和数组,分别使用walkobserveArray,通过defineReactive来完成数据劫持以及使用dep完成依赖收集。

2. mount 阶段

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 调用beforeMount钩子函数
  callHook(vm, 'beforeMount')
  // updateComponent作为Watcher对象的getter函数,用来依赖收集
  let updateComponent
  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._render() 返回 vnode,这里面会就对 data 数据进行取值
      // vm._update 将 vnode 转为真实dom,渲染到页面上
      vm._update(vm._render(), hydrating)
    }
  }
  // 创建一个new ,Watcher的getter为updateComponent函数
 // 用于触发所有渲染所需要用到的数据的getter,进行依赖收集,该Watcher实例会存在所有渲染所需数据的闭包Dep中
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
  if (vm.$vnode == null) {
    // 挂载完成,调用mounted钩子
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

mountComponent函数中会创建一个Watcher类,Watcher对应一个vue component,用于连接Vue组件与Dep,当new Watcher的时候,传入VM,updateComponent作为watcher的getter,

new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

updateComponent执行时,会调用当前组件的render()开始产生虚拟Dom,那么就要用到当前组件的data,就会用到init阶段给data添加的getter属性的方法,同时调用dep.depend()注册观察对象。

3. 更新阶段

当数据开始更新,首先会触发observe中的setter。会开始执行dep.notify(),来执行更新。

set(newVal) {
      if (newVal === value) {
        return
      }
      value = newVal
      // 新值有可能是对象或者数组,childOb收集依赖
      childOb = observe(newVal)
      // 更新视图
      dep.notify()
      return value
    }

dep.notify()会开始循环调用每个watcher的watcher.update()

notify() {
    this.subs.forEach(watcher => watcher.update())
  }
// watcher.update()
update () {
  if (this.lazy) {  
    // 计算属性更新
    this.dirty = true
  } else if (this.sync) {  
   // 同步更新
    this.run()
  } else {
    // 一般的数据都会进行异步更新
    queueWatcher(this)
  }
}

  1. 同步更新,dep通知某个watcher实例需要更新的时候,这个watcher实例直接调用callback方法进行更新。
  2. 异步更新,针对所有的DOM更新,watcher会调用名为queueWatcher的方法将自己放进更新队列,更新队列是一个由watcher组成的数组,Vue会在更新循环中遍历这个数组并依次调用其中每个watcher的callback。

这样我们看官网的图就能理解了:
1是init和mount阶段,2是更新阶段,个人理解如有错误欢迎大佬指正

参考文章:

  1. 最简化 VUE的响应式原理
  2. 手摸手带你理解Vue响应式原理
  3. 【探究Vue原理】watcher的异步更新

你可能感兴趣的:(浅谈Vue响应式原理(下))