欢迎光临建站系统网站,我们的服务范围是一键建站,公司建站等。

建站系统

一个高端新的网络集成营销平台

学习vue2.5源码之第三篇——数据的双向绑定(数据响应式原理)

作者:jcmp      发布时间:2021-05-08      浏览量:0
数据的双向绑定(数据观测)上一章我们在学

数据的双向绑定(数据观测)

上一章我们在学习 initState 的时候发现初始化的时候, initProps 和 initData 都涉及到 observer 文件夹中的一些方法( defineReactive 和 observe ),以及 initComputed 和 initWatch 也涉及到 Watcher 对象和 $watch 方法等等,这些都有是和数据的双向绑定(也叫数据观察)有关,这一章我们就来学习一下vue关于数据双向绑定的代码。核心代码主要是在 src/core/observer 文件夹中。

一些概念

vue是由 render 函数来生成虚拟dom,再映射到页面上。 Observer 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter ,通过 Dep 达到追踪依赖的效果。当我们访问属性时会添加依赖,当我们修改属性时将会通知 Watcher 重新计算,从而致使它关联的组件得以更新。

observe

还记得 initData 是通过 observe() 作为入口进行数据绑定的吗,我们就从这里开始看起吧~先看一下这个函数:

export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( observerState.shouldConvert && // 为真时,改变后的新值也会进行双向绑定 !isServerRendering() && // 非服务端渲染 (Array.isArray(value) || isPlainObject(value)) && // 是一个数组 Object.isExtensible(value) && // 或者是一个单纯的可拓展的对象 !value._isVue ) { ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ // 以此对象作为根节点$data的vue实例数量加一 } return ob}

第一个参数是一个data对象,假如它拥有 __ob__ 属性的话证明该对象已经进行绑定了一个observer,就不用再次创建了;假如没有 __ob__ 属性,而且满足以上条件(注释中有标明)的时候,我们将为它创建一个Observer对象。 ob = new Observer(value) ,并且执行 vmCount++ 。我们转到 Observer 看看:

Observer

export class Observer { value: any; dep: Dep; vmCount: number; constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 // 定义__ob__属性 def(value, '__ob__', this) // 若是数组对象则另作处理(关于数组原型方法的数据监听) if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { // 其他途径走walk() this.walk(value) } } ... }

每一个Observer对应一个Dep依赖,并且为该数据对象添加 __ob__ 属性,值为这个Observer,之前在observe函数中可以看到假如已经存在 __ob__ 属性就不需要再次创建Observer对象了。

接下来要判断传入的值是否是一个数组,假如value不是一个数组而是一个普通对象,那么执行 walk() 函数。

walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) }}

walk() 中遍历对象对每个属性执行 defineReactive ,我们又看看 defineReactive。

defineReactive (划重点)

export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } const getter = property && property.get const setter = property && property.set let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val if (newVal === value || (newVal !== newVal && value !== value)) { return } if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } })}

该方法先是为每个属性创建一个Dep,这样做的原因是每个属性对应自己的依赖集,当有属性发生变化时只触发相应属性的更新而不用更新全部属性,导致资源的浪费。

vue允许用户自己定义getter和setter,假如用户自己设置了的话,vue就使用用户所定义的getter和setter。

现在重头戏来了,vue的数据双向绑定就是通过 Object.defineProperty 这个原生的方法实现的, Object.defineProperty 是 ES5.1 规范中提供的方法,用来修改对象属性的属性描述对象,通过 Object.defineProperty 函数,我们可以通过定义对象属性的存取器(getter/setter)来劫持数据的读取,实现数据变动时的通知功能。

回到源码中来,通过 Object.defineProperty 我们重写了对象的属性描述符,在getter中添加 dep.depend() ,让数据和watcher建立关系,并且对子对象的observer也进行依赖绑定 childOb.dep.depend() ,如果数组元素也是对象,那么它们的observe过程也生成了 __ob__ 实例,那么就让 __ob__ 也收集依赖。

在setter中若新老值有变化的话就添加 dep.notify 来通知watcher更新数据,并监听新的值 childOb = !shallow && observe(newVal) , customSetter 在开发过程中用于输出错误。

注意, defineReactive 中的 dep 和 Observer 构造函数的 dep 是不同的,原因是getter和setter只能监听到属性的更改,不能监听到属性的删除与添加,所以 Observer 构造函数的 dep 会在vue的 set 和 delete 操作中会利用上。

