Vue源码阅读(8):以组件的渲染为例看依赖收集和变化侦测

 我的开源库:

通过上一篇文章 Vue源码阅读(7):将数据转换成响应式的 我们已经了解了数据是怎么转换成响应式的了,接下来我们以组件的渲染为例看 Vue 在运行时是如何进行依赖收集和变化侦测的。

1,从 $mount 开始看

1-1,_init 方法(core/instance/init.js)

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // 初始化 state,包括 props、methods、data、computed、watch
    initState(vm)
 
    // 如果配置中有 el 的话,则自动执行挂载操作
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

 _init 方法中的 initState() 方法是上一篇博客的内容,作用是将数据转换成响应式的,响应式的数据是进行依赖收集和变化侦测的前期准备。接下来,我们以组件的渲染为例,看看依赖收集和变化侦测的具体流程。

1-2,$mount 方法(src/platforms/web/runtime/index.js)

// 运行时版本代码使用的 $mount 函数。调用这个 $mount 函数,模板字符串必须已经编译成 render 函数
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

运行时 $mount 方法内部调用 mountComponent() 方法进行组件的挂载。

1-3,mountComponent(core/instance/lifecycle.js)

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // 将 el 设值到 vm 中的 $el
  vm.$el = el

  // 触发执行 beforeMount 生命周期函数(挂载之前)
  callHook(vm, 'beforeMount')

  // 一个更新渲染组件的方法
  let updateComponent = () => {
    // vm._render() 函数的执行结果是一个 VNode
    // vm._update() 函数执行虚拟 DOM 的 patch 方法来执行节点的比对与渲染操作
    vm._update(vm._render(), hydrating)
  }

  // 这里的 Watcher 实例是一个渲染 Watcher,组件级别的
  vm._watcher = new Watcher(vm, updateComponent, noop)

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

mountComponent() 方法内部执行了一些生命周期函数, 并且创建了一个 Watcher 实例,传递的第二个参数是一个函数(updateComponent),该函数的作用是重新渲染页面。接下来我们看看 Watcher 类的实现。

2,Watcher 类的实现与依赖收集

Watcher 类的简要代码如下:

export default class Watcher {
  vm: Component;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
  ) {
    this.vm = vm
    this.getter = expOrFn
    this.value = this.get()
  }

  get () {
    // 将自身实例赋值到 Dep.target 这个静态属性上(保证全局都能拿到这个 watcher 实例),
    // 使得 getter 函数使用数据的 Dep 实例能够拿到这个 Watcher 实例,进行依赖的收集。
    // pushTarget 操作很重要
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 执行 getter 函数,该函数执行时,会对响应式的数据进行读取操作,这个读取操作能够触发数据的 getter,
      // 在 getter 中会将 Dep.target 这个 Watcher 实例存储到该数据的 Dep 实例中,以此就完成了依赖的收集
      // 依赖收集需要执行 addDep() 方法完成
      value = this.getter.call(vm, vm)
    } catch (e) {
      ......
    }
    
    return value
  }
}

我们从 Watcher 类的构造函数开始看,expOrFn 属性就是上面的 updateComponent 函数,在构造函数中将其设置到了 this.getter 属性上,然后调用 Watcher 类中的 get 方法。

接下来就是重点了,在 get 函数中,我们执行了一个 pushTarget 方法,方法的参数是当前的 Watcher 实例,pushTarget 方法的作用是:将当前的这个 Watcher 实例赋值到 Dep.target 属性上,这是一个静态属性,所以在代码的其他地方也能够访问到该属性,我们看一下 pushTarget 方法的代码实现:

2-1,src/core/observer/dep.js ==> pushTarget()

export function pushTarget (_target: Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}
export default class Dep {
  static target: ?Watcher;
}

pushTarget 方法的实现很简单,就是简单地将这个 Watcher 实例设值到 Dep.target 静态属性上。

接下来执行 this.getter.call(vm, vm)。

2-2,this.getter.call(vm, vm)

this.getter 就是我们传递到 Watcher 构造函数中的 updateComponent 函数,在这里执行它。

// 一个更新渲染组件的方法
let updateComponent = () => {
  // vm._render() 函数的执行结果是一个 VNode
  // vm._update() 函数执行虚拟 DOM 的 patch 方法来执行节点的比对与渲染操作
  vm._update(vm._render(), hydrating)
}

我们知道,在渲染组件的过程中,肯定会获取该组件使用到的数据,进行页面内容的填充。当代码获取这些数据的时候,就会触发这些数据的 getter 函数,我们看一下 getter 函数的相关内容。

export function defineReactive (
  // 对象
  obj: Object,
  // key
  key: string,
  // 值
  val: any,
  customSetter?: ?Function,
  // 浅的
  shallow?: boolean
) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 触发执行上面拿到的 getter
      const value = getter ? getter.call(obj) : val
      / 下面是依赖收集的操作 /
      // 如果 Dep 上的静态属性 target 存在的话
      if (Dep.target) {
        // 向 dep 中添加依赖,依赖是 Watcher 的实例
        dep.depend()
      }
      // getter 返回值
      return value
    },
    set: function reactiveSetter (newVal) {
        ......
    }
  })
}

