您现在的位置是:首页 >技术杂谈 >手写vue-computed网站首页技术杂谈
手写vue-computed
简介手写vue-computed
一、计算属性的使用
1、引入计算属性
插值(即双大括号表达式),支持简单的表达式和过滤器可以对值做一些简单运算和文本转换
模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的
在模板中放入太多的逻辑会让模板过重且难以维护
例如:
<div id="example">
{{ message.split('').reverse().join('') }}
</div>
过长的表达式中包含了3个操作,模板不再是简单的声明式逻辑,复杂的逻辑会使代码变得难以理解(可读性差)和维护,且无法复用
所以,对于任何复杂逻辑,都应使用计算属性
2、认识计算属性
现象:页面中三个地方使用了计算属性fullname
,控制台打印run只有一次
- 依赖的值发生变化才会重新执行用户的方法 计算属性中维护一个dirty值,默认是脏的,第一次取值会执行这个方法, 默认计算属性不会立刻执行
- 计算属性就是一个definePropoty
- 计算属性也是一个Watcher,默认渲染也会创建一个渲染Watcher,计算属性watcher包含在渲染watcher中
<div id="app">{{fullname}} {{fullname}} {{fullname}}</div>
// 默认写一个函数 是get方法
computed: {
//fullname()
//{return this.fistname + this.lastname;},
fullname: {
get() {
console.log("run");
return this.fistname + this.lastname;
},
set(newValue) {
console.log(newVal);
},
},
},
二、实现原理
如何做到渲染?渲染watcher中包含多个计算属性watcher,计算属性里又依赖了多个属性,更新引用的属性值,每个属性有自己的dep,会收集watcher,收集到的就是计算属性watcher
脏值dirty刚刚开始为true,修改计算属性依赖的属性,只会引起dirty的变化,并不会重新渲染视图
需要依赖的属性变化时重新渲染视图
让依赖的属性的dep不仅记住计算属性的watcher还要记住渲染watcher
三、实现计算属性
1、准备
1、1dep.js
在dep.js中增加一个栈结构和两个方法,由于watcher不再是一个,此时包含计算属性watcher和渲染watceher,存储方式改为栈性结构
let stack = []
// 将watcher放入stack中
export function pushTarget(watcher){
stack.push(watcher);
Dep.target = watcher;
}
// 渲染完之后将watcher出栈
export function popTarget(){
stack.pop();
Dep.target = stack[stack.length - 1];
}
1、2wathcer.js
watcher保存方法改变
get() {
pushTarget(this);
// Dep.target = this; // 在类上增加了一个静态属性,静态属性只有一份 this就是watcher
this.getter(); //会去vm上取值 vm._update(vm._render)取值
popTarget();
// Dep.target = null; //渲染完毕后清空
}
2、初始化computed
initState(vm)
方法中初始化计算属性、watch
export function initState(vm) {
const opts = vm.$options;
...
if (opts.computed) {
// 初始化计算属性
initComputed(vm);
}
}
2.1取值
function initComputed(vm) {
const computed = vm.$options.computed;
for (let key in computed) {
let userDef = computed[key];
defineComputed(vm, key, userDef);
}
}
function defineComputed(target, key, userDef) {
// 由于计算属性有两种写法 需要判断是否为方法,是方法则为get方法,否则取对象的get方法
const getter = typeof userDef === "function" ? userDef : userDef.get;
const setter = userDef.set || (() => {});
Object.defineProperty(target, key, {
get: getter,
set: setter,
});
}
问题:
在页面上取值三次,会执行三次计算属性的get方法
2.2 缓存
function initComputed(vm) {
const computed = vm.$options.computed;
let watchers = {};
for (let key in computed) {
let userDef = computed[key];
// 我们需要监控计算属性中get的变化
let fn = typeof userDef === "function" ? userDef : userDef.get;
// 如果直接new Watcher 会直接执行fn,不希望立即执行,希望取值的时候执行
// 将属性和watcher对应起来
watchers[key] = new Watcher(wm, fn, { lazy: true });
defineComputed(vm, key, userDef);
}
}
function defineComputed(target, key, userDef) {
// 由于计算属性有两种写法 需要判断是否为方法,是方法则为get方法,否则取对象的get方法
// const getter = typeof userDef === "function" ? userDef : userDef.get;
const setter = userDef.set || (() => {});
Object.defineProperty(target, key, {
get: createComputedGetter(key),
set: setter,
});
}
实现computed多次取值只执行一次get方法:dirty初始值为true,在取值方法中设置为false,将返回值放到watcher上,后续取值判断dirty为false,则直接取watcher上的值。
// 需要检测是否需要执行getter
function createComputedGetter(key) {
return function () {
const watcher = this._computedWatchers[key]; //获取到对应计算熟悉的watcher
if (watcher.dirty) {
//执行用户传入的函数 还需要改变dirty的值
watcher.evaluate(); //求值后 多次取值,dirty都是false 走缓存
}
return watcher.value; //最后返回的是watcher上的值
};
}
evaluate() {
this.value = this.get(); //获取到用户函数的返回值,并标识为脏
this.dirty = false;
}
get() {
pushTarget(this);
// Dep.target = this; // 在类上增加了一个静态属性,静态属性只有一份 this就是watcher
// 执行getter时的this绑定为vm
let value = this.getter.call(this.vm); //会去vm上取值 vm._update(vm._render)取值
popTarget();
// Dep.target = null; //渲染完毕后清空
// 拿到执行用户函数后的返回值
return value;
}
此时修改firstname的值,并不更新fullname的值
2.3 修改值
更新计算属性依赖的值,将dirty设置为true,取值时重更新执行取值方法
update() {
// 如果是计算属性 依赖的值发生变化 就标识着计算属性是脏值
if (this.lazy) {
this.dirty = true
} else {
// this.get() //重新渲染 这样会每次刷新都更新,希望实现异步更新,将所有同步的更新执行完,再进行
queueWatcher(this); //将所有的watcher放到队列中,对watcher去重
}
}
这样仅仅更新了计算属性的watcher,还需要更新渲染watcher
取值后,让依赖的属性记住渲染watcher
function createComputedGetter(key) {
return function () {
const watcher = this._computedWatchers[key]; //获取到对应计算熟悉的watcher
if (watcher.dirty) {
//执行用户传入的函数 还需要改变dirty的值
watcher.evaluate(); //求值后 多次取值,dirty都是false 走缓存
}
if(Dep.target){ //计算属性watcher出栈后 还有渲染watcher 应该让计算属性watcher里面的属性也去收集上层watcher
watcher.depend()
}
return watcher.value; //最后返回的是watcher上的值
};
}
depend(){
let i = this.deps.length;
while(i--){
// dep上收集watcher
this.deps[i].depend() //让计算属性watcher也收集渲染watcher
}
}
计算属性不会收集依赖,只会让自己依赖的属性去收集依赖
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。