阅读 135

最易懂的Vue源码解析-响应式原理

前言

最近看了下vue的源码(2.0版),决定就核心部分-响应式原理做一个沉淀,欢迎感兴趣的小伙伴阅读

源码解析

Trigger.png

上图是官方给的流程图,为了方便理解,我将从以下顺序进行分析

  1. Data部分,Vue对data做了什么
  2. 为什么要进行依赖收集,又是怎么收集的
  3. Watcher是什么,在其中起了什么作用
  4. 组件的re-render是在什么时候怎么触发的

1.双向绑定

在看源码之前我们就知道,Vue是MVVM的框架,能将数据做到双向绑定,它对data的每个属性都用Object.defineProperty定义了gettersetter,但人家肯定不是这一句话就写完的,具体是怎么实现的呢?

data初始化

让我们打开Vue的源码,翻到vue/src/core/instance,选择这个目录是因为里面的代码基本都是一些初始化的操作,里面一定会有对data的初始化

根据程序员的直觉,我们先看看index.js,可以看到里面主要执行了几个方法

// index.js
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
复制代码

然后我们看下第一个函数initMixin,根据引入目录,我们在init.js中找到了这个函数,看到在里面又执行了几个方法

// init.js
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化时间
initRender(vm) // 初始化render
callHook(vm, 'beforeCreate') // 触发beforeCreate
initInjections(vm) // resolve injections before data/props
initState(vm) // 上面是before,下面是after,中间是什么?
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // 触发created
复制代码

根据命名和注释,我猜data初始化应该是在initState函数中

果然!在state.js里的initState中幸运的找到了initData函数!不亏是大框架层级真深。。。

// 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) // here~
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
复制代码

除了初始化data,这个函数里还处理propsmethodscomputedwatch,这里不多解释,感兴趣的可以自己去看看

这次我们重点看initData

// state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  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
    )
  }
  // 上面代码的意思是要求data必须返回一个函数 至于为什么可以进上面链接看官方解释
  
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  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)) {
      proxy(vm, `_data`, key)
    }
  }
  // 上面代码意思method、data、props里字段命名不能冲突

  observe(data, true /* asRootData */)
}
复制代码

可以看出,其实重点的操作在最后的observe函数

observer

顺着observe引入目录我们来到vue/src/core/observer,在index.js中找到了这个函数本体

