Vue2.6.0源码阅读(二):new Vue时做了什么

简介: Vue2.6.0源码阅读(二):new Vue时做了什么


上一篇我们看了引入Vue时都有哪些操作,这一篇我们来看一下new一个Vue实例时会

发生什么,测试代码如下:


<div id="app"></div>
const app = new Vue({
    el: '#app',
    template: `<h1>{{text}}</h1>`,
    data: {
        text: 'hello'
    }
})


根据上一篇的最后我们知道Vue的构造函数里只调用了一个_init方法,来看看这个函

数都做了什么:


Vue.prototype._init = function (options) {
    // vue里把vue实例都叫做vm
    const vm = this
    // 一个uid
    vm._uid = uid++
    // 避免自身被观察的标志
    vm._isVue = true
    // 合并选项
    if (options && options._isComponent) {
      // 这里针对组件的我们暂时不看
      // 优化内部组件实例化,因为动态选项合并非常慢,并且没有任何内部组件选项需要特殊处理。
      initInternalComponent(vm, options)
    } else {
      // 合并选项
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // 引用自己
    vm._renderProxy = vm
    vm._self = vm
    // ...
}


前半部分主要做的是事情就是合并选项,我们具体来看。


合并选项


首先调用了resolveConstructorOptions(vm.constructor)vm.constructor

就是Vue构造函数:


export function resolveConstructorOptions (Ctor) {
  // 上一节我们知道Vue构造函数的options上面一共初始化了四个属性:components、directives、filters、_base
  let options = Ctor.options
  // 这里super属性为false,所以会直接跳过,我们后续再深入这里
  if (Ctor.super) {
    // ...
  }
  return options
}


接下来看mergeOptions方法:


export function mergeOptions (
  parent,
  child,
  vm
){
  // 我们的options是对象,所以这个分支也跳过
  if (typeof child === 'function') {
    child = child.options
  }
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)
  // ...
}


这里又调用了三个方法,我们一一来看。


1.normalizeProps方法主要是用来处理我们传入的props选项,因为props可以传入

数组类型的也可以传入对象类型的,最后都会被格式为对象格式:


function normalizeProps (options, vm) {
    const props = options.props
    if (!props) return
    const res = {}
    let i, val, name
    // 如果是字符串数组形式的话会转成{val:{ type: null }}
    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 (isPlainObject(props)) {
        for (const key in props) {
            val = props[key]
            name = camelize(key)
            // val可以是一个类型,比如String、Number,也可以是一个对象,如{type: Number, default: 0}
            res[name] = isPlainObject(val)
                ? val
            : { type: val }
        }
    }
    options.props = res
}


camelize方法用来将连字符分隔的字符串转为驼峰式:


const camelizeRE = /-(\w)/g
export const camelize = cached((str) => {
  return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})


所以同一个属性名可以使用驼峰式,也可以使用-连接式。


另外可以看到camelize是使用cached方法调用后返回的一个函数,这主要是为了缓存

计算结果,如果某个计算之前计算过了,下次会直接返回缓存的结果,就不用再次计算

了,cached函数如下:


export function cached (fn) {
  // 通过闭包来保存一个缓存对象
  const cache = Object.create(null)
  return (function cachedFn (str) {
    const hit = cache[str]
    return hit || (cache[str] = fn(str))
  })
}


isPlainObject方法用来判断一个对象是否是普通的对象字面量:


export function isPlainObject (obj) {
  return _toString.call(obj) === '[object Object]'
}


2.normalizeInject方法顾名思义就是用来处理inject选项的,该选项是和

provide选项配合使用的,用来向子孙后代组件注入一个依赖,inject可以是一个字

符串数组或一个对象:


{
    // 数组
    inject: ['foo'],
    // 对象方式1
    inject: {
        foo: { default: 'foo' }
    },
    // 对象方式2
    inject: {
        foo: {
          from: 'bar',// 从一个不同名字的 property 注入
          default: 'foo'
        }
    },
    // 对象方式3
    inject: {
        foo: 'foo'
    }
}


该方法会把上述几种类型都规范化为同样的类型:


