美文网首页
vue源码学习(一)

vue源码学习(一)

作者: Lisburn | 来源:发表于2020-12-25 15:12 被阅读0次

VUE

简介

vue是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。

vue是一套用于构建用户界面的渐进式框架【即主张:每个框架都不可避免会有自己的一些特点,从而会对使用者有一定的要求,这些要求就是主张,主张有强有弱,它的强势程度会影响在业务开发中的使用方式。Vue则不强求你一次性接受并使用它的全部功能特性】。

比如:Angular,它两个版本都是强主张的,必须接受以下东西:

  • 必须使用它的模块机制
  • 必须使用它的依赖注入
  • 必须使用它的特殊形式定义组件

核心

Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统:

<div id="app">
  {{ message }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

我们已经成功创建了一个 Vue 应用!看起来这跟渲染一个字符串模板非常类似,但是 Vue 在背后做了大量工作。

el:

Vue实例挂载的元素节点,值可以是 CSS 选择符,或实际 HTML 元素,或返回 HTML 元素的函数

实例化构造函数【new vue】(将vue的实例挂载到dom对象上从而运用数据驱动的方式来扩展我们的代码)

目录:node_module/vue/src/core/instance/index

options - data / methods
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)
}

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

在js中一切皆函数,其实Vue就是一个函数,在初始化的时候执行原型链上的_init方法

_init

initMixin(Vue)

去掉Performance后:

let uid = 0;

export function initMixin(Vue: Class<Component>) {
  Vue.prototype._init = function(options?: Object) {
    const vm: Component = this;

    vm._uid = uid++; // 当前实例的 _uid 加 1

    //  a flag to avoid this being observed
    // 用 _isVue 来标识当前实例是 Vue 实例, 这样做是为了后续被 observed
    vm._isVue = true;
    
    // merge options 合并options 
    if (options && options._isComponent) { // _isComponent 标识当前为 内部Component
      // 内部Component 的 options 初始化
      initInternalComponent(vm, options); 
    }
    else { // 非内部Component的 options 初始化
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );
    }
    
    // 在render中将this指向vm._renderProxy
    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'); // 回调 beforeCreate 钩子函数
    initInjections(vm); // resolve injections before data/props
    // 初始化 vm 的状态
    initState(vm);
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created'); // vm 已经创建好 回调 created 钩子函数

    if (vm.$options.el) { // 挂载实例
      vm.$mount(vm.$options.el);
    }
  };
}

在初始化方法中主要干了以下几件事

  • 初始化 options 参数
  1. 无论是 jQuery.js 还是 Vue.js,都是在 js 的基础上再次封装的库,都需要创建对应的实例来封装对应的操作。如通过 $('div') 获得一个 jQuery 的 div元素 实例,也称为 jQuery 对象,jQuery 对象包含了对选中的 div元素 的各种操作API,因此 jQuery 实例封装的是对选中元素的各种操作。
  2. 而 Vue.js 在此基础上更近一步,封装了对视图的所有操作,包括数据的读写、数据变化的监听、DOM元素的更新等等,通过 new Vue(options) 来创建出一个 Vue实例 ,也称为 Vue对象 ,该 Vue实例 封装了操作元素视图的所有操作,可通过 Vue实例 来轻松操作对应区域的视图。
  3. options 对象的具体可选属性有很多,具体可分为五大类 : 数据 、DOM 、生命周期钩子 、 资源 、组合
  • 将 _renderProxy 设置为 vm
    initProxy
if (hasProxy) {
    // ...
    vm._renderProxy = new Proxy(vm, handlers)
} else {
    vm._renderProxy = vm;
}

不管开发环境还是生产环境都是为了给实例对象增添一个_renderProxy属性

vm._renderProxy = process.env.NODE_ENV !== 'production' && hasProxy ? new Proxy(vm, handlers) : vm;
  1. 为vm挂载一个_renderProxy属性
  2. 若是开发环境且支持原生Proxy接口,_renderProxy属性挂一个Proxy对象
  3. 若是生产环境或不支持原生Proxy,直接挂vm

renderProxy 渲染代理
Proxy对象用于拦截针对目标对象的所有操作,因此,对_renderProxy属性的操作将被拦截
具体的拦截方式 : handlers对象

const options = vm.$options
const handlers = options.render && options.render._withStripped
    ? getHandler
    : hasHandler
 /*用户传入的render方法*/
options.render

_withStripped存在与否决定了对_renderProxy的拦截方式的不同

const getHandler = {
    get (target, key) {
        if (typeof key === 'string' && !(key in target)) {
            warnNonPresent(target, key)
        }
        return target[key]
    }
}

