Vue2.6.0源码阅读(四):响应性原理

举报
街角小林 发表于 2022/10/28 21:25:52 2022/10/28
【摘要】 这一篇我们来看一下Vue核心的响应性原理,在上一篇我们知道了初始化时Vue会把data选项的数据递归的转换成响应性的数据,具体来说就是给数组和对象创建一个关联的Observer实例,然后对于数组,会拦截它的所有方法,以此来监听数组的变化,对于普通对象,会遍历它自身所有可枚举的属性,将其转换成setter和getter形式,以此来监听某个属性的变化,为什么要这么做呢,我们来看一下。首先简单介绍...

这一篇我们来看一下Vue核心的响应性原理,在上一篇我们知道了初始化时Vue会把data选项的数据递归的转换成响应性的数据,具体来说就是给数组和对象创建一个关联的Observer实例,然后对于数组,会拦截它的所有方法,以此来监听数组的变化,对于普通对象,会遍历它自身所有可枚举的属性,将其转换成settergetter形式,以此来监听某个属性的变化,为什么要这么做呢,我们来看一下。

首先简单介绍一下挂载的逻辑,如果我们传递了模板字符串,那么会进行编译,编译模板这个过程非常复杂,反正最后会将其转换为渲染函数,如果直接是渲染函数了那么就没有这个过程,像我们平时使用Vue单文件的形式开发的话,在打包阶段其实就已经编译好了,渲染函数就是返回虚拟DOM(也就是VNode)的函数,VNode怎么转换成实际的DOM呢,其中涉及到虚拟DOMpatch算法,后面会介绍,只要记得这个过程会获取模板里使用到的数据。接着还会给每个Vue实例创建一个Watcher实例,这部分的代码简化如下:

// 更新组件的方法
let updateComponent = () => {
    // 执行渲染函数产出VNode,然后调用_update方法进行打补丁
    vm._update(vm._render(), hydrating)
}

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

定义了一个更新方法,然后传给Watcher,看Watcher类的实现之前我们先看一下Dep类的实现。

Dep类

dep是一个可观察对象,可以有多个指令订阅它:

let uid = 0
export default class Dep {
  static target;
  id;
  subs;

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

定义了两个实例属性,一个静态属性,subs数组就是用来存放订阅它的对象,有四个实例方法:

1.添加订阅者

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

2.删除订阅者

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

3.依赖收集

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

4.通知订阅者更新

notify () {
    // 只通知此刻存在的订阅者,调用其update方法
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
        subs[i].update()
    }
}

Watcher类

dep的订阅者其实就是Watcher实例:

export default class Watcher {
  constructor (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm
    // 如果是渲染watcher,那么会将该Watcher实例添加到Vue实例的_watcher属性中
    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
    this.active = true
    this.dirty = this.lazy // lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = ''
    // 从表达式中解析出getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
      }
    }
    // 执行取值方法,计算当前的值
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}

watcher做的事情主要是解析表达式、依赖收集、并在表达式的值发生改变时触发回调。Vue把给组件创建的Watcher实例称为渲染watcher

构造函数里先定义了一堆变量,然后判断表达式的类型,是函数的话那么直接使用该函数作为取值函数,否则调用parsePath方法来解析路径,并返回一个取值函数,这是一个简单的解析方法,只支持对.分隔的字符串进行解析:

const bailRE = new RegExp(`[^${unicodeLetters}.$_\\d]`)
export function parsePath (path) {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    // 遍历路径,一层一层取值
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

比如实例的data结构如下:

let vm = new Vue({
    data: {
        a: {
            b: {
                c: 1
            }
        }
    }
})

那么当我们使用表达式a.b.c来创建Watcher实例的话:

new Watcher(vm, 'a.b.c', () => {})

那么以vm为上下文执行parsePath方法返回的函数时就能成功获取到值1

构造函数的最后,如果lazy不为true的话会立即执行取值函数get,计算表达式的当前值。

依赖收集

具体到开头的挂载过程,表达式expOrFn就是updateComponent函数,也就是Watcher实例的this.getter,它会在get函数里调用:

get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
        value = this.getter.call(vm, vm)
    } catch (e) {} finally {
        // "touch"每个属性,以便它们都作为依赖项进行跟踪,以便进行深入观察
        if (this.deep) {
            traverse(value)
        }
        popTarget()
        this.cleanupDeps()
    }
    return value
}

