Vue2源码解读

Posted 石志凯

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue2源码解读相关的知识,希望对你有一定的参考价值。

Vue2源码解读 - $set()使用及实现原理

  • 当我们给响应式的对象新增属性时,新增的属性并不会渲染到页面中
  • 对于响应式的数组,增加元素、修改数组长度时,数组的这些变化也不会反映到页面中

那么如何让新增的对象或数组实现响应式及时渲染页面呢?

使用this.$set()

官方定义

Vue 不允许在已经创建的实例上动态添加新的根级响应式属性 (root-level reactive property)。然而它可以使用 Vue.set(object, key, value)方法将响应属性添加到嵌套的对象上

// Vue.set(object, key, value) 
<template>
    <div>{{obj.k}}</div>
</template>
<script>
export default {
    data() {
        return {
            obj: {
                s: \'1\',
                z: \'2\'
            }
        }   
    },
    mounted() {
        this.$set(this.obj, \'k\', \'3\')
    }
}
</script>

$set原理

直接看源码

function set(target: Array<any> | Object, key: any, val: any): any {
  // isUndef 是判断 target 是不是等于 undefined 或者 null 。
  //isPrimitive 是判断 target 的数据类型是不是 string、number、symbol、boolean 中的一种
  if (process.env.NODE_ENV !== \'production\' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }

  // 数组的处理
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }

  // 对象,并且该属性原来已存在于对象中,则直接更新
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }

  // vue给响应式对象(比如 data 里定义的对象)都加了一个 __ob__ 属性,
  // 如果一个对象有这个 __ob__ 属性,那么就说明这个对象是响应式对象,我们修改对象已有属性的时候就会触发页面渲染。
  // 非 data 里定义的就不是响应式对象。
  const ob = (target: any).__ob__

  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== \'production\' && warn(
      \'Avoid adding reactive properties to a Vue instance or its root $data \' +
      \'at runtime - declare it upfront in the data option.\'
    )
    return val
  }

  // 不是响应式对象
  if (!ob) {
    target[key] = val
    return val
  }

  // 是响应式对象,进行依赖收集
  defineReactive(ob.value, key, val)

  // 触发更新视图
  ob.dep.notify()
  return val
}

vue源码解读0-2

上篇文章已经对index.js中的基本调用情况做了说明,接下来的几篇将对各个函数做仔细的分析,能力有限,文章中不足之处,希望大家能够指正!

上篇中提到在instance/vue中使用了9个高阶函数来构建(install)Vue构造函数(并不会调用该构造函数的进行初始化的过程),一切等在使用new Vue({….})的时候将一个全新的对象作为函数内this的值,返回该新对象作为结果(函数 调用中构造函数调用的方法)

function Vue (options) {
  this._init(options)

}

创建函数中函数申明的创建的方法(涉及知识函数声明的提升),this为函数方法调用的接收者,一般为构造函数调用的方式 new Vue()

initMixin(Vue)

Mixin-mix in( 混入加入) 可能是作者取这个名字的原因吧(只是妄加猜测,具体已作者本人意图为准)

import initMixin from ‘./internal/init’ 会在internal/init中就会存在default的export接下来的分析将会从这个开始着手


逐行代码的分析如

let uid = 0

设置了uid只在当前的块中有效,let具体可以在预热解读中有说明

export default function (Vue) {
.....
}

export default 匿名函数具体用法可以在预热解读中找到,接下来的也即是function中的内容,接收参数为Vue

 Vue.prototype._init = function (options){
    .....
  }

Vue也即是传入进来的参数(函数名),函数会自带一个默认的prototype的属性在新建立之前几乎为空,当使用new创建Vue的实例的时候,会得到自动分配的原型对象,存在User的prototype例如我们使用 var vm=new Vue({})来初始化构造方法的时候(先查找自身的属性再去原型链中进行查找)

这里写图片描述

对于javaScript的继承机制基于原型链(ES5),javaScript的实例对象由构造函数与在实例件共享的原型对象组成,对于原型用的较多的1个创建的方法(m.prototype)与2个获取原型的方法(obj.getPrototypeOf(m)和obj. _ proto _).其中

 options = options || {}

判断options是否为null,0,-0,undefined,false,”,NaN等情况(以上也即是js的7大假值),当options不为假则直接执行赋值,否则为{}。(涉及赋值运算符优先级,||运算时当左边为假才会执行右边;左右options的不一样);

    this.$el = null
    this.$parent = options.parent
    this.$root = this.$parent? this.$parent.$root : this
    this.$children = []
    this.$refs = {}       // child vm references
    this.$els = {}        // element references
    this._watchers = []   // all watchers as an array
    this._directives = [] // all directives

