基于vue2.0原理-自己实现MVVM框架之computed计算属性
基于上一篇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做了这些事情:
为每一个computed建立了watcher。
收集所有computed的watcher,并绑定在Vue实例的_computedWatchers 上。
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值
}
};
}
使用 Object.defineProperty 为实例上computed 属性建立get、set方法。
set 函数默认是空函数,如果用户设置,则使用用户设置。
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、核心流程
- 首次读取 computed 属性值时,dirty 值初始为 true
- 根据getter计算属性值,并保存在观察者value上并设置dirty为false
- 之后再读取 computed 属性值时,dirty 值为 false,不调用 getter 重新计算值,直接返回观察者中的value
- 当 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计算属性的更多相关文章
- 基于vue2.0的一个豆瓣电影App
1.搭建项目框架 使用vue-cli 没安装的需要先安装 npm intall -g vue-cli 使用vue-cli生成项目框架 vue init webpack-simple vue-movie ...
- 基于vue2.0的分页组件开发
今天安排的任务是写基于vue2.0的分页组件,好吧,我一开始是觉得超级简单的,但是越写越写不出来,写的最后乱七八糟的都不知道下句该写什么了,所以重新捋了思路,小结一下- 首先写组件需要考虑: 要从父组 ...
- 基于vue2.0前端组件库element中 el-form表单 自定义验证填坑
eleme写的基于vue2.0的前端组件库: http://element.eleme.io 我在平时使用过程中,遇到的问题. 自定义表单验证出坑: 1: validate/resetFields 未 ...
- vue-swiper 基于Vue2.0开发 轻量、高性能轮播插件
vue-swiper 基于 Vue2.0 开发,基本满足大部分功能 轻量.高性能轮播插件.目前支持 无缝衔接自动轮播.无限轮播.手势轮播 没有引入第三方库,原生 js 封装,打包之后只有 8.2KB ...
- 基于vue2.0打造移动商城页面实践 vue实现商城购物车功能 基于Vue、Vuex、Vue-router实现的购物商城(原生切换动画)效果
基于vue2.0打造移动商城页面实践 地址:https://www.jianshu.com/p/2129bc4d40e9 vue实现商城购物车功能 地址:http://www.jb51.net/art ...
- 基于vue2.0的在线电影APP,
基于vue2.0构建的在线电影网[film],webpack + vue + vuex + vue-loader + keepAlive + muse-ui + cordova 全家桶,cordova ...
- Vue2.0 【第三季】第2节 computed Option 计算选项
目录 Vue2.0 [第三季]第2节 computed Option 计算选项 第2节 computed Option 计算选项 一.格式化输出结果 二.用计算属性反转数组 Vue2.0 [第三季]第 ...
- 043——VUE中组件之使用.sync修饰符与computed计算属性实现购物车原理
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- 饿了么基于Vue2.0的通用组件开发之路(分享会记录)
Element:一套通用组件库的开发之路 Element 是由饿了么UED设计.饿了么大前端开发的一套基于 Vue 2.0 的桌面端组件库.今天我们要分享的就是开发 Element 的一些心得. 官网 ...
随机推荐
- 随笔总结:8086CPU的栈顶超界问题
我们学习编程都知道栈的超界限问题是非常严重的问题,他可能会覆盖掉其他数据,并且我们不知道这个数据是我们自己保存的用于其他用途的数据还是系统的数据,这样常常容易引发一连串的问题. 在学习汇编的时候,我们 ...
- MongoDB 常用启动参数
每日一句 Once you choose your way of life, be brave to stick it out and never return. 生活的道路一旦选定,就要勇敢地走到底 ...
- 秋招如何抱佛脚?2022最新大厂Java面试真题合集(附答案
2022秋招眼看着就要来了,但是离谱的是,很多同学最近才想起来还有秋招这回事,所以纷纷临时抱佛脚,问我有没有什么快速磨枪的方法, 我的回答是:有! 说起来,临阵磨枪没有比背八股文更靠谱的了,很多人对这 ...
- 如何为Java面试准备项目经验
1 提出问题 应届生朋友或Java程序员在找Java方面的工作时,一定会需要准备Java项目经验,但事实上不少求职者,是没有项目经验,或者只具有开源社区等的学习项目经验,这样的话,就很有可能在面试时无 ...
- linux 查询文件命令
jps; 当前服务器中所有的java进程: jps |grep XXX; 查询当前服务器某个进程: locate xxx;查询某个文件的位置:
- MyBatis - MyBatis的层次结构
API接口层 规定了一系列接口,能够向外提供接口,对内进行操作. 数据处理层 负责SQL相关处理工作,如:SQL查找.SQL执行.SQL映射等工作. 基础支撑层 提供基础功能支撑,包括连接管理.事务管 ...
- 万字剖析Ribbon核心组件以及运行原理
大家好,本文我将继续来剖析SpringCloud中负载均衡组件Ribbon的源码.本来我是打算接着OpenFeign动态代理生成文章直接讲Feign是如何整合Ribbon的,但是文章写了一半发现,如果 ...
- Idea创建文件夹自动合成一个
在idea中创建文件夹时,它们总是自动合成一个,如下图: 文件夹自动折叠真的很影响效率,可能会引发一些不经意的失误 解决方法: 取消这个地方的勾选 这样就可以正常创建文件夹了
- NC207028 第k小数
NC207028 第k小数 题目 题目描述 给你一个长度为 \(n\) 的序列,求序列中第 \(k\) 小数的多少. 输入描述 多组输入,第一行读入一个整数 \(T\) 表示有 \(T\) 组数据. ...
- halcon变量窗口的图像变量不显示,重启软件和电脑都没用
有幸遇到halcon变量窗口的图像变量不显示,重启软件和电脑都没用这个沙雕问题,也是找了蛮久才发现解决办法特意记录一下. 这是正常情况下的窗口(左边)和图像变量不显示的窗口(右边): 解决方法: 鼠标 ...