内容简介:前面几篇文章一直都以源码分析为主,其实枯燥无味,对于新手玩家来说很不友好。这篇文章主要讲讲不同吧,分析为主,源码为辅,如果能达到深入浅出的效果那就更好了。「响应式系统」一直以来都是我认为
前言
前面几篇文章一直都以源码分析为主,其实枯燥无味,对于新手玩家来说很不友好。这篇文章主要讲讲 Vue
的响应式系统,形式与前边的稍显
不同吧,分析为主,源码为辅,如果能达到深入浅出的效果那就更好了。
什么是响应式系统
「响应式系统」一直以来都是我认为 Vue
里最核心的几个概念之一。想深入理解 Vue
,首先要掌握「响应式系统」的原理。
从一个官方的例子开始
由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值:
var vm = new Vue({ data: { // 声明 message 为一个空值字符串 message: '' }, template: '<div>{{ message }}</div>' }) // 之后设置 `message` vm.message = 'Hello!'
如果你未在 data 选项中声明 message, Vue
将警告你渲染函数正在试图访问不存在的属性。
当然,仅仅从上面这个例子我们也只能知道, Vue
不允许动态添加根级响应式属性。这意味我们需要将使用到的变量先在 data
函数中声明。
抛砖 引玉
新建一个空白工程,加入以下代码
export default { name: 'JustForTest', data () { return {} }, created () { this.b = 555 console.log(this.observeB) this.b = 666 console.log(this.observeB) }, computed: { observeB () { return this.b } } }
运行上述代码,结果如下:
在上面的代码中我们做了些什么?
- 没有在
data
函数中声明变量(意味着此时没有根级响应式属性) - 定义了一个
computed
属性 ——observeB
,用来返回(监听)变量b
- 使用了变量
b
同时赋值555
,打印this.observeB
- 使用了变量
b
同时赋值666
,打印this.observeB
打印结果为什么都是 555
?
有段简单的代码可以解释这个原因:
function createComputedGetter (key) { return function computedGetter () { var watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { watcher.evaluate(); } if (Dep.target) { watcher.depend(); } return watcher.value } } } ... Watcher.prototype.evaluate = function evaluate () { this.value = this.get(); this.dirty = false; };
createComputedGetter
函数返回一个闭包函数并挂载在 computed
属性的 getter
上,一旦触发 computed
属性的 getter
,
那么就会调用 computedGetter
显然,输出 555
是因为触发了 this.observeB
的 getter
,从而触发了 computedGetter
,最后执行 Watcher.evalute()
然而,决定 watcher.evalute()
函数执行与否与 watcher
和 watcher.dirty
的值是否为空有关
深入了解响应式系统
Object.defineProperty
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
那么这个函数应该怎么使用呢?给个官方的源码当做例子:
function def (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }); } def(value, '__ob__', this);
getter
和 setter
上面提到了 Object.defineProperty
函数,其实这个函数有个特别的参数 —— descriptor
(属性描述符),简单看下 MDN
上的定义:
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是
其中需要特别提到的就是 getter
和 setter
,在 descriptor
(属性描述符)中分别代表 get
方法和 set
方法
get
一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,
set
一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,
小结
getter setter getter setter
依赖收集
Vue
基于 Object.defineProperty
函数,可以对变量进行依赖收集,从而在变量的值改变时触发视图的更新。简单点来讲就是:
Vue
需要知道用到了哪些变量,不用的变量就不管,在它(变量)变化时, Vue
就通知对应绑定的视图进行更新。
举个例子:
Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var 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) { var 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(); } // #7981: for accessor properties without setter if (getter && !setter) { return } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify(); } });
这段代码做了哪些事情呢?主要有以下几点:
- 对于
obj[key]
,定义它的get
和set
函数 - 在
obj[key]
被访问时,触发get
函数,调用dep.depend
函数收集依赖 - 在
obj[key]
被赋值时,调用set
函数,调用dep.notify
函数触发视图更新
如果你再深入探究下去,那么还会发现 dep.notify
函数里还调用了 update
函数,而它恰好就是 Watcher
类所属
的方法,上面所提到的 computed
属性的计算方法也恰好也属于 Watcher
类
Observer
前面所提到的 Object.defineProperty
函数到底是在哪里被调用的呢?答案就是 initData
函数和 Observer
类。
可以归纳出一个清晰的调用逻辑:
- 初始化
data
函数,此时调用initData
函数 - 在调用
initData
函数时,执行observe
函数,这个函数执行成功后会返回一个ob
对象 -
observe
函数返回的ob
对象依赖于Observer
函数 -
Observer
分别对对象和数组做了处理,对于某一个属性,最后都要执行walk
函数 -
walk
函数遍历传入的对象的key
值,对于每个key
值对应的属性,依次调用defineReactive$$1
函数 -
defineReactive$$1
函数中执行Object.defineProperty
函数 - ...
感兴趣的可以看下主要的代码,其实逻辑跟上面描述的一样,只不过步骤比较繁琐,耐心阅读源码的话还是能看懂。
initData
function initData (vm) { var data = vm.$options.data; data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}; if (!isPlainObject(data)) { data = {}; ... } // proxy data on instance var keys = Object.keys(data); var props = vm.$options.props; var methods = vm.$options.methods; var i = keys.length; while (i--) { var key = keys[i]; ... if (props && hasOwn(props, key)) { ... } else if (!isReserved(key)) { proxy(vm, "_data", key); } } // observe data observe(data, true /* asRootData */); }
observe
function observe (value, asRootData) { if (!isObject(value) || value instanceof VNode) { return } var ob; if (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
var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); } };
更加方便的定义响应式属性
文档中提到, Vue
建议在根级声明变量。通过上面的分析我们也知道,在 data
函数中
声明变量则使得变量变成「响应式」的,那么是不是所有的情况下,变量都只能在 data
函数中
事先声明呢?
$set
Vue
其实提供了一个 $set
的全局函数,通过 $set
就可以动态添加响应式属性了。
export default { data () { return {} }, created () { this.$set(this, 'b', 666) }, }
然而,执行上面这段代码后控制台却报错了
<font color=Red> [Vue warn]: Avoid adding reactive properties to a Vue instance or its root $data at runtime - declare it upfront in the data option. </font>
其实,对于已经创建的实例, Vue
不允许动态添加根级别的响应式属性。
$set
函数的执行逻辑:
- 判断实例是否是数组,如果是则将属性插入
- 判断属性是否已定义,是则赋值后返回
- 判断实例是否是
Vue
的实例或者是已经存在ob
属性(其实也是判断了添加的属性是否属于根级别的属性),是则结束函数并返回 - 执行
defineReactive$$1
,使得属性成为响应式属性 - 执行
ob.dep.notify()
,通知视图更新
相关代码:
function set (target, key, val) { if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target)))); } 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 } var ob = (target).__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 } c(ob.value, key, val); ob.dep.notify(); return val }
数组操作
为了变量的响应式, Vue
重写了数组的操作。其中,重写的方法就有这些:
push pop shift unshift splice sort reverse
那么这些方法是怎么重写的呢?
首先,定义一个 arrayMethods
继承 Array
:
var arrayProto = Array.prototype; var arrayMethods = Object.create(arrayProto);
然后,利用 object.defineProperty
,将 mutator
函数绑定在数组操作上:
def(arrayMethods, method, function mutator () { ... })
最后在调用数组方法的时候,会直接执行 mutator
函数。源码中,对这三种方法做了特别
处理:
push unshift splice
observeArray
方法(这里的逻辑就跟前面提到的一样了)
最后的最后,调用 notify
函数
核心代码:
methodsToPatch.forEach(function (method) { // cache original method var original = arrayProto[method]; def(arrayMethods, method, function mutator () { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; var result = original.apply(this, args); var ob = this.__ob__; var inserted; switch (method) { case 'push': case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); } if (inserted) { ob.observeArray(inserted); } // notify change ob.dep.notify(); return result }); });
总结
「响应式原理」借助了这三个类来实现,分别是:
Watcher Observer Dep
初始化阶段,利用 getter
的特点,监听到变量被访问 Observer
和 Dep
实现对变量的「依赖收集」,
赋值阶段利用 setter
的特点,监听到变量赋值,利用 Dep
通知 Watcher
,从而进行视图更新。
参考资料
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 深入理解vue响应式原理
- 深入了解 Vue 响应式原理(数据拦截)
- 深入剖析Vue源码 - 响应式系统构建(上)
- 【2019 前端进阶之路】深入 Vue 响应式原理,活捉一个 MVVM(超详细!)
- 理解响应者和响应链如何处理事件
- 从源码解析vue的响应式原理-响应式的整体流程
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
100个可操作的网络赚钱方法
陶秋丰 / 云南科技 / 2009-12 / 29.80元
《100个可操作的网络赚钱方法》专为有志于网上创业的读者量身打造,作者是“实战型”的网赚高手,在17岁时就通过互联网创业“年人10万”,如今结合自身的亲身实战经验,与大家分享可以实实在在盈利的100个网络赚钱方法和技巧。内容包括:网站创建与推广、竞价广告、联盟赚钱、网站SEO优化、域名投资、广告投放盈利、威客、博客、淘客赚钱等多个方面。 本手册中作者结合自身的网络赚钱经历,通过具体的、可操作......一起来看看 《100个可操作的网络赚钱方法》 这本书的介绍吧!