vue源码学习笔记

Vue的本质

Vue的本质就是用一个Function实现的Class,然后在它的原型prototype本身上面扩展一些属性和方法。

它的定义是在src/core/instance/index.js里面定义

使用ES5的方式,即用函数来实现一个class,不用ES6来实现class的原因:在ES5中,是可以往Vue的原型上挂很多方法,并且可以将不同的原型方法拆分到不同的文件下,这样方便代码的管理,不用再单个文件上把Vue的原型方法都定义一遍

Vue中的全局方法定义在src/core/global-api里面:定义Vue的全局配置


1、数据驱动

Vue.js 一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,对视图的修改,不会直接操作 DOM,而是通过修改数据。它相比我们传统的前端开发,如使用 jQuery 等前端库直接修改 DOM,大大简化了代码量。特别是当交互复杂的时候,只关心数据的修改会让代码的逻辑变的非常清晰,因为 DOM 变成了数据的映射,我们所有的逻辑都是对数据的修改,而不用碰触 DOM,这样的代码非常利于维护

可以采用简洁的模板语法来声明式的将数据渲染为 DOM

数据驱动的两个核心思想:模板和数据是如何渲染成最终的DOM;数据更新驱动视图变化

问题:vue中模板和数据如何渲染成最终的DOM????


2、new Vue()发生了什么?

1)new关键字实例化一个对象,Vue()是一个类,在js中类用Function定义

2)在Vue()函数中调用初始化函数:Vue 初始化主要就干了几件事情,合并配置初始化生命周期初始化事件中心初始化渲染初始化datapropscomputedwatcher 等等。Vue 的初始化逻辑写的非常清楚,把不同的功能逻辑拆成一些单独的函数执行,让主线逻辑一目了然

3)初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM


分析 Vue 的挂载过程



在vue的项目中去调试vue源码,如下图所示:

 
 

import Vue from 'vue'

var vue = new Vue({

el: "#app", data() { return { message: 'hello' } }})

vue的定义是在node_modules下面定义的,在vue文件夹下面的package.json下面定义了如下内容:

"main": "dist/vue.runtime.esm.js",
"module": "dist/vue.runtime.esm.js"
如果项目用vue-cli构建的话,那么vue的引进其实是在build/webpack.base.conf.js中进行配置的
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/distue.esm.js',  //完整的写法:node_modules/vue/dist/vue.esm.js
      '@': resolve('src'),
      'common':resolve('src/common'),
      'components':resolve('src/components')
    }
  }

即:

import Vue from 'vue' 就相当于import Vue from 'node_modules/vue/dist/vue.esm.js'

那调用initMixin()方法时,就是调用这个路径下的文件里面定义的方法,可以在里面添加debugger,然后就可以调试vue中的源码了

function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    debugger
    var vm = this;
    // a uid
    vm._uid = uid$3++;

    var 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);
    }

    // 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);
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created');

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false);
      mark(endTag);
      measure(("vue " + (vm._name) + " init"), startTag, endTag);
    }
    //如果有设置Vue()实例,那么就挂载到该el对象上
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };
}

在data()属性里面定义了变量之后,就可以通过this.变量名访问到在data()里面定义的变量,这是为什么呢?

通过在初始化函数中调用initState(vm),然后调用初始化data的函数initData(),在里面将数据赋给vm_.data,然后通过proxy()将vm._data.key替换成vm.key,在proxy()中对vm._data.key设置setter  getter,然后通过Object.defineProperty(target,key,sharedPropertyDefinition)来实现代理的

import Vue from 'vue' 

