zoukankan      html  css  js  c++  java
  • 手摸手带你理解Vue的Computed原理

    前言

    computedVue 中是很常用的属性配置,它能够随着依赖属性的变化而变化,为我们带来很大便利。那么本文就来带大家全面理解 computed 的内部原理以及工作流程。

    在这之前,希望你能够对响应式原理有一些理解,因为 computed 是基于响应式原理进行工作。如果你对响应式原理还不是很了解,可以阅读我的上一篇文章:手摸手带你理解Vue响应式原理

    computed 用法

    想要理解原理,最基本就是要知道如何使用,这对于后面的理解有一定的帮助。

    第一种,函数声明:

    var vm = new Vue({
      el: '#example',
      data: {
        message: 'Hello'
      },
      computed: {
        // 计算属性的 getter
        reversedMessage: function () {
          // `this` 指向 vm 实例
          return this.message.split('').reverse().join('')
        }
      }
    })
    

    第二种,对象声明:

    computed: {
      fullName: {
        // getter
        get: function () {
          return this.firstName + ' ' + this.lastName
        },
        // setter
        set: function (newValue) {
          var names = newValue.split(' ')
          this.firstName = names[0]
          this.lastName = names[names.length - 1]
        }
      }
    }
    

    温馨提示:computed 内使用的 data 属性,下文统称为“依赖属性”

    工作流程

    先来了解下 computed 的大概流程,看看计算属性的核心点是什么。

    入口文件:

    // 源码位置:/src/core/instance/index.js
    import { initMixin } from './init'
    import { stateMixin } from './state'
    import { renderMixin } from './render'
    import { eventsMixin } from './events'
    import { lifecycleMixin } from './lifecycle'
    import { warn } from '../util/index'
    
    function Vue (options) {
      this._init(options)
    }
    
    initMixin(Vue)
    stateMixin(Vue)
    eventsMixin(Vue)
    lifecycleMixin(Vue)
    renderMixin(Vue)
    
    export default Vue
    

    _init:

    // 源码位置:/src/core/instance/init.js
    export function initMixin (Vue: Class<Component>) {
      Vue.prototype._init = function (options?: Object) {
        const vm: Component = this
        // a uid
        vm._uid = uid++
    
        // merge options
        if (options && options._isComponent) {
          // optimize internal component instantiation
          // since dynamic options merging is pretty slow, and none of the
          // internal component options needs special treatment.
          initInternalComponent(vm, options)
        } else {
          // mergeOptions 对 mixin 选项和传入的 options 选项进行合并
          // 这里的 $options 可以理解为 new Vue 时传入的对象
          vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
          )
        }
    
        // expose real self
        vm._self = vm
        initLifecycle(vm)
        initEvents(vm)
        initRender(vm)
        callHook(vm, 'beforeCreate')
        initInjections(vm) // resolve injections before data/props
        // 初始化数据
        initState(vm)
        initProvide(vm) // resolve provide after data/props
        callHook(vm, 'created')
    
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }
    }
    

    initState:

    // 源码位置:/src/core/instance/state.js 
    export function initState (vm: Component) {
      vm._watchers = []
      const opts = vm.$options
      if (opts.props) initProps(vm, opts.props)
      if (opts.methods) initMethods(vm, opts.methods)
      if (opts.data) {
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
      // 这里会初始化 Computed
      if (opts.computed) initComputed(vm, opts.computed)
      if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
      }
    }
    

    initComputed:

    // 源码位置:/src/core/instance/state.js 
    function initComputed (vm: Component, computed: Object) {
      // $flow-disable-line
      // 1
      const watchers = vm._computedWatchers = Object.create(null)
      // computed properties are just getters during SSR
      const isSSR = isServerRendering()
        
      for (const key in computed) {
        const userDef = computed[key]
        // 2
        const getter = typeof userDef === 'function' ? userDef : userDef.get
    
        if (!isSSR) {
          // create internal watcher for the computed property.
          // 3
          watchers[key] = new Watcher(
            vm,
            getter || noop,
            noop,
            { lazy: true }
          )
        }
    
        // component-defined computed properties are already defined on the
        // component prototype. We only need to define computed properties defined
        // at instantiation here.
        if (!(key in vm)) {
          // 4
          defineComputed(vm, key, userDef)
        }
      }
    }
    
    1. 实例上定义 _computedWatchers 对象,用于存储“计算属性Watcher
    2. 获取计算属性的 getter,需要判断是函数声明还是对象声明
    3. 创建“计算属性Watcher”,getter 作为参数传入,它会在依赖属性更新时进行调用,并对计算属性重新取值。需要注意 Watcherlazy 配置,这是实现缓存的标识
    4. defineComputed 对计算属性进行数据劫持

    defineComputed:

    // 源码位置:/src/core/instance/state.js 
    const noop = function() {}
    // 1
    const sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: noop,
      set: noop
    }
    
    export function defineComputed (
      target: any,
      key: string,
      userDef: Object | Function
    ) {
      // 判断是否为服务端渲染
      const shouldCache = !isServerRendering()
      if (typeof userDef === 'function') {
        // 2
        sharedPropertyDefinition.get = shouldCache
          ? createComputedGetter(key)
          : createGetterInvoker(userDef)
        sharedPropertyDefinition.set = noop
      } else {
        // 3
        sharedPropertyDefinition.get = userDef.get
          ? shouldCache && userDef.cache !== false
            ? createComputedGetter(key)
            : createGetterInvoker(userDef.get)
          : noop
        sharedPropertyDefinition.set = userDef.set || noop
      }
      // 4
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    
    1. sharedPropertyDefinition 是计算属性初始的属性描述对象
    2. 计算属性使用函数声明时,设置属性描述对象的 getset
    3. 计算属性使用对象声明时,设置属性描述对象的 getset
    4. 对计算属性进行数据劫持,sharedPropertyDefinition 作为第三个给参数传入

    客户端渲染使用 createComputedGetter 创建 get,服务端渲染使用 createGetterInvoker 创建 get。它们两者有很大的不同,服务端渲染不会对计算属性缓存,而是直接求值:

    function createGetterInvoker(fn) {
      return function computedGetter () {
        return fn.call(this, this)
      }
    }
    

    但我们平常更多的是讨论客户端渲染,下面看看 createComputedGetter 的实现。

    createComputedGetter:

    // 源码位置:/src/core/instance/state.js
    function createComputedGetter (key) {
      return function computedGetter () {
        // 1
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          // 2
          if (watcher.dirty) {
            watcher.evaluate()
          }
          // 3
          if (Dep.target) {
            watcher.depend()
          }
          // 4
          return watcher.value
        }
      }
    }
    

    这里就是计算属性的实现核心,computedGetter 也就是计算属性进行数据劫持时触发的 get

    1. 在上面的 initComputed 函数中,“计算属性Watcher”就存储在实例的_computedWatchers上,这里取出对应的“计算属性Watcher
    2. watcher.dirty 是实现计算属性缓存的触发点,watcher.evaluate 对计算属性重新求值
    3. 依赖属性收集“渲染Watcher
    4. 计算属性求值后会将值存储在 value 中,get 返回计算属性的值

    计算属性缓存及更新

    缓存

    下面我们来将 createComputedGetter 拆分,分析它们单独的工作流程。这是缓存的触发点:

    if (watcher.dirty) {
      watcher.evaluate()
    }
    

    接下来看看 Watcher 相关实现:

    export default class Watcher {
      vm: Component;
      expression: string;
      cb: Function;
      id: number;
      deep: boolean;
      user: boolean;
      lazy: boolean;
      sync: boolean;
      dirty: boolean;
      active: boolean;
      deps: Array<Dep>;
      newDeps: Array<Dep>;
      depIds: SimpleSet;
      newDepIds: SimpleSet;
      before: ?Function;
      getter: Function;
      value: any;
    
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        this.vm = vm
        // options
        if (options) {
          this.deep = !!options.deep
          this.user = !!options.user
          this.lazy = !!options.lazy
          this.sync = !!options.sync
          this.before = options.before
        } else {
          this.deep = this.user = this.lazy = this.sync = false
        }
        this.cb = cb
        this.id = ++uid // uid for batching
        this.active = true
        // dirty 初始值等同于 lazy
        this.dirty = this.lazy // for lazy watchers
        this.deps = []
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set()
        this.value = this.lazy
          ? undefined
          : this.get()
      }
    }
    

    还记得创建“计算属性Watcher”,配置的 lazy 为 true。dirty 的初始值等同于 lazy。所以在初始化页面渲染,对计算属性进行取值时,会执行一次 watcher.evaluate

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

    求值后将值赋给 this.value,上面 createComputedGetter 内的 watcher.value 就是在这里更新。接着 dirty 置为 false,如果依赖属性没有变化,下一次取值时,是不会执行 watcher.evaluate 的, 而是直接就返回 watcher.value,这样就实现了缓存机制。

    更新

    依赖属性在更新时,会调用 dep.notify:

    notify() {
      this.subs.forEach(watcher => watcher.update())
    }
    

    然后执行 watcher.update:

    update() {
      if (this.lazy) {
        this.dirty = true
      } else if (this.sync) {
        this.run()
      } else {
        queueWatcher(this)
      }
    }
    

    由于“计算属性Watcher”的 lazy 为 true,这里 dirty 会置为 true。等到页面渲染对计算属性取值时,符合触发点条件,执行 watcher.evaluate 重新求值,计算属性随之更新。

    依赖属性收集依赖

    收集计算属性Watcher

    初始化时,页面渲染会将“渲染Watcher”入栈,并挂载到Dep.target

    在页面渲染过程中遇到计算属性,对其取值,因此执行 watcher.evaluate 的逻辑,接着调用 this.get:

    get () {
      // 1
      pushTarget(this)
      let value
      const vm = this.vm
      try {
        // 2
        value = this.getter.call(vm, vm) // 计算属性求值
      } catch (e) {
        if (this.user) {
          handleError(e, vm, `getter for watcher "${this.expression}"`)
        } else {
          throw e
        }
      } finally {
        popTarget()
        this.cleanupDeps()
      }
      return value
    }
    
    Dep.target = null
    let stack = []  // 存储 watcher 的栈
    
    export function pushTarget(watcher) {
      stack.push(watcher)
      Dep.target = watcher
    } 
    
    export function popTarget(){
      stack.pop()
      Dep.target = stack[stack.length - 1]
    }
    

    pushTarget 轮到“计算属性Watcher”入栈,并挂载到Dep.target,此时栈中为 [渲染Watcher, 计算属性Watcher]

    this.getter 对计算属性求值,在获取依赖属性时,触发依赖属性的 数据劫持get,执行 dep.depend 收集依赖(“计算属性Watcher”)

    收集渲染Watcher

    this.getter 求值完成后popTragte,“计算属性Watcher”出栈,Dep.target 设置为“渲染Watcher”,此时的 Dep.target 是“渲染Watcher

    if (Dep.target) {
      watcher.depend()
    }
    

    watcher.depend 收集依赖:

    depend() {
      let i = this.deps.length
      while (i--) {
        this.deps[i].depend()
      }
    }
    

    deps 内存储的是依赖属性的 dep,这一步是依赖属性收集依赖(“渲染Watcher”)

    经过上面两次收集依赖后,依赖属性的 subs 存储两个 Watcher,[计算属性Watcher,渲染Watcher]

    为什么依赖属性要收集渲染Watcher

    我在初次阅读源码时,很奇怪的是依赖属性收集到“计算属性Watcher”不就好了吗?为什么依赖属性还要收集“渲染Watcher”?

    第一种场景:模板里同时用到依赖属性和计算属性

    <template>
      <div>{{msg}} {{msg1}}</div>
    </template>
    
    export default {
      data(){
        return {
          msg: 'hello'
        }
      },
      computed:{
        msg1(){
          return this.msg + ' world'      
        }
      }
    }
    

    模板有用到依赖属性,在页面渲染对依赖属性取值时,依赖属性就存储了“渲染Watcher”,所以 watcher.depend 这步是属于重复收集的,但 watcher 内部会去重。

    这也是我为什么会产生疑问的点,Vue 作为一个优秀的框架,这么做肯定有它的道理。于是我想到了另一个场景能合理解释 watcher.depend 的作用。

    第二种场景:模板内只用到计算属性

    <template>
      <div>{{msg1}}</div>
    </template>
    
    export default {
      data(){
        return {
          msg: 'hello'
        }
      },
      computed:{
        msg1(){
          return this.msg + ' world'      
        }
      }
    }
    

    模板上没有使用到依赖属性,页面渲染时,那么依赖属性是不会收集 “渲染Watcher”的。此时依赖属性里只会有“计算属性Watcher”,当依赖属性被修改,只会触发“计算属性Watcher”的 update。而计算属性的 update 里仅仅是将 dirty 设置为 true,并没有立刻求值,那么计算属性也不会被更新。

    所以需要收集“渲染Watcher”,在执行完“计算属性Watcher”后,再执行“渲染Watcher”。页面渲染对计算属性取值,执行 watcher.evaluate 才会重新计算求值,页面计算属性更新。

    总结

    计算属性原理和响应式原理都是大同小异的,同样的是使用数据劫持以及依赖收集,不同的是计算属性有做缓存优化,只有在依赖属性变化时才会重新求值,其它情况都是直接返回缓存值。服务端不对计算属性缓存。

    计算属性更新的前提需要“渲染Watcher”的配合,因此依赖属性的 subs 中至少会存储两个 Watcher

  • 相关阅读:
    NodeJs 实现 WebSocket 即时通讯(版本二)
    (JavaScript) 时间转为几天前、几小时前、几分钟前
    (IDEA) 从数据库快速生成Spring Data JPA实体类
    (IDEA) 设置编码统一为UTF-8
    ElasticSearch 中文分词插件ik 的使用
    Logstash 安装及简单实用(同步MySql数据到Elasticsearch)(Windows)
    Docker在Linux上 基本使用
    vue设置页面标题
    HTML5实战与剖析之字符集属性(charset和defaultCharset)
    asp.net传多个值到其它页面的方法
  • 原文地址:https://www.cnblogs.com/chanwahfung/p/13193897.html
Copyright © 2011-2022 走看看