美文网首页vue
手写Vue2核心(六):侦听器watch与计算属性实现

手写Vue2核心(六):侦听器watch与计算属性实现

作者: 羽晞yose | 来源:发表于2021-03-10 17:56 被阅读0次

    侦听器watch的实现原理

    官方watch使用方式文档
    Vuewatch的使用方式有多种,包括:

    • 函数形式
    'test' (newVal, oldVal) {}
    
    • 对象形式
    'test': {
        hadler () {}
    }
    
    • 监控当前实例上的方法
    watch: {
        'test': testMethod
    },
    methods: {
        testMethod (newVal, oldVal) {}
    }
    
    • 写成 key 和数组的方式,会逐一调用
    'test': [
        (newVal, oldVal) => {},
        function handle2 (val, oldVal) {},
        {
            handler: function handle3 (val, oldVal) {},
        }
    ]
    

    前面只实现了渲染watcher,现在来实现侦听器watcher(当然都是同一个watcher构造函数)
    改写init.js,将实例方法脱离出来,采用混入的方式来维护

    // init.js
    export function initMixin (Vue) {
    +   stateMixin(Vue)
    -   Vue.prototype.$nextTick = nextTick
    }
    

    上面写了watch多种使用方式,所以需要对watch进行处理,如果是数组则依次调用Vue.$watch来执行,否则则直接执行

    // state.js
    function initWatch (vm) {
        let watch = vm.$options.watch
        for (let key in watch) {
            const handler = watch[key]
    
            if (Array.isArray(handler)) {
                handler.forEach(handle => {
                    createWatcher(vm, key, handler)
                })
            } else {
                createWatcher(vm, key, handler) // 字符串、对象、函数
            }
        }
    }
    
    function createWatcher (vm, exprOrFn, handler, options) { // options 可以用来标识是用户watcher
        if (typeof handler === 'object' && typeof handler !== 'null') {
            options = handler
            handler = handler.handler // 是一个函数
        }
    
        if (typeof handler === 'string') {
            handler = vm[handler] // 将实例的方法作为handler
        }
    
        return vm.$watch(exprOrFn, handler, options)
    }
    
    export function stateMixin (Vue) {
        Vue.prototype.$nextTick = function (cb) {
            nextTick(cb)
        }
        Vue.prototype.$watch = function (exprOrFn, cb, options) {
            // 数据应该迎来这个watcher,数据变化后应该让watcher从新执行
            let watcher = new Watcher(this, exprOrFn, cb, {...options, user: true}) // user: true 用于标识是用户写的侦听器,非渲染watcher
            if (options.immediate) {
                cb() // 如果是immediate,则立即执行
            }
        }
    }
    

    渲染watch与用户传入定义的watch,主要区分在于是否存在user属性,如果有则证明是用户传入的watch,否则为渲染watch
    watch需要对新老值进行比较,如果不一致则去调用绑定回调,因此还需要改写getrun方法,来记录新老值并进行对比(之前仅获取不会保留获取的值)

    // observer\watcher.js
    class Watcher {
        constructor (vm, exprOrFn, cb, options={}) {
    +       this.user = options.user // 用户watcher
    
    +       if (typeof exprOrFn === 'function') {
    +           this.getter = exprOrFn
    +       } else {
    +           this.getter = function () { // exprOrFn传递过来的可能是字符串,也可能是函数
    +               // 当去当前实例上取值时,才会触发依赖收集
    +               let path = exprOrFn.split('.')
    +               let obj = vm
    +               for (let i = 0; i < path.length; i++) {
    +                   obj = obj[path[i]]
    +               }
    +               return obj
    +           }
    +       }
    
            // 默认会先调用一次get方法,进行取值,将结果保存下来
    -       this.get()
    +       this.value = this.get()
        }
        // 这个方法中会对属性进行取值操作
        get () {
            pushTarget(this) // Dep.target = watcher
    -       this.getter() // 取值
    +       let result = this.getter() // 取值
            popTarget()
    
            return result
        }
        // 当属性取值时,需要记住这个watcher,稍后数据变化了,去执行自己记住的watcher即可
        addDep (dep) {
            let id = dep.id
            if (!this.depsId.has(id)) { // dep是非重复的
                this.depsId.add(id)
                this.deps.push(dep)
                dep.addSub(this)
            }
        }
        // 真正触发更新
        run () {
    -       this.get()
    +       let newValue = this.get()
    +       let oldValue = this.value
    +       this.value = newValue // 将老值更改掉
    +       if (this.user) {
    +           this.cb.call(this.vm, newValue, oldValue)
    +       }
        }
        update () { // 多次更改,合并成一次(防抖)
            queueWatcher(this)
        }
    }
    

    computed的实现原理

    computed的主要实现包括以下三要素:

    1. 通过Object.defineProperty进行劫持,因为计算属性主要用于取值,需要进行取值处理,如果值有变更需要通知视图更新
    2. 计算属性watcher,用于取值逻辑与通知视图更新
    3. 具有缓存,通过属性dirty标识,如果dirtytrue则证明需要重新取值,否则直接使用缓存值value即可

    流程太长而且还跟之前的逻辑大幅度耦合,如果要按照实现一步步拆解下来会有超级大量的重复代码,一般流程太长逻辑太绕的我都会将流程一步步用中文描述写下来,有需要的就直接跟着源码与我写下的流程对着看吧~

    1. 如果用户有传入computed属性,则初始化计算属性initComputed
    2. vue._computedWatchers上存储计算属性watcher
    3. 循环遍历计算属性,获取计算属性表达式(如果是对象形式,则获取get属性表达式)
    4. 为该属性分配一个计算属性watcher,并设置lazy: true,用于标识,因为计算属性默认不做任何操作
    5. 定义计算属性defineComputed,返回一个高阶函数。当计算属性被使用时,该高阶函数将会触发对计算属性中所使用的属性值进行依赖收集,属性的依赖收集会将当前watcher进行记录,此时计算属性中使用到的属性值都会记录到该计算属性watcher,记录后则销毁该watcher(popTarget中的stack.pop()),然后判断是否还有watcherDep.target),如果有说明还有渲染watcher,也需要一并被收集起来
    6. 最后通过Object.defineProperty进行劫持(简单总结起来就是,计算属性使用时,里面所使用的属性会记录该计算属性watcher)
      到这一步劫持收集完毕,依赖属性记录的Dep中既有渲染watcher,也有计算属性watcher,发生变更时,触发dep.notify,将存储的watcher逐一执行(栈结构,渲染watcher在栈底,计算属性watcher的update仅为更改dirty标识,而渲染watcher会触发视图更新)
    // state.js
    export function initState (vm) {
    +   if (opts.computed) {
    +       initComputed(vm)
    +   }
    }
    
    + // 初始化计算属性
    + function initComputed (vm) {
    +     let computed = vm.$options.computed
    +     // 1. 需要有watcher 2. 需要通过defineProperty 3. dirty
    +     const watchers = vm._computedWatchers = {} // 用来存放计算属性的watcher
    + 
    +     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) {
    +     const sharedPropertyDefinition = {
    +         enumerable: true,
    +         configurable: true,
    +         get: () => {},
    +         set: () => {}
    +     }
    + 
    +     // 函数式
    +     if (typeof userDef === 'function') {
    +         sharedPropertyDefinition.get = createComputedGetter(key) // 通过dirty来控制是否调用userDef
    +     } else {
    +         sharedPropertyDefinition.get = createComputedGetter(key) // 需要加缓存
    +         sharedPropertyDefinition.set = userDef.set
    +     }
    + 
    +     Object.defineProperty(target, key, sharedPropertyDefinition)
    + }
    + // 用户取值时调用该方法
    + function createComputedGetter (key) {
    +     return function () { // 高阶函数,每次取值调用该方法
    +         const watcher = this._computedWatchers[key]
    +         if (watcher) {
    +             if (watcher.dirty) { // 判断是否需要执行用户传递的方法,默认肯定是脏的
    +                 watcher.evaluate() // 对当前watcher求值
    +             }
    + 
    +             if (Dep.target) {
    +                 watcher.depend()
    +             }
    + 
    +             return watcher.value // 默认返回watcher上存的值
    +         }
    +     }
    + }
    
    // observer\dep.js
    class Dep {
        notify () {
    -       this.subs.forEach(watcher => watcher.update())
    +       this.subs.forEach(watcher => {
    +           watcher.update()
    +       })
    +   }
    }
    
    let stack = []
    
    export function pushTarget (watcher) {
        Dep.target = watcher
    +   stack.push(watcher) // stack有渲染watcher,也有其他watcher
    }
    
    export function popTarget () {
    -   Dep.target = null
    +   stack.pop() // 栈型结构,第一个为渲染watcher,后面的为其他watcher,watcher使用过就出栈
    +   Dep.target = stack[stack.length - 1]
    }
    
    // observer\watcher.js
    class Watcher {
        constructor (vm, exprOrFn, cb, options={}) {
    +       this.lazy = options.lazy // 如果watcher上有lazy属性,说明是一个计算属性
    +       this.dirty = this.lazy // dirty代表取值时是否执行用户提供的方法,可变
    
            // 默认会先调用一次get方法,进行取值,将结果保存下来
    +       // 如果是计算属性,则什么都不做(计算属性默认不执行)
    +       this.value = this.lazy ? void 0 : this.get()
        }
        // 这个方法中会对属性进行取值操作
        get () {
            pushTarget(this) // Dep.target = watcher
            // data属性取值,触发updateComponent,其中this指向的时vm
            // computed属性取值,会执行绑定的函数,该函数中的this指向的是该watcher,所以this指向会有问题,需要call(this.vm)
    -       let result = this.getter() // 取值
    +       let result = this.getter.call(this.vm)
            popTarget()
    
            return result
        }
        update () { // 多次更改,合并成一次(防抖)
    +       if (this.lazy) {
    +           this.dirty = true
    +       } else {
    +           // 这里不要每次都调用get方法,get会重新渲染页面
                queueWatcher(this)
    +       }
        }
    +   evaluate () {
    +       this.value = this.get()
    +       this.dirty = false // 取过值后标识,标识已经取过值了
    +   }
    +   depend () {
    +       // 计算属性watcher会存储dep,dep会存储watcher
    +       // 通过watcher找到对应的所有dep,让所有的dep都记住这个渲染watcher
    +       let i = this.deps.length
    +       while (i--) {
    +           this.deps[i].depend()
    +       }
    +   }
    }
    

    相关文章

      网友评论

        本文标题:手写Vue2核心(六):侦听器watch与计算属性实现

        本文链接:https://www.haomeiwen.com/subject/idpwqltx.html