浅析Vue的响应式原理对象篇

  Vue能数据驱动视图发生变更的关键,就是依赖它的响应式系统。响应式系统如果根据数据类型区分,对象和数组它们的实现会有所不同;解释响应式原理,如果只是为了说明响应式原理而说,但不是从整体流程出发,不在Vue组件化的整体流程中找到响应式原理的位置,对深刻理解响应式原理并不太好

一、对象响应式数据的创建

  在组件的初始化阶段,将对传入的状态进行初始化,以下以data为例,会将传入的数据包装为响应式的数据。

// main.js
new Vue({  // 根组件
  render: h => h(App)
})
// app.vue
<template>
  <div>{{info.name}}</div>  // 只用了info.name属性
</template>
export default {
  data() {
    return {
      info: {
        name: 'cc',
        sex: 'man'  // 即使是响应式数据,没被使用就不会进行依赖收集
      }
    }
  }
}

  我们将以上面代码为例分析,这种结构其实是一个嵌套组件,只不过根组件一般定义的参数比较少而已,理解这个还是很重要的。

  在组件new Vue()后的执行vm._init()初始化过程中,当执行到initState(vm)时就会对内部使用到的一些状态,如propsdatacomputedwatchmethods分别进行初始化,再对data进行初始化的最后有这么一句:

function initData(vm) {  //初始化data
  ...
  observe(data) //  info:{name:'cc',sex:'man'}
}

  这个observe就是将用户定义的data变成响应式的数据,接下来看下它的创建过程

export function observe(value) {
  if(!isObject(value)) {  // 不是数组或对象,return
    return
  }
  return new Observer(value)
}

  简单理解这个observe方法就是Observer这个类的工厂方法,所以还是要看下Observer这个类的定义

export class Observer {
  constructor(value) {
    this.value = value
    this.walk(value)  // 遍历value
  }
  walk(obj) {
    const keys = Object.keys(obj)
    for(let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])  // 只传入了两个参数
    }
  }
}

  当执行new Observer时,首先将传入的对象挂载到当前this下,然后遍历当前对象的每一项,执行defineReactive这个方法,看下它的定义:

export function defineReactive(obj, key, val) {
  const dep = new Dep()  // 依赖管理器
val = obj[key] // 计算出对应key的值 observe(val) // 递归包装对象的嵌套属性

Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { ... 收集依赖 }, set(newVal) { ... 派发更新 } }) }
  这个方法的作用就是使用Object.defineProperty创建响应式数据。首先根据传入的objkey计算出val具体的值;如果val还是对象,那就使用observe方法进行递归创建(如果不是就直接return了),在递归的过程中使用Object.defineProperty将对象的每一个属性都变成响应式数据
data() {
  return {
    info: {
      name: 'cc',
      sex: 'man'
    } 
  }
}
// 这段代码就会有三个响应式数据:
// info, info.name, info.sex
  知识点:Object.defineProperty内的get方法,它的作用就是谁访问到当前key的值就用defineReactive内的dep将它收集起来,也就是依赖收集的意思。set方法的作用就是当前key的值被赋值了,就通知dep内收集到的依赖项,key的值发生了变更,视图请变更吧

  这个时候getset只是定义了,并不会触发。什么是依赖我们接下来说明,首先还是用一张图帮大家理清响应式数据的创建过程

二、依赖收集

  什么是依赖呢?我们看下mountComponent的定义

function mountComponent(vm, el) {
  ...
  const updateComponent = function() {
    vm._update(vm._render())
  }
  
  new Watcher(vm, updateComponent, noop, {  // 渲染watcher
    ...
  }, true)  // true为标志,表示是否是渲染watcher
  ...
}

  我们首先说明下这个Watcher类,它类似与之前的VNode类,根据传入的参数不同,可以分别实例化出三种不同的Watcher实例,它们分别是用户watcher,计算watcher以及渲染watcher

1、用户(user) watcher:也就是用户自己定义的

2、计算(computed) watcher:是当定义计算属性实例化出来的一种

3、渲染(render) watcher:只是用做视图渲染而定义的Watcher实例,再组件执行vm.$mount的最后会实例化Watcher类,这个时候就是以渲染watcher的格式定义的,收集的就是当前渲染watcher的实例

  只要一执行就会执行当前组件实例上的vm._update(vm._render())render函数转为VNode,这个时候如果render函数内有使用到data中已经转为了响应式的数据,就会触发get方法进行依赖的收集,补全之前依赖收集的逻辑:

export function defineReactive(obj, key, val) {
  const dep = new Dep()  // 依赖管理器
  
  val = obj[key]  // 计算出对应key的值
  observe(val)  // 递归的转化对象的嵌套属性
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {  // 触发依赖收集
      if(Dep.target) {  // 之前赋值的当前watcher实例
        dep.depend()  // 收集起来,放入到上面的dep依赖管理器内
        ...
      }
      return val
    },
    set(newVal) {
      ... 派发更新
    }
  })
}

  这时我们知道watcher是个什么东西了,简单理解就是数据和组件之间一个通信工具的封装,当某个数据被组件读取时,就将依赖数据的组件使用Dep这个类给收集起来。

  当前例子data内的属性是只有一个渲染watcher的,因为没有被其他组件所使用。但如果该属性被其他组件使用到,也会将使用它的组件收集起来,例如作为了props传递给了子组件,再dep的数组内就会存在多个渲染watcher

三、派发更新

  如果只是收集依赖,那其实是没任何意义的,将收集到的依赖在数据发生变化时通知到并引起视图变化,这样才有意义。如现在我们对数据重新赋值,这个时候就会触发创建响应式数据时的set方法了,我们再补全那里的逻辑

export function defineReactive(obj, key, val) {
  const dep = new Dep()  // 依赖管理器
  
  val = obj[key]  // 计算出对应key的值
  observe(val)  // 递归转化对象的嵌套属性
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      ... 依赖收集
    },
    set(newVal) {  // 派发更新
      if(newVal === val) {  // 相同
        return
      }
      val = newVal  // 赋值
      observer(newVal)  // 如果新值是对象也递归包装
      dep.notify()  // 通知更新
    }
  })
}

  当赋值触发set时,首先会检测新值和旧值,不能相同;然后将新值赋值给旧值;如果新值是对象则将它变成响应式的;最后让对应属性的依赖管理器使用dep.notify发出更新视图的通知。我们看下它的实现

let uid = 0
class Dep{
  constructor() {
    this.id = uid++
    this.subs = []
  }
  
  notify() {  // 通知
    const subs = this.subs.slice()
    for(let i = 0, i < subs.length; i++) {
      subs[i].update()  // 挨个触发watcher的update方法
    }
  }
}

  这里做的事情只有一件,将收集起来的watcher挨个遍历触发update方法:

class Watcher{
  ...
  update() {
    queueWatcher(this)
  }
}

const queue = []
let has = {}

function queueWatcher(watcher) {
  const id = watcher.id
  if(has[id] == null) {  // 如果某个watcher没有被推入队列
    ...
    has[id] = true  // 已经推入
    queue.push(watcher)  // 推入到队列
  }
  ...
  nextTick(flushSchedulerQueue)  // 下一个tick更新
}
  执行update方法时将当前watcher实例传入到定义的queueWatcher方法内,这个方法的作用是把将要执行更新的watcher收集到一个队列queue之内,保证如果同一个watcher内触发了多次更新,只会更新一次对应的watcher
  详见文章:https://juejin.cn/post/6844903911669628941
posted @ 2018-03-28 20:14  古兰精  阅读(3792)  评论(0编辑  收藏  举报