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. 6)PHP,预定义变量

    预定义变量也叫超全局变量: :预定义变量又叫超全局变量,包括: $_GET, $_POST, $_SERVER, $_REQUEST, $GLOBALS, $_COOKIE, $_SESSION, . ...

  2. layui子弹框调用父弹框方法

    var thisFrame = parent.window.document.getElementById("LAY_layuiStampDuty1").getElementsBy ...

  3. Django中间件-跨站请求伪造-django请求生命周期-Auth模块-seettings实现可插拔配置(设计思想)

    Django中间件 一.什么是中间件 django中间件就是类似于django的保安;请求来的时候需要先经过中间件,才能到达django后端(url,views,models,templates), ...

  4. Perl:正则中问号的四周用途:1.字面意义的问号 2. 量词 3. 表示非贪心的修饰符 4.用以表示不具有记忆功能的圆括号

    Perl:正则中问号的四周用途:1.字面意义的问号  2. 量词   3. 表示非贪心的修饰符  4.用以表示不具有记忆功能的圆括号 非贪心:在量词后面加?即可

  5. python语法基础-异常操作-长期维护

    ###############    python-异常的操作  ############### # 异常:python解释器遇到一个错误,会停止程序的执行,并且提示错误信息,这就是异常, # 抛出异 ...

  6. Image.FromStream(ms) 提示参数无效

    说明ms有问题,首先确保有读到数据,这种情况是保存到库的时候出错的. 原来你可能是这样写的: MemoryStream stream = new MemoryStream();PictureBox1. ...

  7. form中采用图片作为提交按钮

    <span style="font-size:14px;"><FORM name="formName" action="xxxx&q ...

  8. android愤怒小鸟游戏、自定义View、掌上餐厅App、OpenGL自定义气泡、抖音电影滤镜效果等源码

    Android精选源码 精练的范围选择器,范围和单位可以自定义 自定义View做的小鸟游戏 android popwindow选择商品规格颜色尺寸效果源码 实现Android带有锯齿背景的优惠样式源码 ...

  9. MOOC(9)- 登录接口返回的cookie中有多个token

  10. CSS-----样式表案例(沃顿商学院)之高级山寨版

    HTML-CSS设计----------沃顿商学院(高级山寨版) 1.html代码 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Trans ...