首先执行了pushTarget方法:

// 当前正在计算执行中的目标watcher
// 这是全局唯一的,因为一次只能同时执行一个观察者。
Dep.target = null
const targetStack = []

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

Dep.target是一个全局变量,这一步相当于把当前的这个Watcher实例赋值给了Dep.target属性。

随后执行了getter函数,也就是执行updateComponent函数,在vm._render函数中会获取我们定义在data中的数据(如果模板里使用到了的话),此时,就会触发对应数据的getter函数。

比如:

<div>{{a}}</div>
new Vue({
    data: {
        a: 1
    }
})

在执行渲染函数的时候会执行vm.a来获取a的值,属性a已经被转换成了gettersetter的形式:

Object.defineProperty(obj, key, {
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      // Dep.target就是当前的Watcher实例
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    }
})

所以会触发属性aget函数执行,此时Dep.target就是当前的Watcher实例,那么会执行dep.depend方法,dep是属性a的依赖收集对象,depend方法内会执行Dep.target也就是当前的Watcher实例的addDep方法:

export default class Watcher {
    addDep (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)
            }
        }
    }
}

可以看到这里把属性adep添加到了newDeps集合中,为什么不直接添加到deps中呢?而要区分新旧呢?这其实要结合cleanupDeps方法一起看,这个方法在get方法的最后调用了,用来清理依赖项:

export default class Watcher {
    cleanupDeps () {
        let i = this.deps.length
        // 如果之前依赖某个dep,此时不依赖了,那么要从对应dep里删除该watcher
        while (i--) {
            const dep = this.deps[i]
            if (!this.newDepIds.has(dep.id)) {
                dep.removeSub(this)
            }
        }
        // newDepIds转为depIds,并清空旧的depIds
        let tmp = this.depIds
        this.depIds = this.newDepIds
        this.newDepIds = tmp
        this.newDepIds.clear()
        // newDeps转为deps,并清空旧的deps
        tmp = this.deps
        this.deps = this.newDeps
        this.newDeps = tmp
        this.newDeps.length = 0
    }
}

现在应该很清楚为什么要区分新旧了,因为当前watcher之前的依赖项,现在可能已经不依赖了,那么要找出不再依赖的dep,并从中删除当前watcher,所以需要一个新旧对比的过程。

addDep方法执行完后,属性adep里会收集到当前的渲染watcher,渲染watcher里也会保存属性adep

回到agetter函数,depend函数执行完后,接着:

if (childOb) {
    childOb.dep.depend()
    if (Array.isArray(value)) {
        dependArray(value)
    }
}

如果属性a的值是数组或对象的话,那么也会创建一个关联的Observer实例:

let childOb = !shallow && observe(val)

这里相当于当前的渲染watcher也会同时订阅a的值对应的dep对象,比如下面这种情况:

new Vue({
    data: {
        a: {
            b: 1
        }
    }
})

渲染watcherdeps数组一共会收集到两个dep实例,一个是属性a的,一个是a的值的。

最后对于a的值为数组的情况还调用了dependArray方法:

function dependArray(value ) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

递归遍历数组,对所有对象都进行依赖收集。

总结一下,当我们创建一个Vue实例时,内部会创建一个渲染watcherwatcher实例化时会把自身赋值给全局的Dep.target,然后执行取值函数,也就是会执行渲染函数,如果模板里引用了data的数据,那么就会触发对data数据的读取,相应的会触发对应属性的getter,在getter里会进行依赖收集,也就是会把Dep.target属性指向的watcher收集为依赖添加到它的dep实例中,同样该watcher也会保存该dep,如果该属性存在子observer,那么也会进行依赖收集,如果该属性的值是数组类型,还会递归遍历数组,给每个对象类型的数组项都进行依赖收集。

接下来回到Watcher实例的get方法,执行完取值方法把依赖收集完毕后:

if (this.deep) {
    traverse(value)
}

如果deep选项配置为true的话,那么会对值调用traverse方法,对于实例化Vue时,该选项是false,所以并不会走这里,我们以一个其他例子来看:

new Vue({
    data: {
        obj: {
            a: {
                b: 1
            }
        }
    },
    created() {
        this.$watch('obj', () => {
            console.log('变了')
        })
        setTimeout(() => {
          this.obj.a.b = 2
        }, 2000);
    }
})

