在Vue中我们经常修改数据,然后视图就直接修改了,那么这些究竟是怎么实现的呢?
其实Vue使用了E5的语法Object.defineProperty来实现的数据驱动。
那么Object.defineProperty究竟是怎么实现的呢?
我们先来看一下一个简单的demo

<template>
<div class="hello">{{test}}
</div>
</template>
<script>
export default {data () {test: '123'},created () {console.log(this.test === this._data.test) // true}
}
</script>

在vue这段小代码中,this.test === this._data.test其实是等价的。这是为什么呢
其实就是通过Object.defineProperty做的一个小代理实现的。
原理如下:

var obj = {_data: {x: 123,y: 467}
}
function proxy (target, sourceKey, key) {Object.defineProperty(target, key, {enumerable: true,configurable: true,get: function() {return this[sourceKey][key]},set: function(val) {this[sourceKey][key] = val}})
}
proxy(obj, '_data', 'x')
proxy(obj, '_data', 'y')console.log(obj.x)  // 123
console.log(obj.y)  //  467
console.log(obj._data.x) // 123
console.log(obj._data.y) // 467

以上一个demo就是对obj对象的一个代理,通过访问obj.x直接代理到obj._data.x。 Object.defineProperty具体用法可以自行搜索。
那么其实数据双向绑定也是根据Object.defineProperty里面的get和set来实现的,通过set的时候去做一些视图更新的操作。

接下来我们就来看一下Vue源码吧。
双向数据绑定,将分为以下3个部分:

1. Observer。这个模块是用于监听对象上的所有属性,即使用Object.defineProperty来实现get和set
2. Watcher。 这个模块是观察者,当监听的数据值被修改时,执行对象的回调函数。
3. Dep。 连接Observe和Watcher的桥梁,每个Observer对应一个Dep,内部维护一个数组,保存与该Observer相关的Watcher

Observer

首先来看下oberser。
路径: src/core/observer/index.js

export class Observer {value: any;dep: Dep;vmCount: number; // number of vms that has this object as root $dataconstructor (value: any) {this.value = valuethis.dep = new Dep()this.vmCount = 0// 添加__ob__来标示value有对应的Observerdef(value, '__ob__', this)// 对数组的处理if (Array.isArray(value)) {const augment = hasProto? protoAugment: copyAugmentaugment(value, arrayMethods, arrayKeys)this.observeArray(value)} else {// 处理对象this.walk(value)}}// 给每个属性添加getter/setterswalk (obj: Object) {const keys = Object.keys(obj)for (let i = 0; i < keys.length; i++) {defineReactive(obj, keys[i])}}// 观察数组的每一项observeArray (items: Array<any>) {for (let i = 0, l = items.length; i < l; i++) {observe(items[i])}}
}

所以从源码可以看出数据类型主要分为2种,一种是数组,一种是对象。对应的处理方法分别树observe和defineReactive
那我们再来看看这2个函数做了些什么呢?
这2个函数也是在这个文件内部

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}// cater for pre-defined getter/settersconst getter = property && property.getconst setter = property && property.setif ((!getter || setter) && arguments.length === 2) {val = obj[key]}let childOb = !shallow && observe(val)// 这边作出get和set的动态响应的处理Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter () {const value = getter ? getter.call(obj) : valif (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/* eslint-disable no-self-compare */if (newVal === value || (newVal !== newVal && value !== value)) {return}/* eslint-enable no-self-compare */if (process.env.NODE_ENV !== 'production' && customSetter) {customSetter()}if (setter) {setter.call(obj, newVal)} else {val = newVal}childOb = !shallow && observe(newVal)dep.notify()}})
}

// 这个方法具体是为对象的key添加get,set方法;如果用户传入自己本身传入get和set方法也会保留其方法。它会为每一个值都创建一个Dep,在get函数中 dep.depend做了2件事,一是向Dep.target的内部添加dep,二是将dep。target添加到dep内部的subs数组中,也就是建立关系。
在set函数中,如果新值和旧值相同则不处理,如果不同,则通知更新。
接下来看observe

