vue源码剖析(一)

Vue
367
0
0
2022-04-20

Vue 源码

0.获取源码

github.com/vuejs/vue

从github地址,直接download下来就行了。在新建项目的时候也可以node_modelus里的vue搭配着看。

1.数据的挂载

首先先引入vue,然后新建他的实例。

import Vue from 'vue'
var app = new Vue({
  el:'#app',
  data:{
    return {
          message:"hello world!"  
      }
    }
})

首先我们得知道我们引入 的是个什么东西。所以我们找到源码./src/core/instance/index.js里,找到了vue的庐山真面目了,其实vue就是一个类。

function Vue(options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

首先process.env.NODE_ENV是判断你启动时候的参数的,如果符合的话,就警告,否则执行_init方法。值得一提的是一般属性名前面加_默认代表是私有属性,不对外展示。当然如果你打印vue实例的话还是能看见,因为只是_是私有属性人们约定俗成的,没有js语言层面的私有。

那么这个_init是哪来的呢?往下看:

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

可以看到下面有一大串Mixin,_init的方法就在第一个initMixin里。vscode可以直接右键选择跳转到定义或者command加左键点击,可以跳过去看到定义这个方法的地方。

export function initMixin(Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this 
    // a uid
    vm._uid = uid++

   //..

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

    // merge 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)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

   //..  
    /* istanbul ignore else */ 
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props  
    initState(vm)
      // ..

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

​ init就在最开头

 Vue.prototype._init = function (options?: Object) {
    const vm: Component = this 
    // a uid
    vm._uid = uid++

   //..

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

    // merge 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)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

​ init具体包括啥呢,首先将this上下文传给vm这个对象,然后设置_uid然后再机型一系列的初始化的工作。然后再合并options,最后挂载到vm上。

​ 可能有人会好奇,在形参部分,Vue: Class<Component>是什么意思,因为JavaScript是一个动态类型语言,也就是说,声明变量的时候不会指派他是任何一种类型的语言,像java就是典型的静态类型语言。例如:boolean result = true就是声明result是一个布尔类型,而相对的,JavaScript中可以声明var result =true。这样虽然方便很多,但是因为静态类型在编译过程中就会查出错误并提示开发者改正错误,但是像Js这样的动态语言在编译的时候既是存在错误也不会提出,只有在真正运行时才会出错。所以就会有不必要的麻烦,那么如何对Js进行静态类型检查呢?就是插件呗。vue用了flow的插件,让js有了静态类型检查,:后面代表了限定vue这个形参的属性。具体就不展开了,可以去看flow的文档。

Flow:flow.org/

​ 接下来接着说正文,const vm: Component = this可以看到把当前的执行前后文给了vm。然后之后就是一些陆陆续续的挂载,值得注意的就是vm.$options就是填写在vue实例里的参数,例如el,mounted,data都被保存在$options里。

但是平常使用的时候我们没有用到this.$options.data1里,反而是直接用this.data1来调用,这其实vue也在其中进行了操作。

我们会发现在上面的代码段里有一行initState(vm),我们找到initState的定义。

export function initState (vm: Component) {
    // ..  
  const opts = vm.$options 
  if (opts.data) {
    initData(vm)
  } 
  // ..
}

然后我们可以接着转到initData这个方法的定义

function initData (vm: Component) {
  let data = vm.$options.data 
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : 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]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    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)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

把上面的代码拆分来看

 let data = vm.$options.data 
 data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}

上面代码先通过$options获取到data,然后判断data是不是通过返回对象的方式建立的,如果是,那么则执行getData方法。getData的方法主要操作就是 data.call(vm, vm) 这步通过给data调用了vm这个上下文环境,然后直接返回这个包括data的vm对象。

那么现在vm上已经有data了是吗?确实,但是这个data是vm._data也就是说如果你想访问message这个属性你现在只能通过vue._data.message这样来访问。所以我们接着往下看。

  // 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]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    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)) {
      proxy(vm, `_data`, key)
    }
  }

