9. 计算属性的实现原理

计算属性的实现原理

  1. 计算属性也是一个watcher
  2. 计算属性定义成方法, 使用的时候直接vm.XX,是因为使用Object.defineProperty在vm实例上定义了属性
  3. 计算属性的依赖更新值发生改变是通过脏值检测来实现的
  4. 计算属性watcher不能更新视图, 只会更新里面的dirty属性,真正更新的是外层的渲染watcher

在initState中, 与初始化data一样, 初始化computed

export function initState(vm) {
    const opts = vm.$options
    if(opts.data) {
        initData(vm)
    }
	// 初始化computed
    if(opts.computed) {
        initComputed(vm)
    }
}

具体实现

​ 初始化computed时, 每一个computed对应一个计算属性watcher, 挂载在vm实例上, 获取computed值, 其实就是执行的computed[key]方法

​ 计算属性的watcher, 初始化时并不会执行 {lazy: true}, 需要在watcher里面做处理, 与渲染watcher区分

​ 将存储watcher的空间维护成一个栈空间, 便于逻辑处理, 先是渲染watcher, 后是计算属性watcher

// 改造Dep.target

let stack = []
Dep.target = null

export function pushTarget(watcher) {
    stack.push(watcher)
    Dep.target = watcher
}

export function popTarget() {
    stack.pop()
    Dep.target = stack[stack.length -1]
}

// 初始化计算属性
function initComputed(vm) {
    // 得到的computed时一个数组
    const computed = vm.$options.computed

    for(let key in computed) {
        // 获取computed
        let userDef = computed[key]
        // 定义watcher并挂载到实例上, 方便后续通过实例获取, 把key和watcher一一对应
        const watchers = vm._computedWatchers = {}

        let fn = typeof userDef === 'function' ? userDef : userDef.get

        // 创建一个计算属性watcher
        watchers[key] = new Watcher(vm, fn, {lazy: true})

        // 在vue实例上定义这些属性, 所以可以通过vm.fullname访问到
        defineComputed(vm, key, userDef)

    }
}

function defineComputed(target, key, userDef) {
    const setter = userDef.set || (() => {})
    Object.defineProperty(target, key, {
        get: createComputedGetter(key),
        set: setter
    })
}

function createComputedGetter(key) {
    return function() {
        // 这里的this指向上面的target, 也就是vm
        const watcher = this._computedWatchers[key]
        // 如果是脏值, 求值
        if(watcher.dirty) {
            //  求值之后, dirty变成false, 下次就不求值了     需要在watcher上添加evaluate方法
            watcher.evaluate()
        }
        // 上面取值之后会将计算属性watcher pop出来, 如果stack里面还有watcher, 那就是渲染watcher, 需要计算属性里面的deps去记住上层的watcher
        // 因为计算属性watcher不能更新视图, 只有渲染watcher可以
        if(Dep.target) {
            // 添加depend方法
            watcher.depend()
        }
        // 新增value属性表示计算属性的值
        return watcher.value
    }
}

具体实现

state.js

import { observe } from "./observe"
import { Dep } from "./observe/dep"
import { Watcher } from "./observe/watcher"


export function initState(vm) {
    const opts = vm.$options
    if(opts.data) {
        initData(vm)
    }

    if(opts.computed) {
        initComputed(vm)
    }
}

// 初始化数据的具体方法
function initData(vm) {
    let data = vm.$options.data
    data = typeof data === 'function' ? data.call(vm) : data

    vm._data = data

    // 进行数据劫持, 关键方法, 放在另一个文件里面, 新建 observe/index.js
    observe(data)

    // 设置代理, 这个代理只有最外面这一层
    // 希望访问 vm.name 而不是 vm._data.name, 使用vm 来代理 vm._data
    // 在vm上取值时, 实际上是在vm._data上取值
    // 设置值时, 实际上是在vm._data上设置值
    // 每一个属性都需要代理
    for(let key in data) {
        proxy(vm, '_data', key)
    }


}

// 属性代理 vm._data.name  => vm.name
function proxy(vm, target, key) {
    Object.defineProperty(vm, key, {
        get() {
            return vm[target][key]
        },
        set(newValue) {
            vm[target][key] = newValue
        }
    })
}