function normalizeInject (options) {
  const inject = options.inject
  if (!inject) return
  const normalized = options.inject = {}
  if (Array.isArray(inject)) {
    // 字符串数组类型,转化为{ from: val }
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] }
    }
  } else if (isPlainObject(inject)) {
    for (const key in inject) {
      const val = inject[key]
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)// 值也是个对象的话
        : { from: val }// 值是个字符串
    }
  }
}


3.最后一个normalizeDirectives用来处理指令选项,指令选项可以传一个对象,也

可以传一个函数:


// 对象格式
directives: {
    focus: {
        inserted: function (el) {
            el.focus()
        },
        // 其他钩子
    }
}
// 函数格式,该函数会在bind 和 update时调用
directives: {
    focus (el, binding) {
        el.style.backgroundColor = binding.value
    }
}


函数格式是一种简写,会在bindupdate两个钩子执行,所以会把它也转成钩子对象

的形式:


function normalizeDirectives (options) {
    const dirs = options.directives
    if (dirs) {
        for (const key in dirs) {
            const def = dirs[key]
            if (typeof def === 'function') {
                dirs[key] = { bind: def, update: def }
            }
        }
    }
}


接下来回到mergeOptions方法,继续往下走:


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


_base属性目前我们只知道Vue构造函数的选项上是有的,而且就是Vue构造函数自

身,我们传的选项显然没有,所以会进入这个分支,extends选项是用来继承另一个组

件,官方文档上说是为了方便扩展单文件组件的,和mixins类似,反正笔者没使用过,

如果传了该选项,那么会把它和parent选项先进行合并。mixins选项我们应该很熟

悉,各个组件的公共逻辑可能会通过mixins提取出来,因为mixins可以包含所有选项,里面再套mixin也是可以的,所以通过递归来合并,这些都会在我们组件自身的选项之前合并,所以这就是为什么mixin里的生命周期钩子会在组件自身的钩子之前调用。


继续看mergeOptions方法:


const options = {}
let key
// 先遍历父选项进行合并
for (key in parent) {
    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


最后就是遍历所有选项,然后执行合并操作,如果某个选项有特定的合并策略,那么

strats[key]可以获取到,否则就执行默认的合并策略。


默认的合并策略


我们先看默认的合并策略:


const defaultStrat = function (parentVal, childVal) {
    return childVal === undefined
        ? parentVal
    : childVal
}


默认的合并策略很简单,子选项的值不存在那么就使用父选项的。


data/provide选项的合并


strats.data = function (
 parentVal,
 childVal,
 vm
) {
    if (!vm) {
        if (childVal && typeof childVal !== 'function') {
            process.env.NODE_ENV !== 'production' && warn(
                'The "data" option should be a function ' +
                'that returns a per-instance value in component ' +
                'definitions.',
                vm
            )
            return parentVal
        }
        return mergeDataOrFn(parentVal, childVal)
    }
    return mergeDataOrFn(parentVal, childVal, vm)
}


如果vm不存在,那么就相当于不是通过new来调用,可能是定义一个组件,这时候如果

传的data选项不是一个函数就会警告,并且直接忽略,返回默认值,相信用vue的人都

很清楚这个规则。然后会调用mergeDataOrFn这个方法:


export function mergeDataOrFn (
 parentVal,
 childVal,
 vm
) {
    if (!vm) {
        // Vue.extend合并, 必须都是函数
        if (!childVal) {
            return parentVal
        }
        if (!parentVal) {
            return childVal
        }
        // 当父选项和子选项都提供了
        // 我们需要返回一个函数,该函数返回两个函数的合并结果
        // 这里不需要检查parentVal是否是函数,因为它必须是一个函数才能传递以前的合并。
        return function mergedDataFn () {
            return mergeData(
                typeof childVal === 'function' ? childVal.call(this, this) : childVal,
                typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
            )
        }
    } else {
        // 实例化的时候合并
        return function mergedInstanceDataFn () {
            const instanceData = typeof childVal === 'function'
            ? childVal.call(vm, vm)
            : childVal
            const defaultData = typeof parentVal === 'function'
            ? parentVal.call(vm, vm)
            : parentVal
            if (instanceData) {
                return mergeData(instanceData, defaultData)
            } else {
                return defaultData
            }
        }
    }
}


同样也分了两个分支,且最后返回的都是一个函数,也就是说真正的合并时机并不是在

这里。vm不存在则代表是执行Vue.extend合并,也就是定义组件的时候,这种情况

下,父选项和子选项都必须是函数,所以当两个选项都存在时,会先执行这两个函数,

然后对两个对象再调用mergeData方法进行合并。vm存在的时候则代表是创建vue

例,是函数的话就执行一下,当子选项存在最后也是调用mergeData来合并:


function mergeData (to, from) {
    if (!from) return to
    let key, toVal, fromVal
    const keys = hasSymbol
    ? Reflect.ownKeys(from)
    : Object.keys(from)
    // 遍历from对象的keys
    for (let i = 0; i < keys.length; i++) {
        key = keys[i]
        // __ob__是代表该对象被观察过了的标志,不需要合并
        if (key === '__ob__') continue
        toVal = to[key]
        fromVal = from[key]
        // 如果to对象没有该属性,那么直接添加
        if (!hasOwn(to, key)) {
            // set方法会判断目标对象是否是一个响应式对象,是的话添加的新属性也会是响应式的,否则就是单纯添加一个属性
            set(to, key, fromVal)
        } else if (// 否则如果两个的值不一样,且都是对象{}的时候递归进行合并
            toVal !== fromVal &&
            isPlainObject(toVal) &&
            isPlainObject(fromVal)
        ) {
            mergeData(toVal, fromVal)
        }
    }
    return to
}


to代表是子选项/实例选项,from代表是父选项或默认选项,这个方法会遍历父选项/

默认选项的所有属性,然后合并到子选项/实例选项上,合并策略也很简单,当某个属性

子选项没有那么直接添加,当父子都有,那么判断它们的值是否是一个普通的对象,是

的话就递归进行合并,否则就直接使用子选项的值。


provide选项同样也可以接收一个对象或一个返回对象的函数,所以它们的合并策略是

一样的:


strats.provide = mergeDataOrFn


生命周期的合并


const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]
LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})


所有生命周期的合并策略都是一样的,看mergeHook


function mergeHook (
  parentVal,
  childVal
) {
  // childVal不存在,返回parentVal
  // childVal存在,且parentVal也存在,那么两个数组进行合并
  //              且parentVal不存在,把childVal格式化成数组类型
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}


合并策略也很简单,会把父子选项/默认选项实例选项的钩子函数都保存到一个数组里,

父/默认选项的优先,最后会使用dedupeHooks方法做一个去重操作:


function dedupeHooks (hooks) {
  const res = []
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i])
    }
  }
  return res
}


合并资源选项


const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})


这三个选项的合并策略也是一样的:


function mergeAssets (
  parentVal,
  childVal,
  vm,
  key
) {
  const res = Object.create(parentVal || null)
  if (childVal) {
    return extend(res, childVal)
  } else {
    return res
  }
}


创建了一个以父选项的值为__proto__的对象,如果子选项没有值,直接返回该对象,

否则调用extend方法来合并属性。


可以看到这里的属性合并有点不一样,不是直接把父子选项的数据合并到同一个对象

上,而是把父选项的值挂载到子选项值的原型上。


合并watch选项


strats.watch = function (
  parentVal,
  childVal,
  vm,
  key
) {
  // 当父子选项不同时存在
  if (!childVal) return Object.create(parentVal || null)
  if (!parentVal) return childVal
  const ret = {}
  extend(ret, parentVal)
  // 遍历子选项对象的属性
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child]
  }
  return ret
}


watch选项合并也不是覆盖操作,而是会把父子选项的值都保存,比如父子选项都观察

了一个a变量,那么它的两个回调函数都会合并到一个数组里,父选项的回调优先。


其他一些值为对象的选项合并


strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal,
  childVal,
  vm,
  key
) {
  if (!parentVal) return childVal
  const ret = Object.create(null)
  extend(ret, parentVal)
  if (childVal) extend(ret, childVal)
  return ret
}


