Contents

Vue作为当下炙手可热的前端三大框架之一,一直都想深入研究一下其内部的实现原理,去学习MVVM模式的精髓。如果说MVVM是当下最流行的图形用户界面开发模式,那么数据绑定则是这一模式的根基。这也是我为什么要从数据绑定开始了解Vue的原因。

本篇文章首先从Vue构建开始,后面主要了解methodsdata的执行过程以及原理,结合Vue文档来分析,做到知其然且知其所以然。对于计算属性、组件系统、指令等将在后续文章中分析。

源代码基于vue1.0,最新版本为2.x,其中的差异我会在文章尽量列出来。

Vue构造过程

1
2
3
4
function  (options) {
this._init(options)
}

Vue构造函数调用了一个_init函数,Vue所有的内置属性和方法都以_或者$开头:

1
2
3
4
5
exports.isReserved = function (str) {
var c = (str + '').charCodeAt(0)
return c === 0x24 || c === 0x5F
}

_init函数调用了若干个初始化函数其中就包含了一个初始化状态属性相关的函数:

1
2
3
4
5
6
7
8
//instance/state.js
exports._initState = function () {
this._initProps()
this._initMeta()
this._initMethods()
this._initData()
this._initComputed()
}

看到调用函数的名称都知道是什么意思,这里主要研究一下_initMethods_initData两个函数的实现原理。其余的会在后续文章分析。

_initMethods:

1
2
3
4
5
6
7
8
exports._initMethods = function () {
var methods = this.$options.methods
if (methods) {
for (var key in methods) {
this[key] = _.bind(methods[key], this)
}
}
}

对于methods的初始化相对比较简单,这个函数的主要作用就是把用户定义在methods属性内的一些方法绑定到当前的Vue实例中。由于ES6的箭头函数会导致bind失败,这也是为什么Vue在文档中提示:

不要在选项属性或回调上使用箭头函数,比如 created: () => console.log(this.a) 或 vm.$watch(‘a’, newValue => this.myMethod())。因为箭头函数是和父级上下文绑定在一起的,this 不会是如你所预期的 Vue 实例,经常导致 Uncaught TypeError: Cannot read property of undefined 或 Uncaught TypeError: this.myMethod is not a function 之类的错误。

_initData:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
exports._initData = function () {
var propsData = this._data
var optionsDataFn = this.$options.data
var optionsData = optionsDataFn && optionsDataFn()
if (optionsData) {
this._data = optionsData
for (var prop in propsData) {
if (process.env.NODE_ENV !== 'production' &&
optionsData.hasOwnProperty(prop)) {
_.warn(
'Data field "' + prop + '" is already defined ' +
'as a prop. Use prop default value instead.'
)
}
if (this._props[prop].raw !== null ||
!optionsData.hasOwnProperty(prop)) {
_.set(optionsData, prop, propsData[prop])
}
}
}
//...
}

对于子组件而言,propsData表示父组件传递过来的数据,因为initProp先执行_data填充的是父组件传递过来的数据。optionsDataFn表示组件自身的数据。 为什么这里看到的是一个函数呢?这是因为在Vue的初始化函数_init内调用了util/option.js下的mergeOptions这个方法,为了方便合并父组件和子组件的数据,它定义了一系列策略把组件传入的参数替换了。为了避免父组件的数据被子组件原生的数据覆盖需要做一次判定,发现有数据覆盖就警告用户。需要注意的是属性值为null且子组件原生就有的数据字段是不会被覆盖的。

在把数据合并之后,接下来要对组件数据做一个代理:

1
2
3
4
5
6
7
8
9
10
11
//...
var data = this._data
// proxy data on instance
var keys = Object.keys(data)
var i, key
i = keys.length
while (i--) {
key = keys[i]
this._proxy(key)
}
//...

数据代理的作用就是为了实现:vm.prop === vm._data.prop的效果。代码位置在instance/state.js下的_proxy函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
exports._proxy = function (key) {
if (!_.isReserved(key)) {
// need to store ref to self here
// because these getter/setters might
// be called by child scopes via
// prototype inheritance.
var self = this
Object.defineProperty(self, key, {
configurable: true,
enumerable: true,
get: function proxyGetter () {
return self._data[key]
},
set: function proxySetter (val) {
self._data[key] = val
}
})
}
}

为了避免覆盖Vue内置的属性所以做一次判定,接下来就是对数据的访问做一个代理。

仅仅代理数据是不够的,接下来要看到的是监控数据的变化:

1
2
3
4
5
exports._initData = function () {
//...
// observe data
Observer.create(data, this)
}

