vue如何做响应式数组 | 青训营

61 阅读4分钟

1、找到响应式处理入口

进入页面,Vue开始初始化,执行new Vue(),进入到Vue的构造函数中,在构造函数中执行了_init()方法。

_init()这个方法中,调用了initState()方法 => 在initState()方法中,调用了initData()方法 => 在initData()方法中,调用了observe()方法。

observe()方法就是Vue进行响应式处理的入口。

2、找到数组响应式处理入口

在observe()方法中,创建了Observer实例,在创建Observer实例的过程中,对传入的数据进行了判断,如果是数组,单独对数组进行处理,如果不是数组,调用walk方法对数据进行响应式处理。

constructor (value: any) {
    // 如果是数组,对数组进行特殊处理
    if (Array.isArray(value)) {
      // 判断浏览器是否支持原型 __proto__ 下面两个方法实质处理方式是一样的
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 遍历数组的每一项,调用observe方法对数组的每一项以递归的方式进行响应式处理
      this.observeArray(value)
    } else {
      this.walk(value)
    }
 }

3、对数组进行处理

这里假设浏览器支持__proto__原型,进入到protoAugment方法中,这个方法很简单, target.__proto__ = src 就是将数组的原型指向arrayMethods。我们主要关心的就是arrayMethods,这也是对数组处理的核心部分。

const arrayProto = Array.prototype
// arrayMethods 的原型就是数组的原型
export const arrayMethods = Object.create(arrayProto)
// 数组中会修改自身的7个方法
const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse']
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  // 通过def方法将数组中会修改自身的方法进行重写
  def(arrayMethods, method, function mutator (...args) {
    // 实质还是调用数组原型上面的方法
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 如果数组中增加了数据,遍历新增的数据进行响应式处理
    if (inserted) ob.observeArray(inserted)
    // notify change 数组改变后调用 notify方法 通知 Watcher 去更新视图
    ob.dep.notify()
    return result
  })
})

从源码上很容易可以看出,vue对数组的响应式处理,其实质还是调用了数组原型上的方法,在数组发生变化后调用了notify去派发更新。

4、几个需要注意的点

1> ES6新增的fill方法、copyWithin方法也会改变数组自身,不会触发视图更新

2> 使用数组索引的方式修改数据,不会触发视图更新

3> 修改数组的length属性,不会触发视图更新

vue依赖收集的具体时机

1、首先,了解下vue生命周期函数

vue生命周期分为beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed

2、从源码层面走个流程找到依赖收集的具体时机

1> 进入页面,Vue开始初始化,执行new Vue(),进入到Vue的构造函数中,在构造函数中执行了_init()方法。

2> 在_init()这个方法里,首先初始化绑定事件和生命周期钩子,然后调用 beforeCreate 这个钩子函数,在这个钩子函数中还没有初始化数据,所以在这个钩子函数中一般不进行操作。

3> 紧接着进行props、data、methods、computed、watch等的初始化,这个过程中已经将数据转换为了响应式数据。紧接着调用了 created 这个钩子函数,在这个钩子函数中已经可以拿到数据,我们可以在这个钩子函数中向后端发起请求,异步获取到数据,这个时候修改数据不会调用update函数,也不会触发其他生命周期钩子。

// vm 的声明周期相关变量初始化
// $children/$parent/$root/$refs
initLifecycle(vm)
// vm 的事件监听初始化,父组件绑定在当前组件上的事件
initEvents(vm)
// $slots/$scopedSlots/_c/$createElement/$attress/$listeners
initRender(vm)
// 生命钩子的回调
callHook(vm, 'beforeCreate')
// 把 inject 的成员注入到 vm 上
initInjections(vm) // resolve injections before data/props
// 初始化 vm 的 _props/methods/_data/computed/watch
initState(vm)
//  初始化 provide
initProvide(vm) // resolve provide after data/props
// 生命钩子的回调
callHook(vm, 'created')
// 紧接着调用$mount函数
if (vm.$options.el) {
    vm.$mount(vm.$options.el)
}

4> 紧接着调用mount函数,在mount函数,在mount函数中将 template/el 转化成 render 函数,准备渲染。然后调用了mountComponent,在mountComponent中,首先调用了beforeMount钩子,在这个钩子中,模板已经编译好了,还没有转为真实DOM挂载到页面,这个钩子函数中也可以请求数据,修改数据也不会触发updata函数以及其他生命周期钩子函数。

5> 紧接着定义了updateComponent,给updateComponent赋值。然后初始化Watcher实例,并将updateComponent作为参数传入Watcher,在Watcher构造器中,判断了expOrFn(即updateComponent参数)是否是函数,如果是函数,赋给this.getter,然后调用了this.get()方法

callHook(vm, 'beforeMount')
let updateComponent
updateComponent = () => {
    vm._update(vm._render(), hydrating)
}
// updateComponent 作为回调函数cb
new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
}, true /* isRenderWatcher */)

6> 重点来了,依赖收集就是发生在这个get方法中,在beforeMount钩子函数之后。在get方法中,首先调用pushTarget()方法将当前Watcher实例入栈,并设置Dep.target = Watcher实例(启用依赖收集)。然后调用this.getter(),即调用updateComponent这个回调函数,在这个回调函数中,首先调用_render()方法将虚拟DOM渲染为真实DOM,然后调用_render()方法将真实DOM挂载到页面上。在这个过程中,触发了c方法,v方法,s方法,会访问到页面显示所依赖的数据,触发getter,然后判断Dep.target是否存在,我们在pushTarget中已经启用了依赖收集,所以这个时候就会通过判断,执行depend方法,调用Watcher的addDep方法,在addDep方法中,首先获取dep的id,然后判断newDepIds数组中是否存在这个id,防止重复收集依赖。如果不存在,将dep.id存到newDepIds数组中,并将这个Watcher实例增加到dep的subs数组中,依赖收集完成。接着将真实DOM挂载到页面上,触发mounted钩子函数。

constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    // options
    if (options) {
      this.lazy = !!options.lazy
    }
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    }
    this.value = this.lazy
      ? undefined
      : this.get()
}
get () {
    // 开启依赖收集
    pushTarget(this)
    const vm = this.vm
    try {
      // 调用 updateComponent 期间访问到依赖数据触发getter进行依赖收集
      value = this.getter.call(vm, vm)
    } catch (e) {
      
    } finally {
      // 当前组件依赖收集完毕,如果有父组件,继续收集父组件依赖
      popTarget()
      this.cleanupDeps()
    }
    return value
}

7> 当数据更新后,就会立即执行beforeUpdate钩子函数,然后 Vue 的虚拟 dom 机制会重新构建虚拟 dom 与上一次的虚拟 dom 树利用 diff 算法进行对比之后重新渲染,一般不做什么事 。

8> 当更新完成后,执行 updated 钩子函数,数据已经更改完成,dom 也重新 render 完成,可以操作更新后的虚拟 dom 。

9> 当经过某种途径调用$destroy 方法后,立即执行 beforeDestroy 钩子函数,一般在这里做一些善后工作,例如清除计时器、清除绑定的事件等等。组件的数据绑定、监听...去掉后只剩下 dom 空壳,这个时候,执行 destroyed 钩子函数,在这里做善后工作也可以 。

最后:

青训营即将结束,我在此祝愿青训营的小伙伴们都能实现自己的愿望,诸事如意。