看回 Observer 构造函数中的第二种情况,假如value是一个数组对象,会做另外的处理。由于传进来的数组对象只是一个引用地址,对于数组一些原有的操作(push、splice、sort等)是无法进行数据监听的,所以vue中有两种操作:1,在环境支持 __proto__ 时,执行protoAugment,改写value的 __proto__ ;2,不支持 __proto__ 时,执行copyAugment,直接在value上重新定义这些方法。

function protoAugment (target, src: Object, keys: any) { target.__proto__ = src}function copyAugment (target: Object, src: Object, keys: Array) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) }}

重点是在于来自同级文件array.js的 arrayMethods ,它是一个改写了数组方法的对象。

import { def } from '../util/index'const arrayProto = Array.prototypeexport const arrayMethods = Object.create(arrayProto);[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) { const original = arrayProto[method] 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) // 添加数据更新 ob.dep.notify() return result })})。

特别对push、unshift、splice参数大于2时的三种情况进行调用 ob.observeArray ,因为他们都添加了新元素需要进行监听,然后给所有的原型方法都添加了数据更新 ob.dep.notify() 。然后对原型方法进行重新定义后,遍历这个数组为每个元素执行 observe ,监听新插入的值。

observeArray (items: Array) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) }}

Dep

前面提及了好几次dep中的方法,而且我们也提到了dep的基本概念,就是联系Observer和Watcher的依赖收集器,我们现在看一下Dep对象。

export default class Dep { static target: ?Watcher; id: number; subs: Array; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }}

dep中有个静态属性 target 指向目标Watcher, id 作为Dep的唯一标识, subs 是用于存放watcher的数组。 addSub 向数组添加watcher, removeSub 从数组中删除watcher, depend 是在getter中为目标Watcher添加依赖,调用了watcher的addDep(稍后分析), notify 在setter时触发watcher的更新。

Watcher

let uid = 0export default class Watcher { constructor ( ) { this.vm = vm vm._watchers.push(this) this.id = ++uid this.cb = cb this.getter = expOrFn this.value = this.get() } get () let value const vm = this.vm value = this.getter.call(vm, vm) return value } update () { this.run() } run () { const value = this.get() this.cb.call(this.vm, value, oldValue) }}

emmm...我们直接看一下超级无敌简化版的watcher,走一下最常见的流程,例如最简单的: vm.$watch("mydata",()=>console.log("我是回调")) 。主要是看传来的监听表达式/函数 expOrFn (mydata)和响应回调函数 cb (()=>console.log("我是回调")),大概流程就是observer监听到属性值更改后(setter)调用 dep.notify 通知watcher更新 update , update 中调用了函数 run , run 通过 get 获取到监听的值 expOrFn ,最后调用回调函数cb进行响应式更新(大概思路:update => run => get => cb)。

现在我们再具体看看那几个函数

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 } } finally { if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value}

来个小插曲,什么是dep.target呢?它是Watcher实例,为什么要放在Dep.target里呢?是因为在getter中会执行 dep.depend() ,执行 Dep.target.addDep(this) ,而在 addDep 中通过 dep.addSub(this) 又把Watcher实例添加到dep的观察集中。

// Dep.jsdepend () { if (Dep.target) { Dep.target.addDep(this) }}

// Watcher.jsaddDep (dep: Dep) { const 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.jsaddSub (sub: Watcher) { this.subs.push(sub)}

看回 get() 函数,先是执行 pushTarget(this) ,将这个watcher设为target,对传来的表达式expOrFn进行一定的运算得到value,这个过程触发target相关依赖的getter(例如表达式为‘a+b’的话就会触发a和b的getter),也就是说会把target加到各自的dep.subs中,也就是成功地绑定了所有相关的依赖,完成操作后再执行 popTarget() 将target又恢复为null。每次进行数据监听都会设定当前watcher为新的target,完成更新后又会将target设为空值,这样数据之间就不会互相污染。

update && run