我们可以看到,在 getter 函数中,判断 Dep.target 上有没有值,如果有值的话,就执行 dep.depend() 方法进行依赖的收集,这个 dep 实例是和当前的数据相对应的,也就是说上面我们 new 的 Watcher 实例是依赖于该数据的,所以应该把这个 Watcher 实例存储到该数据对应的 dep 中。我们看下,depend() 方法的具体内容。

export default class Dep {  
  // 依赖函数
  // 执行该函数可以将 Dep.target 依赖 push 进 subs 数组中
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
}

depend 方法的内部执行了 Watcher 实例的 addDep 方法,参数是其本身。

export default class Watcher {
  addDep (dep: Dep) {
    const id = dep.id
    // 通过 if (!this.newDepIds.has(id)) 防止同一 dep 重复进入里面的逻辑
    // 进入 if 代码块中的,每一次都是不同的 dep
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
}

在 addDep 方法中,如果这个 dep 是新的,不是和以前的 dep 重复的话,就进入最里面的逻辑,执行 dep 实例的 addSub 方法,参数是当前的 Watcher 实例。

export default class Dep {
  // 用于收集依赖的数组
  subs: Array<Watcher>;

  // 向 subs 数组添加依赖的函数
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
}

addSub 方法的内部,将 Watcher 实例 push 到了 subs 数组中,至此我们就完成了一次完整的依赖收集。

接下来说说变化侦测。

3,变化侦测的流程

在 Vue 应用中,一旦我们改变模板中使用的某个数据,这个组件就会重新进行渲染。改变数据的这个动作,我们可以通过 Object.defineProperty 中的 setter 检测到,如果是通过数组的原型函数改变数据的话,我们也能够在重写的原型函数中捕获到。

Object.defineProperty 中的 setter:

// 在此进行派发更新
set: function reactiveSetter (newVal) {
  // 拿到旧的 value
  const value = getter ? getter.call(obj) : val
  /* eslint-disable no-self-compare */
  if (newVal === value || (newVal !== newVal && value !== value)) {
    return
  }
  // 如果存在用户自定义的 setter 的话,用这个用户自定义的 setter 赋值这个 value
  if (setter) {
    setter.call(obj, newVal)
  } else {
    // 否则就直接将 newVal 赋值给 val
    val = newVal
  }
  // 将新设置值中的 keys 也转换成响应式的
  childOb = !shallow && observe(newVal)
  // 触发依赖的更新
  dep.notify()
}

重写的原型函数:

[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
  // 进行遍历处理
.forEach(function (method) {
  // 缓存原生的相应方法
  const original = arrayProto[method]
  // 定义该 method 对应的自定义方法
  def(arrayMethods, method, function mutator (...args) {
    // 执行原生方法拿到执行结果值,在最后将这个结果值返回
    const result = original.apply(this, args)
    // 这里的 this 是执行当前方法的数组的实例。在 Vue 中,每个数据都会有 __ob__ 属性,这个属性
    // 是 Observer 的实例,该实例有一个 dep 属性(Dep 的实例),该属性能够收集数组的依赖
    const ob = this.__ob__
    // 数组有三种新增数据的方法。分别是:'push','unshift','splice'
    // 这些新增的数据也需要变成响应式的,在这里,使用 inserted 变量记录新增的数据
    let inserted
    switch (method) {
      // 如果当前的方法是 push 或者 unshift 的话,新增的数据就是 args,将 args 设值给 inserted 即可
      case 'push':
      case 'unshift':
        inserted = args
        break
      // 如果当前的方法是 splice 的话,那么插入的数据就是 args.slice(2)
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 如果的确新增了数据的话,将 inserted 作为参数执行 observer.observeArray() 方法,把新增的每个元素都变成响应式的
    if (inserted) ob.observeArray(inserted)
    // 通知 ob.dep 中的依赖
    ob.dep.notify()
    // 在最后,返回 Array 方法执行的结果
    return result
  })
})

我们可以看到,这两种变化侦测的方法中,最后都会执行 dep.notify(),notify 方法的代码如下:

// 触发 subs 数组中依赖的更新操作
notify () {
  // 数组的 slice 函数具有拷贝的作用
  const subs = this.subs.slice()
  // 遍历 subs 数组中的依赖项
  for (let i = 0, l = subs.length; i < l; i++) {
    // 执行依赖项的 update 函数,触发执行依赖
    subs[i].update()
  }
}

notify 方法的内容很简单,就是遍历执行依赖了对应数据 Watcher 实例的 update 方法,我们看下 Watcher 实例中的 update 方法。

export default class Watcher {  
  update () {
    this.run()
  }

  run () {
    if (this.active) {
      const value = this.get()
    }
  }

  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
      }
    }
    // 将 expOrFn 对应的值返回出去
    return value
  }
}

我们可以看到,依次执行了:update() ==> run() ==> get() 方法,在 get 方法的内部会执行 this.getter,通过上面的内容我们知道,这个 this.getter 就是用于组件渲染的 updateComponent() 方法,执行这个方法,就会进行组件的重新渲染。好了,以上就是变化侦测到组件重新渲染的整个流程。

4,下集预告

以上这三篇博客就是与响应式原理有关的内容。接下来,先说说与响应式原理有关 API 的底层原理,然后再讲解模板编译原理相关的内容,敬请期待。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值