一个开始

有如下代码,full是一个计算属性,开始,他的值是'hello world',1s后,msg变成了‘I like’, full的值同步变成了'I like world';其原理解析来看一下。

<div id="app">
<span :msg="msg"></span>
<div> {{full}}</div>
</div>
<script src="./vue.js"></script>
<script>
new Vue({
el: '#app',
data: {
msg: 'hello ',
},
computed: {
full() {
return this.msg + 'world';
},
},
mounted() {
setTimeout(() => {
this.msg = 'I like ';
}, 1000);
}
}) </script>

从入口开始

new Vue时,首先vue执行了_init方法,在这里做了vue的初始化工作,其中执行了一个initState函数,该函数进行的数据处理。

函数内容如下

function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
if (opts.methods) { initMethods(vm, opts.methods); }
// data数据绑定,数据驱动核心
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
// 计算属性绑定
if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}

其中有两个核心流程,data绑定和computed初始化。首先来看一下计算属性初始化干了什么事。

计算属性初始化

执行initComputed时,会执行以下操作,会为每一个computed属性创建watcher并且执行defineComputed,在开始的示例中,会给full属性new一个watcher。

function initComputed (vm, computed) {
// 创建watchers对象缓存watcher
var watchers = vm._computedWatchers = Object.create(null);
for (var key in computed) {
// 计算属性的执行函数/get、set描述对象
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
if (!(key in vm)) {
defineComputed(vm, key, userDef);
}
}
}

defineComputed做了什么呢?

function defineComputed (
target,
key,
userDef
) {
// 只论述传入的值为函数的情况
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
watcher.depend();
return watcher.evaluate()
}
}
sharedPropertyDefinition.set = noop;
}
// 进行了属性的描述定义。
Object.defineProperty(target, key, sharedPropertyDefinition);
}

defineComputed函数会给当前的vue实例挂载计算属性,defineProperty定义其描述,其中get执行的函数如上。

那么现在回到开始。开始的示例中,定义了full计算属性,并且template中使用了full属性,当模板中渲染full时,做了什么(这是vnode解析并渲染部分)?我们假设会获取full的值并且填充到模板中

,因此我们会触发了full的get函数,就是以上get代码。

首先获取当前vue实例中的计算属性对应的监听器Watcher,然后进行depend方法执行,然后执行evaluate()方法,接下来我们走进监听器Watcher。

Vue中的Watcher

简单介绍watcher的作用,watcher顾名思义,监听器,1.监听什么2.要干什么事,这是我们关心的。

var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
this.deep = !!options.deep;
this.computed = !!options.computed;
} else {
this.deep = this.user = this.computed = this.sync = false;
}
this.cb = cb;
this.id = ++uid$1; // uid for batching
this.active = true;
this.dirty = this.computed; // for computed watchers
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
}
// 如果是计算属性,执行if,如果不是执行else
if (this.computed) {
this.value = undefined;
this.dep = new Dep();
} else {
this.value = this.get();
}
};

以上代码是Watcher的入口,看我们关心的入参数:

vm:当前vue对象

expOrFn:监听器触发的函数(2.要干什么)

options:其他参数

计算属性在new Watcher时,会传入getter函数给expOrFn,从上面代码看,如果expOrFn是函数,就会给getter属性赋值expOrFn,这是没问题的。

同时计算实行new Watcher时,传递{computed: true}给options, 从以上代码看出,如果是计算属性的watcher,会与其他watcher不同的逻辑。

计算属性的Watcher会new Dep赋值给this.dep。

那么Watcher到底是干嘛的,Watcher是监听器,Vue会提供观察者去订阅他,如果观察者发现需要更新某个操作,会通知Watcher,watcher会执行update进行更新。

那么Dep是什么。

Vue中的Dep

Dep是个订阅器,观察者想要订阅监听器,需要订阅器Dep来实现。

同时计算属性的Watcher也会有订阅器,那么他订阅什么呢?同样是其他的Watcher,比如render Watcher, 当计算属性发生变化时,他可以通知render Watcher进行view渲染。

回到主链路

现在我们知道了,计算属性初始化会new Watcher,并计算属性在渲染到视图层时会触发getter,getter中计算属性会触发自己的watcher的两个函数,depend和evaluate,

depend函数会将当前的订阅对象添加到自己的订阅器中,此时的订阅对像则是render watcher,此步骤可以自己做详细了解。

