vue2-生命周期3

2021-06-01  本文已影响0人  AAA前端

初始化 methods

初始化method时, 只需要循环选项中的methods对象, 将每个属性挂载到vm上


// 初始化方法
function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  // 遍历vm.$options中的methods
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      // method不是 函数 警告
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      // 如果 props 中已经 有 这个 key 警告
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      // 如果 vm[key] 实例上已经什么过 警告
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    // 把method方法 挂载 到 vm实例上 
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

这里先声明一个变量props, 用来判断 methods中的方法是否和props发生了重复,然后 for...in循环methods对象。

methods中的某个方法已经存在于vm中, 并且方法名以 $ 或 _开头,会发出警告

初始化data

data 中的 数据最终会保存 到vm_data中,然后 在vm上设置一个代理,使得通过 vm.x可以访问到 vm._data中的x属性。 最后调用 observe函数将 data 转换为响应式数据。


// 初始化 data
function initData (vm: Component) {
  let data = vm.$options.data
  // data 是 函数 ,执行并转换为响应式 , 否则 就是 自己 
  data = vm._data = typeof data === 'function'
    ? getData(data, vm) // getData 把 data 里面 的值 转换为响应式 ,并返回 对象类型
    : data || {}
    // data 如果不是 对象 设置默认值为 空对象
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
   // 这里 methods 仅仅警告,下面的proxy 依旧会代理
    if (process.env.NODE_ENV !== 'production') {
      // 方法 中已经 存在 该 key 警告 (在生产环境下 是可以 重复命名的)
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
   // props 中 已经存在 会警告, 这里进入 后就不会 调用 proxy代理了
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      //  不是以  $ or _ 开头的话, 把 _data上的key 代理 到vm上。 (不能以 $ _开头设置 key)
      // 直接 访问 vm[key] 就可以访问 vm._data[key]
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  // 观察data, 转换为响应式
  observe(data, true /* asRootData */)
}

我们得到data后,保存在 data变量中, 判断data 的类型,如果是函数,执行函数得到返回值 并赋值给data 和 vm._data。

最终得到data 的值应该是 Object类型, 否则开发环境会警告, 并且 设置data 为 一个空对象的默认值。

然后判断 当前循环的key 是否存在与methods中
判断 props中是否存在某个属性与key相同

如果data中某个 key 与methods发生了重复 ,依然会将 data 代理到实例中, 当如果与props重复 , 不会将data 代理到实例中。

最后proxy 函数实现代理功能。

初始化 computed

computed是定义在 vm上的一个特殊的getter方法。 vm上定义getter方法时, get并不是用户提供的函数, 而是vue.js内部的一个代理函数, 在代理函数中 结合 watcher实现缓存 与 收集依赖等功能。

计算属性的结果会被缓存,如何知道 计算属性的返回值时候发生了变化呢? 这是结合 watcher 的 dirty属性来分辨的。 当dirty 为true 时, 说明需要重新计算,否则,不需要重新计算。

当计算属性中的 内容发生变化后, 计算属性 的watcher 和 组件的watcher 都会得到通知。

计算属性的watcher 会把dirty 属性设置为true,此时不会执行计算。
同时 组件的watcher 也会得到通知,从而 执行render函数 进行重新渲染操作,由于要重新执行render,所以会重新读取计算属性的值,此时计算属性的watcher上的dirty为true,会重新就上计算属性的值,用于本次渲染。

计算属性的一个特点是缓存。 计算属性函数所依赖的数据在没有发生变化的情况下,会反复读取计算属性,而计算属性并不会反复执行。

// 设置 watcher 时的 lazy 为true , 实例化时 告诉 watcher 生成一个 供计算属性使用的watcher 实例
const computedWatcherOptions = { lazy: true }

function initComputed(vm: Component, computed: Object) {
    // $flow-disable-line
    // 保存计算属性的watcher 实例
    const watchers = vm._computedWatchers = Object.create(null)
        // computed properties are just getters during SSR
        // 计算属性 在SSR 环境,只是一个 普通的 getter 方法
    const isSSR = isServerRendering()

    //  遍历 computed 对象
    for (const key in computed) {
        const userDef = computed[key]
            // 获取 getter , 函数直接本身,对象获取对象上的get函数
        const getter = typeof userDef === 'function' ? userDef : userDef.get
        if (process.env.NODE_ENV !== 'production' && getter == null) {
            warn(
                `Getter is missing for computed property "${key}".`,
                vm
            )
        }

        //  服务端环境 没必要
        if (!isSSR) {
            // create internal watcher for the computed property.
            // 比如: fullName : function(){return this.firstname + this.lastname}
            //  由于 computed 的wathcer lazy,所有开始并不会 执行 get.
            //  new Watcher(vm, updateComponent, noop, 。。。) 这个 组件更新的watcher ,会 对模板仅仅访问,获取到当前
            // computed的 值,才出发 sharedPropertyDefinition.get 函数.
            // watcher.evaluate() 会 执行 computed的函数 比如上面的 return this.firstname + this.lastname
            // 这样 fullname这个watcher 就会 保存 firstanme 和 lastname 的 dep 
            // 记录完之后 继续, 这个时候 的Dep.target 时 updateCOmponent对于的 Watcher.
            // computed 的 watcher.depend()。 会把 computed里面的 所有dep (firstname, lastname的dep),分别收集
            // updateCOmponent对于的 Watcher。 这样 ,当 firstname或者 lastname变化时, 它们的dep 执行 dep.notify
            // 通知 每个 watcher 执行 update 方法 。  而 computed第 三个参数 是noop。所有没有回调执行。
            // 而 updateCOmponent 的watcher update 会让页面 重新 render
            watchers[key] = new Watcher(
                vm,
                getter || noop,
                noop,
                computedWatcherOptions
            )
        }

        // 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)) {
            defineComputed(vm, key, userDef)
        } else if (process.env.NODE_ENV !== 'production') {
            if (key in vm.$data) {
                warn(`The computed property "${key}" is already defined in data.`, vm)
            } else if (vm.$options.props && key in vm.$options.props) {
                warn(`The computed property "${key}" is already defined as a prop.`, vm)
            } else if (vm.$options.methods && key in vm.$options.methods) {
                warn(`The computed property "${key}" is already defined as a method.`, vm)
            }
        }
    }
}

