computed出现的问题
<div id="app"> {{fullName}} </div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.6/vue.js"></
script>
<script>
let vm = new Vue({
el: '#app',
data() {
return { age: 23, name: '哈哈哈' }
},
computed: {
fullName() {
return { age: this.age, name: this.name }
}
}
});
// 将vm.age = 100;和vm.fullName.name = '啊啊啊';上下换位结果不同
setTimeout(() => { // {age:100,name:'啊啊啊'}
vm.age = 100;
vm.fullName.name = '徐志峰';
}, 1000);
/* setTimeout(() => { // {age:100,name:'哈哈哈'}
vm.fullName.name = '啊啊啊';
vm.age = 100;
}, 1000); */
</script>
原理
// age收集了计算属性watcher和渲染watcher,在这里已经走了计算属性watcher将dirty的值改为true。
vm.age = 100;
// 1、vm.fullName.name会走get,因为只是对fullName做了defineProperty,而fullName里的值没有做代理,此时fullName的值已经为{age:100,name:'啊啊啊'}了。
// 2、当执行这行,会走computed的get取值,此时dirty已经为true了,会走evaluate,执行this.get(),将this.dirty = false。
vm.fullName.name = '啊啊啊';
// 当更新操作执行完,要走run(),执行get,此时dirty已经是false了,则直接返回计算属性老值{age:100,name:'啊啊啊'}。
// vm.fullName.name会走get(因为只是对fullName做了defineProperty,而fullName里的值没有做代理),此时dirty为false,返回fullName的老值{age:23,name:'哈哈哈'}。
vm.fullName.name = '啊啊啊';
// 虽然fullName={age:23,name:'啊啊啊'},但是age收集了计算属性watcher,和渲染watcher,执行计算属性watcher将dirty改为true,把渲染watcher放入更新队列中。
vm.age = 100;
// 因为是异步渲染,多次改值只执行一次,当更新操作执行完,要走run(),执行get,会去编译模板中取值{{fullName}},会走计算属性的defineProperty的get,此时dirty已经是true了,又会走evaluate(),执行get,会重新执行computed中用户传的函数,在this上取name和age的值,此时age=100,name='哈哈哈',所以fullName={age:100,name:'哈哈哈'}
Vue2依赖收集
Vue初始化会先执行用户传入的options选项,包括data、props、methods、computed、watch等,对于data中的对象类型数据进行defineProperty代理,对于计算属性会new Watcher()。而vue的编译流程是将模板通过正则 => AST抽象语法树再通过字符串拼接 => 渲染函数 => 虚拟DOM => 真实DOM,当执行挂载逻辑时,会new Watcher()默认执行渲染函数去更新DOM。
state.js 初始化options
export function initState(vm) {
let opts = vm.$options;
if (opts.data) {
initData(vm)
}
if (opts.computed) {
initComputed(vm);
}
}
function proxy(vm, key, value) {
Object.defineProperty(vm, value, { // vm._data.arr = vm.arr
get() {
return vm[key][value]
},
set(newValue) {
vm[key][value] = newValue;
}
})
}
function initData(vm) {
let data = vm.$options.data;
data = typeof data == "function" ? data.call(vm) : data;
vm._data = data;
for (let key in data) {
proxy(vm, '_data', key);;
}
observe(data);
}
function initComputed(vm) {
let computed = vm.$options.computed;
const watchers = vm._computedWatchers = {};
for (let key in computed) {
const userDef = computed[key];
const getter = typeof userDef == "function" ? userDef : userDef.get;
watchers[key] = new Watcher(vm, getter, () => {}, {
lazy: true
});
defineComputed(vm, key, userDef);
}
};
function defineComputed(target, key, userDef) {
let setter = userDef.set || (() => {});
Object.defineProperty(target, key, {
get: createComputedGetter(key),
set: setter
});
}
// 计算属性根本不会收集依赖,只会让自己的依赖属性去收集依赖
function createComputedGetter(key) {
return function () {
const watcher = this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value;
}
}
}
watcher.js 依赖收集
let id = 0;
class Watcher {
constructor(vm, exprOrFn, cb, options={}) {
this.id = id++;
this.vm = vm;
this.exprOrFn = exprOrFn;
this.deps = [];
this.depsId = new Set();
this.cb = cb;
this.options = options;
this.lazy = options.lazy; // 如果watcher上有lazy属性 说明是一个计算属性
this.dirty = this.lazy; // dirty代表取值时是否执行用户提供的方法
this.getter = exprOrFn;
this.lazy ? undefined : this.get();
}
addDep(dep) {
let id = dep.id;
if (!this.depsId.has(id)) {
this.deps.push(dep);
this.depsId.add(id);
dep.addSub(this); // watcher已经记住了dep了而且去重了,此时让dep也记住watcher
}
}
evaluate() {
this.value = this.get();//获取到用户函数的返回值,并且还要标识为脏
this.dirty = false; // 取过一次值之后 就标识成已经取过值了
}
get() {
pushTarget(this); // 当前watcher实例
let result = this.getter.call(this.vm);
popTarget(); // 渲染完成后 将watcher删掉了
return result;
}
depend(){
let i=this.deps.length;
while(i--){
// dep.depend
this.deps[i].depend(); // 让dep去存储渲染watcher
}
}
update() {
if(this.lazy){
// 如果是计算属性 依赖的值变化了 就标识计算属性是脏值了
this.dirty=true;
}else{
queueWatcher(this);
}
}
run() {
console.log('执行');
this.get();
}
}
dep.js
let id = 0;
class Dep {
constructor() {
this.id = id++;
this.subs = [];// 这里存放着当前属性对应的watcher有哪些
}
depend() {
Dep.target.addDep(this);// 让watcher记住dep
}
addSub(watcher){
this.subs.push(watcher);
}
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
Dep.target = null;
let stack=[];
export function pushTarget(watcher) {
stack.push(watcher);// 有渲染watcher 其它的watcher
Dep.target = watcher;// 保留watcher
}
export function popTarget() {
stack.pop();
Dep.target =stack[stack.length-1];// 将变量删除掉
}
export default Dep;
lifecycle.js
初始化和更新逻辑
export function mountComponent(vm, el) {
vm.$el = el;
callHook(vm,'beforeMount');
let updateComponent = () => {
vm._update(vm._render());
};
let watcher = new Watcher(vm, updateComponent, () => {
callHook(vm,'updated');
});
callHook(vm,'mounted');
}
export function lifecycleMixin(Vue) {
Vue.prototype._update = function (vnode) {
const vm = this;
// 既有初始化的功能 又有更新
vm.$el = patch(vm.$el, vnode);
}
}
export function callHook(vm, hook) {
const handlers = vm.$options[hook]; // vm.$options.created=[a1,a2,a3]
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
handlers[i].call(vm); // 更改生命周期中的this
}
}
}