基于上一篇data的双向绑定,这一篇来聊聊computed的实现原理及自己实现计算属性。

一、先聊下Computed的用法

写一个最简单的小demo,展示用户的名字和年龄,代码如下:

<body>
<div id="app">
<input type="text" v-model="name"><br/>
<input type="text" v-model="age"><br/>
{{NameAge}}
</div>
<script>
var vm = new MYVM({
el: '#app',
data: {
name: 'James',
age:18
},
computed:{
NameAge(){
return this.$data.name+" "+this.$data.age;
}
},
})
</script>
</body>

运行结果:

从代码和运行效果可以看出,计算属性NameAge依赖于data的name属性和age属性。

特点:

1、计算属性是响应式的

2、依赖其它响应式属性或计算属性,当依赖的属性有变化时重新计算属性

3、计算结果有缓存,组件使用同一个计算属性,只会计算一次,提高效率

4、不支持异步

适用场景:

当一个属性受多个属性影响时就需要用到computed

例如:购物车计算价格

只要购买数量,购买价格,优惠券,折扣券等任意一个发生变化,总价都会自动跟踪变化。

二、原理分析

1、 computed 属性解析

每个 computed 属性都会生成对应的观察者(Watcher 实例),观察者存在 values 属性和 get 方法。computed 属性的 getter 函数会在 get 方法中调用,并将返回值赋值给 value。初始设置 dirty 和 lazy 的值为 true,lazy 为 true 不会立即 get 方法(懒执行),而是会在读取 computed 值时执行。

function initComputed(vm, computed) {
// 存放computed的观察者
var watchers = vm._computedWatchers = Object.create(null);
//遍历computed属性
for (var key in computed) {
//获取属性值,值可能是函数或对象
var userDef = computed[key];
//当值是一个函数的时候,把函数赋值给getter;当值是对象的时候把get赋值给getter
var getter = typeof userDef === 'function' ? userDef: userDef.get; // 每个 computed 都创建一个 watcher
// 创建watcher实例 用来存储计算值,判断是否需要重新计算
watchers[key] = new Watcher(vm, getter, {
lazy: true
}); // 判断是否有重名的属性
if (! (key in vm)) {
defineComputed(vm, key, userDef);
}
}
}

代码中省略不需要关心的代码,在initComputed中,Vue做了这些事情:

  1. 为每一个computed建立了watcher。

  2. 收集所有computed的watcher,并绑定在Vue实例的_computedWatchers 上。

  3. defineComputed 处理每一个computed。

2、将computed属性添加到组件实例上

function defineComputed(target, key, userDef) {
// 设置 set 为默认值,避免 computed 并没有设置 set
var set = function(){}
// 如果用户设置了set,就使用用户的set
if (userDef.set) set = userDef.set
Object.defineProperty(target, key, {
// 包装get 函数,主要用于判断计算缓存结果是否有效
get:createComputedGetter(key),
set:set });
}
// 重定义的getter函数
function createComputedGetter(key) {
return function computedGetter() {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
// true,懒执行
watcher.evaluate(); // 执行watcher方法后设置dirty为false
}
if (Dep.target) {
watcher.depend();
}
return watcher.value; //返回观察者的value值
}
};
}
  1. 使用 Object.defineProperty 为实例上computed 属性建立get、set方法。

  2. set 函数默认是空函数,如果用户设置,则使用用户设置。

  3. createComputedGetter 包装返回 get 函数。

3、页面初始化时

页面初始化时,会读取computed属性值,触发重新定义的getter,由于观察者的dirty值为true,将会调用原始的getter函数,当getter方法读取data数据时会触发原始的get方法(数据劫持中的get方法),将computed对应的watcher添加到data依赖收集器(dep)中。观察者的get方法执行完后,更新观察者的value,并将dirty置为false,表示value值已更新,之后执行观察者的depend方法,将上层观察者也添加到getter函数中data的依赖收集器(dep)中,最后返回computed的value值;

4、当 computed 属性 getter 函数依赖的 data 值改变时

