数据响应式处理

通过查看源码解决问题

  • vm.msg = {count:0},重新给属性 msg 赋值,是否是响应式的?(是)
  • vm.arr[0] = 4,给数组元素赋值,视图是否会更新?(不是)
  • vm.arr.length = 0, 修改数组的length,视图是否会更新?(不是)
  • vm.arr.push(4),视图是否会更新?(是)

响应式处理的入口

  • 响应式处理入口 在 initState 中
  • initState方法在 src/core/instance/state.js 中实现
  • 方法中重要方法为 observer

    ...
    // 参数:options中的data 参数2表示根数据(根数据额外处理)
    observe(data,true /* asRootData */)
    ...

    解析observe方法

  • observe方法定义在 src/core/observer/index.js
  • 如果当前 data 有 observer 则直接返回,没有给属性创建一个observer,并返回
  • 对 data对象进行判断,如果有 __ob__ 属性,返回 observer。没有,则 new Observer(data) (内部把它变成响应式,注册getter/setter),再返回
  • 在observe中的getter会收集依赖,setter会派发更新 (首次编译会触发getter)
  • Dep.target在watcher类中定义,参考 mountComponent方法,方法中会new Watcher

    export function observe (value: any, asRootData: ?boolean): Observer | void {
    
    // 判断value是否是对象或 VNode实例
    if (!isObject(value) || value instanceof VNode) {
      return
    }
    
    let ob: Observer | void
    
    // 如果 value 有 __ob__(Observer 类的属性) 结束
    // 做过响应式就不用再操作了,相当于加了个缓存
    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
    ) {
      // 创建一个observer对象,
      ob = new Observer(value)
    }
    
    // 根数据data,会额外++标记
    if (asRootData && ob) {
      ob.vmCount++
    }
    
    // 返回一个Obserrver类
    return ob 
    }

    解析Observer类

  • 内部会给 value 数据定义 __ob__ 属性 ,值为 Observer,且不可枚举。
  • 初始化 Dep 实例,每个Observer对应一个 Dep实例
  • 将来遍历 value,给它内部所有属性设置getter,setter时不考虑 此属性。

    // this 指 Observer
    def(value, '__ob__', this)
  • 判断 value 数据类型。如果数组,做数组响应式处理。对象则调用 walk 方法遍历对象属性添加 getter,setter,并依赖注入和派发更新。

    if (Array.isArray(value)) {
    
    // 当前浏览器是否支持对象原型 __proto__
    // 修补数组中的原型方法,如果数组发生变化调用 notify更新视图
    if (hasProto) {
      protoAugment(value, arrayMethods)
    } else {
      copyAugment(value, arrayMethods, arrayKeys)
    }
    
    // 遍历数组所有元素,是对象的元素会转化为响应式对象,而非所有属性!!!!
    // 下标和length是属性,无法通过 vm.msg[0]更新,因为性能问题
    // 可以使用 vm.msg.splice(0,1,100) 代替
    this.observeArray(value)
    
    } else {
    // 遍历对象中的每个属性,转换成 setter/getter
    this.walk(value)
    }

    defineReactive方法解析

  • walk 方法内部会调用 用来给对象属性做响应式处理的
  • 方法中的 shallow 参数,true 表示浅层响应,非浅层需要递归设置属性响应式
  • 给对象属性创建一个 Dep实例,用于在get 方法中收集依赖, set 方法中派发更新
  • 在对象属性劫持的 get,和set 中,会判断用户是否给属性设置了getter/setter,如果设置了,也会调用。

set依赖收集解析

  • Dep.target 静态对象 存在则把 Watcher 添加到 dep的 subs数组中

    get: function reactiveGetter () {
    
    const value = getter ? getter.call(obj) : val
    
    // 如果存在当前依赖目标,即watcher 对象,则建立依赖
    if (Dep.target) {
    
      // dep是为当前属性收集依赖的对象。收集当前的watcher对象到当前属性的dep的subs数组中
      dep.depend()
    
      // 给当前 属性对应的值 收集依赖,如果值是对象,则调用值对应Observer的dep
      // 这里的dep是为递归的值observer收集依赖,与上面的dep不同
      // 例如 data:{ msg:{name:"zhangsan"} },这里给 name添加依赖
      if (childOb) {
        childOb.dep.depend()
    
        // 如果属性值是数组,则特殊处理收集数组对象依赖 例如 data:{arr:[1,2,3]} 此方法处理 [1,2,3]
        // 外层处理 arr
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
    }
    return value
    }
    let childOb = !shallow && observe(val)

    Dep.target是哪里赋值的?

  • 在首次渲染时候 ,new Watcher内部赋值的,内部的 pushTarget 赋值的
  • Dep.target 全局唯一,一个组件对应一个watcher,一个watcher对应一个Dep.target
  • 一个组件挂载时会创建一个watcher,同时赋值给 Dep.target
  • push到栈是因为,如果有组件嵌套,(则watcher嵌套,先把父watcher放入栈中)执行子watcher,执行完成从栈中弹出,执行父组件渲染

    Dep.target = null
    const targetStack = []
    
    export function pushTarget (target: ?Watcher) {
    // 为什么要入栈?
    // 每个组件对应watcher对象(每个组件有个mountComponent)
    // 如果 a组件嵌套b组件,渲染按时发现有子组件,就先渲染b子组件,a组件的渲染过程先被被挂载起来
    // a组件对应的watcher对象被存储在栈中,(栈特点先进后出)
    // 当b子组件渲染完成会从栈中弹出,然后执行父组件渲染
    
    targetStack.push(target)
    Dep.target = target
    }
    
    export function popTarget () {
    // 出栈操作
    targetStack.pop()
    Dep.target = targetStack[targetStack.length - 1]
    }

    这里赋值了,哪里触发的数据劫持的 get方法呢?

  • 首次渲染在给 Dep.target 赋值过后会调用 updateComponent 方法,生成虚拟dom更新视图

    updateComponent = () => {
    
    // _render 或获取虚拟dom  _update将虚拟dom转为真实dom
    vm._update(vm._render(), hydrating)
    }
  • 在 _render方法中,会触发 数据劫持的 get 方法,可以调试一下
  • 渲染相关方法:_c 方法是createElement方法h函数,_s方法是toString(),_v方法是创建文本虚拟节点createTextVNode
  • 如图访问 msg 属性会触发数据劫持中的 get方法,从而在 get 方法内收集依赖,收集 msg 属性的Dep到当前组件的watcher 内定义的 dep 的 subs 数组中,如果收集过则不重复收集 (就是 data中的msg在视图中使用两次,只会收集一次依赖)
  • 因此, get中首次依赖收集,是在首次编译时触发的!

image.png


mcgee0731
60 声望4 粉丝

不会做饭的程序猿不是一个好厨子