export function observe (value: any, asRootData: ?boolean): Observer | void {if (!isObject(value) || value instanceof VNode) {return}let ob: Observer | voidif (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {ob = value.__ob__} else if (shouldObserve &&!isServerRendering() &&(Array.isArray(value) || isPlainObject(value)) &&Object.isExtensible(value) &&!value._isVue) {ob = new Observer(value)}if (asRootData && ob) {ob.vmCount++}return ob
}

对数组中的每一项进行检测,该方法用于观察一个对象,如果不是对象则直接返回,如果是对象则返回该对象Observer对象。
但是这样紧紧对数组中的每一项的对象进行了观察,如果数组本身的长度修改那么又如何触发呢。Vue专门对数组做了特出的处理。
回过头来看Observer的类中有这么一段,在处理数组的时候,处理了这些代码

const augment = hasProto? protoAugment: copyAugmentaugment(value, arrayMethods, arrayKeys)this.observeArray(value)

我们来细看这些函数,见如下

function protoAugment (target, src: Object, keys: any) {/* eslint-disable no-proto */target.__proto__ = src/* eslint-enable no-proto */
}function copyAugment (target: Object, src: Object, keys: Array<string>) {for (let i = 0, l = keys.length; i < l; i++) {const key = keys[i]def(target, key, src[key])}
}const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'
]/*** Intercept mutating methods and emit events*/
methodsToPatch.forEach(function (method) {// cache original methodconst original = arrayProto[method]def(arrayMethods, method, function mutator (...args) {const result = original.apply(this, args)const ob = this.__ob__let insertedswitch (method) {case 'push':case 'unshift':inserted = argsbreakcase 'splice':inserted = args.slice(2)break}if (inserted) ob.observeArray(inserted)// notify changeob.dep.notify()return result})
})

我们可以看到protoAugment很简单,就是执行了一段value._proto_ = arrayMethods
copyAugment中循环把arrayMethods上的arrayKeys方法添加到value上。
arrayMethods又是重写了数组的操作方法['push','pop','shift','unshift','splice','sort','reverse']。
通过调用数组这些方法的时候,通知dep.notify。 至此Observer部分已经结束

Dep

Dep相对就简单点。
源码路径:src/core/observer/dep.js

export default class Dep {static target: ?Watcher;id: number;subs: Array<Watcher>;constructor () {this.id = uid++this.subs = []}// 添加观察者addSub (sub: Watcher) {this.subs.push(sub)}// 删除观察者removeSub (sub: Watcher) {remove(this.subs, sub)}// 调用watcherdepend () {if (Dep.target) {Dep.target.addDep(this)}}notify () {// stabilize the subscriber list firstconst subs = this.subs.slice()for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}}
}

内部有个唯一的id标识,还有一个保存watcher的数组subs。

Watcher

