赞
踩
get/set:javascript中的对象属性分为两类,分别是数据属性和访问器属性。数据属性可以真实地用来存取数据,而访问器属性则只对外提供一组get/set操作api。当我们读取访问器属性时,实际上就是在调用对应的get方法,而在设置访问器属性的值时,则是在调用对应的set方法。数据属性直接定义在对象中即可,而访问器属性一般使用Object.defineProperty(它也可以用来定义数据属性,但一般没有必要这样做)来定义,该api的前两个参数分别是目标对象和要定义的属性名,第三个参数则是属性描述符,对于访问器属性一般需要提供get和set两个方法,用于在读写该属性时调用,如:
let message = {
title: "old" // 数据属性
};
let value = "old";
Object.defineProperty(message, "title", {
get: function(){
console.log("你现在调用了message.title的get方法!");
return value; // 该访问器属性返回value的值
},
set: function( newVal ){
console.log("你现在正在修改message.title的值");
value = newVal; // 设置该属性的值时,实际上是写入到了value中
}
})
现在我们为message对象设置了一个名为title的访问器属性,当我们执行let temp = message.title时,控制台就会输出get方法中的提示消息,并把value的值赋值给temp变量;而执行message.title = "abc"时,就会执行set方法中的提示消息,并把"abc"写入到value变量中。通过定义这两个方法,我们就可以对数据的变化进行监听,Vue的响应式系统正是基于该接口实现的。
注意:这是目前2.x版本Vue的实现原理,Vue的作者表示3.0版本将使用ES6提供的proxy(代理)来实现。
Vue的响应式系统是一个精心搭建的数据监控系统,它负责监测项目中的数据变化,然后通知对该数据“感兴趣”的订阅者进行相关操作。
数据的监测分为两类,一类是对对象的监测,另一类是对数组的监测。对对象监测的基本原理是设置对象每个属性的get/set方法,使其变为响应式属性;对数组监测的基本原理是拦截数组的七个原型方法(如push、pop等),并将数组的对象或数组成员继续进行响应式转化。
我们先来理解“数据”、“感兴趣”以及“订阅者”这三个关键词。
这里指的数据,就是options中的data配置项,它通过以下两种方式定义:
//单文件中使用Vue var app = new Vue({ el: "#app", data: { ... }, template: "", ... }) //单文件组件 <template> ... </template> <script> name: "", data(){ return { ... } } </script>
通常我们把与数据操作相关的逻辑称为业务逻辑,而这些数据如何展现在页面上则是由视图层负责。MVVM的核心思想正是让开发者把目光聚焦于业务逻辑,关注如何进行数据操作,而不是考虑视图如何更新(虽然Vue不是严格的MVVM框架,但这里的思想是一致的)。
那么什么叫“感兴趣”呢?我们举个例子来说明,假设有下面一个组件:
//CustormForm.vue <template> <h1>{{ title }}</h1> </template> <script> name: "custom-form", data() { title: "表单", }, watch:{ "form.title": function(newVal, oldVal){ console.log("标题发生了变化:" + newVal); } } </script>
现在组件中有一个被监听的数据title。我们在模板中以{{ title }}的形式将它的值绑定在模板里,这样在渲染该组件时,Vue就会把title的值替换进去,模板会被渲染为下面的样子:
<h2>表单</h2>
如果之后title的值发生了变化,Vue的响应式系统就会监听到这个变化,然后更新上面的视图。因为这里视图的渲染和更新需要依赖title的值,而在Vue中是借助一个watcher(订阅者)来根据数据变化进行视图的渲染和更新的,因此我们说这个watcher对title“感兴趣”。类似的,上面我们在watch中对title注册了一个回调函数,它会在title的值变化后,向控制台输出新的title值。而Vue也是通过生成一个watcher来负责根据数据变化执行上述回调函数的,因此这个watcher也对title的变化“感兴趣”。
简单来说,只要是关注某个数据的变化,就是对该数据“感兴趣”。
那什么是订阅者呢?
一个订阅者就是一个在数据变化后可以执行一定操作的javascript对象(也就是上面提到的watcher)。具体来说,在上面的例子中,当title发生变化时,会有一个对象负责重新渲染视图,同时会有另一个对象负责向控制台输出新的值。这两个对象都是订阅者。它们会被注册到title的依赖者列表中,当title变化时,就会调用它们的update方法进行对应的操作。
简单来说,订阅者就是数据变化时执行操作的对象。
响应式系统的简介就到这里,下面我们来看响应式系统的实现。现在假如我们有如下的Vue实例:
let app = new Vue({
el: "#app",
template: "<div>{{message.title}}</div>",
data: {
message: {
title: ""
},
author: []
}
})
我们以上面的例子为基础,分别介绍Vue对对象和数组的监听原理。
对象的监测系统基于三个核心类:Observer(观察者)、Dep(依赖)和Watcher(订阅者)。
从作用上来看:
从结构上来看:
data(){
return {
message: {title: ""}
}
}
执行new Observer(message)就可以把message变成响应式的对象,执行之后的message对象将变成(这里省略了__proto__属性,它与响应式系统无关):
message: {
title: "",
__ob__: Observer {value: {…}, dep: Dep, vmCount: 0},
get str: ƒ reactiveGetter(),
set str: ƒ reactiveSetter(newVal)
}
message新增了一个属性__ob__,它是一个Observer实例。具有该属性也表明message已经被封装为了一个响应式的对象(即它的属性具备在值发生变化时自动触发某些事件的能力)。get和set实际上是由它的父对象(在这里就是data)执行Object.defineProperty时为它定义的,get用于收集对message的变化“感兴趣”的订阅者,set用于触发响应。
message: {
title: "",
__ob__: {
dep: Dep {id: 1, subs: Array(0)},
value: {__ob__: Observer},
vmCount: 0
},
get str: ƒ reactiveGetter()
set str: ƒ reactiveSetter(newVal)
}
上面是作为属性存在的Dep,用于收集对当前数据“感兴趣”的watcher,另外还有一种作为闭包存在的Dep,后面会讲到。
message: {
title: "",
__ob__: {
dep: {
id: 1,
subs: [watcher1, watcher2, ...]
},
value: {__ob__: Observer},
vmCount: 0
},
get str: ƒ reactiveGetter()
set str: ƒ reactiveSetter(newVal)
}
每个watcher定义了在数据发生变化时该进行什么样的操作,它向外提供了一个update方法来执行该操作。
监测对象变化的大体思路是:在new Observer(data)时修改data每个属性的get/set(如果该属性是对象,就递归下去),并生成一个与该属性对应的Dep实例。一旦某个watcher通过get获取该属性的值,它就会被收集到对应的Dep实例的subs数组中。当属性值变化时,就会触发属性的set,Dep实例会依次调用subs中收集的watcher提供的update方法执行操作。
注意:响应式系统在进行响应式转化的时候是递归的,因此如果对象的某个属性是对象,那么它的每个属性也都会被递归地转化为响应式属性。有人可能疑惑,基本数据类型的属性在Vue中不是响应式的吗?当然不是。因为Vue构造响应式系统时最基本的操作是将一个对象的属性(而不是对象本身)转化为响应式的。也就是说,只要是以某个对象的属性存在的,它就是响应式的(无论它是什么类型的属性,data参数里声明的变量,都是以data的属性存在的)。同样这也意味着,作为响应式系统入口的根对象data本身不是响应式的(实际上Vue不允许直接修改该对象)。
上面说到,Vue的整个响应式系统是以data对象为入口的。我们回顾一下Vue实例的初始化过程:
//摘自src/core/instance/index.js function Vue(options){ ... this._init(options); } initMixin(Vue); ... //摘自src/core/instance/init.js function initMixin(){ Vue.prototype._init = function(options){ const vm = this; ... initState(vm); ... } } //摘自src/core/instance/state.js function initState(vm){ vm._watchers = [] const opts = vm.$options ... if(opts.data){ initData(vm); } } function initData(vm){ let data = vm.$options.data; ... observe(data, true/* as root data */); }
从observe函数开始,我们就正式进入到响应式系统的模块了。这个observe函数引自src/core/observer/index.js,负责将一个普通对象的属性转化为响应式的,这里我们要进行转化的就是根数据对象data。
首先我们来看observe函数所在文件的大致结构,它是响应式系统的入口文件。
//摘自src/core/observer/index.js import Dep from './dep' import VNode from '../vdom/vnode' import { arrayMethods } from './array' ... //定义Observer类 export class Observer { value: any; dep: Dep; vmCount: number; constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) //如果是数组,需要使用observeArray进行监测 if (Array.isArray(value)) { //向数组原型添加拦截器,如果浏览器不支持__proto__,则直接添加到数组上 //这里会在数组篇讨论 if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { //如果是对象,则使用walk遍历所有属性 this.walk(value) } } //用for循环遍历所有属性,使用defineReactive将其转化为响应式属性, //这个defineReactive是对象观测的核心方法 walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } //观测数组,第二篇讨论 observeArray(){ ... } } ... //观测对象的方法,负责将value转化为响应式对象 function observe (value, asRootData){ //为了方便理解,这里对源码进行了提取 let ob; //当前对象有__ob__属性,说明已经是响应式对象了,直接返回__ob__属性即可 if(hasOwn(value, __ob__)){ ob = value.__ob__; } else if(/*value具备转化为响应式对象的条件*/){ ob = new Observer(value); } ... return ob; } //负责将对象属性转化为响应式属性,这个函数接下来将详细分析 function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ){ ... }
我们知道,响应式系统的监测对象就是当前Vue实例的data对象(这个监测是递归的,也就是说data内的任何后代属性都会被监测),而对data的观测是以observe(data)为入口的。代码中value(此时,局部变量value就指代data)转化为响应式的流程如下:
下面我们详细来看defineReactive函数的实现:
function defineReactive ( obj: Object, //当前属性所属的对象 key: string, //需要转化为响应式的属性 val: any, //当前的属性值 customSetter?: ?Function, //自定义的setter shallow?: boolean //是否为浅观测,即如果当前属性是对象,是否要递归监测子属性 ) { //闭包形式存在的Dep实例,用于收集对当前属性“感兴趣”的watcher const dep = new Dep() ... //如果是深度监测,就递归调用observe函数监测该属性的子属性 let childOb = !shallow && observe(val) //修改该属性的get/set,响应式系统的核心 Object.defineProperty(obj, key, { enumerable: true, //规定该属性可遍历 configurable: true, //规定该属性可配置(可修改) get: function reactiveGetter () { //先取出该属性的值 const value = getter ? getter.call(obj) : val //当某个watcher取当前属性的值时,就会将自身赋值给Dep类的 //静态属性target,这样在get中就可以把该watcher添加到dep中 if (Dep.target) { dep.depend() //如果当前属性还有子属性,那么子属性的dep中也要添加当前watcher, //因为当前属性变化意味着其内存地址发生了变化,显然它的子属性也会变化 if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { //这里的getter是修改前的get方法,先获取该属性的原始值 const value = getter ? getter.call(obj) : val //只在属性值变化时才发出通知,当值不发生变化时(如a.b = a.b)是不需要触发响应式操作的 if (newVal === value || (newVal !== newVal && value !== value)) { return } //允许开发者提供自定义的setter if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } //如果setter不存在,说明当前属性不允许修改,直接return if (getter && !setter) return //调用setter修改属性,或直接通过赋值修改属性 if (setter) { setter.call(obj, newVal) } else { val = newVal } //将该属性新的值转化为响应式的 childOb = !shallow && observe(newVal) //数据发生了变化,通知dep去触发watcher dep.notify() } }) }
让我们来仔细分析一下上面的代码。
这里的第三步我们还需要详细展开。首先我们需要先了解一下Dep和Watcher的结构。
Dep:
class Dep { //静态属性,watcher在访问某个属性时,会临时把自身保存在该静态属性中, //然后在该属性的get方法中,通过dep.depend()将这个watcher添加到依赖列表中 static target: ?Watcher; id: number; //dep实例的唯一id subs: Array<Watcher>; //依赖者列表 //构造函数,只是简单的初始化 constructor () { this.id = uid++ this.subs = [] } //向依赖数组subs中添加watcher的方法 addSub (sub: Watcher) { this.subs.push(sub) } //从依赖数组中移除watcher,取消对某个属性的监听时会用到 removeSub (sub: Watcher) { remove(this.subs, sub) } //将当前dep注册到目标watcher的depIds中,防止重复注册,同时, //watcher的addDep方法会将自身添加到dep的subs中,这里的Dep.target //是一个watcher实例,调用的addDep是一个watcher的方法 depend () { if (Dep.target) { Dep.target.addDep(this) } } //属性值变化时通知dep的接口 notify () { ... //依次调用依赖者列表中每个watcher的update方法, //这里就是对数据变化进行响应的分发接口 for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
我们看到,dep的作用就是收集watcher,并在数据变化时依次触发这些watcher的update。
然后我们看一下watcher的结构:
class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; lazy: boolean; sync: boolean; dirty: boolean; active: boolean; deps: Array<Dep>; newDeps: Array<Dep>; depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { //渲染类watcher,在模板中绑定数据时就是这类watcher vm._watcher = this } vm._watchers.push(this) //将watcher注册到当前Vue实例的列表中 ... //调用watcher的get方法为value赋值 this.value = this.lazy ? undefined : this.get() } get () { //将当前的watcher添加为Dep.target,这样在取目标属性值时就会触发 //依赖收集,dep就会把Dep.target(即当前watcher)添加到依赖者列表 pushTarget(this) let value const vm = this.vm //这里是简写,源码中还包括取值失败时的操作,以及对属性的深度观测 //这里会触发目标属性的get方法,由于已经在上面将当前watcher赋值 //给Dep.target,这样dep就可以把当前watcher收集进依赖列表 value = this.getter.call(vm, vm) ... popTarget() //释放对Dep.target的占用 this.cleanupDeps() //释放失效的依赖 return value } //将当前watcher添加到传过来的dep的subs数组中,完整依赖收集,同时将 //该dep的id添加到自身相关的dep列表中,防止重复触发依赖收集 addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } } //清除已经失效的dep cleanupDeps () { ... } //watcher提供的更新方法,它负责定义数据变化时的操作 update () { if (this.lazy) { //懒更新,给当前watcher打上标志 this.dirty = true } else if (this.sync) { //同步更新,直接调用run重新渲染视图或执行回调 this.run() } else { //异步更新,将当前watcher推入循环队列,等主线程操作完成再触发回调 queueWatcher(this) } } //数据变化时需要执行的实际操作 run () { ... //run方法最重要的就是执行传入的回调函数,可能是虚拟DOM的patch方法, //也可能是通过$watch定义的回调函数。当是前者时,当前的watcher就是 //一个renderWatcher(渲染类watcher),通过虚拟DOM提供的patch //方法进行视图更新 this.cb.call(this.vm, value, oldValue); ... } //取目标属性值 evaluate () { this.value = this.get() this.dirty = false } ... //注销当前watcher teardown () { if (this.active) { if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) //从实例的_watchers中移除当前watcher } let i = this.deps.length while (i--) { this.deps[i].removeSub(this)//从与它相关的dep中移除当前watcher } this.active = false } } }
现在我们可以重新来理解defineReactive的第三步所做的事了。我们把defineReactive的核心部分再精简一下:
function defineReactive (obj, key, val, customSetter, shallow) { const dep = new Dep() ... Object.defineProperty(obj, key, { ... get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() ... } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val ... val = newVal dep.notify() } }) }
我们来看上面的结构,首先通过闭包为当前属性生成一个dep,然后修改该属性的get/set。在之后的某个时间(如开发者通过$watch方法对该属性进行了监听),那么Vue就会执行new Watcher(vm)来生成一个watcher负责执行开发者传入的回调。该watcher在初始化时会取当前属性的值,这会触发该属性的get,由于watcher在取值之前已经把自己添加到Dep.target中,因此该属性对应的dep就会通过depend方法,将该watcher保存到自己的依赖者列表(subs)中(之所以选择Dep的静态属性来临时保存watcher,是因为dep实例和watcher实例都可以访问到它)。这样就完成了依赖收集。
随后,如果该属性的值发生了变化,就会触发它的set方法。而在set方法中,Vue通过dep.notify()将这个变化通知给dep。这个方法的行为就是遍历当前dep的依赖者列表subs,依次调用subs中每个watcher的update方法。最终watcher的update方法就会执行开发者传入的回调函数。就这样,属性值的变化最终导致回调函数的自动调用。如果这个回调函数是更新视图用的,那么数据的变化将引起视图的自动更新。
如果当前的watcher是渲染类watcher(也就是更新视图用的watcher),那么它的回调将是虚拟DOM提供的patch方法,只要调用这个patch方法,就可以对虚拟DOM进行修补,根据修补结果进行视图更新。这样数据的变化就导致了视图的自动更新,这是响应式系统在Vue中最大的价值所在。patch的实现会在虚拟DOM部分进行探讨。
提示:了解过Vue的同学应该知道,Vue无法监听到对象属性的添加和删除。如我们在代码中手动为message对象添加了一个属性message.date = “”,或者手动删除了一个属性delete message.title,Vue无法知道我们进行了这样的操作。原因是为对象添加和删除属性既不会触发对象的get/set,也不会触发该属性的get/set(因为实际上我们没有操作该属性的“值”,只有对属性值的操作才会触发get/set),因此这些操作导致的数据变化无法被Vue监测到。官网建议对于需要用到的属性,即使没有初始值,也需要提前传入data并赋予一个空值,以进行响应式转化。而要删除属性时,可以使用Vue提供的响应式方法$delete,如this.$delete(message, “title”)。
在Vue的响应式里,普通对象和数组(实际上也是对象)是区分对待的。对于普通对象,上一篇已经详细介绍,这里不再赘述;对于数组来说,Vue会通过设置拦截器来拦截数组原型对象上的七个方法push、pop、shift、unshift、splice、sort、reverse。如果开发者用到了这七个方法,实际上调用的是Vue的同名方法,而不是真正的数组原型方法。不过,Vue不会改变这些原型方法的默认行为,它只是向方法中添加了触发响应的逻辑。同时Vue会调用observeArray方法,来遍历数组成员,将其中对象成员的属性转化为响应式的。注意,这里只是将对象成员的属性转化为响应式,不包括该对象成员自身,举个例子:
<template> <div> <div>{{ arr[0] }}</div> <div>{{ arr[0].name }</div> <div> </template> <script> data(){ return { arr: [{name: "夕山雨"}], } }, mounted(){ this.arr[0].name = "Carter"; this.arr[0] = {name: "MrGoblet"}; } </script>
上面的例子中,我们在该实例的mounted生命周期钩子函数中分别修改了arr[0].name和arr[0]的值。然后我们会看到,前一个语句会触发视图的更新,但是后一个语句并不会导致视图更新。同样的,如果数组成员是普通成员,通过index修改它的值,也不会触发视图更新。这就是说,只要是通过index来修改数组元素的值,如arr[0] = xx,都无法触发视图更新。出现这种情况的原因,是因为Vue区分对待数组和普通对象,并且在对待数组时,没有通过Object.defineProperty来转化数组元素(实际上经过测试,这种转化是可行的,个人补充中会谈到)。下面我们就分两个阶段来看数组的监控过程,分别是:原型方法拦截和遍历数组元素进行响应式转化。
我们知道,数组原型上的push、pop、shift、unshift、splice、sort、reverse这七个方法都会导致数组的元素变化,如果我们可以在开发者调用这些方法时拦截它,并在里面添加自己的逻辑(这里指的就是触发响应式系统),那么不就可以实现对数组元素的监听了吗?这就是Vue监测数组的基本原理了。
举个例子:
let originalPush = Array.prototype.push; //先保存默认的push方法
Array.prototype.push = function(...args){
console.log("你现在调用的是被我拦截过的push方法");
//执行Array原型上原本的push方法,这样就不会改变push本身的行为
originalPush.apply(this, args);
}
let arr = [];
arr.push("name");
现在Array.prototype上的push方法已经被替换为我们自己定义的push方法了,之后调用arr.push时,实际上调用的都是我们自己的push方法,因此语句arr.push(“name”)就会在控制台输出提示信息。我们自己定义的这个push方法就被称为原始push方法的拦截器。Vue就是在这里注入了响应式系统的逻辑,实现对数组的监听。
现在我们就来看一下源码是如何对这七个方法进行拦截的。
const arrayProto = Array.prototype //将Array的原型保存在局部变量中 //原型式继承,得到一个以Array.prototype为原型的空对象 export const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ //需要拦截的方法,原型方法中只有这七个会修改数组 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] //遍历上述数组,分别为每个方法设置拦截器 methodsToPatch.forEach(function (method) { //保存原方法。拦截器不能改变原型方法的基本功能,因此后面要调用这个原始方法 const original = arrayProto[method] //为Array.prototype添加自定义拦截器,mutator就是用于拦截原始方法的拦截器 def(arrayMethods, method, function mutator (...args) { //调用原始方法,保证该方法的基本功能不变 const result = original.apply(this, args) const ob = this.__ob__ let inserted //push、unshift和splice可能向数组添加新元素, //需要将这些新添加的元素转化为响应式 switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } //将新添加的元素纳入响应式系统 if (inserted) ob.observeArray(inserted) //通知dep数据发生了改变 ob.dep.notify() return result }) })
经过上述步骤,我们得到了如下一个对象arrayMethods:
arrayMethods有7个实例方法,也就是我们定义的拦截器,它的原型对象正是数组的原型对象。假如我们把arrayMethods替换为一个数组的原型对象,那么根据原型链的查找规则,arrayMethods中的七个实例方法将覆盖它的原型(也就是数组真正的原型)上的七个同名方法。而除了这七个方法以外的其他方法都可以借助原型链正常访问,也就是说我们成功的拦截了这七个方法,并保证了其他方法不受影响。
Vue中并没有直接把上述arrayMethods替换到Array.prototype(即Array.prototype = arrayMethods,可能是为了避免对原生类型Array进行操作,因为这样会影响到所有的数组实例),而是直接替换当前数组实例的__proto__(它默认指向构造函数Array的原型对象,但是修改__proto__的指向只会影响到当前的数组实例),如果当前浏览器不支持__proto__,Vue会将这七个拦截器直接添加到数组实例上,作为实例方法存在。代码如下:
if (Array.isArray(value)) { if (hasProto) { //浏览器支持__proto__ protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) //观测数组 } //数组实例的__proto__存在时,将它指向我们上面的对象 function protoAugment (target, src: Object) { /* eslint-disable no-proto */ target.__proto__ = src /* eslint-enable no-proto */ } //浏览器不支持__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]) } }
现在每当我们调用数组的这七个原型方法时,调用的实际上就是Vue的拦截器。那么Vue是如何在调用这七个方法时触发响应式的呢?非常简单,就是拦截器中的一行代码:
ob.dep.notify()
因为对当前数组元素的依赖都已经被收集到了该数组对应的dep中,那么我们只需要触发dep的notify方法,就可以通知订阅者执行回调或者更新视图。无论我们通过这七个方法的哪一个修改了数组,Vue都知道数组已经被更新,需要触发响应式系统。
现在当使用这七个原型方法修改数组时已经能触发响应式了,但是数组的某个元素如果是个对象,而视图中绑定的是这个对象的属性呢?比如:
//template
<div>{{ arr[0].name }}</div>
//mounted
this.arr[0].name = "123";
显然当我们修改这些属性时,不会触发数组的原型方法(因为我们不是在修改数组元素本身,而是它的某个属性)。同时,因为在对data进行递归转化时,一旦遇到数组就会执行专门针对数组的处理方法,所以数组的对象成员此时并不是响应式的。但是我们当然希望上述语句能够触发视图更新,所以Vue又专门写了observeArray这个函数,遍历数组元素,将它的对象成员的属性也转化为响应式的。observeArray的实现如下:
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
非常简单,遍历数组成员, 使用observe函数(对象篇提到过)将每个成员转化为响应式(注意,这里转化为响应式指的是成员的属性,而不是成员自身,如同我们将data对象转化为响应式时,并没有把data自身纳入响应式系统,而是它的所有属性)。这样,this.arr[0].name = xx这样的语句就会触发视图更新了。
现在我们来看看源码中是如何分别对待对象和数组的:
export class Observer { value: any; dep: Dep; vmCount: number; constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { //判断当前value是不是数组 if (hasProto) { //对数组来说,先拦截原型上的七个方法, //如果支持__proto__,就替换原型对象, //否则直接覆盖到数组上,作为实例方法 protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } //遍历并观测数组的对象成员,将对象成员的属性转化为响应式 this.observeArray(value) } else { //这里是对象的处理逻辑 this.walk(value) } } ... }
现在只要我们是通过调用数组的那七个原型方法修改了数组,视图就会自动更新,同时数组的对象成员的属性也都是响应式的,可以触发视图更新。但是arr[0] = xx这样的操作却不是响应式的(因为observeArray只将这些对象成员的属性转化为响应式,但不包括该成员本身,这里指的数组成员,是声明在data里的,而不是后来通过arr[1000]添加进去的)。
阅读Vue的响应式系统相关的代码,最大的收获就是明白了它的工作原理,以及它为什么不能响应某些数据变化。这样,以后使用Vue时,就可以不光知其然,还可以知其所以然。
下一篇文章将讨论Vue的编译器。它的用途就是将template模板编译为渲染函数,而得到渲染函数的目的,就是通过嵌套调用生成虚拟DOM树。由于编译器的实现涉及的细节特别多,下文可能只会介绍它的实现原理,而不会完全展开分析(否则可能比本文还要长…)。所以敬请期待!
Vue源码笔记之项目架构
Vue源码笔记之初始化
Vue源码笔记之响应式系统
Vue源码笔记之编译器
Vue源码笔记之虚拟DOM
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。