// 初始化计算属性
function initComputed(vm) {
    // 得到的computed时一个数组
    const computed = vm.$options.computed

    for(let key in computed) {
        // 获取computed
        let userDef = computed[key]
        // 定义watcher并挂载到实例上, 方便后续通过实例获取, 把key和watcher一一对应
        const watchers = vm._computedWatchers = {}

        let fn = typeof userDef === 'function' ? userDef : userDef.get

        // 创建一个计算属性watcher
        watchers[key] = new Watcher(vm, fn, {lazy: true})

        // 在vue实例上定义这些属性, 所以可以通过vm.fullname访问到
        defineComputed(vm, key, userDef)

    }
}

function defineComputed(target, key, userDef) {
    const setter = userDef.set || (() => {})
    Object.defineProperty(target, key, {
        get: createComputedGetter(key),
        set: setter
    })
}

function createComputedGetter(key) {
    return function() {
        // 这里的this指向上面的target, 也就是vm
        const watcher = this._computedWatchers[key]
        // 如果是脏值, 求值
        if(watcher.dirty) {
            //  求值之后, dirty变成false, 下次就不求值了     需要在watcher上添加evaluate方法
            // 这里再求值的时候, 会获取依赖的值, 就会触发对应依赖的get,从而实现依赖收集 **
            watcher.evaluate()
        }
        // 上面取值之后会将计算属性watcher pop出来, 如果stack里面还有watcher, 那就是渲染watcher, 需要计算属性里面的deps去记住上层的watcher
        // 因为计算属性watcher不能更新视图, 只有渲染watcher可以
        if(Dep.target) {
            // 添加depend方法
            watcher.depend()
        }
        // 新增value属性表示计算属性的值
        return watcher.value
    }
}

watcher.js

import { Dep, popTarget, pushTarget } from "./dep"


let id = 0

export class Watcher {
    constructor(vm, fn, options) {
        this.id = id ++
        this.vm = vm

        this.deps = []
        this.depsId = new Set()

        // 是否时渲染watcher
        this.renderWatcher = options

        this.lazy = options.lazy
        this.dirty = this.lazy
        // 重新渲染的方法
        this.getter = fn
        // 渲染watcher需要立即执行一次, 计算属性watcher初始化时不执行
        this.lazy ? undefined : this.get()
    }


    get() {
        // 开始渲染时, 让静态属性Dep.target指向当前的watcher, 那么在取值的时候, 就能在对应的属性中记住当前的watcher
        // Dep.target = this
        pushTarget(this)
        let value = this.getter.call(this.vm)
        // 渲染完毕之后清空
        // Dep.target = null
        popTarget()
        return value
    }

    // watcher里面添加dep, 去重
    addDep(dep) {
        if(!this.depsId.has(dep.id)) {
            this.deps.push(dep)
            this.depsId.add(dep.id)
            // 去重之后, 让当前的dep,去记住当前的watcher
            dep.addSub(this)
        }
    }
    // 让计算属性watcher里面的dep收集外层的watcher
    depend() {
        let length = this.deps.length
        while(length--) {
            this.deps[length].depend()
        }
    }

    update() {
        if(this.lazy) {
            this.dirty = true
        } else {
            // 更新, 需要重新收集依赖
            queueWatcher(this)      // 把当前的watcher暂存起来
        }
    }

