Vue源码(上篇)

某课网有个488人名币的源码解读视频看不起,只能搜很多得资料慢慢理解,看源码能知道大佬的功能模块是怎么分块写的,怎么复用的,已经vue是怎么实现的

资料来自
vue源码
喜欢唱歌的小狮子
web喵喵喵
Vue.js源码全方位深入解析
恰恰虎的博客
learnVue
最后四集视频

总文件目录

  • scripts:包含构建相关的脚本和配置文件。作者声明一般开发不需要关注此目录
  • dist:构建出的不同分发版本,只有发布新版本时才会跟新,开发分支的新特性不会反映在此
  • packages:包含服务端渲染和模板编译器两种不同的NPM包,是提供给不同使用场景使用的
  • test:包含所有测试代码
  • flow:这是一个类型检查工具,可以加入类型的限制,提高代码质量,导致源码代码变成了这样
function sum(a: number, b:number) {
  return a + b;
}
  • src:Vue的源码,使用ES6和Flow类型注释编写的
  • types:使用TypeScript定义的类型声明,并且包含了测试文件,不太明白为什么要同时使用两种静态类型检查语言

src文件夹

  • Compiler 编译器

    • parser:解析器的作用是将模板转换成元素AST对象。
    • optimizer:优化器负责检测静态抽象树的渲染进行优化。
    • codegen:代码生成器直接从AST对象生成代码字符串。
  • Core 核心
    • Observer:观察者系统,实现监测数据变化的功能。
    • Vdom:Vue虚拟节点树,实现虚拟节点的创建和刷新功能。
    • instance:Vue类,包含构造函数和原型方法的创建。
    • Global-API:全局API。
    • Components:通用抽象组件。
    • util:辅助函数。
  • Platforms 平台,不同平台的区别代码
  • Server 服务器渲染,ssr
  • Sfc 单文件组件文件编译,这一文件夹目前只包含了一个叫parser.js的文件,用来将单文件组件解析为SFC描述对象,输出给编译器继而执行模板编译。
  • Shared 共享常量和函数,是一个放工具的文件夹

Global-api

  • util:虽然暴露了一些辅助方法,但官方并不将它们列入公共API中,不鼓励外部使用。
  • set:设置响应式对象的响应式属性,强制触发视图更新,在数组更新中非常实用,不适用于根数据属性。
  • delete:删除响应式属性强制触发视图更新, 使用情境较少。
  • nextTick:结束此轮循环后执行回调,常用于需要等待DOM更新或加载完成后执行的功能。
  • use:安装插件,自带规避重复安装。
  • mixin:常用于混入插件功能,不推荐在应用代码中使用。
  • extend:创建基于Vue的子类并扩展初始内容。
  • directive:注册全局指令。
  • component:注册全局组件。
  • filter:注册全局过滤器。

Vue的核心

  • 创建Vue的构造函数,src/core/instance/index.js
function Vue (options) {
  // 安全性判断,如果不是生产环境且不是Vue的实例,在控制台输出警告
  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)
}

// 下面这些Mixin都是往Vue函数的原型对象里添加方法
// 挂载初始化方法
initMixin(Vue)
// 挂载状态处理相关方法
stateMixin(Vue)
// 挂载事件响应相关方法
eventsMixin(Vue)
// 挂载生命周期相关方法
lifecycleMixin(Vue)
// 挂载视图渲染方法
renderMixin(Vue)

// 这里就是暴露给Global-api去添加全局的方法
export default Vue
  • initMixin,添加_init方法,里面有后续的所有重要的