// index.js
/**
 * 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 {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  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
  ) {
    ob = new Observer(value) 
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
复制代码

结合注释和代码我们大概了解这段代码其实就做了一件事情:返回一个新的Observer或者返回已存在的Observer

Observer是啥?又干了什么事?不着急,慢慢往下看

刚说到new了下Observer,所以我们直接看Observer类的构造函数

// index.js
constructor (value: any) {
  this.value = value
  this.dep = new Dep()
  this.vmCount = 0
  def(value, '__ob__', this)
  if (Array.isArray(value)) {
    if (hasProto) {
      protoAugment(value, arrayMethods)
    } else {
      copyAugment(value, arrayMethods, arrayKeys)
    }
    this.observeArray(value)
  } else {
    this.walk(value)
  }
}
复制代码

可以看出,我们的data通过observe改名为value然后传到constructor最终被做了这样的处理数组的话执行observerArray函数,不是数组则执行walk函数,而这两个函数源码就放在了构造函数下面很容易就找到了!

// index.js
/**
 * 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++) {
    // 把data每一项都definRective
    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,最终还是相当于每一项都definRective
    observe(items[i])
  }
}
复制代码

所以最终都会到达defineReactive函数:

// index.js
/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  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]
  }

  let childOb = !shallow && observe(val)
  
  // 定义getter和setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* 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()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

复制代码

getter和setter

这个函数看起来很长,其实可以归纳如下:

 // 1.创建了个dep
 const dep=new Dep()

 Object.defineProperty(obj, key, {
    get: function reactiveGetter () {
        // 2.判断target存在,就执行depend(),这里就是收集依赖
    	if (Dep.target) {
          dep.depend()
        }
      return value
    },
    set: function reactiveSetter (newVal) {
    	val = newVal

        // 3.通知依赖更新
        dep.notify()
    }
  }
复制代码

这样是不是就简单多啦,所以可以看出Vuedata就是在这里被定义gettersetter

当我们对data执行get操作时就会触发getter,执行set操作时就会触发setter

思考1:如果我动态往data里新加一个属性a,操作a是否会触发getter和setter?
答案:不会 cn.vuejs.org/v2/api/#Vue…

思考2:在Vue中我们用push方法向data的某个数组变量中增加一个属性,会触发setter吗?结论是会的,Vue对数组方法做了特殊处理,想知道时怎么处理的话就去源码(observer/array.js)中寻找答案吧~

2.观察者模式

Dep

我们注意到gettersetter中有个出现频率很高的词——dep。那么dep是什么,又是怎么通过它进行依赖收集呢?

顺着引入路径,我们来到observer/dep.jsdefineReactive归纳后我标注的1、2、3三个步骤的操作分别对应如下:

// dep.js
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  
  // 1.new Dep()时创建了空的subs,这是用来存依赖的数组
  constructor () {
    this.id = uid++
    this.subs = []
  }
  
  // 2.dep.depend() 执行了target的addDep()
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  
  // 3.dep.notify()对subs数组里每个项执行了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)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
复制代码

这么一看,好像没有向依赖数组subs存入内容的操作,因为depend函数也并没有对subs做类似push的行为,收集依赖的步骤在哪呢?

好奇的小伙伴肯定注意到了target,这又是啥,从哪来的,是做啥的?

其实这是理解依赖收集的关键,通过在dep.js搜索'target'我们在上面代码第一行看到关于target类型的定义是一个Watcher类,然后在最下面搜到这样的代码:

// dep.js

// 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]
}
复制代码

target应该就是在这里被注入灵魂的,注释上说Dep.target必须保持全局有且只有一个,这也解释了这段代码的功能,也是为什么在依赖收集时可以直接判断Dep.target

那这俩函数pushTargetpopTarget在哪调用的呢?而Watcher,根据命名和平时的使用,我们猜测,Watcher应该是个能观察到数据变化的工具,先去Watcher里看看吧

Watcher--观察者

带着疑惑我们打开了observer/watcher.js,果然在里面找到了pushTargetpopTarget的调用!它在一个名叫get的函数里

// watcher.js

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    // 1.赋值target Dep.target = this
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 2.执行this.getter
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // 3.收集完成后删掉target
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
复制代码

也就是说,target赋值this,也就是当前的Watcher

this.getter是什么呢,我们在Watcher构造函数中找到了他

// watcher.js

// expOrFn是创建Watcher时传进来的,要观察的表达式,这里把表达式转成了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.getter相当于是观察内容的的赋值逻辑,怎么理解呢:

举例我创建了一个Watcher观察一个属性a,而a赋值逻辑data.b + data.c,那么this.getter就相当于执行data.b + data.c

接着在构造函数中我们也幸运的找到了上面提到的get函数的调用位置

// watcher.js

this.value = this.lazy
      ? undefined
      : this.get()
复制代码

依赖收集

现在我们已经解开了大部分谜题,为了方便理解,我们模拟一下整个流程,还是刚才的例子,我们有一个变量a,而a赋值逻辑涉及了data.bdata.c

现在我们new Watcher()把它传入,然后走到了构造函数,执行this.get()get()会创建target=Watcher并执行this.getter(),而this.getter()就相当于执行a赋值逻辑,就会 “触碰”data.bdata.c,因为data所有属性都在初始化的时候被定义了gettersetter,当被 “触碰” 就会触发data.bdata.cgetter

前方高能

也就是执行

// observe.js

// 创建Watcher与data的桥梁dep(记住是data.b或data.c的dep,跟a无关)
const dep=new Dep()

// getter:
if (Dep.target) {
    // target有吗?有的,刚才get()里创建了,值为当前Watcher
    dep.depend()
}
复制代码

执行

// observe.js

dep.depend()
复制代码

等于

// dep.js

Dep.target.addDep(this)
复制代码

也就是执行Watcher里的addDep:

// watcher.js

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.js

dep.addSub(this)
复制代码

等于

// dep.js

addSub (sub: Watcher) {
  this.subs.push(sub)
}
复制代码

就是这样,兜兜转转,最终data.bdata.csubs里成功放入了aWatcher,也就完成了依赖收集的步骤!

依赖更新

依赖收集完了,Watcher存好了,怎么更新的呢?

假设现在data.bdata.cset了新的值,那么就会触发他们的setter

继续高能

也就是执行

// observe.js

// setter:
dep.notify()
复制代码

也就是

// dep.js

 notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs are not 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++) {
      // here~ 这里subs里只有一个,就是a的Watcher
      subs[i].update()
    }
  }
复制代码

也就是

// watcher.js

update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
复制代码

默认会走到this.run(),其他两种模式这次就先不做解释啦

run () {
    if (this.active) {
      const value = this.get()
      
      // 下面是一些value变化后回调逻辑
      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
        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)
        }
      }
    }
  }
复制代码

约等于

this.value=this.get()
复制代码

get函数刚刚有解释哦,里面会执行a赋值逻辑a就顺利被更新了!

至此,整条响应式链路就理清楚了!

组件的响应式

当然a、b、c都是我的举例,实际组件的响应式和例子也很相似。

首先每一个组件就相当于例子中的a,都会配一个Watcher,只不过而组件的赋值逻辑有一个专属函数,也就是渲染函数render

render执行过程中就会生成DOM,而DOM中所需要的data就相当于例子中的b、c一样会被 “触碰” ,然后就像例子中一样,往所有data依赖中放入组件Watcher

data变化时就会触发Watcher的执行赋值逻辑re-render,然后组件就成功被更新啦~

思考:这部分源码在哪里呢?试着找到然后研究下?

总结

  • Watcher的作用印证了我们的猜想,它就是一个观察者,可以观察一个数据变化
  • Dep则是一个订阅者,主要的作用就是收集依赖也就是收集观察者Watcher和通知观察者更新
  • Observer的主要作用是使用Object.defineProperty方法对Data的每一个子属性定义gettersetter,然后劫持他们的getset操作

——————————————end——————————————

才疏学浅,大佬轻喷~

文章分类
前端
文章标签