这几个选项值都是对象的属性合并都是一样的,且很简单,就是创建一个新对象,然后

把父子选项值的属性都添加上去,如果有重复,那么子的属性会覆盖父的。


初始化操作


回到_init方法,合并选项结束后,接下来会初始化一堆东西:


initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')


现在我们大致可以知道,beforeCreate生命周期前做的事情有合并选项、初始化生命

周期、初始化事件、初始化渲染,beforeCreatecreated两个生命周期之间做的事

情有初始化依赖注入、初始化状态,这个状态里面其实包含了很多我们熟悉的props

methodsdatacomputedwatch等。接下来一一来看:


初始化生命周期


export function initLifecycle (vm) {
  const options = vm.$options
  // 找到第一个非抽象的父级,然后将该实例添加到其$children属性中,这样我们就可以在父级上通过该属性找到它的子组件
  // 抽象组件有transition、keep-alive
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
  // 关联父组件
  vm.$parent = parent
  // 当前组件树的根vue实例,如果没有父实例,那么就是自己
  vm.$root = parent ? parent.$root : vm
  // 当前实例的直接子组件
  vm.$children = []
  // dom元素和组件实例
  vm.$refs = {}
  // 下面这些属性在后面用到时再来了解
  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}


初始化生命周期方法主要做的事情是关联父子组件,然后定义了一些对外或者对内的 相

关变量。


初始化事件


export function initEvents (vm) {
  // 创建了一个保存事件的对象
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // 初始化父附加事件
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}


初始化事件先定义了两个变量,然后判断是否存在父组件的附加事件,这个具体是做什

么的暂时还不知道,后面再看。


初始化渲染


export function initRender (vm) {
  vm._vnode = null // 子树的根节点
  vm._staticTrees = null // v-once 缓存树
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // 父树中的占位符节点
  const renderContext = parentVnode && parentVnode.context
  // 处理slot
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // 给当前实例绑定一个createElement方法
  // 这样我们就可以在其中获得适当的渲染上下文。
  // 参数列表: tag, data, children, normalizationType, alwaysNormalize
  // 内部版本由从模板编译的渲染函数使用
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // 公开版本,用于用户编写渲染函数时使用
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
   // $attrs 和 $listeners 用于更方便的创建高阶组件 HOC
  // 它们需要是响应性的,这样使用它们的HOC才会始终得到更新
  const parentData = parentVnode && parentVnode.data
  defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
  defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}


初始化渲染方法也是先定义了几个变量,然后处理了一下插槽,接着定义了两个变量用

于引用创建虚拟dom节点的createElement方法,最后,给实例添加了两个响应性的

属性$attrs$listenersdefineReactive方法的实现我们放到下一篇去看。


触发生命周期


触发声明周期的方法如下:


callHook(vm, 'beforeCreate')


让我们看看callhook方法的实现:


export function callHook (vm, hook) {
  // #7573 调用生命周期钩子时禁止依赖收集
  pushTarget()
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  // 此处暂时不知道其用处
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}


调用生命周期钩子不允许进行依赖收集,通过pushTarget来实现:


Dep.target = null
const targetStack = []
export function pushTarget (target) {
  targetStack.push(target)
  Dep.target = target
}


不带参数执行,相当于设置Dep.target = undefined,所以即使触发了某个属性的

getter,也不会收集到任何watcher


接下来遍历生命周期的钩子函数,通过invokeWithErrorHandling方法执行:


export function invokeWithErrorHandling (
  handler,
  context,
  args,
  vm,
  info
) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    // 处理函数返回值是promise的情况
    if (res && !res._isVue && isPromise(res)) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}


其实就是把函数放在try catch里执行,这样能捕捉错误,详细的错误捕捉会在后面单

独写一篇文章来介绍。


最后执行了popTarget


export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}


也就是恢复到上一个watcher


初始化依赖注入


initInjections是在initProvide之前执行的,不过为了好理解,我们先看

initProvide


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


很简单,给vue实例添加了一个_provided属性,用来存放provide对象。