主要的逻辑在evaluate中,evaluate中如果是计算属性并且没有被污染则调用get方法,来看一下get方法

Watcher.prototype.get = function get () {
// 将自身设置为订阅对象
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};

get方法中,首先,计算属性的watcher会将自己设置为订阅对象,共观察者订阅。然后执行getter,那么this.getter我们前面提到了,是我们写的计算属性函数 () {return this.msg + 'world'};

当此getter执行时,我们来想一下。this.msg触发了msg属性的get,那么我们前面提到vue启动了2个核心流程 ,那么这里computed的流程中进入到了data流程中。

initData简介

function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
observe(data, true /* asRootData */);
}

简化后,initData就做了这个事情,将data包装为观察者,observe方法中最终会针对data中每一个属性做defineReactive操作,并且递归调用。

defineReactive便是我们双向数据绑定的主要部分。vue将msg属性进行defineReactive重写get/set,并且将它作为一个观察者。

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 ("development" !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});

当我们执行this.msg, 进行msg的get时,以上get方法执行,并且此时我们说过计算属性full将自己的watcher设置为订阅对象Dep.target,因此msg的get中会执行dep.depend,depend方法中会将当前的Dep.target添加到订阅器中,因此msg的订阅列表会有full的watcher。

再次回到主链路

前面说到,计算属性在初始化时会创建一个watcher,并且计算属性会被定义为vue实例的一个属性Object.defineProperty,并且其getter会触发自身watcher的两个方法。

sharedPropertyDefinition.get = function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
watcher.depend();
return watcher.evaluate()
}
}

getter的返回值是watcher.evaluate();

Watcher.prototype.evaluate = function evaluate () {
if (this.dirty) {
this.value = this.get();
this.dirty = false;
}
return this.value
};

evaluate方法返回了this.value,其实此时value就是计算好的值 hello world。计算的逻辑上面讲述了,连贯的叙述一遍:

在full计算属性getter执行时,会使用this.msg的值,触发this.msg的get,在这里,发现目前拥有被观察对象Dep.target(也就是计算属性full的监听器),msg的订阅器会添加此观察对象进行观察,msg getter返回msg的值,因此full的getter执行完毕,返回了'hello world',这就是初始化的整个过程。


计算属性的动态性实现

计算属性的初始化已经讲述完成了。那么在msg改变时,full怎么动态改变的呢。

大概你应该明白么,前面讲到了,msg作为双向数据绑定的属性,会包装为观察者,其有订阅器会订阅监听器。当full计算属性初始化时,msg的订阅器订阅了full的watcher,

那么在msg值改变时,也就是setter时,我只需要通知full的watcher同步更新就好了。

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 ("development" !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
console.log(dep)
dep.notify();
}
});

dep.notify()执行后,会通知所有观察的watcher进行更新,因此full的watcher自然也会触发更新,

