zoukankan      html  css  js  c++  java
  • vue2.x源码解析(第二部分)

    上一章节已经讲vue的所有初始都讲了一遍,下面就来讲下当创建一个vue实例的时候,vue都干了些什么.下面来看一个例子

    new Vue({
        data: {
            el: '#app',
            num: 2
        }
    })
    

    当创建这个实例时,就会去执行其_init函数,那么现在就来看下其都做了什么?现在看下面这句代码:

    const vm: Component = this
        // a uid
    vm._uid = uid++ //为每个实例添加个标记
    let startTag, endTag
    /* istanbul ignore if  主要在不是生产环境下为相应的视点做标记*/
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)//mark方法用于为相应的视点做标记
    }
    // 中间代码省略...
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      ...
      mark(endTag)//初始化结束
      measure(`vue ${vm._name} init`, startTag, endTag)//对这两个标记点进行性能计算
    }

    可以看到首先为实例添加一个_uid属性,以我的理解这个就是为这个实例添加一个标识符,因为会有很多个实例,通过这个标识符更容易区分. 下面可以看到在非生产环境 && 当开启性能追踪的开关(performance)时,就可以追踪一下4个场景的性能:

    1、组件初始化(component init)
    2、编译(compile),将模板(template)编译成渲染函数
    3、渲染(render),其实就是渲染函数的性能,或者说渲染函数执行且生成虚拟DOM(vnode)的性能
    4、打补丁(patch),将虚拟DOM渲染为真实DOM的性能
    

    通过mark函数对代码的起始和结束分别打上2个标记,然后通过measure函数对2个标记进行性能计算。这个可以自己去试一下,在全局配置中将 Vue.config.performance设置为true就行了.我觉得其实就是chromeperformance工具.

    // a flag to avoid this being observed
    vm._isVue = true;
    

    后面就是这句代码,可以看到这段代码有段注释,这段注释的意思就是**一个避免被观察到的标志**.什么意思呢,其实就是避免实例被观测,具体等讲到 obsever的时候就知道了

    合并选项

    接下来继续看下面的代码,下面这段代码主要讲的就是合并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)//为当前组件实例的$option赋值
    } else {
      //合并默认的一些option,并且格式化option中的一些属性,使其符合要求,并对不合理的警告提示,
      // 比如规范化props、规范化Inject、规范化Directives等等
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    可以看到这里产生了不同的情况,一种是options参数传入了&&_isComponent属性为true时做的处理,另一种就是没有或为false做的处理。 首先来讲下第一种,看到这个你会发现不对啊,我在官方文档中没看到这个属性啊,难道是内部的属性?确实是一个内部的属性,这个是创建组件的时候 会生成的,这里不在多讲,等用到的时候在讲。

    选项的来源

    那么现在主要讲第二种情况,可以看到它是执行了mergeOptions函数,其参数分别是:resolveConstructorOptions(vm.constructor)和创建 实例时传入的参数options,最后将该函数返回的值赋值给实例的$options属性。首先来看下第一个参数:

    export function resolveConstructorOptions (Ctor: Class<Component>) {
      // 获取构造函数的options参数
      let options = Ctor.options
      // 当前构造函数有继承函数时
      if (Ctor.super) {...}
      // 将options输出
      return options
    }

    第一个参数是个函数并且传入的参数是实例的构造函数,按这个例子来讲的话这个构造函数就是Vue函数。那么可以看到上面这个函数首先获取的是 Vue函数的options属性,这个在上一章节中已经把所有的初始值都列出来了,所以在这里就不多讲了,可以往全翻翻就知道了。现在往下看,然后 是判断Vue函数的super属性是否存在,存在则执行判断里面的代码,没有则直接将获取到的options属性输出,作为mergeOptions函数的第一 个参数。诶,这个super属性我记得初始化的时候没有啊,哪里来的?这里可以提前说一下,这个是在调用Vue.extend函数的时候会生成的,具体 用到的时候在讲,在这个例子中是没有super属性的,所以最后的结果就是输出Vue函数的options属性.接下来就可以看mergeOptions这个 函数都干了些什么,这个函数来源于/core/util/options.js这个文件.

    检查组件名称是否符合要求

    if (process.env.NODE_ENV !== 'production') {
        checkComponents(child)//检验组件名和标签并给予相应冲突的警告
    }

    这个是mergeOptions函数的第一段代码,可以看到在非生产环境下执行了checkComponents函数,并把mergeOptions函数的第二个参数作为 参数传入其中,那么来看checkComponents函数:

    function checkComponents (options: Object) {
      for (const key in options.components) {
        validateComponentName(key) //解析组件名,防止出现不合理或与原html标签冲突
      }
    }

    可以看到其主要做的事情就是循环参数(创建实例的时候传入的参数)的components属性,也就是子组件名,然后依次执行validateComponentName 函数并把子组件名作为参数传入,那么现在来看validateComponentName函数:

    export function validateComponentName (name: string) {
      if (!/^[a-zA-Z][w-]*$/.test(name)) {
        warn(
          'Invalid component name: "' + name + '". Component names ' +
          'can only contain alphanumeric characters and the hyphen, ' +
          'and must start with a letter.'
        )
      }
      /*与slot,component字段名冲突*/  /*与html标签和svg标签名冲突*/
      if (isBuiltInTag(name) || config.isReservedTag(name)) {
        warn(
          'Do not use built-in or reserved HTML elements as component ' +
          'id: ' + name
        )
      }
    }

    可以看到这个函数的作用就是检测组件名是否合格并且是否与slotcomponenthtml标签和svg标签名冲突.那么checkComponents函数 就是检测所有的子组件名是否符合要求的。

    选项可以是构造函数

    现在在回到mergeOptions函数中来,接下来看其后面的代码:

    if (typeof child === 'function') {
        child = child.options
    }

    这段代码是为了其他地方做的兼容,在Vue源码中这个函数会在多个地方用到,现在可以提一笔就是mergeOptions函数第二个参数可以是构造函数, 不管是vue.extend生成的还是Vue函数都是带有options属性的,在现在这个例子中可以忽略。

    规范化props

    那么现在继续往下看:

    normalizeProps(child, vm) //规范化props

    执行了normalizeProps这个函数并传入了2个参数:mergeOptions函数的第二个参数(在这个例子中就是用户传入的options)、当前Vue实例. 找到这个函数,这个函数也在/core/util/options.js文件中。

    function normalizeProps (options: Object, vm: ?Component) {
      const props = options.props //获取参数中的props
      if (!props) return
      const res = {}
      let i, val, name
      //props是数组转换成object格式
      if (Array.isArray(props)) {
        i = props.length
        while (i--) {
          val = props[i]
          if (typeof val === 'string') {
            name = camelize(val)//把-改成驼峰写法
            res[name] = { type: null }
          } else if (process.env.NODE_ENV !== 'production') {
            //props使用数组语法时,数组各项必须是字符串
            warn('props must be strings when using array syntax.')
          }
        }
      } else if (isPlainObject(props)) {
        //props是对象类型
        for (const key in props) {
          val = props[key]
          name = camelize(key) //把-改成驼峰写法
          res[name] = isPlainObject(val)
            ? val
            : { type: val }
        }
        /*格式化成标准的props格式*/
      } else if (process.env.NODE_ENV !== 'production') {
        warn(
          `Invalid value for option "props": expected an Array or an Object, ` +
          `but got ${toRawType(props)}.`,
          vm
        )
      }
      options.props = res
    }

    可以看到第一段代码获取的是传入参数的props属性,在我们的例子中并没有,那就直接结束了该函数。不过不要紧,我们可以把平时的组件拿来当例子嘛! 初始化了一个res对象,然后看到上面的代码对props属性的两种形式分别作了处理: 数组形式和对象形式。那么分别来讲下:

      看到的工具函数我就不说了,自己去shared/util.js文件中去找,我只说这个工具函数时干什么用的

    1、数组形式: 首先循环该数组,判断数组各项值是否为字符串
           是: 将数组各项值的驼峰写法改成 kebab-case (短横线分隔命名)写法,并作为res对象的属性,值为{ type: null }:
               表示没有类型限制,父组件可以传任何类型的值,没有要求。
           否: 提示错误信息'使用数组语法时,数组各项必须是字符串'
    
    2、对象形式: 循环该对象属性名,获取该属性值,将属性名的驼峰写法改成 kebab-case (短横线分隔命名)写法并作为res对象的属性,
                然后判断属性值是否为纯对象类型[object Object]:
                     是: 直接作为res[属性名]的值
                     否: res[属性名] = {type: 属性值}
    

      为对象形式的举个例子:

    props: {
        count: Number
    }
    //规范化后...
    props: {
        count: {
            type: Number
        }
    }

    上面就是对两种形式的处理,如果两者都不符合,那么就会进入最后了情况(前提在非生产环境下),该情况只做一件事,就是打印警告信息'选项props的值无效: 期望得到一个数组或对象,但得到其他类型'。最后就是将处理完后的res对象赋值给传入参数的props属性。到这里就将props规范化了。

    规范化Inject

    上面已经规范化了props,那么继续往下看:

    normalizeInject(child, vm) //规范化Inject

    执行了normalizeInject这个函数并传入了2个参数,这两个参数和normalizeProps传入的参数一样,这里就不讲了。直接去看代码:

    function normalizeInject (options: Object, vm: ?Component) {
      const inject = options.inject //缓存参数中的inject属性
      if (!inject) return
      const normalized = options.inject = {} //清空参数中的inject属性
      //inject属性为数组类型
      if (Array.isArray(inject)) {
        for (let i = 0; i < inject.length; i++) {
          normalized[inject[i]] = { from: inject[i] }
        }
        /*将inject转换成标准的格式 val: {
          from: 'val'  //匹配父组件中的provide的key名
        }*/
      } else if (isPlainObject(inject)) {
        //inject为对象格式,遍历对象
        for (const key in inject) {
          const val = inject[key] //获取key值
          //判断key值是否为对象,是对象则将标准格式与值合并,否则转换成标准格式
          normalized[key] = isPlainObject(val)
            ? extend({ from: key }, val)
            : { from: val }
        }
      } else if (process.env.NODE_ENV !== 'production') {
        warn(
          `Invalid value for option "inject": expected an Array or an Object, ` +
          `but got ${toRawType(inject)}.`,
          vm
        )
      }
    }

    同样在这个例子中也没有inject属性,当然这个属性也比较不常见,好像很少用到,在这里就说一下它的用途:

     它是和当前组件的祖先组件实例的provide属性配合着用的,provide属性里面的值用于传递给子孙组件的,而inject属性就是接收器,
     里面定义子孙组件需要的数据,当然这个接手值只能读取不能修改,修改了就会打印警告信息。

    那么作用也知道了,现在就来具体看下这个函数做了什么?首先第一段代码就是缓存inject属性,然后将该属性值重置为一个空对象,接着又 是处理2种形式的情况:

     1、数组形式: 循环数组,把数组各项的值作为normalized对象的属性,设置属性值为{from: 数组项的值},其实就是处理成对象格式
     2、对象形式: 循环对象的key,把key名作为normalized对象的key名,然后判断对象的属性值是否为对象:
                            是: 将属性值和{ from: 属性名 }合并
                            否: 将normalized[key]设置为{ from: 属性值 }
    

    其实和规范props差不多,只不过这个在属性值为对象的情况下是合并而不是直接替换,最后from的意思是表示来源的意思,这个后面也会讲到。

    规范化Directives

    上面已经规范化了inject属性和props属性,那么继续往下看还要规范化什么:

    normalizeDirectives(child) //规范化Directives
    

    可以看到函数名已经说了就是规范化Directives,直接看代码:

    function normalizeDirectives (options: Object) {
      const dirs = options.directives //缓存options.directives的值
      if (dirs) {
        //遍历对象
        for (const key in dirs) {
          const def = dirs[key] //获取options.directives对象的key值
          //值的类型为function类型
          if (typeof def === 'function') {
            //重新赋值key值为其添加属性值都为def方法的bind、update属性
            dirs[key] = { bind: def, update: def }
          }
        }
      }
    }

    可以看到第一段代码获取的是参数的directives属性,看到这里就知道vue是可以设置全局指令和局部指令,一个是所有组件都可以用的, 一个是只能当前组件用的。不废话继续往下看,同样是对存在directives属性值时做的处理,循环directives中的属性名并缓存属性值, 当属性值为函数时,将属性值替换为{bind: 属性值, update: 属性值},也就是将属性值规范成对象形式的。

    处理extends

    const extendsFrom = child.extends //缓存extends属性为extendsFrom
    if (extendsFrom) {
    //重新赋值parent为一个原parent和extendsFrom合并的全新对象
    parent = mergeOptions(parent, extendsFrom, vm)
    }

    可以看到首先获取的是mergeOptions函数第二个参数的extends属性缓存为extendsFrom,这个参数主要是用于扩展组件用的(可以是一个 选项对象或者构造函数,这里主要是选项对象)。当extendsFrom存在值时,同样执行mergeOptions函数这样就形成了递归,所以我们就先叫它 extendsMergeOptions函数,这个函数的两个参数是:mergeOptions函数的第一个参数parentextendsFrom变量.那么也就是说 extendsFrom变量会将上面的步骤重新走一遍,后面的等讲完mergeOptions函数在举个例子就明了了。现在继续往下看:

    处理mixins

    if (child.mixins) {
        for (let i = 0, l = child.mixins.length; i < l; i++) {
          parent = mergeOptions(parent, child.mixins[i], vm)
        }
      }

    其实这个和extends其实相差不多,都是用于扩展的,可以看到mixins属性是个数组,它是循环执行mergeOptions函数,我们就叫它mixinsMergeOptions 函数,其第一个参数还是mergeOptions函数的第一个参数parent,第二个是mixins数组各项值。同样mixins数组各项值又走了遍上面的步骤, 也是等讲完mergeOptions函数来举个完整的例子。现在继续往下看:

    对合并选项进行最后的处理

    const options = {}
    let key
    for (key in parent) {
        // 不管前面mixins || extends是否存在最后都会被option中的属性覆盖
        mergeField(key)
    }
    for (key in child) {
        if (!hasOwn(parent, key)) {
          mergeField(key)
        }
    }
    function mergeField (key) {
        const strat = strats[key] || defaultStrat
        options[key] = strat(parent[key], child[key], vm, key)
    }
    return options

    这是mergeOptions函数最后一段代码,可以看到首先是初始化了options变量为空对象,然后循环parent参数的属性名缓存为key变量,在这里再次提一笔, 在这个例子中,parent参数就是Vue函数的options属性.循环执行了mergeField函数,参数keykey变量,那么mergeField函数做了什么呢?

    其首先是初始化了strat变量,其值为判断参数key是否在strats对象中?存在则缓存strats[key],不存在则默认是defaultStrat变量。在这个 mergeOptions函数中好像没这两个变量啊,那么就往函数外找吧,发现这两个变量也在/core/util/options.js这个文件中,但是先把这个两个变量放一放, 先把mergeField函数执行下去,可以看到最后是把参数key当做options变量的属性名,值先不管,不过可以知道其是执行的strat函数,参数是 parent[参数key]child[参数key]Vue实例、参数key.

    那么前面提到的循环parent参数的属性名,也就是说是把所有属性名(包括原型链中的)都拿到了options变量中来。在来看后面的是循环child参数的属性名,同样是缓存为 key变量,然后是判断的该key变量是否存在于parent参数对象本身?

       存在: 不做任何事情
       不存在(也就是存在于原型链中或没有这个属性): 执行mergeField函数,那也就是说替换了options中已存在参数`key`的属性名。
    

      

      总结这段代码的意思就是: 将parent参数拿到了options变量中,然后将child参数中不存在于parent参数对象本身的属性名拿出来放入options对象中, 所以有可能是将options对象中的属性名替换了,也可能是增加了属性,但替换了的属性值不会变,总的来说就是合并parent参数和child参数,最后输出一个合并过的 对象。举个例子:

     function cs () {
        this.data = {
            count: 2
        }
     };
     cs.prototype = {
        watch: {}
     };
     parent = new cs();
     child = {
        methods: {},
        watch: {}
    }
    // 处理过后...
    options = {
        data: {count: 2},
        watch: {},
        methods: {}
    }

    对extends和mixins举例子

    // 属性值我就不写了
    let mixins = {
        data: {},
        mounted () {},
        created () {}
    }
    let exten = {
        data: {},
        watch:{},
        created () {}
    };
    new Vue({
        mixins: [mixins],
        extends: exten,
        data: {},
        methods:{}
    })
    Vue.options ={
      directives: {},
      components: {},
      filters: {},
      _base: Vue
    }
    // extends处理过后
    parent = {
        directives,
        components,
        filters,
        _base,
        data,
        watch,
        created
    }
    // mixins处理过后
    parent = {
        directives,
        components,
        filters,
        _base,
        data,
        watch,
        created,
        mounted
    }
    // 最后处理成
    options = {
        directives,
        components,
        filters,
        _base,
        data,
        watch,
        created,
        mounted,
        methods
    }

    对于属性值的处理会在下一章节讲,这一节就讲这么多,不过可以提前讲下属性值问题,不管前面parentmixins还是extends是否存在最后都会 被options中的属性覆盖,优先级一定是child > mixins > extends > parent.

    下一章节我们来说说     各个选项合并的结果(strats和defaultStrat)

     详细信息可访问https://github.com/maomao93/vue-2.x/tree/master/personal

  • 相关阅读:
    java.util.ConcurrentModificationException故障分析
    Eclipse常见问题总结-持续更新
    MySQL学习—简单存储过程
    Mysql学习——触发器
    MySQL学习—多表查询(内连接,外链接,全连接)
    JDK环境变量配置
    Spring学习总结(二)——容器对Bean的管理
    Spring学习总结(一)——Spring容器的实例化
    类加载机制
    手写数据库连接池
  • 原文地址:https://www.cnblogs.com/maomao93/p/14156093.html
Copyright © 2011-2022 走看看