我们先声明了变量 computedWatcherOptions, 作用是在实例化watcher时,通过参数告诉 watcher 类应该生成一个 供计算属性使用的watcher实例。

随后vm._computedWatchers属性用来保存所有计算属性的watcher实例。

Object.create(null)创建出来的对象没有原型, 它不存在proto属性

接下来, for...in循环computed 对象, 初始化每个计算属性。

随后判断 当前环境不是服务端渲染时, 创建watcher实例。

第二个参数的getter 其实是用户设置的计算属性 get函数。

最后,判断当前循环到的计算属性的名字是否存在vm中。 如果不存在,使用 defineComputed函数在vm上设置一个计算属性。

defineComputed函数

// 默认属性描述符
const sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
}
// 属性的 getter 和 setter 根据 userDef 设置
export function defineComputed(
    target: any,
    key: string,
    userDef: Object | Function
) {
    // 非服务端环境下 计算属性才缓存
    const shouldCache = !isServerRendering()
        // userDef 可能是 函数  也可能是 对象 {get: fn, set: fn}
        // 如果是函数 ,将函数作为 getter函数
        // 如果是对象, 将get方法作为getter方法,set方法作为setter方法
    if (typeof userDef === 'function') {
        sharedPropertyDefinition.get = shouldCache ?
            createComputedGetter(key) :
            createGetterInvoker(userDef) // 不需要缓存 ,仅仅执行 函数即可
        sharedPropertyDefinition.set = noop
    } else {
        sharedPropertyDefinition.get = userDef.get ?
            shouldCache && userDef.cache !== false ?
            createComputedGetter(key) :
            createGetterInvoker(userDef.get) :
            noop
        sharedPropertyDefinition.set = userDef.set || noop
    }
    if (process.env.NODE_ENV !== 'production' &&
        sharedPropertyDefinition.set === noop) {
        sharedPropertyDefinition.set = function() {
            warn(
                `Computed property "${key}" was assigned to but it has no setter.`,
                this
            )
        }
    }
    Object.defineProperty(target, key, sharedPropertyDefinition)
}

先定义了 sharedPropertyDefinition , 提供一个默认的属性描述符。

接着函数 接受三个参数 target, key, userDef. 属性的getter和setter根据 userDef的值来设置。

然后函数中shouldCache 变量 ,在非服务端渲染环境下,计算属性才有缓存。

在定义计算属性时, 判断userDef的类型。 如果是函数, 则将函数理解为getter, 如果是对象, 则将队形的get 方法作为getter. set方法作为setter.

计算属性的缓存与响应式功能 主要在于是否将getter方法设置到createComputedGetter函数执行后的返回结果。

// 缓存 key
function createComputedGetter(key) {
    // 返回的getter 在 initComputed的时候不会执行 (lazy 为 true  constructor时 不会执行 this.get)
    // 等到 lifecycle 中 beforeMount 生命周期 之后  
    // 对 new Watcher(vm, updateComponent, noop, ...)  //  (updateComponent 先调用渲染函数 获取一份最新的Vnode节点树, 然后通过 _update方法 对最新的 Vnode和 旧Vnode进行对比,更新DOM节点。)
    // new Watcher 会 由于, 没有lazy 会执行 this.get() ,此时的Dep.target 为 updateComponent的watcher(组件的watcher),而 updateComponent 会 对 所有节点进行对比 ,所有 会触发 computed 这里的 getter
    // 这里 watcher.dirty(constructor 的时候 this.dirty = this.lazy), evaluate 此时 执行 this.get 
    // 把 computed 对应的 watcher 保存 到 Dep.target中 , 执行 computed的函数 或 get 获取 返回值,popTarget之后 Dep.target 继续变为 updateComponent的Watcher(组件的watcher)
    // 执行 到 Dep.target 时  其实 就是 updateComponent的Watcher(组件的watcher),watcher.depend 把 computed 的 watcher中 用到 的dep依赖列表循环(所有的computed的 dep 依赖列表), 都 添加 当前 updateComponent的Watcher (组件的watcher)
    // 这样 当 computed 的 getter 中 有变化,会 触发 updateComponent的Watcher (组件的watcher)的更新,即页面 更新
    return function computedGetter() {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
            // dirty 设置 为 true之后 ,下一次读取 计算属性才会 重新计算
            if (watcher.dirty) {
                watcher.evaluate()
            }
            // 把 当前 读取计算属性的watcher 添加到 计算属性所有的依赖列表中
            // 这样 依赖列表 有变化 ,都会 通知 当前 这个watcher 更新(比如更新视图)
            if (Dep.target) {
                watcher.depend()
            }
            return watcher.value
        }
    }
}