这个例子中,两秒后我们修改了obj.a.b的属性值,它并不会触发回调,当我们deep设为true时,就会触发:

this.$watch('obj', () => {
    console.log('变了')
}, {
    deep: true
})

这就是上面的traverse函数做的事情,这里的取值函数返回的value就是obj的值。

const seenObjects = new Set()
export function traverse (val) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

调用了_traverse方法:

export function traverse (val) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val, seen) {
  let i, keys
  const isA = Array.isArray(val)
  // 非数组和对象就返回
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    // 这里我猜测是避免循环引用
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  // 递归
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

其实就是一个简单的递归函数,递归遍历了所有的数组项和对象的key,为什么这样就能实现监听深层值的变化呢,很简单,因为读取了所有的属性,也就是触发了它们的getter函数,所以所有属性的dep都收集了当前的watcher实例,那么当然如何一层里的任何一个属性修改了都会触发该watcher实例的更新。

回到get函数,接下来的逻辑:

popTarget()
this.cleanupDeps()

get函数的开始调用了pushTarget,依赖收集完毕当然要撤销这个操作:

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

cleanupDeps方法前面已经说过,不再赘述。

到这里,Watcher实例化的过程就已经结束了,现在我们的依赖都已经收集完毕,那么它们有什么用呢?

触发依赖更新

例子如下:

new Vue({
  el: '#app',
  template: `
    <ul>
      <li v-for="item in list">{{item}}</li>
    </ul>
  `,
  data: {
    list: [1, 2, 3]
  },
  created() {
    setTimeout(() => {
      this.list = [4, 5, 6]
    }, 5000);
  }
})

先来考考大家,这个例子里面的渲染watcher会收集到几个dep呢?

<summary>点击查看答案</summary>

2个,list属性一个,list的属性值数组一个

五秒后我们修改了list的值,显然会触发它的setter

Object.defineProperty(obj, key, {
    set: function reactiveSetter(newVal) {
        const value = getter ? getter.call(obj) : val
        // 值没有变化则直接返回
        if (newVal === value || (newVal !== newVal && value !== value)) {
            return
        }
        // #7981: 对于不带setter的访问器属性
        if (getter && !setter) return
        if (setter) {
            setter.call(obj, newVal)
        } else {
            val = newVal
        }
        // 观察新的值
        childOb = !shallow && observe(newVal)
        // 触发更新
        dep.notify()
    }
})

首先更新属性值,然后如果新的值是数组或对象的话那么会调用observe方法来将它转成响应式的,这样当我们修改这个新数组或新对象本身时才能触发更新。

最后通知依赖更新,这里的依赖就是渲染watchernotify方法里会调用watcherupdate方法:

export default class Watcher {
    update () {
        if (this.lazy) {
            this.dirty = true
        } else if (this.sync) {
            this.run()
        } else {
            queueWatcher(this)
        }
    }
}

可以看到有三个分支,如果lazytrue的话那么只设置一下dirty属性,这个属性具体有什么作用后面我们如果遇到了再说,如果是同步的,那么会直接执行run函数:

run () {
    if (this.active) {
        const value = this.get()
        if (
            value !== this.value ||
            // 即使值相同,深度的watcher和对象/数组上的watcher也应该触发,因为值可能已经发生了变化。
            isObject(value) ||
            this.deep
        ) {
            // 设置为新值
            const oldValue = this.value
            this.value = value
            // 用户的watcher,即通过$watch方法或watch选项设置的
            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)
            }
        }
    }
}

这个函数的核心就是调用了get方法更新当前表达式的值,然后当值有变化则会调用回调函数。

当也不是同步的watcher,那么会执行queueWatcher方法,这个涉及到Vue的调度功能,也就是异步批量执行的功能,我们会单独开一篇文章来介绍。

总结一下如何触发依赖更新,当依赖收集完毕后,如果我们更新了某个属性的值,那么该属性值的getter函数会被执行,就会通知保存在该属性值的dep里的所有依赖,调用它们的update方法进行更新。

对于我们开头的例子,也就是会调用渲染watcherupdate方法,重新执行取值函数,也就是会执行updateComponent方法来重新执行渲染函数,并进行打补丁更新组件,达到数据更新页面自动更新的效果。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。