this实例化之后也即是Vue对象,未指定调用接收者为undefined;先来了解下基本的含义,在后面涉及到会仔细介绍:
$parent存在的话则为父实例; $root:当前组件树的根 Vue 实例。如果当前实例没有父实例为自身。$children 当前实例的直接子组件。
$refs:一个对象,包含注册有 v-ref 的子组件。\\$els对象中包含注册有 v-el 的 DOM 元素。

  // a uid
    this._uid = uid++ //上文中定义的let uid

    // a flag to avoid this being observed  设置标志避免被检测到
    this._isVue = true

    // events bookkeeping  事件统计
    this._events = {}            // registered callbacks
    this._eventsCount = {}      // for $broadcast optimization  //$broadcast的优化

    // fragment instance properties fragment实例属性
    this._isFragment = false
    this._fragment =         // @type {DocumentFragment}
    this._fragmentStart =    // @type {Text|Comment}
    this._fragmentEnd = null // @type {Text|Comment}

    // lifecycle state  生命周期状态
    this._isCompiled =
    this._isDestroyed =
    this._isReady =
    this._isAttached =
    this._isBeingDestroyed =
    this._vForRemoving = false
    this._unlinkFn = null

各个实例到底什么意思,相信也很困惑,这里只要稍微有印象即可在之后的分析与学习中会逐步解释

  // context:
    // if this is a transcluded component, context
    // will be the common parent vm of this instance
    // and its host.
    如果这是一个嵌入式的组件,上下文将是这个实例共有父实例(或宿主)
    this._context = options._context || this.$parent

    // scope:
    // if this is inside an inline v-for, the scope
    // will be the intermediate scope created for this
    // repeat fragment. this is used for linking props
    // and container directives.
    如果这是在一个内联的v-for,将由这个循环的片段产生中间的作用域范围,被用在链接父组件的数据和指令容器
    this._scope = options._scope

    // fragment:
    // if this instance is compiled inside a Fragment, it
    // needs to reigster itself as a child of that fragment
    // for attach/detach to work properly.
    如果这个实例在某个片段里已经编译,需要在该片段上进行注册,利于attach或detach的正常工作
    this._frag = options._frag
    if (this._frag) {
      this._frag.children.push(this)
    }

    // push self into parent / transclusion host
    如果存在父实例则将其建立双方的链接
    if (this.$parent) {
      this.$parent.$children.push(this)
    }

    // merge options.
    合并options,含有一个mergeOptions的函数
    options = this.$options = mergeOptions(
      this.constructor.options,
      options,
      this
    )

import { mergeOptions } from ‘../../util/index’

export * from './lang'
export * from './env'
export * from './dom'
export * from './options'  //options
export * from './component'
export * from './debug'
export { defineReactive } from '../observer/index'

export * 也即是将所有的标记过的均导出

这里写图片描述

在options.js中可以看到

  /**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 * 主要用于在实例化与继承
 * @param {Object} parent
 * @param {Object} child
 * @param {Vue} [vm] - if vm is present, indicates this is
 *                     an instantiation merge. 
 *
  options = this.$options = mergeOptions(
      this.constructor.options,
      options,
      this
    )
 */
export function mergeOptions (parent, child, vm) {

}

下面均是该函数内的代码片段

  guardComponents(child)
  guardProps(child)
  function guardComponents (options){
   ...
  }

对于guardComponents主要用作options中的组件构造,下文的代码为guardComponents中的代码


var vm = new Vue({
  el: '...',
  data:{},
  components: {
  'a':{},
  'b':{}
  }
})
 if (options.components) {
    ......
  }

如果在options中存在components的存在,则会进行下部分的代码

 var components = options.components =
      guardArrayAssets(options.components)

赋值语句从右至左,使用guardArrayAssets函数将数组形式的转化为键值对的形式


guardArrayAssets:

function guardArrayAssets (assets) {
//assets 也即是传递过来的options.components
//1.components:{'s':{},'d':{}}
//2.componets:[{'name':'...','id':'...'}]
//3.?
  if (isArray(assets)) {
    var res = {}
    var i = assets.length
    var asset
    //数组循环取值组成键值对的形式 key值由id决定
    while (i--) {
      asset = assets[i]
      var id = typeof asset === 'function'
        ? ((asset.options && asset.options.name) || asset.id)
        : (asset.name || asset.id)
        //id异常情况
      if (!id) {
        process.env.NODE_ENV !== 'production' && warn(
          'Array-syntax assets must provide a "name" or "id" field.'
        )
      } else {
      //规整为key-value的形式
        res[id] = asset
      }
    }
    return res
  }
  return assets
}

可以看出有3种方式填写的option.components,主要目的是规整为字典的形式便于后面的直接调用

下面回到guardComponents

 var components = options.components =
      guardArrayAssets(options.components)

var ids = Object.keys(components)

这里用到了一个Object.keys方法,获取规整后的components的键值数组

The Object.keys() method returns an array of a given object’s own enumerable properties, in the same order as that provided by a for…in loop (the difference being that a for-in loop enumerates properties in the prototype chain as well).
返回一个枚举所有对象属性的数组,类似于for-in 枚举(并不保证按对象的顺序输各个属性 ,不可预测的顺序unpredicted order)