这一大段上面聚焦的是prop data methods 们如果相同之后就会提出相应的警示。为什么要他们不一样呢,因为他们都是通过this.XX来调用的,如果重名,vue分不清他们是谁。如果都没问题了,我们就把_datas上的值直接赋给vm,然后转到最后一步proxy(vm, _data, key) ,然后我们转移到proxy这个方法中:

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

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

就是通过sharedPropertyDefinition.getsharedPropertyDefinition.set的设置的get和set方法,然后在通过Object.defineProperty来定义访问target.key的时候调用sharedPropertyDefinition的set和get。

也就是相当于,我要求vm.message,就会触发sharedPropertyDefinition的get,然后返回vm._data.message

至此数据就可以通过vm.message的方式访问了。

2.实例挂载

在plantforms/entry-runtime-with-compiler.js上看到

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component{
  // ..
}

先从Vue中的原型链子找到$mount然后赋值给mount(),然后再定义一遍,是因为前面的适用于compilers版本,如果是runtime with compiler版本则上面的娶不到,所以只能自己在下面重新定义一遍。

Ps:最开始的runtime with compiler版本的$mount在./util/index.js里有定义。

现在看定义的Vue.prototype.$mount里面都有啥

el = el && query(el)

先获取到el的dom对象。

接下来的判断是不让你挂载到html和body标签里

 /* istanbul ignore if */ 
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

需要知道的就是你的template就是需要进行compiler的,编译过后他就转化成了render。所以我们下一步看他的options是否有render

    const options = this.$options
  // resolve template/el and convert to render function  
  if (!options.render) {
    // ...
  }

如果没有render,那么就想尽办法把template弄成一个字符串返回出来,下面是具体办法

let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */ 
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }

拿着这个字符串,然后进行一系列的编译,把template渲染成render,然后挂载到options上。

if (template) {
           // ...
      options.render = render
      options.staticRenderFns = staticRenderFns
            // ... 
}

然后我们拿到这个render函数,我们就开始执行mounte方法

return mount.call(this, el, hydrating)

这个mount是开头说的在./platforms/runtime/index.js里的那个函数,而不是当前作用域的这个mount函数。所以我们直接跳转到mount方法上去

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined 
  return mountComponent(this, el, hydrating)
}

我们可以看到开头又是校验了一波el,然后执行mountComponent函数,那接下来我们开始分析mountComponent。

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
   vm.$el = el
  // ...

  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
   //...
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  // ...
}

重点是vm._update(vm._render(), hydrating)这句,vm._render()生成一个虚拟节点,然后vm._update用来将这个虚拟节点上传。

然后我们定义完了这个updateComponent方法,我们在下面建立一个渲染watcher

new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

保证我们每次在更新的时候都能重新的渲染数据。

4.vm._render()

/src/core/instance/render.js

 Vue.prototype._render = function (): VNode{
    const vm: Component = this 
    const { render, _parentVnode } = vm.$options 
    //。。。  
     try {
      // There's no need to maintain a stack because all render fns are called  
      // separately from one another. Nested component's render fns are called  
      // when parent component is patched.
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement)
    }catch(e){
      // ...
    }
 }

然后vnode = render.call(vm._renderProxy, vm.$createElement)是重点.vm.$createElement是创建创建元素用的,和他一起的还有一个vm._c_c使用来处理编译过的render,$creatElement用来处理用户手写的render。

  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)

然后vm._renderProxy呢,在/core/instance/init.js里

if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }

如果是在开发环境,则执行InitProxy如果是生产环境,则直接把this赋给他。