export function initMixin (Vue: Class<Component>) {
  // 在Vue类的原型上挂载_init()方法
  // 接收类型为原始对象的options形参,此参数为非必选参数
  Vue.prototype._init = function (options?: Object) {
    // 将实例对象赋值给vm变量
    // 这里会再次进行Component类型检查确保vm接收到的是Vue类的实例
    const vm: Component = this

    // 合并options对象
    if (options && options._isComponent) {
      // 内部组件的options初始化
      initInternalComponent(vm, options)
    } else {
      // 否则执行合并options函数,并赋值给vm的公共属性
      // 在这里的合并函数主要是解决与继承自父类的配置对象的合并
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // 暴露实例对象
    vm._self = vm
    // 初始化实例的生命周期相关属性
    initLifecycle(vm)
    // 初始化事件相关属性和监听功能
    initEvents(vm)
    // 初始化渲染相关属性和功能
    initRender(vm)
    // 调用生命周期钩子函数beforeCreate
    callHook(vm, 'beforeCreate')
    // 初始化父组件注入属性
    initInjections(vm) // resolve injections before data/props
    // 初始化状态相关属性和功能
    initState(vm)
    // 初始化子组件属性提供器
    initProvide(vm) // resolve provide after data/props
    // 调用生命周期钩子函数created
    callHook(vm, 'created')
    // 执行DOM元素挂载函数
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

生命周期相关

  • lifecycleMixin
// 导出lifecycleMixin函数,接收形参Vue,
// 使用Flow进行静态类型检查指定为Component类
export function lifecycleMixin (Vue: Class<Component>) {
  // 为Vue原型对象挂载_update私有方法
  // 接收vnode虚拟节点类型参数和一个可选的布尔值hydrating
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { ... }

  // 为Vue实例挂载$forceUpdate方法,实现强制更新
  Vue.prototype.$forceUpdate = function () { ... }

  // 为Vue实例挂载$destroy方法
  Vue.prototype.$destroy = function () { ... }
}
  • initLifecycle,主要是在生命周期开始之前设置一些相关的属性的初始值,组件也有自己的生命周期
// 导出initLifecycle函数,接受一个Component类型的vm参数
export function initLifecycle (vm: Component) {
  // 获取实例的$options属性,赋值为options变量
  const options = vm.$options

  let parent = options.parent
  // 判断是否存在且非抽象
  if (parent && !options.abstract) {
    // 遍历寻找最外层的非抽象父级
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    // 将实例添加到最外层非抽象父级的子组件中
    parent.$children.push(vm)
  }

  // 初始化实例的公共属性
  // 设置父级属性,如果之前的代码未找到父级,则vm.$parent为undefined
  vm.$parent = parent
  // 设置根属性,没有父级则为实例对象自身
  vm.$root = parent ? parent.$root : vm

  // 初始化$children和$refs属性
  // vm.$children是子组件的数组集合
  // vm.$refs是指定引用名称的组件对象集合
  vm.$children = []
  vm.$refs = {}

  // 生命周期相关的私有属性
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

调用生命周期的方法callHook

export function callHook (vm: Component, hook: string) {
  //记录当前watch的实例
  pushTarget()
  //获取相应钩子的事件处理方法数组
  const handlers = vm.$options[hook]
  //执行对应的事件方法
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  //触发@hook:定义的钩子方法
  if (vm._hasHookEvent) {
    // 发布订阅调用模式,$emit来自initEvent
    vm.$emit('hook:' + hook)
  }
  //释放当前的watch实例
  popTarget()
}

函数相关

  • eventsMixin,添加几个用于订阅发布的方法
export function eventsMixin (Vue: Class<Component>) {

  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    // 定义实例变量
    const vm: Component = this
    // 如果传入的event参数是数组,遍历event数组,为所有事件注册fn监听函数
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.$on(event[i], fn)
      }
    }
    // 返回实例本身
    return vm
  }

  // 为Vue原型对象挂载$once方法
  // 参数event只接受字符串,fn是监听函数
  Vue.prototype.$once = function (event: string, fn: Function): Component { ... }

  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component { ... }
  // 为Vue原型对象挂载$emit方法,只接受单一event
  Vue.prototype.$emit = function (event: string): Component { ... }
}
  • initEvents
// 定义并导出initEvents函数,接受Component类型的vm参数
export function initEvents (vm: Component) {
  //初始化vm._events对象
  // 存储父组件绑定当前子组件的事件,保存到vm._events中
  vm._events = Object.create(null);
  //是否存在hook:钩子事件
  vm._hasHookEvent = false;
  // init parent attached events
  var listeners = vm.$options._parentListeners;
  if (listeners) {
    updateComponentListeners(vm, listeners);
  }
}

渲染相关

  • renderMixin
export function renderMixin (Vue: Class<Component>) {

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

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

    vm.$vnode = _parentVnode
    // render 就是 render表达式,
    // vnode 就是 虚拟dom
    let vnode = render.call(vm._renderProxy, vm.$createElement)
    return vnode
  }
}
  • initRender,就是定义几个属性而已
export function initRender (vm: Component) {
  // 初始化实例的根虚拟节点
  vm._vnode = null
  // 定义实例的静态树节点
  vm._staticTrees = null
  // 获取配置对象
  const options = vm.$options
  // 设置父占位符节点
  const parentVnode = vm.$vnode = options._parentVnode
  // renderContext存储父节点有无声明上下文
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject

  /*将createElement函数绑定到该实例上,该vm存在闭包中,不可修改,vm实例则固定。这样我们就可以得到正确的上下文渲染*/
  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)

  // $attrs & $listeners这个属性进行数据监听
  const parentData = parentVnode && parentVnode.data
  defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
  defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}

参数处理

  • stateMixin
export function stateMixin (Vue: Class<Component>) {

  const dataDef = {}
  dataDef.get = function () { return this._data }
  const propsDef = {}
  propsDef.get = function () { return this._props }

  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)

  Vue.prototype.$set = set
  Vue.prototype.$delete = del

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}
  • initState,这里才真正跟MVVM连接起来
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)
  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
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}

  // 判断props对象跟data对象跟methods对象里有没有名字一样的key值
  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.`
        )
      }
    }
    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.`
      )
    } else if (!isReserved(key)) {
      // 代理data的值,当访问this.message时,实际上访问的是this[_data][message]
      proxy(vm, `_data`, key)
    }
  }
  // 对内容进行监听
  observe(data, true)
}

上面出现的几个重要的方法

  • proxy,用于代理参数,当访问this.message时,实际上访问的是this[_data][message]
  • defineReactive,最核心的对象监听方法
  • observe和Observe,这两个都是在内部调用了defineReactive