update () { if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { // 异步队列 queueWatcher(this) }}run () { if (this.active) { const value = this.get() if ( value !== this.value || isObject(value) || this.deep ) { const oldValue = this.value this.value = value if (this.user) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } }}

当属性值发生变化时触发setter,就会执行 dep.notify ,然后调用了watcher的 update ,在 update 中有两种情况,同步的话就执行 run ,异步的话执行 queueWatcher 。后者我们稍后再讨论,先看 run ,若新旧值不同的话就执行回调函数 this.cb.call(this.vm, value, oldValue) 。只有在用户设定了 sync:true 时才会直接执行数据更新,否则的话都会走另外一条路:异步更新队列。

sync && immediate

说到sync,再说说immediate,设置 immediate:true 也能直接执行回调,但是和sync是不一样的。代码实现在src/core/instance/state.js中有提及:

if (options.immediate) { cb.call(vm, watcher.value)}

区别在于,当指定 immediate:true 时,会立即以表达式的当前值触发回调函数,不会等到数据发生变化时才进行回调;而指定 sync:true 时是数据发生变化时不将watcher放进异步队列中而是立即执行回调。

queueWatcher

简单来说异步更新队列就是开启一个队列,将一个事件循环内的全部数据变动缓冲在其中,并对 watcher 做去重操作。在下一个 tick 中我们再将队列中的 watcher 依次取出并执行更新。这对避免不必要的计算和DOM操作非常重要。

export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } if (!waiting) { waiting = true nextTick(flushSchedulerQueue) } }}

flushing 变量是正在刷新,当页面不在刷新时,把 watcher push到一个队列尾部,若正在刷新的话则把watcher放到队列相应的位置中(根据id); waiting 变量是正在等待,假如没有处于等待状态的话则等到下一个 nexttick 中执行 flushSchedulerQueue 。

flushSchedulerQueue

function flushSchedulerQueue () { flushing = true let watcher, id queue.sort((a, b) => a.id - b.id) for (index = 0; index < queue.length; index++) { watcher = queue[index] id = watcher.id has[id] = null watcher.run() } const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() resetSchedulerState() callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue) if (devtools && config.devtools) { devtools.emit('flush') }}

此函数将队列中的 watcher 根据它们的id从小到大排列依次执行它们的 run 方法,完成后 resetSchedulerState 重置 flushing , waiting 等状态,再 callUpdatedHooks(updatedQueue)。

callUpdatedHooks

function callUpdatedHooks (queue) { let i = queue.length while (i--) { const watcher = queue[i] const vm = watcher.vm if (vm._watcher === watcher && vm._isMounted) { callHook(vm, 'updated') } }}

vm._watcher保存的是渲染模板时创建的watcher,如果队列中有该watcher,则说明模板有变化,调用 updated 生命钩子。

在vue官网中对异步更新队列的解释是

set && del

前面提到了使用 defineReactive 进行数据监听有个缺陷,就是当属性进行删除和新增时,getter和setter并不会触发,所以vue为了弥补这个缺陷,提供Vue.set和Vue.delete方法让我们设置和删除对象的属性。

export function set (target: Array | Object, key: any, val: any): any { if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) target.splice(key, 1, val) return val } if (key in target && !(key in Object.prototype)) { target[key] = val return val } // Observer构造函数中定义的dep const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== `production` && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val } if (!ob) { target[key] = val return val } defineReactive(ob.value, key, val) ob.dep.notify() return val}export function del (target: Array | Object, key: any) { if (Array.isArray(target) && isValidArrayIndex(key)) { target.splice(key, 1) return } // Observer构造函数中定义的dep const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid deleting properties on a Vue instance or its root $data ' + '- just set it to null.' ) return } if (!hasOwn(target, key)) { return } delete target[key] if (!ob) { return } ob.dep.notify()}

还记得 Observer 构造函数中定义了 dep 吗,这是为每个数组和对象都创建另一个依赖收集器,挂在它们的 __ob__ 属性上。在依赖收集阶段, watcher 将被同时注册在这两个 dep 上,准备接收响应。 setter 操作,通知 set/get 中的 dep ;非 setter 操作,如对象 key 添加删除、数组变异方法调用,通知 __ob__ 中的 dep 。

deep

当我们设置 deep: true 时即进行深度监听。举个例子,有这么个对象 {a:{b:{c:{d:1}}}} ,当我们访问 a.b.c 的值时,依次触发了 a.b 和 a.b.c 的 getter ,将 watcher 注册进对应的 dep ,所以当我们修改 a.b.c 上游的值时 a.b.c 会监测到发生改变;但是相反,当我们修改 a.b.c 的值时 a.b.c 上游是不会监测到改变的,因为 setter 只会检测到引用变化,不会监测到对象内部的变化。所以这个时候我们可以设置 deep: true ,在代码中的实现是 traverse(value) ,当发现依赖目标为一个对象时,递归进去遍历每一个子属性,这就会主动触发了深度属性的 getter 。实现深度监听的功能~

这就是vue中实现数据双向绑定的整个过程,如有描述不妥或不清晰之处请随时提出,谢谢~