草庐IT

基于vue2.0原理-自己实现MVVM框架之computed计算属性

自强不息 厚德载物 2023-03-28 原文

基于上一篇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. ruby-on-rails - 如果为空或不验证数值,则使属性默认为 0 - 2

    我希望我的UserPrice模型的属性在它们为空或不验证数值时默认为0。这些属性是tax_rate、shipping_cost和price。classCreateUserPrices8,:scale=>2t.decimal:tax_rate,:precision=>8,:scale=>2t.decimal:shipping_cost,:precision=>8,:scale=>2endendend起初,我将所有3列的:default=>0放在表格中,但我不想要这样,因为它已经填充了字段,我想使用占位符。这是我的UserPrice模型:classUserPrice回答before_val

  2. ruby-on-rails - 在混合/模块中覆盖模型的属性访问器 - 2

    我有一个包含模块的模型。我想在模块中覆盖模型的访问器方法。例如:classBlah这显然行不通。有什么想法可以实现吗? 最佳答案 您的代码看起来是正确的。我们正在毫无困难地使用这个确切的模式。如果我没记错的话,Rails使用#method_missing作为属性setter,因此您的模块将优先,阻止ActiveRecord的setter。如果您正在使用ActiveSupport::Concern(参见thisblogpost),那么您的实例方法需要进入一个特殊的模块:classBlah

  3. ruby - 多个属性的 update_column 方法 - 2

    我有一个具有一些属性的模型:attr1、attr2和attr3。我需要在不执行回调和验证的情况下更新此属性。我找到了update_column方法,但我想同时更新三个属性。我需要这样的东西:update_columns({attr1:val1,attr2:val2,attr3:val3})代替update_column(attr1,val1)update_column(attr2,val2)update_column(attr3,val3) 最佳答案 您可以使用update_columns(attr1:val1,attr2:val2

  4. ruby - Nokogiri 剥离所有属性 - 2

    我有这个html标记:我想得到这个:我如何使用Nokogiri做到这一点? 最佳答案 require'nokogiri'doc=Nokogiri::HTML('')您可以通过xpath删除所有属性:doc.xpath('//@*').remove或者,如果您需要做一些更复杂的事情,有时使用以下方法遍历所有元素会更容易:doc.traversedo|node|node.keys.eachdo|attribute|node.deleteattributeendend 关于ruby-Nokog

  5. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  6. ruby-on-rails - Rails 模型——非持久类成员或属性? - 2

    对于Rails模型,是否可以/建议让一个类的成员不持久保存到数据库中?我想将用户最后选择的类型存储在session变量中。由于我无法从我的模型中设置session变量,我想将值存储在一个“虚拟”类成员中,该成员只是将值传递回Controller。你能有这样的类(class)成员吗? 最佳答案 将非持久属性添加到Rails模型就像任何其他Ruby类一样:classUser扩展解释:在Ruby中,所有实例变量都是私有(private)的,不需要在赋值前定义。attr_accessor创建一个setter和getter方法:classUs

  7. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  8. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  9. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  10. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

随机推荐