ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

9. 计算属性的实现原理

2022-06-27 04:31:51  阅读:152  来源: 互联网

标签:计算 vm watcher let key target 原理 属性


计算属性的实现原理

  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方法
            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

标签:计算,vm,watcher,let,key,target,原理,属性
来源: https://www.cnblogs.com/littlelittleship/p/16414917.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有