首页IT科技vue中mvvm原理(基于vue2.0原理-自己实现MVVM框架之computed计算属性)

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

时间2025-09-19 05:43:37分类IT科技浏览6725
导读:基于上一篇data的双向绑定,这一篇来聊聊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                ,自己实现的这部门还有改进空间                          ,有能力的朋友帮忙改进哈!不明白的朋友可以加好友一起交流               。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
热血江湖网游官网(热血江湖私服构架)