    run() {
        this.get()
    }

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


let queue = []  // 用于存放需要更新吧的watcher
let has = {}    // 用于去重
let pending = false     // 防抖

function flushScheduleQueue() {
    let flushQueue = queue.slice(0)     // copy一份
   
    queue = []      // 刷新过程中, 有新的watcher, 重新放到queue中
    has = {}
    pending = false
    flushQueue.forEach(q => q.run())    // 添加一个run方法,真正的渲染
}

function queueWatcher(watcher) {
    const id = watcher.id
    if(!has[id]) {      // 对watch进行去重
        queue.push(watcher)
        has[id] = true
        // 不管update执行多少次, 但是最终值执行一次刷新操作

        if(!pending) {
            // 开一个定时器     里面的方法只执行一次, 并且是在所有的watcher都push进去之后才执行的
            // setTimeout(() => {
            //     console.log('杀心')
            // }, 0)
            // setTimeout(flushScheduleQueue, 0)

            nextTick(flushScheduleQueue, 0)     // 内部使用的是nextTick, 第二个参数估计可以不要

            pending = true
        }
    }
}

let callbacks = []
let waiting = false
// 跟上面的套路一样
function flushCallBacks() {
    let cbs = callbacks.slice(0)
    waiting = false
    callbacks = []
    cbs.forEach(cb => cb())
}

// vue内部 没有直接使用某个api, 而是采用优雅降级的方式
// 内部先使用的是promise(ie不兼容),   MutationObserver (h5的api)  ie专享的 setImmediate  最后setTimeout

let timerFunc
if(Promise) {
    timerFunc = () => {
        Promise.resolve().then(flushCallBacks)
    }
} else if(MutationObserver) {
    let observer = new MutationObserver(flushCallBacks)     // 这里传入的回调时异步执行的
    let textNode = document.createTextNode(1)   // 应该是固定用法
    observer.observe(textNode, {
        characterData: true
    })
    timerFunc = () => {
        textNode.textContent = 2
    }
} else if(setImmediate) {
    timerFunc = () => {
        setImmediate(flushCallBacks)
    }
} else {
    timerFunc = () => {
        setTimeout(flushCallBacks)
    }
}

export function nextTick(cb) {         // setTimeout是过一段事件后, 执行cb, nextTick是维护了一个队列, 后面统一执行
    callbacks.push(cb)
    if(!waiting) {
        // setTimeout(() => {
        //     flushCallBacks()
        // }, 0)
        timerFunc()
        waiting = true
    }
}

dep.js



let id = 0


export class Dep{
    constructor() {
        this.id = id ++
        // 用来存储watcher
        this.subs = []
    }

    depend() {
        // 让dep记住当前的watcher, 但是这样做会重复, 并且不能实现多对多
        // this.subs.push(Dep.target)

        // 先让当前的watcher记住dep, 然后在addDep里面去重, 
        Dep.target.addDep(this)
    }

    addSub(watcher) {
        // 这里就不用再次去重了
        this.subs.push(watcher)
    }

    notify() {
        // subs里面的每一个watcher 分别更新
        this.subs.forEach(watcher => watcher.update())
    }
}

// 改造Dep.target

let stack = []
Dep.target = null

export function pushTarget(watcher) {
    stack.push(watcher)
    Dep.target = watcher
}

export function popTarget() {
    stack.pop()
    Dep.target = stack[stack.length -1]
}

dist/7.computed.html

<!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>计算属性</title>
</head>

<body>
    <div id="app" style="color:yellow;backgroundColor:blue;">
            {{fullname}}  {{fullname}}  {{fullname}}
          
    </div>
    <script src="vue.js"></script>
    <!-- <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.js"></script> -->
    <script>
        const vm = new Vue({
            data() {
                return {
                  firstname: 'yang',
                  lastname: 'jerry'
                }
            },
            el: '#app',  // 将数据解析到el元素上
            // 急速属性, 依赖的指发生变化才会重新执行, 要维护一个dirty属性, 默认计算属性不会立即执行
            // 计算属性就是一个defineProperty  
            // 计算属性也是一个watcher
            computed: {
                // 写法1
                fullname() {
                    console.log('run')
                    return this.firstname + '-' + this.lastname
                }

                // 写法2
                // fullname: {
                //     get() {
                //         console.log('get')
                //     },
                //     set(newVal) {
                //         console.log('newVal:', newVal)
                //     }
                // }
            }
        })
        
        setTimeout(() => {
               vm.firstname = '888'
        },1000)
     
    </script>
</body>

</html>


总结一下流程:

页面在渲染的时候会生成一个渲染watcher, 默认会自动执行里面的get方法, 将自身push进stack, Dep.target 指当前的watcher. 在取值计算属性的时候, 会将initComputed的时候早就生成的计算属性watcher 也push 到stack, 进行取值计算, 同时将dirty属性变成false, 取值完毕之后,会pop stack 如果Dep.target还有值, 就让计算属性watcher里面的deps记住当前的Dep.target所指向的watcher, 此时watcher里面的deps不仅记住了计算属性watcher, 也记住了外层的渲染watcher

当依赖的属性发生变化时, 会调用该属性的subs里面所有watcher的update方法, 计算属性的update方法指挥将dirty属性变成true, 下次取值时就会重新渲染, 真正渲染页面的时外层的渲染watcher

posted @ 2022-06-27 04:08  littlelittleship  阅读(246)  评论(0编辑  收藏  举报