Watcher.prototype.update = function update () {
var this$1 = this;
// 如果是计算属性,执行这里
if (this.computed) {
this.getAndInvoke(function () {
this$1.dep.notify();
});
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};

按照我们的代码走,执行到getAndInvoke。

Watcher.prototype.getAndInvoke = function getAndInvoke (cb) {
var value = this.get();
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// set new value
var oldValue = this.value;
this.value = value;
this.dirty = false;
if (this.user) {
try {
cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
cb.call(this.vm, value, oldValue);
}
}
};

通过getAndInvoke方法,我们又一次执行了this.get,此时,msg值已经变为了'I like ',因此这里获取到了新的full值。并执行了cb,cb是什么呢,就是上一步的代码

this.getAndInvoke(function () {
this$1.dep.notify();
});

this$1指向计算属性full的watcher对象自己,this$1.dep是full watcher的订阅器,这段代码就是通知full watcher订阅的watcher进行update。前面说到,计算属性在初始getter时候,进行了

watcher.depend并添加了订阅对象render watcher,所以在这里,计算属性通知更新的watcher也就是render watcher。 render watcher是什么,是整个vue实例的渲染watcher,承载着vnode

渲染真实dom的角色。

结尾

到这里此次分析已经完成了,此次分析从computed初始化为入口,以双向数据绑定为辅助,完成的整个解析思路,以上代码片段均已删减。

能力有限,源码比较庞大,有些错误的地方请指正。

vue v2.5.17-beta.0版本。

Vue的computed计算属性是如何实现的的更多相关文章

  1. Vue之computed计算属性

    demo.html <!DOCTYPE html> <html lang="en" xmlns:v-bind="http://www.w3.org/19 ...

  2. Vue中computed计算属性

    话不多说,使用方法直接上代码//在模板中调用computedTest这个函数,切记不要在函数后添加()<template> <div class="home"&g ...

  3. Vue中computed(计算属性)、methods、watch的区别

    实现效果:字符串的动态拼接 methods方法 html: <div id="app"> <!-- 监听到文本框数据的改变 --> <input ty ...

  4. vue的computed计算属性

    computed可定义一些函数,这些函数叫做[计算属性] 只要data里面的数据发生变化computed会同步改变 引用[计算属性]时不要加  () ,应当普通属性使用 例:console.log(t ...

  5. Vue的computed(计算属性)使用实例之TodoList

    最近倒腾了一会vue,有点迷惑其中methods与computed这两个属性的区别,所以试着写了TodoList这个demo,(好土掩面逃~); 1. methods methods类似react中组 ...

  6. vue中computed计算属性与methods对象中的this指针

    this 指针问题 methods与computed中的this指针 应该指向的是它们自己,可是为什么this指针却可以访问data对象中的成员呢? 因为new Vue对象实例化后data中的成员和c ...

  7. 小白学习vue第三天,从入门到精通(computed计算属性)

    computed计算属性 <body> <div id="app"> <div>{{myName}}</div> </div& ...

  8. 深入理解 Vue Computed 计算属性

    Computed 计算属性是 Vue 中常用的一个功能,我们今天来说一下他的执行过长 拿官网简单的例子来看一下: <div id="example"> <p> ...

  9. Vue(七):computed计算属性

    简介 计算属性关键词: computed. 计算属性在处理一些复杂逻辑时是很有用的. 实例1 可以看下以下反转字符串的例子: <div id="app"> {{ mes ...

随机推荐

  1. hive中文字符乱码 解决方法【转】

    一.个人初始开发环境的基本情况以及Hive元数据库说明 ①hive的元数据库改成了mysql(安装完mysql之后也没有进行其它别的设置) ②hive-site.xml中设置元数据库对应的配置为  j ...

  2. json2.js的作用与使用示例

    工作中,如果公司要求你兼容ie6.7,那么你可以辞职了,开个玩笑: 关于json,本文不作介绍,介绍一下json字符串和对象的相互转换: 在各大主浏览器及ie8+,我们可以使用内置方法JSON.str ...

  3. [2012山东ACM省赛] Pick apples (贪心,全然背包,枚举)

    Pick apples Time Limit: 1000MS Memory limit: 165536K 题目描写叙述 Once ago, there is a mystery yard which ...

  4. 【原型图】Mockplus

    Mockplus   原型设计工具

  5. SQL语句查询关键字中含有特殊符号怎么处理, 例如 'SMI_'

    SQL语句查询关键字中含有特殊符号怎么处理, 例如 'SMI_' 错误:select * from emp  where ename like '%SML_%' 正确:select * from em ...

  6. Delphi的idhttp报508 Loop Detected错误的原因

    一般是访问https时才出现“508 Loop Detected”,idhttp+IdSSLIOHandlerSocketOpenSSL,这个在上篇文章中讲过了. 由于该问题网上资料极少,连外文资料也 ...

  7. Oracle Data Provider for .NET – Microsoft .NET Core and Entity Framework Core

    http://www.oracle.com/technetwork/topics/dotnet/tech-info/odpnet-dotnet-ef-core-sod-4395108.pdf Orac ...

  8. 传统路由和OVS区别

    本文主要描述了一种将三层路由变成二层交换转发(以及二层转发变成三层路由)的实现方式,以应对OVS(OpenFlow)跨网段路由复杂的问题:当然技术本身是客观的,具体应用还要看场景. 随着SDN技术不断 ...

  9. 一个将当前目录下HEX文件的第一行数据删除的程序

    为什么要写这样一个函数 在使用SoftConsole开发M3程序时,生成的hex文件,必须要把第一行数据删除,才能在Libero中使用,所以写了这个小工具,这是2.0版本了,第一版是直接删除第一行数据 ...

  10. ant property file刷新不及时

    一.问题 ant脚本定义file的property,有时往里面写了新的值,去访问时还是旧的值 二.原因分析 应该是已定义的file property,后续更新其值的时候,ant的内存缓存没有及时更新, ...