var vue = new Vue({

   el: "#app",
   mounted: {
      console.log(this.message) //相当于this._data_message,通过proxy做一层代理,主要应用Object.defineProperty()实现一个代理
   },
   data() {
     return {
        message: 'hello'
     }
   }
})
在init.js文件中初始化函数中有一个initState(vm),而这个函数是定义在state.js中
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  //如果定义了data,就初始化data
  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)
  }
}
initData()函数定义如下:
function initData (vm: Component) {
  let data = vm.$options.data  //在Vue()中的data()中定义的对象
  data = vm._data = typeof data === 'function'  // data = vm._data
    ? getData(data, vm)      //从vm中拿到data  getData(data, vm)
    : 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]
    if (process.env.NODE_ENV !== 'production') {
      //methods和data中不能有同名的属性变量,有的话就报警告
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    //props和data中不能有同名的属性变量,有的话就报警告
    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添加getter  setter
      proxy(vm, `_data`, key)
    }
  }
  // observe data  初始化的时候对data做了一个响应式的处理
  observe(data, true /* asRootData */)
}

3、Vue实例挂载的实现,也就是执行vm.$mount()做了哪些事情

用的是runtime+compiler的版本,所以入口在entre-runtime-with-compiler.js

Vue中是通过$mount实例方法挂载vm

$mount方法定义在Vue的原型上:Vue.prototype.$mount

1)首先缓存了原型上的 $mount 方法

2)重新定义该方法

a)  它对 el 做了限制,Vue 不能挂载在 body、html 这样的根节点上

b)  很关键的逻辑 —— 如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法。这里我们要牢记,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,那么这个过程是 Vue 的一个“在线编译”的过程,它是调用 compileToFunctions 方法实现的

3)  最后,调用原先原型上的 $mount 方法挂载

原先原型上的 $mount 方法在 src/platform/web/runtime/index.js 中定义,之所以这么设计完全是为了复用,因为它是可以被 runtime only 版本的 Vue 直接使用的

原先原型上的方法$mount会调用mountComponent(),定义在src/core/instance/lifecycle.js

mountComponent ()核心

a)  先调用 vm._render 方法生成VNode

b)  再实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,最终调用 vm._update 更新 DOM

c)  最后判断为根节点时,设置 vm._isMounted true(表示这个实例已经挂载了,同时执行 mounted 钩子函数。 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例

Watcher的作用

a)  初始化的时候执行回调函数

b)  当 vm 实例中,监测的数据发生变化的时候执行回调函数


mountComponent()方法中的vm._reneder()和vm._update()解析

(1)vm._render():定义在Vue.prototype._render上---把实例渲染成一个虚拟 Node

vm._render() 最终是通过执行 createElement 方法,然后返回 vnode


Virtual  DOM:用VNode类描述

用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。在 Vue.js 中,Virtual DOM 是用 VNode 这么一个 Class 去描述,它是定义在 src/core/vdom/vnode.js 中的

 VNode 是对真实 DOM 的一种抽象描述,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其它属性都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的


Virtual DOM映射到真实的DOM上要经历过VNodecreate    diff   patch等过程,Vnode的create是通过createElement()方法创建的


createElement()创建Vnode

定义在'src/core/vdom/create-elemenet.js'

createElement 方法实际上是对 _createElement 方法的封装,它允许传入的参数更加灵活,在处理这些参数后,调用真正创建 VNode 的函数 _createElement


createElement 函数的流程略微有点多—— 这里主要介绍children 的规范化以及 VNode 的创建

children 的规范化

由于 Virtual DOM 实际上是一个树状结构,每一个 VNode 可能会有若干个子节点,这些子节点应该也是 VNode 的类型。_createElement 接收的第 4 个参数 children 任意类型的,因此我们需要把它们规范成 VNode 类型

根据normalizationType 的不同,调用了 normalizeChildren(children) 和 simpleNormalizeChildren(children) 方法,

这两个方法在src/core/vdom/helpers/normalzie-children.js定义

1)simpleNormalizeChildren 方法调用场景是 render 函数当函数是编译生成的。理论上编译生成的 children 都已经是 VNode 类型的,但这里有一个例外,就是 functional component 函数式组件返回的是一个数组而不是一个根节点,所以会通过 Array.prototype.concat 方法把整个 children 数组打平,让它的深度只有一层











猜你喜欢

转载自blog.csdn.net/tangxiujiang/article/details/80718733