Observer.create是Vue响应式数据绑定的核心:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Observer.create = function (value, vm) {
if (!value || typeof value !== 'object') {
return
}
var ob
if (
value.hasOwnProperty('__ob__') &&
value.__ob__ instanceof Observer
) {
ob = value.__ob__
} else if (
(_.isArray(value) || _.isPlainObject(value)) &&
!Object.isFrozen(value) &&
!value._isVue
) {
ob = new大专栏  Vue数据绑定(一)> Observer(value)
}
if (ob && vm) {
ob.addVm(vm)
}
return ob
}

数据监听只针对对象类型,监听对象会内嵌到被监听的对象,这样可以避免重复监听数据对象:

1
2
3
4
5
function Observer (value) {
//...
_.define(value, '__ob__', this)
//...
}

需要注意的是,Vue对象实例不会被监听,通过_isVue属性来辨别。对于被冻结的对象也是不能监听的,Vue通过接口Object.isFrozen来判定,官方文档也有说明:

这里唯一的例外是使用 Object.freeze(),这会阻止修改现有的属性,也意味着响应系统无法再追踪变化。

Observer对象会反向引用Vue实例对象,这是为了在用户调用$delete的时候能够反向通知到Vue实例对象, 把挂在实例上的被删除属性去除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
exports.delete = function (obj, key) {
if (!obj.hasOwnProperty(key)) {
return
}
delete obj[key]
var ob = obj.__ob__
if (!ob) {
return
}
ob.notify()
if (ob.vms) {
var i = ob.vms.length
while (i--) {
var vm = ob.vms[i]
vm._unproxy(key)
vm._digest()
}
}
}

数据的变化追踪分为两类:对象和数组类型。对象类型遍历属性监听每个属性的变化:

1
2
3
4
5
6
7
Observer.prototype.walk = function (obj) {
var keys = Object.keys(obj)
var i = keys.length
while (i--) {
this.convert(keys[i], obj[keys[i]])
}
}

convert函数调用了数据追踪最关键的一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function defineReactive (obj, key, val) {
var dep = new Dep()
var childOb = Observer.create(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function metaGetter () {
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
}
return val
},
set: function metaSetter (newVal) {
if (newVal === val) return
val = newVal
childOb = Observer.create(newVal)
dep.notify()
}
})
}

由于对象的属性可能还是一个对象或者数组。所以需要递归的追踪内嵌数据的变化。数据的监听者存放在Dep模块内。每次设置新的对象需要重新监听数据属性。


数组类型的数据监听追踪比较特殊,Vue通过拦截几个数组方法来追踪数组的变化

