zoukankan      html  css  js  c++  java
  • 手写Vue源码之计算属性

    计算属性 特点: 默认不执行,等用户取值的时候再去执行,会缓存取值结果,
    如果依赖的值发生了变化 会更新dirty的属性,再次取值时,可以重新求值
    
    
    import {pushTarget,popTarget} from './dep'
    import { utils } from '../utils'
    let id = 0 //watcher的唯一标识
    class Watcher{ //每次产生一个watcher都要有一个唯一标识
        //vm ,msg, (newValue,oldValue)=>{}, {user:true}
        //vm, ()=>this.firstName+this.lastName  ()=>{} {lazy:true}
        constructor(vm,exprOrfn,cb = {},opts = {}){ //cb = {},opts = {} 默认值
            this.vm = vm //当前组件实例
            this.exprOrfn = exprOrfn //判断用户传入的是表达式还是函数msg
            if(typeof exprOrfn === 'function') {
                this.getter = exprOrfn //getter就是传入的第二个参数
            }else { //如果是表达式就将对应得表达式取出来 赋值给getter
                this.getter = function () {
                    return utils.getValue(vm,exprOrfn)
                }
            }
            if(opts.user){ //标识用户自己写的Watcher
                this.user = true
            }
            this.lazy = opts.lazy //如果这个值为true说明是计算属性
            this.dirty = this.lazy //计算属性的缓存
            this.cb = cb //用户传入的回调函数 vm.$watch('msg',cb)
            this.opts = {} //其他参数
            this.id = id++
            this.deps = []
            this.depId = new Set()
            //创建watcher得时候先将表达式对应得老值取出来,更新得时候再取新值
            this.value = this.lazy ? undefined : this.get() //如果是计算属性的话不会立即调用get方法
             
    
            this.get() //每次创建一个watcher就执行自己的get方法
        }
        get() {
            //Dep.target也会存放用户的watcher = user watcher
            pushTarget(this) //渲染watcher保存起来 Dep.target = watcher
    
            let value = this.getter.call(vm) //让当前传入的函数updataComponent执行,从实例上取值渲染页面 会调用Object.defineProperty 的get方法
            popTarget() //取完值后 清空当前watcher
            return value //返回新值
        }
        addDep(dep){ //过滤,不要重复存入watcher到dep中,也不要重复存dep。
            let id = dep.id
            if(!this.depId.has(id)) {
                this.depId.add(id)
                this.deps.push(dep) //将dep存入watcher
                dep.addSub(this) //将watcher存入dep
            }
        }
        eveluate(){
            this.value = this.get() //通过这个将计算属性watcher放进栈,并且Dep.target指向了计算属性watcher,然后走getter
            this.dirty = false //值求过了,下次渲染的时候不用求了,当计算属性的值发生变化了就会调用updata方法
    
        }
        depend(){
            let i = this.deps.length
            while(i--){
                this.deps[i].depend()
            }
        }
    /**
     * 重要部分updata()方法
     * 加入页面有四个地方需要更新属性,那我们希望不要更新四次,而是一次性更新
     * 防止不停的更新
        把需要更新的watcher先存起来 放进一个异步队列queueWatcher,然后通过nextTick异步执行
        把重复的watcher过滤掉
        等待这轮更新完就清空队列 就是说等待主执行栈执行完了就执行异步任务,也可以理解为页面所有属性都赋值完再执行这个异步方法
     */
        updata(){ //如果立即调用get 会导致页面刷新频繁 用异步更新来解决
            if(this.lazy){
                this.dirty = true //计算属性依赖的值发生了变化,需要取值重新计算
            }else {
                // this.get()
                queueWatcher(this) //queueWatcher异步队列
            }
            
        }
        run(){
            let value = this.get() //新值
            if(this.value !== value) {
                this.cb(value,this.value)
            }
        }
        
    }
    //渲染watcher、计算属性、vm.$watch 都属于Watcher实例
    export default Watcher
    
    function flushQueue () {
        queue.forEach(watcher => watcher.run) //执行更新
        //清空,下次使用
        has = {}
        queue = []
    }
    let has ={};
    let queue = []
    function queueWatcher(watcher){
        let id = watcher.id
        if(has[id] == null){
            has[id] =true
            queue.push(watcher) //相同的Watcher只会存一个到queue中
    
            //延迟清空队列
            // setTimeout(flushQueue,0) 这个方法也可以但vue内部原理是nextTick
            nextTick(flushQueue)
        }
    }
    let callbacks =[] //有可能用户也会写一个nextTick方法,这时候就需要把nextTick的回调函数放进一个数组里面,再依次执行,
    function nextTick(cb){
        callbacks.push(cb)
    
        //异步刷新这个callbacks 
        //异步任务先执行微任务在执行宏任务,微任务:Promise, mutationObserver, 宏任务:setImmediate setTimeout
        let timeFunc = () => {
            flushCallbacks()
        }
        //判断当前浏览器执行的异步方法
        if(Promise) {
            return Promise.resolve().then(timeFunc)
        }
        if(MutationObserver){ //创建并返回一个新的 MutationObserver 它会在指定的DOM发生变化时被调用
            let observe = new MutationObserver(timeFunc);
            let textNode = document.createTextNode(1)
            observe.observe(textNode,{characterData:true})
            textNode.textContent(2)
            return
        }
        if(setImmediate) {
            return setImmediate(timeFunc)
        }
        //以上方法都不支持的时候就调用setTimeout setTimeout是宏任务,会在下一轮执行 事件循环机制的相关知识,但我们希望尽量再本轮执行,所以先判断支不支持微任务
        setTimeout(timeFunc,0)
    }
    
    
    
    //计算属性 特点: 默认不执行,等用户取值的时候再去执行,会缓存取值结果,
    //如果依赖的值发生了变化 会更新dirty的属性,再次取值时,可以重新求值
    function initComputed(vm, computed) {
        let wathcers = vm.watchersComputed = Object.create(null) //创建存储计算属性的watcher对象
        for (let key in computed){
            let userDef = computed[key]
            //new Watcher 此时什么都不会做 配置了lazy和dirty
            wathcers[key] = new Watcher(vm, userDef, ()=>{}, {lazy:true}) //lazy:true表示是计算属性,默认刚开始不会执行
            Object.defineProperty(vm,key,{  //将计算属性的方法定义到vm上
                get: createComputedGetter(vm, key) //用户在vm上取值的时候会触发这个方法
            }) 
        }
    
    } 
    
    function createComputedGetter (vm, key) {
        let watcher = vm__watchersComputed[key] //这个watcher就是我们定义的计算属性watcher的实例
        return function () { //用户取值执行此方法
            if(watcher){
                //如果dirty是false的话 不需要重新执行计算属性中的方法
                if(watcher.dirty){ //如果页面取值,dirty为true,就会调用watcher的get方法
                    watcher.eveluate()
                }
                if(Dep.target){ //watcher就是计算属性watcher
                    watcher.depend() //将计算属性的watcher添加到依赖列表
                }
                return watcher.value
            }
        }
    }

     重要步骤:

    1.计算属性主要就是创建一个watcher,并且有个标识lazy:true,表示是计算属性watcher,首次渲染并不会执行get方法,当用户需要取值的时候才会执行get方法。
    this.value = this.lazy ? undefined : this.get() //如果是计算属性的话不会立即调用get方法
    2.首次渲染的时候会创建一个渲染watcher,并将渲染watcher放在栈里,然后执行getter,编译模板,然后要取fullName的值,会调用watcher的evaluate进行求值并将dirty改为false表示,已经取完了
    eveluate(){
        this.value = this.get() //通过这个将计算属性watcher放进栈,并且Dep.target指向了计算属性watcher,然后走getter
        this.dirty = false //值求过了,下次渲染的时候不用求了,当计算属性的值发生变化了就会调用updata方法
    
    }
    3.接下来执行get方法将计算属性wathcer放进栈里,并让Dep.target指向这个计算属性watcher,然后执行getter,将当前依赖的值的watcher存到dep中,并且让Dep.target指向渲染watcher,
    get() {
        //Dep.target也会存放用户的watcher = user watcher
        pushTarget(this) //渲染watcher保存起来 Dep.target = watcher
    
        let value = this.getter.call(vm) //让当前传入的函数updataComponent执行,从实例上取值渲染页面 会调用Object.defineProperty 的get方法
        popTarget() //取完值后 清空当前watcher
        return value //返回新值
    }
    4.然后建立计算属性的watcher和渲染wathcer的依赖关系,当计算属性的返回值发生了变化就会通知渲染watcher重新渲染。
    depend(){
        let i = this.deps.length
        while(i--){
            this.deps[i].depend()
        }
    }
    不积跬步无以至千里
  • 相关阅读:
    [python]CompressionError: bz2 module is not available
    [App Store Connect帮助]四、添加 App 图标、App 预览和屏幕快照(2)添加一个 App Store 图标
    [App Store Connect帮助]四、添加 App 图标、App 预览和屏幕快照(1)App Store 图标、App 预览和屏幕快照概述
    [App Store Connect帮助]三、管理 App 和版本(8)编辑 App 的用户访问权限
    [App Store Connect帮助]三、管理 App 和版本(7)移除 App
    [App Store Connect帮助]三、管理 App 和版本(6.3)转让 App:接受 App 转让
    [App Store Connect帮助]三、管理 App 和版本(6.3)转让 App:发起 App 转让
    [App Store Connect帮助]三、管理 App 和版本(6.2)转让 App:App 转让条件
    [App Store Connect帮助]三、管理 App 和版本(6.1)转让 App:App 转让概述
    [App Store Connect帮助]三、管理 App 和版本(5)添加平台以创建通用购买
  • 原文地址:https://www.cnblogs.com/lyt0207/p/12525098.html
Copyright © 2011-2022 走看看