这个函数是一个高阶函数,接受一个参数key并方法另一个函数computedGetter。

先使用key 读取 watcher 并赋值给 变量 watcher。 如果watcher 存在,那么判断的wathcer.dirty是否为true。

但计算属性所依赖的状态发生变化时,会将wathcer.dirty设置为true。 这样当下一次读取计算属性时,会发现watcher.dirty为true, 此时会重新计算返回值, 否则就直接使用之前的计算结果。

随后判断Dep.target是否存在,如果存在,则调用watcher.depend方法。 读取计算属性的那个watcher添加到计算属性说依赖的所有状态的 依赖列表中。

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
....
if (options) {
this.lazy = !!options.lazy
} else {
  this.lazy = false
}

this.dirty  = this.lazy

this.value = this.lazy ? undefined: this.get()

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

evaluate方法的逻辑很简单,执行this.get方法重新计算一下值,然后将this.dirty设置为false。

watcher.depend方法会遍历thisdeps属性(保存了计算属性用到的所有状态的dep实例,而每个属性的dep实例也保存了它的所有依赖),并依次执行dep实例的depend方法。

depend方法,将组件的watcher实例添加到dep实例的依赖列表中。

组件的watcher观察到计算属性中用到的状态发生变化时,组件的wathcer 会收到通知,从而进行重新渲染操作。

初始化watch

  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }

初始化watch的时候要判断watch选项不是 浏览器的原生watch时,才能初始化watch操作。 因为 Firefox浏览器中的 Object.prototype上有一个watch方法。

由于watch使用时,值 可以是 回调函数、方法名,包含选项的对象。

watch:{
  a: function(val, oldVal){},
  b: 'someMethod',
  c: {
       hander: function(val, oldVal){},
        deep: true,
        immediate: true
      },
  e: [
    function fn1(val, oldVal){},
    function fn2(val, oldVal){}
  ],
  g.a: function(val, oldVal){}
}

实现思路: 循环watch选项,将对象中每一项依次调用vm.$watch方法来观察表达式 或 computed 在 vue.js实例上的变化即可。

由于watch 选项的值支持不同类型,所有要做适配

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

先处理数组的情况, 如果handler的类型是数组,那么遍历数组,将每一项依次调用 createWatcher函数来创建watcher. 如果不知直接调用 createWatcher函数创建一个Watcher.

createWatcher函数主要负责处理其他类型的 handler 并调用 vm.$watch 创建 Watcher观察表达式。

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 如果是对象,options设置为handler
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // 如果是字符串,从vm中取出方法赋值给handler
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  // 初始化 watch
  return vm.$watch(expOrFn, handler, options)
}

handler的类型有三种情况:字符串、函数、对象。

针对不同类型的值处理完毕后, handler变量是回调函数,options为vm.watch的选项,接下来调用vm.watch即可完成初始化watch的任务。

初始化provide

provide选项应该是一个对象或者是返回一个对象的函数。 可以使用ES2015Symbl作为key,当它只有在原生支持 Symbol和 Reflect.ownkeys的环境下工作。

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

这里判断provide的类型是否是函数,是函数执行将返回值赋值给vm._provided。否则直接将变量赋值给vm._provided。

总结

Vue.js整体生命周期分为4个阶段: 初始化阶段、模板编译阶段、挂载阶段、卸载阶段。

初始化阶段结束后,会触发created钩子函数。 在created钩子函数与beforeMount钩子函数之间的阶段叫模板编译阶段, 这个阶段在不同的构建版本中不一定存在。

挂载阶段在beforeMount钩子函数与mounted期间,挂载完毕后, Vue.js处于已挂载阶段。 已挂载阶段会持续追踪状态的变化,当数据变化时, watcher会通知虚拟DOM重新渲染视图。

在渲染钳触发beforeUpdate钩子函数,渲染完毕后触发updated钩子函数。

当vm.$destroy被调用时,组件进入卸载阶段。 卸载前会触发 beforeDestroy钩子函数, 卸载后悔触发destroyed钩子函数。

new Vue()被执行后, Vue.js进入初始化阶段,然后选择性进入模板编译与挂载阶段。

上一篇下一篇

猜你喜欢

热点阅读