You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
<body><divid="mvvm-app"><inputv-model="title"><h2>{{title}}</h2><buttonv-on:click="clickBtn">数据初始化</button></div></body><scriptsrc="../dist/bundle.js"></script><scripttype="text/javascript">
var vm = new MVVM({el: '#mvvm-app',data: {title: 'hello world'},methods: {clickBtn: function(e){this.title='hello world';}},});
</script>
functioninitData(vm: Component){letdata=vm.$options.datadata=vm._data=typeofdata==='function'
? getData(data,vm)
: data||{}if(!isPlainObject(data)){data={}process.env.NODE_ENV!=='production'&&warn('data functions should return an object:\n'+'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',vm)}// proxy data on instanceconstkeys=Object.keys(data)constprops=vm.$options.propsconstmethods=vm.$options.methodsleti=keys.lengthwhile(i--){constkey=keys[i]if(process.env.NODE_ENV!=='production'){if(methods&&hasOwn(methods,key)){warn(`Method "${key}" has already been defined as a data property.`,vm)}}if(props&&hasOwn(props,key)){process.env.NODE_ENV!=='production'&&warn(`The data property "${key}" is already declared as a prop. `+`Use prop default value instead.`,vm)}elseif(!isReserved(key)){proxy(vm,`_data`,key)}}// observe dataobserve(data,true/* asRootData */)}
data 的初始化主要过程也是做两件事,一个是对定义 data 函数返回对象的遍历,通过 proxy 把每一个值 vm._data.xxx 都代理到 vm.xxx 上;另一个是调用 observe 方法观测整个 data 的变化,把 data 也变成响应式,我们接下去主要介绍 observe 。
xportclassObserver{value: any;dep: Dep;vmCount: number;// number of vms that have this object as root $dataconstructor(value: any){this.value=valuethis.dep=newDep()this.vmCount=0def(value,'__ob__',this)if(Array.isArray(value)){if(hasProto){protoAugment(value,arrayMethods)}else{copyAugment(value,arrayMethods,arrayKeys)}this.observeArray(value)}else{this.walk(value)}}/** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */walk(obj: Object){constkeys=Object.keys(obj)for(leti=0;i<keys.length;i++){defineReactive(obj,keys[i])}}/** * Observe a list of Array items. */observeArray(items: Array<any>){for(leti=0,l=items.length;i<l;i++){observe(items[i])}}}
Observer 的构造函数逻辑很简单,首先实例化 Dep 对象, Dep 对象,我们第2小节会介绍。接下来会对 value 做判断,对于数组会调用 observeArray 方法,否则对纯对象调用 walk 方法。可以看到 observeArray 是遍历数组再次调用 observe 方法,而 walk 方法是遍历对象的 key 调用 defineReactive 方法,那么我们来看一下这个方法是做什么的。
exportdefaultclassDep{statictarget: ?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)}depend(){if(Dep.target){Dep.target.addDep(this)}}notify(){// stabilize the subscriber list firstconstsubs=this.subs.slice()if(process.env.NODE_ENV!=='production'&&!config.async){// subs aren't sorted in scheduler if not running async// we need to sort them now to make sure they fire in correct// ordersubs.sort((a,b)=>a.id-b.id)}for(leti=0,l=subs.length;i<l;i++){subs[i].update()}}}// The current target watcher being evaluated.// This is globally unique because only one watcher// can be evaluated at a time.Dep.target=null
前言
当被问到 Vue 数据双向绑定原理的时候,大家可能都会脱口而出:Vue 内部通过
Object.defineProperty
方法属性拦截的方式,把data
对象里每个数据的读写转化成getter
/setter
,当数据变化时通知视图更新。虽然一句话把大概原理概括了,但是其内部的实现方式还是值得深究的,本文就以通俗易懂的方式剖析Vue
内部双向绑定原理的实现过程。然后再根据Vue
源码的数据双向绑定实现,来进一步巩固加深对数据双向绑定的理解认识。以下为我们实现的数据双向绑定的效果图:github地址为:github.com/fengshi123/…,上面汇总了作者所有的博客文章,如果喜欢或者有所启发,请帮忙给个 star ~,对作者也是一种鼓励。
一、什么是 MVVM 数据双向绑定
MVVM
数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:即:
Data
中的数据同步变化。即View
=>Data
的变化。Data
中的数据变化时,文本节点的内容同步变化。即Data
=>View
的变化。其中,
View
变化更新Data
,可以通过事件监听的方式来实现,所以我们本文主要讨论如何根据Data
变化更新View
。我们会通过实现以下 4 个步骤,来实现数据的双向绑定:
1、实现一个监听器
Observer
,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者;2、实现一个订阅器
Dep
,用来收集订阅者,对监听器Observer
和 订阅者Watcher
进行统一管理;3、实现一个订阅者
Watcher
,可以收到属性的变化通知并执行相应的方法,从而更新视图;4、实现一个解析器
Compile
,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化。以上四个步骤的流程图表示如下:
该实例的源码已经放到 github 上面:/~https://github.com/fengshi123/mvvm_example 。
二、监听器 Observer 实现
监听器
Observer
的实现,主要是指让数据对象变得“可观测”,即每次数据读或写时,我们能感知到数据被读取了或数据被改写了。要使数据变得“可观测”,Vue 2.0
源码中用到Object.defineProperty()
来劫持各个数据属性的setter / getter
,Object.defineProperty
方法,在 MDN 上是这么定义的:2.1、Object.defineProperty() 语法
Object.defineProperty
语法,在 MDN 上是这么定义的:(1)参数
obj
要在其上定义属性的对象。
prop
要定义或修改的属性的名称。
descriptor
将被定义或修改的属性描述符。
(2)返回值
被传递给函数的对象。
(3)属性描述符
Object.defineProperty()
为对象定义属性,分 数据描述符 和 存取描述符 ,两种形式不能混用。数据描述符和存取描述符均具有以下可选键值:
configurable
当且仅当该属性的
configurable
为true
时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。enumerable
当且仅当该属性的
enumerable
为true
时,该属性才能够出现在对象的枚举属性中。默认为 false。数据描述符具有以下可选键值:
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
writable
当且仅当该属性的
writable
为true
时,value
才能被赋值运算符改变。默认为 false。存取描述符具有以下可选键值:
get
一个给属性提供
getter
的方法,如果没有getter
则为undefined
。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this
对象(由于继承关系,这里的this
并不一定是定义该属性的对象)。默认为undefined
。set
一个给属性提供
setter
的方法,如果没有setter
则为undefined
。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为undefined
。2.2、监听器 Observer 实现
(1)字面量定义对象
首先,我们先看一下假设我们通过以下字面量的方式定义一个对象:
我们可以通过
person.name
和person.age
直接读写这个person
对应的属性值,但是,当这个person
的属性被读取或修改时,我们并不知情。那么,应该如何定义一个对象,它的属性被读写时,我们能感知到呢?(2)Object.defineProperty() 定义对象
假设我们通过
Object.defineProperty()
来定义一个对象:我们通过
object.defineProperty()
方法给person
的name
属性定义了get()
和set()
进行拦截,每当该属性进行读或写操作的时候就会触发get()
和set()
,这样,当对象的属性被读写时,我们就能感知到了。测试结果图如下所示:(3)改进方法
通过第(2)步的方法,
person
数据对象已经是“可观测”的了,能满足我们的需求了。但是如果数据对象的属性比较多的情况下,我们一个一个为属性去设置,代码会非常冗余,所以我们进行以下封装,从而让数据对象的所有属性都变得可观测:通过以上方法封装,我们可以直接定义
person
:这样定义的
person
的 两个属性都是“可观测”的。三、订阅器 Dep 实现
3.1、发布 —订阅设计模式
发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态改变时,所有依赖于它的对象都将得到通知。
(1)发布—订阅模式的优点:
(2)发布—订阅模式的生活实例
我们以售楼处的例子来举例说明发布-订阅模式:
小明最近看上了一套房子,到了售楼处之后才被告知,该楼盘的房子早已售罄。好在售楼 MM 告诉小明,不久后还有一些尾盘推出,开发商正在办理相关手续,手续办好后便可以购买。
但到底是什么时候,目前还没有人能够知道。 于是小明记下了售楼处的电话,以后每天都会打电话过去询问是不是已经到了购买时间。除 了小明,还有小红、小强、小龙也会每天向售楼处咨询这个问题。一个星期过后,售楼 MM 决定辞职,因为厌倦了每天回答 1000个相同内容的电话。
当然现实中没有这么笨的销售公司,实际上故事是这样的:小明离开之前,把电话号码留在 了售楼处。售楼 MM 答应他,新楼盘一推出就马上发信息通知小明。小红、小强和小龙也是一样,他们的电话号码都被记在售楼处的花名册上,新楼盘推出的时候,售楼 MM会翻开花名册,遍历上面的电话号码,依次发送一条短信来通知他们。这就是发布-订阅模式在现实中的例子。
3.2、订阅器 Dep 实现
完成了数据的'可观测',即我们知道了数据在什么时候被读或写了,那么,我们就可以在数据被读或写的时候通知那些依赖该数据的视图更新了,为了方便,我们需要先将所有依赖收集起来,一旦数据发生变化,就统一通知更新。其实,这就是前一节所说的“发布订阅者”模式,数据变化为“发布者”,依赖对象为“订阅者”。
现在,我们需要创建一个依赖收集容器,也就是消息订阅器
Dep
,用来容纳所有的“订阅者”。订阅器Dep
主要负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数。创建消息订阅器
Dep
:有了订阅器,我们再将
defineReactive
函数进行改造一下,向其植入订阅器:从代码上看,我们设计了一个订阅器
Dep
类,该类里面定义了一些属性和方法,这里需要特别注意的是它有一个静态属性Dep.target
,这是一个全局唯一 的Watcher
,因为在同一时间只能有一个全局的Watcher
被计算,另外它的自身属性subs
也是Watcher
的数组。四、订阅者 Watcher 实现
订阅者
Watcher
在初始化的时候需要将自己添加进订阅器Dep
中,那该如何添加呢?我们已经知道监听器Observer
是在 get 函数执行了添加订阅者 Wather 的操作的,所以我们只要在订阅者Watcher
初始化的时候触发对应的get
函数去执行添加订阅者操作即可,那要如何触发get
的函数,再简单不过了,只要获取对应的属性值就可以触发了,核心原因就是因为我们使用了Object.defineProperty( )
进行数据监听。这里还有一个细节点需要处理,我们只要在订阅者Watcher
初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在Dep.target
上缓存下订阅者,添加成功后再将其去掉就可以了。订阅者Watcher
的实现如下:订阅者
Watcher
分析如下:订阅者
Watcher
是一个 类,在它的构造函数中,定义了一些属性:node
节点的v-model
等指令的属性值 或者插值符号中的属性。如v-model="name"
,exp
就是name
;Watcher
绑定的更新函数;当我们去实例化一个渲染
watcher
的时候,首先进入watcher
的构造函数逻辑,就会执行它的this.get()
方法,进入get
函数,首先会执行:实际上就是把
Dep.target
赋值为当前的渲染watcher
,接着又执行了:在这个过程中会对
vm
上的数据访问,其实就是为了触发数据对象的getter
。每个对象值的
getter
都持有一个dep
,在触发getter
的时候会调用dep.depend()
方法,也就会执行this.addSub(Dep.target)
,即把当前的watcher
订阅到这个数据持有的dep
的watchers
中,这个目的是为后续数据变化时候能通知到哪些watchers
做准备。这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了吗?其实并没有,完成依赖收集后,还需要把
Dep.target
恢复成上一个状态,即:而
update()
函数是用来当数据发生变化时调用Watcher
自身的更新函数进行更新的操作。先通过let value = this.vm.data[this.exp];
获取到最新的数据,然后将其与之前get()
获得的旧数据进行比较,如果不一样,则调用更新函数cb
进行更新。至此,简单的订阅者
Watcher
设计完毕。五、解析器 Compile 实现
5.1、解析器 Compile 关键逻辑代码分析
通过监听器
Observer
订阅器Dep
和订阅者Watcher
的实现,其实就已经实现了一个双向数据绑定的例子,但是整个过程都没有去解析dom
节点,而是直接固定某个节点进行替换数据的,所以接下来需要实现一个解析器Compile
来做解析和绑定工作。解析器Compile
实现步骤:我们下面对 '{{变量}}' 这种形式的指令处理的关键代码进行分析,感受解析器
Compile
的处理逻辑,关键代码如下:5.2、简单实现一个 Vue 实例
完成监听器
Observer
、订阅器Dep
、订阅者Watcher
和解析器Compile
的实现,我们就可以模拟初始化一个Vue
实例,来检验以上的理论的可行性了。我们通过以下代码初始化一个Vue
实例,该实例的源码已经放到 github 上面:/~https://github.com/fengshi123/mvvm_example ,有兴趣的可以 git clone:运行以上实例,效果图如下所示,跟实际的 Vue 数据绑定效果是不是一样!
六、Vue 源码 — 数据双向绑定
以上第二章节到第六章节,从监听器
Observer
、订阅器Dep
、订阅者Watcher
和解析器Compile
的实现,完成了一个简单的Vue
数据绑定实例的实现。本章节,我们从Vue
源码层面分析监听器Observer
、订阅器Dep
、订阅者Watcher
的实现,帮助大家了解Vue
源码如何实现数据双向绑定。6.1、监听器 Observer 实现
我们在本小节主要介绍 监听器 Observer 实现,核心就是利用
Object.defineProperty
给数据添加了getter
和 setter,目的就是为了在我们访问数据以及写数据的时候能自动执行一些逻辑 。(1)initState
在
Vue
的初始化阶段,_init
方法执行的时候,会执行initState(vm)
方法,它的定义在src/core/instance/state.js
中。initState
方法主要是对props
、methods
、data
、computed
和wathcer
等属性做了初始化操作。这里我们重点分析data
,对于其它属性的初始化我们在以后的文章中再做介绍。(2)initData
data
的初始化主要过程也是做两件事,一个是对定义data
函数返回对象的遍历,通过proxy
把每一个值vm._data.xxx
都代理到vm.xxx
上;另一个是调用observe
方法观测整个data
的变化,把data
也变成响应式,我们接下去主要介绍 observe 。(3)observe
observe
的功能就是用来监测数据的变化,它的定义在src/core/observer/index.js
中:observe
方法的作用就是给非 VNode 的对象类型数据添加一个Observer
,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个Observer
对象实例。接下来我们来看一下Observer
的作用。(4)Observer
Observer
是一个类,它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新:Observer
的构造函数逻辑很简单,首先实例化Dep
对象,Dep
对象,我们第2小节会介绍。接下来会对value
做判断,对于数组会调用observeArray
方法,否则对纯对象调用walk
方法。可以看到observeArray
是遍历数组再次调用observe
方法,而walk
方法是遍历对象的 key 调用defineReactive
方法,那么我们来看一下这个方法是做什么的。(5)defineReactive
defineReactive
的功能就是定义一个响应式对象,给对象动态添加getter
和setter
,它的定义在src/core/observer/index.js
中:defineReactive
函数最开始初始化Dep
对象的实例,接着拿到obj
的属性描述符,然后对子对象递归调用observe
方法,这样就保证了无论obj
的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改obj
中一个嵌套较深的属性,也能触发 getter 和 setter。6.2、订阅器 Dep 实现
订阅器
Dep
是整个getter
依赖收集的核心,它的定义在src/core/observer/dep.js
中:Dep
是一个Class
,它定义了一些属性和方法,这里需要特别注意的是它有一个静态属性target
,这是一个全局唯一Watcher
,这是一个非常巧妙的设计,因为在同一时间只能有一个全局的Watcher
被计算,另外它的自身属性subs
也是Watcher
的数组。Dep
实际上就是对Watcher
的一种管理,Dep
脱离Watcher
单独存在是没有意义的。6.3、订阅者 Watcher 实现
订阅者
Watcher
的一些相关实现,它的定义在src/core/observer/watcher.js
中Watcher
是一个Class
,在它的构造函数中,定义了一些和Dep
相关的属性 ,其中,this.deps
和this.newDeps
表示Watcher
实例持有的Dep
实例的数组;而this.depIds
和this.newDepIds
分别代表this.deps
和this.newDeps
的id
Set 。(1)过程分析
当我们去实例化一个渲染
watcher
的时候,首先进入watcher
的构造函数逻辑,然后会执行它的this.get()
方法,进入get
函数,首先会执行:实际上就是把
Dep.target
赋值为当前的渲染watcher
并压栈(为了恢复用)。接着又执行了:这个时候就触发了数据对象的
getter
。么每个对象值的
getter
都持有一个dep
,在触发getter
的时候会调用dep.depend()
方法,也就会执行Dep.target.addDep(this)
。刚才我们提到这个时候
Dep.target
已经被赋值为渲染watcher
,那么就执行到addDep
方法:这时候会做一些逻辑判断(保证同一数据不会被添加多次)后执行
dep.addSub(this)
,那么就会执行this.subs.push(sub)
,也就是说把当前的watcher
订阅到这个数据持有的dep
的subs
中,这个目的是为后续数据变化时候能通知到哪些subs
做准备。所以在vm._render()
过程中,会触发所有数据的getter
,这样实际上已经完成了一个依赖收集的过程。当我们在组件中对响应的数据做了修改,就会触发
setter
的逻辑,最后调用watcher
中的update
方法:这里会对于
Watcher
的不同状态,会执行不同的更新逻辑。6.4、Vue 数据双向绑定原理图
以上主要分析了 Vue 数据双向绑定的关键代码,其原理图可以表示如下:
七、总结
本文通过监听器
Observer
、订阅器Dep
、订阅者Watcher
和解析器 ·的实现,模拟初始化一个Vue
实例,帮助大家了解数据双向绑定的基本原理。接着,从Vue
源码层面介绍了Vue
数据双向绑定的实现过程,了解Vue
源码的实现逻辑,从而巩固加深对数据双向绑定的理解认识。希望本文对您有帮助。github地址为:github.com/fengshi123/…,上面汇总了作者所有的博客文章,如果喜欢或者有所启发,请帮忙给个 star ~,对作者也是一种鼓励。
参考文献
1、Vue 的双向绑定原理及实现:https://www.cnblogs.com/canfoo/p/6891868.html
2、Vue 技术揭秘:https://ustbhuangyi.github.io/vue-analysis/
The text was updated successfully, but these errors were encountered: