美文网首页
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