将会根据之前依赖收集的观察者,依次调用观察者的 update 方法,先调用 computed 观察者的 update 方法,由于 lazy 为 true,将会设置观察者的 dirty 为 true,表示 computed 属性 getter 函数依赖的 data 值发生变化,但不调用观察者的 get 方法更新 value 值。再调用包含页面更新方法的观察者的 update 方法,在更新页面时会读取 computed 属性值,触发重定义的 getter 函数,此时由于 computed 属性的观察者 dirty 为 true,调用该观察者的 get 方法,更新 value 值,并返回,完成页面的渲染。

5、核心流程

  1. 首次读取 computed 属性值时,dirty 值初始为 true
  2. 根据getter计算属性值,并保存在观察者value上并设置dirty为false
  3. 之后再读取 computed 属性值时,dirty 值为 false,不调用 getter 重新计算值,直接返回观察者中的value
  4. 当 computed 属性getter依赖的data发生变化时,再次设置dirty为true,通知页面更新,重新计算属性值

三、自定义实现

基于上一篇文章实现的自定义框架,增加computed属性的解析和绑定。

1、首先在index.html定义并使用计算属性

<body>
<div id="app">
<span v-text="name"></span>
<input type="text" v-model="age">
<input type="text" v-model="name">
{{name}}<br/>
{{fullName}}<br/>
{{fullName}}<br/>
{{fullName}}<br/>
{{fullName}}<br/>
{{fullNameAge}}<br/>
{{fullNameAge}}<br/>
</div>
<script>
var vm = new MYVM({
el: '#app',
data: {
name: 'James',
age:18
},
//定义计算属性
computed:{
fullName(){
return this.$data.name+" Li";
},
fullNameAge(){
return this.$computed.fullName+" "+this.$data.age;
}
},
})
</script>
</body>
</html>

定义了两个计算属性fullName和fullNameAge,并在模板中进行了调用。

2、MYVM.js中增加对计算属性的解析和处理

function MYVM(options){
//属性初始化
this.$vm=this;
this.$el=options.el;
this.$data=options.data;
//获取computed属性
this.$computed=options.computed;
//定义管理computed观察者的属性
this.$computedWatcherManage={}; //视图必须存在
if(this.$el){
//添加属性观察对象(实现数据挟持)
new Observer(this.$data)
new ObserverComputed(this.$computed,this.$vm); // //创建模板编译器,来解析视图
this.$compiler = new TemplateCompiler(this.$el, this.$vm)
}
}

增加$computed属性用来存储计算属性,$computedWatcherManage用来管理计算属性的Watcher,ObserverComputed用来劫持计算属性和生成对应的watcher。

3、ObserverComputed创建computed的Watcher实例,劫持computed属性

//数据解析,完成对数据属性的劫持
function ObserverComputed(computed,vm){
this.vm=vm;
//判断computed是否有效且computed必须是对象
if(!computed || typeof computed !=='object' ){
return
}else{
var keys=Object.keys(computed)
keys.forEach((key)=>{
this.defineReactive(computed,key)
})
}
}
ObserverComputed.prototype.defineReactive=function(obj,key){
//获取计算属性对应的方法
let fun=obj[key];
let vm=this.vm;
//创建计算属性的Watcher,存入到$computedWatcherManage
vm.$computedWatcherManage[key]= new ComputedWatcher(vm, key, fun);
let watcher= vm.$computedWatcherManage[key]; Object.defineProperty(obj,key,{
//是否可遍历
enumerable: true,
//是否可删除
configurable: false, //get方法
get(){
//判断是否需要重新计算属性
//dirty 是否使用缓存
//$computedWatcherManage.dep 是否是创建Watcher收集依赖时执行
if(watcher.dirty || vm.$computedWatcherManage.dep==true){
let val=fun.call(vm)
return val
}else{
//返回Watcher缓存的值
return watcher.value
} },
})
}

vm.$computedWatcherManage[key]= new ComputedWatcher(vm, key, fun);创建Watcher实例

其它的注释都比较细致,不细说了哈

4、ComputedWatcher 缓存value,管理页面订阅者,更新页面