总结所有的init

当参数处理完毕,data,prop等数据已经被监听,Dep数组也已经创建好,但是是空的,这时可以看到上面的init的生命周期才写到created,我们知道在生命周期mounted前是没有dom元素的,所以在$mount阶段才开始生成虚拟dom,$mount方法在哪里定义的呢,查看下一篇笔记

原文地址:https://www.cnblogs.com/pengdt/p/12304000.html

时间: 2024-10-12 09:35:20

Vue源码(上篇)的相关文章

Vue源码(下篇)

上一篇是mount之前的添加一些方法,包括全局方法gloal-api,XXXMixin,initXXX,然后一切准备就绪,来到了mount阶段,这个阶段主要是 解析template 创建watcher并存入Dep 更新数据时更新视图 Vue源码里有两个mount 第一个 // src/platform/web/runtime/index.js Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean

vue源码解读预热-0

vueJS的源码解读 vue源码总共包含约一万行代码量(包括注释)特别感谢作者Evan You开放的源代码,访问地址为Github 代码整体介绍与函数介绍预览 代码模块分析 代码整体思路 总体的分析 从图片中可以看出的为采用IIFE(Immediately-Invoked Function Expression)立即执行的函数表达式的形式进行的代码的编写 常见的几种插件方式: (function(,){}(,))或(function(,){})(,)或!function(){}()等等,其中必有

Vue源码后记-钩子函数

vue源码的马拉松跑完了,可以放松一下写点小东西,其实源码讲20节都讲不完,跳了好多地方. 本人技术有限,无法跟大神一样,模拟vue手把手搭建一个MVVM框架,然后再分析原理,只能以门外汉的姿态简单过一下~ 想到什么写什么了,这节就简单说说钩子函数吧! vue中的钩子函数主要包含初始化的beforeCreated/created,Virtual Dom更新期间的beforeUpdate/updated,页面渲染期间的beforeMount/mounted,组件销毁期间的beforeDestroy

VUE源码解析心得

解读vue源码比较好奇的几个点: 1.生命周期是如何实现的 2.如何时间数据监听,实现双向绑定的 =======================华丽的分割线======================================================== 1. 官方图解如https://cn.vuejs.org/v2/guide/instance.html#生命周期图示,beforeCreate -> 观察数据变化 + 事件初始化  -> created -> el tem

大白话Vue源码系列(01):万事开头难

阅读目录 Vue 的源码目录结构 预备知识 先捡软的捏 Angular 是 Google 亲儿子,React 是 Facebook 小正太,那咱为啥偏偏选择了 Vue 下手,一句话,Vue 是咱见过的最对脾气的 MVVM 框架.之前也使用过 knockout,angular,react 这些框架,但都没有让咱产生 follow 的冲动.直到见到 Vue,简直是一见钟情啊. Vue 的官方文档已经对 Vue 如何使用提供了最好的教程,建议 Vue 新手直接去官网学习,而不要在网上找些质量参差不齐的

Vue源码阅读笔记,持续更新

/ / Vue.js v2.1.3 源码阅读记录 使用的文件为使用es2015的本地文件 2018年4月20日 14:06:30 */ 第一章,Vuejs的整体架构 1. 入口 入口处使用一个闭包(function (global,factory) {factory()})(this,factory): 其中factory是工厂的意思,它的内部实现是一个工厂函数,其中直接声明的function为私有成员. 2. 生命周期的理解 理解vue的生命周期对通读vue源码的效率有较好的帮助,它的生命周期

Vue源码思维导图-------------Vue 初始化

上一节看完<Vue源码思维导图-------------Vue 构造函数.原型.静态属性和方法>,这节将会以new Vue()为入口,大体看下 this._init()要做的事情. 1 function Vue (options) { 2 if (process.env.NODE_ENV !== 'production' && 3 !(this instanceof Vue) 4 ) { 5 warn('Vue is a constructor and should be ca

从vue源码看Vue.set()和this.$set()

前言 最近死磕了一段时间vue源码,想想觉得还是要输出点东西,我们先来从Vue提供的Vue.set()和this.$set()这两个api看看它内部是怎么实现的. Vue.set()和this.$set()应用的场景 平时做项目的时候难免不会对数组或者对象进行这样的骚操作操作,结果发现,咦~~,他喵的,怎么页面没有重新渲染. const vueInstance = new Vue({ data: { arr: [1, 2], obj1: { a: 3 } } }); vueInstance.$d

vue源码分析:响应式之依赖收集

今天记录下vue源码的响应式的源码学习,第一步,我们想下,为什么为们写一个data.然后里边的数据变化就能映射到dom的变化上.这里用到了Object.defineProperty(obj,key, {}),如果你还不了解建议去jsmdn上仔细了解下,这个函数的目的就是检测你得赋值和取值的过程用来定义自己的行为,比如obj[key]会拿到配置里边的get函数定义,obj[key] = 'xxx'会调用配置中的set.那么vue也是一样的,在get中做了依赖收集(也就是说哪些dom用到了我定义的这