响应式数据

文章详细介绍了Vue.js中实现数据响应式的机制,包括Dep如何作为被观察者存储观察者Watcher,Watcher如何进行双向数据收集,以及Observe如何初始化和观测数据。特别提到了数组的响应式处理,通过重写数组方法来确保页面更新。
摘要由CSDN通过智能技术生成


在开始分析之前,先了解一下Dep,Watcher,Observe 这三个类的关系和作用

Dep

它在整个数据响应式的过程当中充当了被观察者的作用,每一个数据都有一个dep实例,dep里面有一个subs数组,用来存放收集了它的Watcher,当这一个数据发生变化时,遍历subs数组, 通知收集过它的Watcher重新渲染页面或重新求值

Dep.target具有全局唯一性,指向的是当前使用的 Watcher实例

class Dep {
    
    constructor() {
        this.id = id++
        // 如果多个组件都使用到了这个数据,则分别存放对应组件的Watcher
        this.subs = [] // 用来存放watcher
    }
    depend() {
    	// Dep.target == 此时正在使用的Watcher
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    }
    addSub(watcher) {
        this.subs.push(watcher)
    }
    notify() {
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}
Dep.target = null

Watcher

它在整个数据响应式的过程当中充当了观察者的作用,一共有2种Watcher

  1. 每个组件都有一个对应的渲染用的Watcher
  2. computed用于惰性求值的Watcher(不渲染页面)

每一个Watcher内部都有一个deps 数组,保存着这个Watcher观察的所有Dep。

class Watcher {
    constructor(vm, exprOrFn, cb, options) {
        this.id = id++ // 给watcher添加标识       
        this.getter = exprOrFn
        this.deps = []
        this.get()
    }
    get() {
    	// getter 其实就是调用了 vm.update(vm.render()) 渲染页面
        this.getter()
    }
    update() {
    	// 更新视图时调用update,其本身依然调用的是 vm.update(vm.render())
        this.get()
    }
    addDep(dep) {
    	// 收集需要被观察的dep,一个数据在多处被使用时,只要收集一次即可
    	// 把这个Watcher加入到dep的subs数组中
        let id = dep.id
        if (!this.depsId.has(id)) {
            this.depsId.add(id)
            this.deps.push(dep)
            dep.addSub(this)
        }
    }
}

observe

观测数据的入口, 主要功能就是检测属性是否带有 __ob__确认是否已经被观测过,否则的话就调用Obserber进一步处理

function observe (value, asRootData) {
  // 非对象类型或者value为VNode 不做处理
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  // 如果属性带有 __ob__ ,就说明该属性已经被观测过,就不再处理
  let ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    // 不是服务端渲染,不是Vue实例 
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 观测数据
    ob = new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob
}

Observer

在这里检测类型是数组还是对象,分别采用不同的处理方法处理数据

class Observer {
  constructor (value) {
    // 初始化一个Dep实例
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    // 在当前观测的对象上添加一个__ob__属性,值就是 Observer 实例
    def(value, '__ob__', this);
    // 数组类型的处理
    if (Array.isArray(value)) {
      if (hasProto) {
      	// 将已经重写过方法的数组原型设置为当前对象的原型
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      // 遍历数组的每个元素,依次调用 observe
      this.observeArray(value);
    } else {
    // 对象类型的处理
      this.walk(value);
    }
  }
  walk (obj) {
    // 遍历对象逐个添加劫持方法
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      defineReactive$$1(obj, keys[i]);
    }
  }
  // 当为数组时,遍历每一项依次调用 observe ,重复上面的步骤
  observeArray (items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  }
}

defineReactive$$1

在这里为数据设置 seter 、getter , 添加数据劫持

function defineReactive$$1 (obj,key,val,customSetter,shallow) {
  var dep = new Dep();
  // 读取当前属性的描述符
  var property = Object.getOwnPropertyDescriptor(obj, key);
  // 如果当前的属性还是对象,则继续调用observe重复上面的步骤
  var childOb = !shallow && observe(val);

  var getter = property && property.get;
  var setter = property && property.set;
  	
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 添加数据劫持 get
    get: function reactiveGetter () {},
    // 添加数据劫持 set
    set: function reactiveSetter (newVal) {}
  });
}

组件在调用render创建VNode的过程中会读取属性, 属性在 mergeOptions 阶段已经被代理到实例上,所以可以通过vm.xx读取属性, 由于在前面 defineReactive 阶段为属性添加了劫持方法get,此时就会开始收集"被观测者"

// 例:
<TestComponent :name="fullName" />

// 该组件对应的render函数
return _c("TestComponent",{attrs:{name: _vm.fullName}})

get

读取属性时,在defineReactive中设置的get函数就会被调用, 它通过 addDep 函数完成双向的数据收集。

// 每一个数据都有一个 dep
var dep = new Dep()
get: function(){
	// 在 mergeOptions 合并选项时,data可能是一个闭包函数 
	var value = getter ? getter.call(obj) : val;
     if (Dep.target) {
       // dep.depend(); 这是源码,为了便于理解把调用的代码改动一下
       Dep.target.addDep(dep)
     }
     return value
}
Watcher.prototype.addDep 双向收集
  • 每个dep实例都有一个id,检查是否已被该Watcher收集过,无需重复收集
  • 将新的 dep 收集到 Watcher.newDeps
  • 将当前的 Watcher 添加到 dep.subs
Watcher.prototype.addDep = function(dep){
	var id = dep.id
	if(!this.newDepIds.has(id)) {
		this.newDepIds.add(id)
		this.newDeps.push(dep)
		if(!this.depIds.has(id)) {
			// dep.addSub(this) 此为源码,为了便于理解,替换为实际调用的代码
			dep.subs.push(this)
		}
	}
}

set

数据变更时,当新值和旧值不一致就调用dep.notify 通知之前收集的Watcher

var dep = new Dep()
set: function reactiveSetter (newVal) {
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      dep.notify()
}
dep.notify 更新通知

取出当前dep收集的所有Watcher,依次遍历调用update将自身推入更新队列,再通过nextTick下一次更新视图时,更新页面的数据

notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // Watcher.update()
    }
}

数组的响应式

以 Array.prototype 为原型创建一个对象,重写其中的 [push,pop,shift,unshift,splice,sort,reverse] 方法,这些方法均会触发页面更新。当在Observer中检测到当前的对象为数组类型时,把此对象设置为原型

const arrayProto = Array.prototype
// 创建一个新的对象,原型就是数组的prototype,后面重写的方法就添加到该对象上
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

// 遍历即将重写的方法
methodsToPatch.forEach(function (method) {
  // 取出数组原型上对应的方法	
  const original = arrayProto[method]
  // 这些方法逐个添加到 arrayMethods 上
  def(arrayMethods, method, function mutator (...args) {
  	// 解构的 args 为一个数组
  	// 调用原型上的方法
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      // push , unshift 的时候属于新增元素	
      case 'push':
      case 'unshift':
        inserted = args
        break
      // splice 第三个参数也是新增  
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 遍历 args 为每一项新增的元素设置响应式
    if (inserted) ob.observeArray(inserted)
    // 通知页面更新
    ob.dep.notify()
    return result
  })
})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值