转过头来看看initProxy

 initProxy = function initProxy (vm) {
    if (hasProxy) {
      // determine which proxy handler to use  
      const options = vm.$options 
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
  }

  const hasHandler = {
    has (target, key) {
      const has = key in target
      const isAllowed = allowedGlobals(key) ||
        (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
      if (!has && !isAllowed) {
        if (key in target.$data) warnReservedPrefix(target, key)
        else warnNonPresent(target, key)
      }
      return has || !isAllowed
    }
  }

这里用到了proxy语法,proxy是es6新添加的代理器,可以称为拦截器,详情可看阮一峰的《es6标准入门》或者参考MDN文档:

es6.ruanyifeng.com/#docs/proxy
developer.mozilla.org/zh-CN/docs/W...

总之,这里initProxy就是通过拦截vm实例,然后判断你所要求的属性到底有没有在vm上,如果没有就发出警告。

5.creatElement创建虚拟DOM

上面说了,最后就是这个createElement,创建一个虚拟DOM

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)

首先我们找到createElement方法

export function createElement(
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

可以看到开始是对参数做一个位置的调整,然后返回的是另一个方法的执行,那么我们接着看_createElement的方法。

export function _createElement(
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // ...
}

然后经过一系列处理,返回一个VNode。

6.update渲染虚拟节点

当创建好vnode的时候,就应该去执行update了,注意我们现在从456讲的都是讲

  vm._update(vm._render(), hydrating)

这一句话.当vm._render()生成了虚拟dom之后,我们要通过_update来进行dom的更新操作。

_update 的核心就是调用 vm.__patch__ 方法,而且dom的更新操作一般有两种原因,一种是因为初次渲染,另一种是数据更新,所以在我们执行_update这个方法的时候,我们需要判断我们现在是否是初次渲染,然后根据是否初次渲染来选择相应的参数

if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }

另外因为vue在weex和web平台上是不一样的,所以相应的方法和属性也有变化,例如上面的这个vm.__patch__,在src/platforms/web/runtime/index.js里我们可以看到它是根据是否在浏览器里做出的判断。

Vue.prototype.__patch__ = inBrowser ? patch : noop

noop就相当于一个空函数了,而如果在浏览器里,__patch__就指向了src/platforms/web/runtime/patch.js

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })

该方法的定义是调用 createPatchFunction 方法的返回值,这里传入了一个对象,包含 nodeOps 参数和 modules 参数。其中,nodeOps 封装了一系列 DOM 操作的方法,modules 定义了一些模块的钩子函数的实现,我们这里先不详细介绍,来看一下 createPatchFunction 的实现,它定义在 src/core/vdom/patch.js 中:

export function createPatchFunction (backend) {
  // ...  
  const { modules, nodeOps } = backend
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // ...
  }
}

createPatchFunction 内部定义了一系列的辅助方法,最终返回了一个 patch 方法,这个方法就赋值给了 vm._update 函数里调用的 vm.__patch__

那么我们最后见到了patch这个方法,那么为什么我们要翻来覆去折腾这么半天呢?

是因为我们要选择不同的平台,然后选择合适的nodeOps来操作dom。

先来回顾我们的例子:

var app = new Vue({
  el: '#app',
  render: function (createElement) {
    return createElement('div', {
      attrs: {
        id: 'app'
      },
    }, this.message)
  },
  data: {
    message: 'Hello Vue!'
  }
})

然后我们在 vm._update 的方法里是这么调用 patch 方法的:

// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

结合我们的例子,我们的场景是首次渲染,所以在执行 patch 函数的时候,传入的 vm.$el 对应的是例子中 id 为 app 的 DOM 对象,这个也就是我们在 index.html 模板中写的 ``, vm.$el 的赋值是在之前 mountComponent 函数做的,vnode 对应的是调用 render 函数的返回值,hydrating 在非服务端渲染情况下为 false,removeOnly 为 false。

确定了这些入参后,我们回到 patch 函数的执行过程,看几个关键步骤。然后创建完成。

2.组件化

1.createcomponent

在创建vnode的时候,也就是_render的时候,我们会去选择,如果是正常的节点的话就会触发CreatElement,如果是一个组件的话就会触发createcomponent.

createcomponent在src/core/vdom/create-component.js里。

  // context.$options._base is Vue  
  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor  
  if (isObject(Ctor)) {
    // Ctor Inherit a lot of vue capabilities  
    // extend inherits some of the vue's methods through a prototype
    Ctor = baseCtor.extend(Ctor)
  }

然后再和后面加上相应的钩子:

installComponentHooks(data)

然后最后创建vnode:

const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

返回。