vue源码 - computed实现原理

349 阅读3分钟
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        let depId = 0
        let watcherId = 0

        const vm = {
            _data: {
                name: "盲僧",
                actionInfo: '会马氏三角杀吗'
            },
            computed: {
                liQing() {
                    return '你的' + vm.name + vm.actionInfo
                }
            }

        };

        function render() {
            document.body.innerHTML = vm.liQing
        }

        function defineReactive(target, key) {
            const dep = new Dep();
            Object.defineProperty(target, key, {
                get() {
                    if (Dep.target) {
                        dep.depend(Dep.target);
                    }
                    return vm._data[key];
                },
                set(newVal) {
                    vm._data[key] = newVal;
                    dep.notify();
                },
            });
        }

        class Dep {
            constructor() {
                this.subs = [];
                this.id = depId++
            }
            depend() {
                Dep.target.addDep(this)
            }
            notify() {
                this.subs.forEach((watcher) => watcher.update());
            }
            addWatcher(watcher) {
                this.subs.push(watcher)
            }
        }

        const stack = []
        Dep.target = null

        function pushStack(watcher) {
            stack.push(watcher)
            Dep.target = watcher
        }
        function popStack() {
            stack.pop()
            Dep.target = stack[stack.length - 1]
        }

        class Watcher {
            constructor(vm, effectFn, options = {}) {
                this.getter = effectFn;
                this.vm = vm;
                this.id = watcherId++
                this.depsId = new Set()
                this.deps = []
                this.lazy = options.lazy
                this.dirty = this.lazy
                this.get();
            }
            get() {
                pushStack(this)
                this.value = this.getter();
                popStack()
                return this.value
            }
            update() {
                if (this.lazy) {
                    this.dirty = true
                }
                this.get();
                console.log("值改变了-触发了更新");
            }
            evaluate() {
                this.get()
                this.dirty = false
            }
            addDep(dep) {
                if (!this.depsId.has(dep.id)) {
                    this.depsId.add(dep.id)
                    this.deps.push(dep)
                    dep.addWatcher(this)
                }
            }
        }

        function observer(obj) {
            Object.keys(obj).forEach(key => {
                defineReactive(vm, key)
            })
        }

        function initComputed() {
            const computed = vm.computed
            const watchers = vm._computedWatchers = Object.create(null)
            for (let key in computed) {
                watchers[key] = new Watcher(vm, computed[key], { lazy: true })
                Object.defineProperty(vm, key, {
                    get() {
                        const watcher = vm._computedWatchers[key]
                        if (watcher.dirty) {
                            watcher.evaluate()
                        }
                        if (Dep.target) {
                            watcher.deps.forEach(dep => {
                                dep.depend()
                            })
                        }
                        return watcher.value
                    }
                })
            }
        }

        observer(vm._data)
        initComputed()

        new Watcher(vm, render)

        setTimeout(() => {
            vm.name = '瞎子'
        }, 3000)

    </script>
</body>

</html>

代码解读

function initComputed() {
     const computed = vm.computed
     const watchers = vm._computedWatchers = Object.create(null)
     for (let key in computed) {
         watchers[key] = new Watcher(vm, computed[key], { lazy: true })
         Object.defineProperty(vm, key, {
              get() {
                 const watcher = vm._computedWatchers[key]
                 if (watcher.dirty) {
                    watcher.evaluate()
                 }
                 if (Dep.target) {
                     watcher.deps.forEach(dep => {
                         dep.depend()
                     })
                 }
                 return watcher.value
              }
        })
     }
}

以上代码是初始化computed,其函数中可以看出在vm上挂载一个_computedWatchers属性,紧接着循环computed,为每一个计算属性添加一个观察者(Watcher),computed和watcher是一个带有标记的watcher,这块new Watcher时传入了{ lazy: true },该lazy作用有两个,1>. 标记当前watcher是一个computeWatcher, 2> 我们都知道computed有缓存功能,只有在其依赖的属性发生变化时,才去重新计算值,否则直接返回旧值。那么这块在watcher舒适化的时候定义一个变量, watcher.dirty = lazy ,后续只有在watcher.dirty为true的时候去重新计算,为false直接返回旧值。那么在什么时候去改变watcher.dirty的状态呢?

evaluate() {
   this.get()
   this.dirty = false
}

evaluate这个函数是Watcher内部的函数,他在什么时候调用呢 ,可以看到initComputed函数内部对每个计算属性做了劫持,当我们读取计算属性时,会触发get函数,紧接着拿到当前计算属性的watcher,判断其watcher.dirty属性是不是为true,那么第一次这块肯定是true,那么会调用上边的evaluate函数,在函数内部会去触发get函数,也就是重新去计算计算属性的值并保存在watcher.value中,并将watcher.dirty = false,那么在下次访问计算属性的时候,不会走到watcher.evaluate(),就直接 return watcher.value。到这computed缓存功能就实现了。

其次就是computed的依赖发生改变时怎么通知当前计算属性去更新,并且如果模版中如果使用了计算属性{{computed}},改怎么去通知试图更新呢?

先来说下他们的流程:

  1. 首先在初始化的时候回去初始化computed也就是上边的initComputed函数

  2. template:"<div>{{computed}}</div>"

    2.1. 在vue模版变异的时候会将模版生成render函数,然后在去挂载组件时会去调用vm._update(vm._render()), 在这块也会去new Watcher(vm,vm._update(vm._render())),

    2.2. 所以我们为什么要用一个stack,一个队列去保存当前watcher,在这是当前页面渲染watcher推到队列中,stack = [渲染watcher],

    2.3. 紧接着当编译到 {{computed}} 这块时会触发computed的get方法,

    2.4. 这时computed 的 Watcher.dirty = true,所以会去触发 Watcher.evaluate(),

    2.5. 然后会调用 watcher.get(), Watcher.dirty = false,并且把当前的computedWatcher推到站中,stack = [渲染watcher,计算属性watcher]

    2.6. watcher.get()执行完,将watcher.value = this.getter(),也就是计算属性的调用结果 return this.a + this.b,保存起来,并且computedWatcher出栈,然后回到computed的get方法中,stack = [渲染watcher]

    2.7. if (Dep.target) 此时的Dep.target就是还在stack中的渲染watcher,那么当前计算属性的中依赖的属性也需要去收集当前渲染watcher,将当前watcher也推到属性的dep中,其目的就是在属性改变时去,dep.notify(),通知其依赖的watcher更新试图或者通知其计算属性的watcher重新计算值。

总结:

以上就是computed的实现原理,如有地方不对,还望大佬指点纠正。最后说下大概流程:

首次渲染:

-> 生成渲染new Watcher(vm,vm._update(vm._render()))

-> wathcer.get()

-> 渲染wathcer入栈

-> 模版编译

-> 解析到{{computed}}触发computedWatcher.get()

-> computedWatcher入栈

-> 计算computedWatcher.value并保存起来,并且更新computedWatcher.dirty = false

-> computedWatcher出栈

-> 回到渲染watcher继续解析模版并渲染视图

当计算属性中依赖值改变时:

-> 依赖值改变会触发依赖的set()

-> 通知其所有观察者dep.notity() -(watcher更新)

-> 调用watcher.update(),里边会判断如果是computeWatcher时,会将当前的computedWatcher.dirty = true,所以在调用渲染atcher.update()时,又会走上边的模版编译....