let uid = 0export default class Watcher {constructor (vm: Component,expOrFn: string | Function,cb: Function,options?: Object) {this.vm = vmvm._watchers.push(this)...this.cb = cbthis.id = ++uid...this.expression = process.env.NODE_ENV !== 'production'? expOrFn.toString(): ''if (typeof expOrFn === 'function') {this.getter = expOrFn} else {this.getter = parsePath(expOrFn)if (!this.getter) {this.getter = function () {}}}this.value = this.get()}get () {pushTarget(this)let valueconst vm = this.vm...value = this.getter.call(vm, vm)...popTarget()this.cleanupDeps()return value}...update () {...queueWatcher(this)}run () {if (this.active) {const value = this.get()if (value !== this.value ||isObject(value) ||this.deep) {// set new valueconst oldValue = this.valuethis.value = valueif (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)}}}}...
}

Watcher就是用于把变化放入观察,并通知其变化更新。
queueWatcher就是把变化者放入数组queue,然后通过nextTick去更换新数组queue中的变化。
在生命周期挂载元素时,就会通过创建Watcher,然后来更新创新模块。

vm._watcher = new Watcher(vm, updateComponent, noop)

这边数据双向绑定差不多就结束了。最后再附上一张简要的流程图来进一步清晰自己的思路。

下一章节通过数据绑定原理结合jquery来实现数据驱动更新的demo。之所以采用jquery操作dom是因为现在Vue源码还没到解析html模板那一步。
所以一步步来。等之后学完模板解析后。再去制作一个MVVM的简易demo。

如果对您有帮助,请点个赞,谢谢!

Vue源码学习(三)——数据双向绑定相关推荐

  1. Vue源码学习 - 组件化(三) 合并配置

    Vue源码学习 - 组件化(三) 合并配置 合并配置 外部调用场景 组件场景 总结 学习内容和文章内容来自 黄轶老师 黄轶老师的慕课网视频教程地址:<Vue.js2.0 源码揭秘>. 黄轶 ...

  2. Vue源码学习之Computed与Watcher原理

    前言  computed与watch是我们在vue中常用的操作,computed是一个惰性求值观察者,具有缓存性,只有当依赖发生变化,第一次访问computed属性,才会计算新的值.而watch则是当 ...

  3. vue源码学习--vue源码学习入门

    本文为开始学习vue源码的思路整理.在拿到vue项目源码的之后看到那些项目中的文件夹,会很困惑,不知道每个文件夹内的世界,怎么变换,怎样的魔力,最后产生了vue框架.学习源码也无从学起.我解决了这些困 ...

  4. VUE源码学习第一篇--前言

    一.目的 前端技术的发展,现在以vue,react,angular为代表的MVVM模式以成为主流,这三个框架大有三分天下之势.react和angular有facebook与谷歌背书,而vue是以一己之 ...

  5. Vue源码学习 - 组件化一 createComponent

    Vue源码学习 - 组件化一 createComponent 组件化 createComponent 构造子类构造函数 安装组件钩子函数 实例化 VNode 总结 学习内容和文章内容来自 黄轶老师 黄 ...

  6. Vue.js render函数的数据双向绑定

    在Vue.js render函数中数据双向绑定较为复杂.

  7. Vue源码学习 - 准备工作

    Vue源码学习 - 准备工作 准备工作 认识Flow 为什么用 Flow Flow 的工作方式 类型推断 类型注释 数组 类和对象 null Flow 在 Vue.js 源码中的应用 flow实践 总 ...

  8. Vue源码学习之initInjections和initProvide

    Vue源码学习之initInjections和initProvide 在进行源码阅读之前先让我们了解一个概念:provide/inject,这个是Vue在2.2.0版本新增的一个属性,按照Vue官网的 ...

  9. Vue源码学习: 关于对Array的数据侦听

    摘要 我们都知道Vue的响应式是通过Object.defineProperty来进行数据劫持.但是那是针对Object类型可以实现, 如果是数组呢? 通过set/get方式是不行的. 但是Vue作者使 ...

最新文章

  1. R语言层次聚类(hierarchical clustering):使用scale函数进行特征缩放、hclust包层次聚类(创建距离矩阵、聚类、绘制树状图dendrogram,在树状图上绘制红色矩形框)
  2. [TJOI2016][HEOI2016]排序
  3. Android 8 WiFi断流,安卓8.0曝出重大bug,比国产手机WiFi断流严重多了
  4. PMP-【第1章 引论】-2020-12-07(18页-24页)
  5. 寄存器、存储器、内存的区别
  6. .NET Core 2.1 Preview 2发布 - April 10, 2018
  7. js中innerHTML和innerText jQuery中html()和text()的区别
  8. c语言 多文件 学生系统,编的学生成绩管理系统 从文件中读取保存数据总会多读入一组乱码数据...
  9. java伪代码生成器_JAVA单例模式的实现伪代码
  10. UVA 12101 Prime Path (素数筛+BFS)
  11. Java中线程的创建有两种方式
  12. 活动、节假日、促销等营销方式的因果效应评估——特征工程篇(一)
  13. 6.3 tensorflow2实现FM推荐系统——Python实战
  14. BGP消息格式-UPDATE
  15. 【NLP】word2vec 模型
  16. Annovar软件注释肿瘤基因突变(一):COSMIC数据库最新版下载与使用
  17. IDEA修改项目war包名称
  18. 华为手机助手 android,华为手机助手(安卓版)
  19. 手机被偷后,让小偷欲哭无泪的高招
  20. 如何kill掉Ubuntu的僵死进程

热门文章

  1. Mysql支持中文全文检索的插件mysqlcft-应用中的问题
  2. Python函数进阶
  3. Android 微光闪烁效果之更强Shimmer-android
  4. 通过demo搞懂encode_utf8和decode_utf8
  5. 【数据泵】EXPDP导出表结构
  6. Objective-C Memory Management Being Exceptional 异常处理与内存
  7. ASP.Net缓存总结
  8. C#中i=i++值不变的一个解释
  9. (转)CKEditor和CKFinder在ASP.NET中的应用
  10. c语言:最长对称子串(3种解决方案)