接下来看initInjections


export function initInjections (vm) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      defineReactive(vm, key, result[key])
    })
    toggleObserving(true)
  }
}


先执行了resolveInject方法:


export function resolveInject (inject, vm) {
  if (inject) {
    const result = Object.create(null)
    const keys = hasSymbol
      ? Reflect.ownKeys(inject)
      : Object.keys(inject)
    // 遍历inject对象的属性,每个属性代表是注入的一个依赖
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      // #6574 跳过被观察过的注入对象...
      if (key === '__ob__') continue
      const provideKey = inject[key].from
      let source = vm
      // 从当前实例,依次往上查找,直到某个实例的provide提供了对应的inject
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
      // 如果一个实例都没有
      if (!source) {
        // 如果存在default属性,那么使用default的属性值
        if ('default' in inject[key]) {
          const provideDefault = inject[key].default
          result[key] = typeof provideDefault === 'function'
            ? provideDefault.call(vm)
            : provideDefault
        }
      }
    }
    return result
  }
}


这个方法其实就是求出注入的每个inject属性的值,如果有值的话,接下来执行了

toggleObserving(false)


export let shouldObserve = true
export function toggleObserving(value) {
  shouldObserve = value
}


也就是把shouldObserve标志位设为false,这个标志位如果为false的话那么调用

observe方法时将不会对传入的对象进行观察,因为接下来紧接着把注入的属性都添加

到了vue实例上:


Object.keys(result).forEach(key => {
    defineReactive(vm, key, result[key])
})


defineReactive方法是给一个对象添加一个响应性的属性的,如果该属性的值也是对

象或数组的话就会调用observe方法来观察该数组,所以就是为了不进行这一步操作,

vue的文档中我们也可以看到:


提示:provideinject 绑定并不是可响应的。这是刻意为之的。然而,如果你

传入了一个可监听的对象,那么其对象的 property 还是可响应的。


初始化状态


initState里面就包含着vue的核心,数据观察:


export function initState(vm) {
  vm._watchers = []// 声明了一个存放watcher的数组
  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 */ )
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}


很清晰,分别处理了propsmethodsdatacomputedwatch,接下来一一来看:


初始化props


function initProps(vm, propsOptions) {
  // propsData:创建实例时传递 props。主要作用是方便测试,只用于 new 创建的实例中
  const propsData = vm.$options.propsData || {}
  // 存储props
  const props = vm._props = {}
  // 缓存prop的key,这样在后面更新prop时可以通过遍历数组,而不是枚举动态对象的属性
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // 根实例的props需要被观察
  if (!isRoot) {
    toggleObserving(false)
  }
  // 遍历所有prop
  for (const key in propsOptions) {
    keys.push(key)
    // 校验props,返回其默认值
    const value = validateProp(key, propsOptions, propsData, vm)
    defineReactive(props, key, value)
    // 在Vue.extend()期间,静态prop已在组件的原型上代理。我们只需要在这里代理实例化时定义的prop
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}


先定义了几个内部变量,然后对于非根实例,也是关闭了observe观察的标志位,然后遍历propsvalidateProp方法用来确定该prop的默认值,然后将该prop响应性的添加到vm._props对象上,因为shouldObserve设为false了,所以不会对value进行观察,最后,对于新增的prop,会将其访问代理到_props上,也就是当我们访问this.xxx时,实际访问的是this._props.xxx


export function proxy(target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}


初始化方法


function initMethods(vm, methods) {
  for (const key in methods) {
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}


initMethods方法很简单,先判断是否是函数,是的话先绑定一下它的this,然后把该方法添加到实例上即可。我们知道在methods的函数里访问this都是指向当前vue实例的,就是通过这个bind方法来绑定的,接下来看一下它的实现:


export const bind = Function.prototype.bind
  ? nativeBind
  : polyfillBind


如果当前浏览器支持原生的bind方法,那么直接使用原生方法:


function nativeBind (fn, ctx) {
  return fn.bind(ctx)
}


如果不支持,就只能polyfill一个:


function polyfillBind (fn, ctx) {
  function boundFn (a) {
    const l = arguments.length
    return l
      ? l > 1
        ? fn.apply(ctx, arguments)
        : fn.call(ctx, a)
      : fn.call(ctx)
  }
  boundFn._length = fn.length
  return boundFn
}


可以看到就是一个最简单的实现方式,参数大于一个时,使用apply方法调用,否则使用call方法调用。这个方法其实没有太大的存在必要,因为几乎所有浏览器都原生支持bind方法,这里只是为了能兼容之前的版本,以让它支持在PhantomJS 1.x的环境里运行。


初始化data


这部分会在下一篇文章里详细介绍。


初始化计算属性


function initComputed(vm, computed) {
  // 保存用于计算属性的watcher
  const watchers = vm._computedWatchers = Object.create(null)
  for (const key in computed) {
    // 计算属性支持两种写法:普通函数、对象形式:{ get: Function, set: Function }
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // 创建一个内部的 watcher 
    watchers[key] = new Watcher(
      vm,
      getter || noop,
      noop,
      {
        lazy: true
      }
    )
    // 组件定义的计算属性已在组件原型上定义。我们只需要在这里定义实例化时定义的计算属性。
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}


可以看到为每个计算属性都创建了一个watcher实例,watcher的作用我们后续再说,然后调用了defineComputed方法,这块涉及到计算属性的缓存原理,我们后续作为一篇单独的文章进行介绍。


初始化watch


function initWatch(vm, watch) {
  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)
    }
  }
}