//声明一个订阅者
//vm 全局vm对象
//expr 属性名
//fun 属性对应的计算方法
function ComputedWatcher(vm, expr,fun) {
//初始化属性
this.vm = vm;
this.expr = expr;
this.fun=fun;
//计算computed属性的值,进行缓存
this.value=this.get();
//是否使用缓存
this.dirty=false;
//管理模板编译后的订阅者
this.calls=[];
}
//执行computed属性对应的方法,并进行依赖收集
ComputedWatcher.prototype.get=function(){
//设置全局Dep的target为当前订阅者
Dep.target = this;
//获取属性的当前值,获取时会执行属性的get方法,get方法会判断target是否为空,不为空就添加订阅者
this.vm.$computedWatcherManage.dep=true
var value = this.fun.call(this.vm)
//清空全局
Dep.target = null;
this.vm.$computedWatcherManage.dep=false
return value;
} //添加模板编译后的订阅者
ComputedWatcher.prototype.addCall=function(call){
this.calls.push(call)
} //更新模板
ComputedWatcher.prototype.update=function(){
this.dirty=true
//获取新值
var newValue = this.vm.$computed[this.expr]
//获取老值
var old = this.value;
//判断后
if (newValue !== old) {
this.value=newValue;
this.calls.forEach(item=>{
item(this.value)
})
}
this.dirty=false
}

ComputedWatcher核心功能:

1、计算computed属性的值,进行缓存

2、执行computed的get方法时进行依赖收集,ComputedWatcher作为监听者被添加到data属性或其它computed属性的依赖管理数组中

3、模板解析识别出计算属性后,调用addCall向ComputedWatcher添加监听者

4、update方法获执行computed计算方法调用,遍历执行依赖数组的函数更新视图

5、TemplateCompiler解析模板函数的修改

// 创建模板编译工具
function TemplateCompiler(el,vm){
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if (this.el) {
//将对应范围的html放入内存fragment
var fragment = this.node2Fragment(this.el)
//编译模板
this.compile(fragment)
//将数据放回页面
this.el.appendChild(fragment)
}
} //是否是元素节点
TemplateCompiler.prototype.isElementNode=function(node){
return node.nodeType===1
} //是否是文本节点
TemplateCompiler.prototype.isTextNode=function(node){
return node.nodeType===3
} //转成数组
TemplateCompiler.prototype.toArray=function(arr){
return [].slice.call(arr)
} //判断是否是指令属性
TemplateCompiler.prototype.isDirective=function(directiveName){
return directiveName.indexOf('v-') >= 0;
} //读取dom到内存
TemplateCompiler.prototype.node2Fragment=function(node){
var fragment=document.createDocumentFragment();
var child;
//while(child=node.firstChild)这行代码,每次运行会把firstChild从node中取出,指导取出来是null就终止循环
while(child=node.firstChild){
fragment.appendChild(child)
}
return fragment;
} //编译模板
TemplateCompiler.prototype.compile=function(fragment){
var childNodes = fragment.childNodes;
var arr = this.toArray(childNodes);
arr.forEach(node => {
//判断是否是元素节点
if(this.isElementNode(node)){
this.compileElement(node);
}else{
//定义文本表达式验证规则
var textReg = /\{\{(.+)\}\}/;
var expr = node.textContent;
if (textReg.test(expr)) {
expr = RegExp.$1;
//调用方法编译
this.compileText(node, expr)
}
}
});
} //解析元素节点
TemplateCompiler.prototype.compileElement=function(node){
var arrs=node.attributes;
this.toArray(arrs).forEach(attr => {
var attrName=attr.name;
if(this.isDirective(attrName)){
//获取v-text的text
var type = attrName.split('-')[1]
var expr = attr.value;
CompilerUtils[type] && CompilerUtils[type](node, this.vm, expr)
}
});
} //解析文本节点
TemplateCompiler.prototype.compileText=function(node,expr){
CompilerUtils.text(node, this.vm, expr)
} CompilerUtils = {
/*******解析v-model指令时候只执行一次,但是里面的更新数据方法会执行n多次*********/
model(node, vm, expr) {
if(vm.$data[expr]){
var updateFn = this.updater.modelUpdater;
updateFn && updateFn(node, vm.$data[expr]) /*第n+1次 */
new Watcher(vm, expr, (newValue) => {
//发出订阅时候,按照之前的规则,对节点进行更新
updateFn && updateFn(node, newValue)
}) //视图到模型(观察者模式)
node.addEventListener('input', (e) => {
//获取新值放到模型
var newValue = e.target.value;
vm.$data[expr] = newValue;
})
}
}, /*******解析v-text指令时候只执行一次,但是里面的更新数据方法会执行n多次*********/
text(node, vm, expr) {
//判断是否是data属性
if(vm.$data[expr]){
/*第一次*/
var updateFn = this.updater.textUpdater;
updateFn && updateFn(node, vm.$data[expr]) /*第n+1次 */
new Watcher(vm, expr, (newValue) => {
//发出订阅时候,按照之前的规则,对节点进行更新
updateFn && updateFn(node, newValue)
})
}
//认为是计算属性
else{
this.textComputed(node,vm,expr)
}
}, //新增text computed属性的解析方法
textComputed(node, vm, expr) {
var updateFn = this.updater.textUpdater;
//获取当前属性的监听者
let watcher=vm.$computedWatcherManage[expr]; //第一次
updateFn(node,vm.$computed[expr]); //添加更新View的回调方法
watcher.addCall((value)=>{
updateFn(node, value);
})
}, updater: {
//v-text数据回填
textUpdater(node, value) {
node.textContent = value;
},
//v-model数据回填
modelUpdater(node, value) {
node.value = value;
}
}
}

