本次可以说为自己看的第一部分吧,我们按照自顶向下的顺序来看,我是整体按照下面这位大大的分析过程来的,也是尤大自己推荐的一篇vue源码分析的博客文章,可以说是质量很不错的了。
参考文章:HcySunYang-vue源码解析
这里我们按照文章中找到Vue的构造函数这一节,本篇就是分析这一节了 => src/core/instance/index.js
, 为什么说这就是vue的构造函数了,大家可以看到 src/core/index.js
里面引入的 Vue 就是这里释出的再加上大概看看代码就一清二楚了。
下面开始
// src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
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)
export default Vue
我们看到构造函数中主要进行的是整体的初始化注入,主要是几点的注入,整体围绕Vue的生命周期进行(自行对照Vue的生命周期图)
$set、 $delete、 $watch
, 后续详解下面是上述各项详解(尽量吧,因为实在太多了,讲不清楚的,还是需要大家自己去看,可能我就讲个大概思路吧, 没写详解的就不写了,各位自己看吧,太多了哈,我也不是能一次全理解的),需要注意在源码中有 _ 和 $ 两项原型函数区分,而类型检测使用了 flow , 大型项目的多人协作中适合半路引入, 试了下,确实是简单易用,不过没实际项目用过,如果是从0-1的话还是ts吧,虽然我只看过ts的一些语法,不过确实好,自身携带了很多高级的设计模式,这都是ES需要自己写的。
下面是详解了
这里主要是暴露了 _init 这个自用初始化函数
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-init:${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(`${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
前面实例的组件选项合并不说了,大家自己看~从选项合并后开始讲个函数
const name = child.data.slot
, 当然前面室友判断 child.data 的存在性的,具体要大家去看下 createElemen 的用法,当增加了 functional: true
后,组件变为函数式组件后,会增加context 参数,最后就利用 createElement 函数将一颗 抽象的dom树(一个包含children的对象) 抽象为 dom 元素了,关于模板编译我不是特别了解,不敢乱写太多,大家需要了解的可以去看看 react diff算法的实现,里面已经解释了如何将 dom 抽象为一颗 dom 树,在编译回来, 如果自己写的话,这篇都不够写,还是引用别人的吧如何实现一个 Virtual DOM 算法new Proxy({}, handler)
,言归正传,这里就是遍历 injdect 注入的属性遍历后,每个都调用 defineReactive 函数,若在非生产环境下会传入警告函数,会在尝试改变值触发 setter 访问器时触发警告函数, 这是组件间传递信息继 props 与 $emit 之后又一单向流方法 Vue provider-inject;initState(vm) => ./state.js 这里初始化的就是大家熟知的属性了
initProvide(vm) => ./inject 跟props差不多的,非常简单,这里不说了,这里真的实现的很简单~后面就是回调生命周期钩子函数 created 了 callHook(vm, 'created')
, 并在其后判断是否存在 el 所指引的 dom 目标,存在则触发 vm.$mount(vm.$options.el)
绑定,拉出单独说说$mount
callHook(vm, 'beforeMount')
, 最后当虚拟dom还未生成时,触发一次钩子 callHook(vm, 'mounted')
,这就按照 Vue 生命周期图的顺序一步步下来了。下面是 initMethods 中所说的demo,这里只是最简单的例子,为了不污染全局变量,大家可利用原型模式,将其写成构造函数,即类,或者利用闭包做一个局部作用域,甚至可以利用闭包做离线缓存和预事件触发哈哈。
var Event = (function () {
var clientList = {},
listen,
trigger,
remove;
listen = function (key, fn) {
if (!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn);
}
trigger = function () {
var key = Array.prototype.shift.call(arguments),
fns = clientList[key];
if (!fns || fns.length === 0) {
return false;
}
for (var i = 0, fn;fn = clientList[key][i++];) {
fn.apply(this, arguments);
}
}
remove = function (key, fn) {
var fns = clientList[key];
if (!fns) {
return false;
}
if (!fn) {
fns && (fns.length = 0)
} else {
for (var l = fns.length; l >= 0; l--) {
var _fn = fns[l];
if (fn === _fn) {
fns.splice(l, 1);
}
}
}
}
return {
listen: listen,
trigger: trigger,
remove: remove
}
})()
Event.listen('squareMeter88', function (price) {
console.log('价格 = ' + price);
})
Event.trigger('squareMeter88', 20000);
这里就是几个实例原型的api
vm.$data => return this._data
, 如果触发 setter 访问器 => 警告不能改变root $data;也是几个实例的api
vm:{ _events: { click: [fn1, fn2] || [{ fn: fn1 }, { fn: fn2 }], keyup: [fn1, fn2] || [{ fn: fn1 }, { fn: fn2 }] } }
;$on(event, on)
, on 函数内为先 $off 对应 event 然后立刻将绑定函数 fn.apply(vm, arguments)
, 最后 return vm, 需注意vm._events[event] 可能有两种形式 ,所以删除时源码中有 cb === fn || cb.fn === fn 这样的比较或赋值 [fn1, fn2, fn3]
;[{fn: fn1}, {fn: fn2}, {fn: fn3}]
;$emit: 传入需要触发的函数名,在 vm._events[event] 找到函数数组并循环,这里不说了,跟上述的事件流的机制是一样的,唯一不一样的是通过 this.$emit(event, args[Array]) 触发的函数,可以传入默认的参数,因为通过 emit 触发时你可以预先传入参数, 实际上就是fn.apply(vm, args) 你们懂的吧,不多说了。
我略过了 lifecycleMixin 函数,因为我上面写大概要说的概论时大概已经说过,不具体说了,大家自行跟着看源码就行,并不难,这里说一个实例api nextTick, 一个自用函数 _render, _render 这里就不说了,前面有给大家的参考解读文章。
vue中 nextTick 思路:
其实 nextTick 就两点,第一利用闭包实现单例模式的队列任务, 第二利用 Promise、MutationObserver、setTimeout 做 js 运行环境下的兼容性事件队列。如果明白这两点,后面的可以不看,都是废话!
已经大概说完了 Vue 构造函数的分解过程,整个过程如果大家自顶向下看完,会发现就是围绕着 Vue 的生命周期图来写的,我们在看源码学习时,一定要自顶向下,围绕生命周期的思想去看整个大框架,我这里也是按照自己的理解解读,肯定会存在很多遗漏与错误,因为我也是在深入学习,如果有错,大家可以一起交流~最后放上生命周期图,官方文档也可以随便找到。