遍历watch选项对象的key,依次调用createWatcher方法:


function createWatcher(
  vm,
  expOrFn,
  handler,
  options
) {
  // {handler: '', deep...}形式
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // 回调是字符串的话,那么一般指methods里的方法
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}


适配了两种非函数的形式,最后调用了$watch方法,这个方法是vue原型上的一个方法,用于观察 vue 实例上的一个表达式或者一个函数计算结果的变化:


Vue.prototype.$watch = function (
 expOrFn,
 cb,
 options
) {
    const vm = this
    if (isPlainObject(cb)) {
        // createWatcher方法会从对象里解析出cb,options,然后又会重新调用$watch方法
        return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // 指定了immediate为true,那么立刻执行一次回调
    if (options.immediate) {
        try {
            cb.call(vm, watcher.value)
        } catch (error) {
            handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
        }
    }
    // 返回一个用于解除观察的方法
    return function unwatchFn() {
        watcher.teardown()
    }
}


这个方法逻辑也很简单,主要就是实例化了一个Watcher实例,Watcher的相关内容后续再说,然后判断是否要立即执行一次回调,最后返回一个解除观察的方法。


挂载


最后,如果提供了el选项,那么会进行挂载:


if (vm.$options.el) {
    vm.$mount(vm.$options.el)
}


挂载的详细内容也会在后面单独讨论。


new Vue的过程到这里就结束了,注意,本文考虑的只是最最最基础的使用方式的实例化过程,当你使用的vue功能越多,实例化要做的事情也会更多,这些我们都后面再看,毕竟,路漫漫其修远兮。


相关文章
|
7天前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
vue学习第四章
|
7天前
|
JavaScript 前端开发
vue学习第九章(v-model)
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生,自学前端2年半,正向全栈进发。此篇介绍v-model在不同表单元素中的应用及修饰符的使用,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
vue学习第九章(v-model)
|
7天前
|
JavaScript 前端开发 开发者
vue学习第十章(组件开发)
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文深入讲解Vue组件的基本使用、全局与局部组件、父子组件通信及数据传递等内容,适合前端开发者学习参考。持续更新中,期待您的关注!🎉🎉🎉
vue学习第十章(组件开发)
|
12天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
存储 前端开发 JavaScript
为什么我不再用Vue,改用React?
当我走进现代前端开发行业的时候,我做了一个每位开发人员都要做的决策:选择一个合适的框架。当时正逢 jQuery 被淘汰,前端开发者们不再用它编写难看的、非结构化的老式 JavaScript 程序了。
|
13天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
13天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
13天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
13天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
14天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。