这个函数主要做了2点修改:

1、修改text方法,如果data里不包含该属性,当做计算属性处理

2、新增textComputed方法,把该节点的更新函数添加到watcher的依赖数组

6、为该框架增加一个简易的计算属性就完成了,下面看下运行效果:

初始化的时候会输出:fullName 1 fullNameAge 1 fullName 1

先解释fullName 1为什么输出2次?

fullName和fullNameAge都是计算属性。

fullNameAge依赖于fullName,fullName依赖与data的属性name

Index.html中有输出了四个fullName计算属性,实际fullName计算属性只执行了一次计算,把值缓存了下来,剩余3个直接取缓存的值。输出第二个fullName 1是因为fullNameAge依赖与fullName,需要把fullNameAge的监听者添加到data的属性name的依赖数组中,这样name属性有更新的时候会执行到fullNameAge的监听函数。

ok,自己实现的这部门还有改进空间,有能力的朋友帮忙改进哈!不明白的朋友可以加好友一起交流。

基于vue2.0原理-自己实现MVVM框架之computed计算属性的更多相关文章

  1. 基于vue2.0的一个豆瓣电影App

    1.搭建项目框架 使用vue-cli 没安装的需要先安装 npm intall -g vue-cli 使用vue-cli生成项目框架 vue init webpack-simple vue-movie ...

  2. 基于vue2.0的分页组件开发

    今天安排的任务是写基于vue2.0的分页组件,好吧,我一开始是觉得超级简单的,但是越写越写不出来,写的最后乱七八糟的都不知道下句该写什么了,所以重新捋了思路,小结一下- 首先写组件需要考虑: 要从父组 ...

  3. 基于vue2.0前端组件库element中 el-form表单 自定义验证填坑

    eleme写的基于vue2.0的前端组件库: http://element.eleme.io 我在平时使用过程中,遇到的问题. 自定义表单验证出坑: 1: validate/resetFields 未 ...

  4. vue-swiper 基于Vue2.0开发 轻量、高性能轮播插件

    vue-swiper 基于 Vue2.0 开发,基本满足大部分功能 轻量.高性能轮播插件.目前支持 无缝衔接自动轮播.无限轮播.手势轮播 没有引入第三方库,原生 js 封装,打包之后只有 8.2KB ...

  5. 基于vue2.0打造移动商城页面实践 vue实现商城购物车功能 基于Vue、Vuex、Vue-router实现的购物商城(原生切换动画)效果

    基于vue2.0打造移动商城页面实践 地址:https://www.jianshu.com/p/2129bc4d40e9 vue实现商城购物车功能 地址:http://www.jb51.net/art ...

  6. 基于vue2.0的在线电影APP,

    基于vue2.0构建的在线电影网[film],webpack + vue + vuex + vue-loader + keepAlive + muse-ui + cordova 全家桶,cordova ...

  7. Vue2.0 【第三季】第2节 computed Option 计算选项

    目录 Vue2.0 [第三季]第2节 computed Option 计算选项 第2节 computed Option 计算选项 一.格式化输出结果 二.用计算属性反转数组 Vue2.0 [第三季]第 ...

  8. 043——VUE中组件之使用.sync修饰符与computed计算属性实现购物车原理

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  9. 饿了么基于Vue2.0的通用组件开发之路(分享会记录)

    Element:一套通用组件库的开发之路 Element 是由饿了么UED设计.饿了么大前端开发的一套基于 Vue 2.0 的桌面端组件库.今天我们要分享的就是开发 Element 的一些心得. 官网 ...