1
2
3
4
5
6
7
8
9
10
11
12
function Observer (value) {
//...
if (_.isArray(value)) {
var augment = _.hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}

arrayMethods是一个以Array.prototype为原型的对象://observer/array.js

1
2
var arrayProto = Array.prototype
var arrayMethods = Object.create(arrayProto)

通过_.hasProto方法判定代理数组对象的若干个方法:

1
2
3
function protoAugment (target, src) {
target.__proto__ = src
}

至此Vue的数据追踪流程执行完毕。Vue提供了两个全局方法Vue.setVue.delete。下面来研究一下两个函数的实现,Vue.set最终会调用到util/lang.js下的set方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
exports.set = function set (obj, key, val) {
if (obj.hasOwnProperty(key)) {
obj[key] = val
return
}
if (obj._isVue) {
set(obj._data, key, val)
return
}
var ob = obj.__ob__
if (!ob) {
obj[key] = val
return
}
ob.convert(key, val)
ob.notify()
if (ob.vms) {
var i = ob.vms.length
while (i--) {
var vm = ob.vms[i]
vm._proxy(key)
vm._digest()
}
}
}

如果设置的属性之前已经有了,这个时候直接设置就行,会促发相应的更新逻辑。如果是Vue对象则设置到_data属性内。如果数据对象不是响应式的则直接新增数据属性。这个时候不会触发视图更新等操作。反之通知相应的监听方,并且递归追踪新增的数据值。Vue官方文档有如下提示:

向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性。

Vue.delete最终调用util/lang.js下的delete方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
exports.delete = function (obj, key) {
if (!obj.hasOwnProperty(key)) {
return
}
delete obj[key]
var ob = obj.__ob__
if (!ob) {
return
}
ob.notify()
if (ob.vms) {
var i = ob.vms.length
while (i--) {
var vm = ob.vms[i]
vm._unproxy(key)
vm._digest()
}
}
}

被删除的属性如果不是响应式的,则直接删除然后退出函数。反之,通知各个监听对象,并且通过_unproxy方法把挂在Vue实例上的属性删除。

Vue数据绑定(一)的更多相关文章

  1. Vue数据绑定

    gitHub地址:https://github.com/lily1010/vue_learn/tree/master/lesson04 一 双括号用来数据绑定 (1)写法一: {{message}}, ...

  2. 浅析vue数据绑定

    前言:最近团队需要做一个分享,脚进脑子,不知如何分享.最后想着之前一直想研究一下 vue 源码,今天刚好 "借此机会" 研究一下. 网上研究vue数据绑定的文章已经非常多了,但是自 ...

  3. Vue数据绑定和响应式原理

    Vue数据绑定和响应式原理 当实例化一个Vue构造函数,会执行 Vue 的 init 方法,在 init 方法中主要执行三部分内容,一是初始化环境变量,而是处理 Vue 组件数据,三是解析挂载组件.以 ...

  4. 17: VUE数据绑定 与 Object.defineProperty

    VUE数据绑定原理:https://segmentfault.com/a/1190000006599500?utm_source=tag-newest Object.defineProperty(): ...

  5. (三)vue数据绑定及相应的命令

    vue数据绑定及相应的命令 {{ Text }} 双括号进行数据渲染 动态绑定数据 例如:{{message}} data: { return{ message: 'Hello Vue!' } } 2 ...

  6. 「每日一题」有人上次在dy面试,面试官问我:vue数据绑定的实现原理。你说我该如何回答?

    关注「松宝写代码」,精选好文,每日一题 ​时间永远是自己的 每分每秒也都是为自己的将来铺垫和增值 作者:saucxs | songEagle 来源:原创 一.前言 文章首发在「松宝写代码」 2020. ...

  7. vue数据绑定原理

    一.定义 vue的数据双向绑定是基于Object.defineProperty方法,通过定义data属性的get和set函数来监听数据对象的变化,一旦变化,vue利用发布订阅模式,通知订阅者执行回调函 ...

  8. vue 数据绑定实现的核心 Object.defineProperty()

    vue深入响应式原理 现在是时候深入一下了!Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是普通的 JavaScript 对象.而当你修改它们时,视图会进行更新.这使得状态管理非常简 ...

  9. vue数据绑定数组,改变元素时不更新view问题

    关于这个问题,官网上说的很清楚官方文档  写个例子HTML<body> <div class="box"> <div v-for="aa i ...

随机推荐

  1. Debian8.8为普通用户添加sudo权限

    1.进入root用户,su root 输入密码,我们首先修改 /etc/sudoers 文件的属性为可写权限# chmod +w /etc/sudoers2.编辑 vim /etc/sudoers,添 ...

  2. Python语言学习前提:条件语句

    一.条件语句 1.条件语句:通过一条或多条语句的执行结果(True或False)来决定执行额代码块.python程序语言指定任何非0或非空(null)的值为true,0或null为false. 2. ...

  3. SoapUI+excel接口自动化测试简述

    1.自动化测试工具介绍 由于系统前后端分离,所以接口测试势在必行,在接触了几天接口测试框架,包括postman.httpclient.loadrunner.soapUI等,下面具体讲讲最终决定使用so ...

  4. mplayer 的安装步骤

        编译mplayer: make distclean ./configure --disable-png --disable-gif   //加后面的是因为编译时出错了,也可以直接  ./con ...

  5. linux 添加常用长命令别名

    ## 设置linux下常用命令别名,提高效率 将要使用的命令别名写入到~/.bashrc文件,通过source ~/.bashrc命令使变更生效 alias sst='systemctl status ...

  6. 新年在家学java之基础篇--类&方法

    面向对象 面向对象OOP,面向过程POP 面向对象三大特征 封装 继承 多态 类 类由属性(对应类中的成员变量)和行为(成员方法)来构成 类的成员变量可以先声明,不用初始化,有默认值 方法名称如果多个 ...

  7. C#面向对象---对象成员、方法加载、引用类库

    一.方法重载: 1.两个函数同名,就互相构成方法的重载关系 2.重载的函数,必须跟其他函数之间具有不同的参数类型或参数个数 二.字段与属性 类的字段: 类里面是可以直接定义变量的,这些变量就叫类的字段 ...

  8. Linux SSH 使用密钥登陆

    Linux SSH 使用密钥登陆 通常我们登录 Linux 服务器,我们需要使用密码进行登录,但是密码存在被暴力破解的可能. 可以将默认服务端口 22 改成其他不常用的端口. 可以设置非常复杂的密码. ...

  9. 简化Java编程的法宝,让工作更高效

    如果你没有看过之前的文章,也不要紧,这并不影响你对接下来的内容的理解,不过为了照顾直接看到第二篇的同学,还是有必要介绍一下HuTool的引入方式. 在项目的pom.xml的dependencies中加 ...

  10. python中sort和sorted排序的相关方法

    Python list内置sort()方法用来排序,也可以用python内置的全局sorted()方法来对可迭代的序列排序生成新的序列. 1)排序基础 简单的升序排序是非常容易的.只需要调用sorte ...