接下来飘逸与自然的for循环如下:


 for (var i = 0, l = ids.length; i < l; i++) {
      var key = ids[i]
      if (commonTagRE.test(key) || reservedTagRE.test(key)) {
        process.env.NODE_ENV !== 'production' && warn(
          'Do not use built-in or reserved HTML elements as component ' +
          'id: ' + key
        )
        continue
      }
      // record a all lowercase <-> kebab-case mapping for
      // possible custom element case error warning
      if (process.env.NODE_ENV !== 'production') {
        map[key.replace(/-/g, '').toLowerCase()] = hyphenate(key)
      }
      def = components[key]
      if (isPlainObject(def)) {
        components[key] = Vue.extend(def)
      }
    }

其中commonTagRE与reservedTagRE为options.js中导入的两个属性
import { commonTagRE, reservedTagRE } from './component'

export const commonTagRE = /^(div|p|span|img|a|b|i|br|ul|ol|li|h1|h2|h3|h4|h5|h6|code|pre|table|th|td|tr|form|label|input|select|option|nav|article|section|header|footer)$/i

export const reservedTagRE = /^(slot|partial|component)$/i

const为es6中的关键字,表示不可以修改常量只在当前模块中有效,想要在其他模块中引用也即是利用前面提到的export命令,不会提升,必须先申明后使用

变量的提升:某一作用域范围内
console.info(v) ==> var v
var v=’tev’ console.info(v)
v=’tev’

正则表达式中:

/i (忽略大小写)
/g (全文查找出现的所有匹配字符)
/m (多行查找)
/gi(全文查找、忽略大小写)
/ig(全文查找、忽略大小写)
键值中

 process.env.NODE_ENV !== 'production' && warn(
          'Do not use built-in or reserved HTML elements as component ' +
          'id: ' + key
        )

不要使用保留的slot,partial,component与Html的标签作为键值

def = components[key]
      if (isPlainObject(def)) {
        components[key] = Vue.extend(def)
      }

使用vue.extend定义组件,如下例子将更好解释

 components:{
    'my-component':{
      template:'<div>A custom component!</div>'
    }
  },

html页面中使用<\\my-component><\\/\\my-component>等同于

 components:[{
    // 'id':'my-component',
    'name':'my-component',
    'template':'<div>A custom component!</div>'
    }
  ],

等同于:

var MyComponent = Vue.extend({
  template: '<div>A custom component!</div>'
})
Vue.component('my-component', MyComponent)

这里写图片描述

上面的代码中,这里涉及到两个isplainObject与Vue.extend,x下面将对其进分析

 if (isPlainObject(def)) {
        components[key] = Vue.extend(def)
      }
import {
  isArray,
  isPlainObject,
} from './lang'
/**
 * Strict object type check. Only returns true
 * for plain JavaScript objects.
 *
 * @param {*} obj
 * @return {Boolean}
 */
//使用toString()方法判断类型,可以表面toString对null的判断方法,如下图所示

var toString = Object.prototype.toString
var OBJECT_STRING = '[object Object]'
export function isPlainObject (obj) {
  return toString.call(obj) === OBJECT_STRING
}

/**
 * Array type check.
 *
 * @param {*} obj
 * @return {Boolean}
 */
//也即是调用Array方法中的isArray方法
export const isArray = Array.isArray

这里写图片描述

Vue.extend在global-api.js中在接下来的中会分析


感觉跑偏了很远这样流水式的分析要知道自己要回到哪个地方

mergeOptions

export function mergeOptions (parent, child, vm) {
//在Options之前将options:components与props定义好
  guardComponents(child)
  guardProps(child)
  ....
}

Vue.prototype._init

 Vue.prototype._init = function (options){
 ...
 options = this.$options = mergeOptions(
      this.constructor.options,
      options,
      this
    )
}

props的定义

A list/hash of attributes that are exposed to accept data from the parent component(从父组件中获得数据). It has a simple Array-based syntax (数组形式)and an alternative Object-based(对象形式) syntax that allows advanced configurations such as type checking, custom validation and default values(对象形式用于高级的设置如 类型检查,自定义验证,默认值等).

 guardProps(child)将所有的props规格化为基于对象的格式(虽然支持数组与对象的两种形式),child也即是为init中传入的options
 props: ['size', 'myMessage']
  props: [{'name':'size'},{'name':'myMessage'}],
  props: {
    // 只检测类型
    size: Number,
    // 检测类型 + 其它验证
    name: {
      type: String,
      required: true,
      // 双向绑定
      twoWay: true
    }
  }
function guardProps (options) {
 var props = options.props
  var i, val
  if (isArray(props)) { //为数组类型
    options.props = {}
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
      //为String类型的时候将其值设置为空 'size':null
        options.props[val] = null

      } else if (val.name) {
      //取val.name
        options.props[val.name] = val
      }
    }
  } else if (isPlainObject(props)) {
    var keys = Object.keys(props)
    i = keys.length
    while (i--) {

      val = props[keys[i]]
      if (typeof val === 'function') { //{ 初始为Object类型 {} }
        props[keys[i]] = { type: val }
      }
    }
  }
 }

以上是关于Vue2源码解读的主要内容,如果未能解决你的问题,请参考以下文章

vue2源码学习

vue2源码学习

#yyds干货盘点# mybatis源码解读:executor包(语句处理功能)

Vue2总结虚拟DOM

Vuex深入解读(适用于Vue2)

手把手教你读Vue2源码-2