随机推荐

  1. 第24章 Java 数据类型转换

    每日一句 井底点灯深烛伊,共郎长行莫围棋. 每日一句 What we call "failure" is not falling down, but the staying dow ...

  2. Jwt隐藏大坑,通过源码帮你揭秘

    前言 JWT是目前最为流行的接口认证方案之一,有关JWT协议的详细内容,请参考:https://jwt.io/introduction 今天分享一下在使用JWT在项目中遇到的一个问题,主要是一个协议的 ...

  3. 【转】理解 CI 和 CD 之间的区别

    有很多关于持续集成(CI)和持续交付(CD)的资料.很多文章用技术术语来进行解释,以及它们怎么帮助你的组织.可惜的是,在一些情况下,这些方法通常与特定工具.甚至供应商相关联.在公司食堂里非常常见的谈话 ...

  4. CabloyJS自带工作流引擎的文档清单

    文档清单 CabloyJS自带工作流引擎文档已经整理出来,欢迎大家围观.拍砖 介绍 介绍 演示:CMS审批工作流 单元测试用例集 流程定义 基本概念 JSON规范 listener规范 listene ...

  5. 认识弹性盒子flex

    认识弹性盒子flex 来源:https://blog.xybin.top/2022/flex 1.定义弹性布局(父级上定义)display:flex; 如果说内核为webkit 的必须前面加上 -we ...

  6. Bika LIMS 开源LIMS集——实验室检验流程概述及主页、面板

    主页 主页左侧为功能入口菜单.右侧含待办提醒,中间为工作区. 工作区功能将主要工作页面置于首页,便于用户操作. Dashboard 面板 系统面板 包括待排定的实验任务.实验中的任务数.复核/审核中的 ...

  7. ElasticSearch7.3学习(三十二)----logstash三大插件(input、filter、output)及其综合示例

    1. Logstash输入插件 1.1 input介绍 logstash支持很多数据源,比如说file,http,jdbc,s3等等 图片上面只是一少部分.详情见网址:https://www.elas ...

  8. bat-配置环境变量

    查看环境变量 set 查看当前所有变量 set path 查看变量path的值 echo %xxx% 查看某一个环境变量 临时设置环境变量 set xxx=xxx set xxx= 永久设置环境变量 ...

  9. NC19916 [CQOI2010]扑克牌

    NC19916 [CQOI2010]扑克牌 题目 题目描述 你有n种牌,第i种牌的数目为 \(c_i\) .另外有一种特殊的牌:joker,它的数目是m.你可以用每种牌各一张来组成一套牌,也可以用一张 ...

  10. 分布式事务(Seata) 四大模式详解

    前言 在上一节中我们讲解了,关于分布式事务和seata的基本介绍和使用,感兴趣的小伙伴可以回顾一下<别再说你不知道分布式事务了!> 最后小农也说了,下期会带给大家关于Seata中关于sea ...