1 .拦截对_renderProxy的指读操作
2 .若访问的属性不存在就报警
3 .不改变默认返回值

const warnNonPresent = (target, key) => {
    warn(
        `Property or method "${key}" is not defined on the instance but ` +
        'referenced during render. Make sure that this property is reactive, ' +
        'either in the data option, or for class-based components, by ' +
        'initializing the property. ' +
        'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
        target
    )
}

在渲染过程中调用的属性或方法在实例对象上不存在
实例渲染时,渲染函数会从vm._renderProxy对象上读取所需要的数据,一般来说他指向的是实例对象,也就是说直接从实例对象上读取数据用于渲染。但在开发环境,且支持Proxy的情况下,程序会给这个操作增加一层校验:渲染所需数据不存在时报警

校验render函数是否引用了vm上不存在数据或非特许的数据
校验自定义的快捷键名是否和Vue内置的快捷键修饰符重名

  • 初始化生命周期
    Vue的生命周期分为三个阶段,分别为: 初始化,运行中, 销毁,一共8个钩子函数

vue中的生命周期指的是组从创建到销毁的一个过程,在这个过程中,我们在每一个特定的阶段会触发一些方法,我们给这些方法起了个名字叫做生命周期钩子函数/ 组件钩子

因为我们想在生命周期钩子中实现项目功能,那么我们必须知道每一个钩子函数的具体用途:
比如 :页面初始化、数据进出顺序、页面状态控制

  • 初始化 Render
    renderMixin
export function renderMixin (Vue: Class<Component>) {
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    // vm.$options.render & vm.$options._parentVnode
    const { render, _parentVnode } = vm.$options

    if (_parentVnode) {
      vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
    }

    vm.$vnode = _parentVnode
    let vnode
    try {
      // 执行 vue 实例的 render 方法
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      if (process.env.NODE_ENV !== 'production') {
        if (vm.$options.renderError) {
          try {
            vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
          } catch (e) {
            handleError(e, vm, `renderError`)
            vnode = vm._vnode
          }
        } else {
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    }
    // 返回空vnode避免render方法报错退出
    if (!(vnode instanceof VNode)) {
      vnode = createEmptyVNode()
    }
    // 父级Vnode
    vnode.parent = _parentVnode
    return vnode
  }
}

源码执行了 installRenderHelpers 方法,然后定义了 Vue 的 $nextTick 和 _render 方法
installRenderHelpers :

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber // 数字
  target._s = toString // 字符串
  target._l = renderList // 列表
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
}

用在Vue生成的渲染函数中

在 $nextTick 函数中执行了 nextTick 函数
nextTick:

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

_render 方法,关键在这个 try...catch 方法中,执行了Vue实例中的 render 方法生成一个vnode。如果生成失败,会试着生成 renderError 方法。如果vnode为空,则为vnode传一个空的VNode,最后返回vnode对象。

initRender

export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // 将 createElement 方法绑定到这个实例,这样我们就可以在其中得到适当的 render context。
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // 规范化一直应用于公共版本,用于用户编写的 render 函数。
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  // 父级组件数据
  const parentData = parentVnode && parentVnode.data
  // 监听事件
  defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
  defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
  • 初始化 vm的状态,prop/data/computed/method/watch都在这里完成初始化
  • 挂载实例

vue没有把所有的方法都写在函数内部,这样从代码上来说,每次实例化的时候不会生成重复的代码
主要还是代码结构更清晰,利用mixin的概念,把每个模块都抽离开,这样代码在结构和扩展性都有很大提高,这里的每个mixin先不说,先看以一下整体结构,这里定义完还要被core里的index.js再次包装调用initGlobalAPI(Vue)来初始化全局的api方法,在web下runtime文件夹下引用再次封装,vue是分为运行时可编译和只运行的版本,所以如果需要编译,在Vue原型上添加了$mount方法,先来看一下initGlobalAPI,在instance中都是在原型链上扩展方法,在这里是直接在Vue上扩展静态方法

现在数据和 DOM 已经被建立了关联,所有东西都是响应式的。

注意我们不再和 HTML 直接交互了。一个 Vue 应用会将其挂载到一个 DOM 元素上 (对于这个例子是 #app) 然后对其进行完全控制。那个 HTML 是我们的入口,但其余都会发生在新创建的 Vue 实例内部。

相关文章

网友评论

      本文标题:vue源码学习(一)

      本文链接:https://www.haomeiwen.com/subject/qdwxnktx.html