美文网首页
vue2-生命周期3

vue2-生命周期3

作者: AAA前端 | 来源:发表于2021-06-01 22:55 被阅读0次

    初始化 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中, 并且方法名以 $ 或 _开头,会发出警告

    • 将方法挂载到vm中
      判断 methods[key]是否存在,如果不存在,则将 noop 赋值到 vm[key]中, 如果存在,该方法通过 bind 改写它的this后,再赋值给vm[key]中。
      这样我们就可以通过vm.x访问 methods中的x方法了。

    初始化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的类型有三种情况:字符串、函数、对象。

    • 如果是函数,不用特殊处理, 直接传递给vm.$watch即可。
    • 如果是对象,说明用户设置了包含选项的对象,把options的值设置为handler,并且将变量handler设置为 handler对象的handler方法。
    • 如果是字符串, 从 vm中取出方法,将它赋值给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进入初始化阶段,然后选择性进入模板编译与挂载阶段。

    相关文章

      网友评论

          本文标题:vue2-生命周期3

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