vue源码学习(一)

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 实例内部。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容