文章内容输出来源:拉勾教育大前端高薪训练营
src
|——compiler 编译相关
|——core Vue核心库
|——platforms 平台相关代码
|——server SSR、服务器端渲染
|——sfc .vue 文件编译为js对象
|——shared 公共的diamante
// @flow
或者 /* @flow */
声明/* @flow */
function square(n: number): number {
return n * n
}
square("2") // Error!
打包工具Rollup
安装依赖
yarn
设置sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
执行dev
yarn dev
执行打包,用的是Rollup,-w参数是监听文件的变化,文件变化自动重新打包执行build
yarn build
生成了不同版本的Vuenpm run build
重新打包所有文件|| UMD | CommonJS | ES Module (基于构建工具使用) | ES Module (直接用于浏览器) | |
| :---------------------------- | :----------------- | :--------------------------- | :------------------------- | ---------------------- |
| 完整版 | vue.js | vue.common.js | vue.esm.js | vue.esm.browser.js |
| 只包含运行时版 | vue.runtime.js | vue.runtime.common.js | vue.runtime.esm.js | - |
| 完整版 (生产环境) | vue.min.js | - | - | vue.esm.browser.min.js |
| 只包含运行时版 (生产环境) | vue.runtime.min.js | - | - | - |
标签直接用在浏览器中。jsDelivr CDN 的 https://cdn.jsdelivr.net/npm/vue 默认文件就是运行时 + 编译器的 UMD 版本 (vue.js
)。pkg.main
) 是只包含运行时的 CommonJS 版本 (vue.runtime.common.js
)。pkg.module
) 是只有运行时的 ES Module 构建 (vue.runtime.esm.js
)。
直接导入。vue脚手架创建的Vue项目中,引用的Vue版本就是运行时版本:vue.runtime.esm.js
我们可以在Vue项目中执行:
vue inspect > output.js
,将webpack配置输出到output.js文件中查看,resolve: { alias: { '@': '/Users/mac/JALProjects/lagou-fed/fed-e-task-03-02/code/01-demo/src', vue$: 'vue/dist/vue.runtime.esm.js' }, }
查看dist/vue.js的构建过程
执行构建: yarn dev
src/platform/web/entry-runtime-with-compiler.js
通过查看源码解决下面问题:
const vm = new Vue({
el: '#app',
template: 'Hello template
',
render (h) {
return h('h4', 'Hello render')
}
})
答:是在init方法中调用的,可以在$mount函数定义的地方打断点,然后看调用栈的函数
调试的方法
Vue的构造函数在哪个?
Vue实例的成员/Vue的静态成员从哪里来的?
web平台相关的入口
重写了平台相关的
$mount()
方法
注册了Vue.compile()方法,传递一个HTML字符串返回render函数
v-model
、 v-show
v- transition
,v-transition-group
_patch_
: 把虚拟DOM转换成真实DOM$mount
:挂载方法在vscode的settings.json里,将"javascript.validate.enable"设为false
在vscode插件商城里安装Babel JavaScript这个插件,可以让泛型后面的代码可以高亮显示,但失去了跳转链接的作用
./src/core/global-api/index.js
// 设置keep-alive组件
extend(Vue.options.components, builtInComponents)
// 注册Vue.use()用来注册组件
initUse(Vue)
// 注册Vue.mixin()实现混入
initMixin(Vue)
// 注册Vue.extend()基础传入的options返回一个组件的构造函数
initExtend(Vue)
// 注册Vue.directive()、Vue.component()、Vue.filter()
initAssetRegisters(Vue)
./src/core/instance/index.js
// 注册vm的_init()方法,初始化vm
initMixin(Vue)
// 注册vm的$data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相关方法
// $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入render
// $nextTick/_render
renderMixin(Vue)
构造函数中调用了init方法。
./src/core/instance/init.js
// vm的生命周期相关变量初始化
// $children/$parent/$root/$refs
initLifecycle(vm)
// vm的事件监听初始化,父组件绑定在当前组件上的事件
initEvents(vm)
// vm的编译render初始化
// $slots/$scopedSlots/_c/$createElement/$attrs/$listeners
initRender(vm)
// beforeCreate 生命钩子回调
callHook(vm, 'beforeCreate')
// 把inject的成员注入到vm上
initInjections(vm) // resolve injections before data/props
// 初始化vm的_props/methods/_data/computed/watch
initState(vm)
// 初始化provide
initProvide(vm) // resolve provide after data/props
// created 生命钩子的回调
callHook(vm, 'created')
首先进行Vue的初始化,也就是初始化Vue的实例成员以及静态成员。
当初始化结束之后,开始调用构造函数,在构造函数中调用this._init()
,这个方法相当于我们整个Vue
的入口。
在_init()
中最终调用了this.$mount()
,共有两个$mount()
,第一个$mount()
是entry-runtime-with-compiler.js
入口文件的$mount()
,这个$mount()
的核心作用是帮我们把模板编译成render
函数,但它首先会判断一下我们当前是否传入了render
选项,如果没有传入的话,它会去获取我们的template
选项,如果template
选项也没有的话,他会把el
中的内容作为我们的模板,然后把模板编译成render
函数,它是通过compileToFunctions()
函数,帮我们把模板编译成render
函数的,当把render
函数编译好之后,它会把render
函数存在我们的options.render
中。
那接下来会调用runtime/index.js
中的$mount()
方法,这个方法中,首先会重新获取我们的el
,因为如果是运行时版本的话,是不会entry-runtime-with-compiler.js
这个入口中获取el
,所以如果是运行时版本的话,我们会在runtime/index.js
的$mount()
中重新获取el
。
接下来调用mountComponent()
,mountComponent()
是在src/core/instance/lifecycle.js
中定义的,在mountComponent()
中,首先会判断render
选项,如果没有但是传入了模板,并且当前是开发环境的话会发送警告,警告运行时版本不支持编译器。
接下来会触发beforeMount
这个生命周期中的钩子函数,也就是开始挂载之前。
然后定义了updateComponent()
,在这个方法中,定义了_render
和_update
,_render
的作用是生成虚拟DOM,_update
的作用是将虚拟DOM转换成真实DOM,并且挂载到页面上来。
再接下来就是创建Watcher
对象,在创建Watcher
时,传递了updateComponent
这个函数,这个函数最终是在Watcher
内部调用的。在Watcher
创建完之后还调用了get
方法,在get
方法中,会调用updateComponent()
。
然后触发了生命周期的钩子函数mounted
,挂载结束,最终返回Vue
实例。
initState() --> initData() --> observe()
observe(value)
__ob__
,如果有直接返回Observer
__ob__
属性,记录当前的observer对象defineReactIve
收集依赖
Watcher
Vue.set()定义位置:global-api/index.js
// 静态方法:set/delete/nextTick
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
vm.$set()定义位置:instance/index.js
// 注册vm的$data/$props/$set/$delete/$watch
stateMixin(Vue)
// instance/state.js
Vue.prototype.$set = set
使用:Vue.set(target, attrName, attrValue)
或vm.$set(target, attrName, attrValue)
例如:Vue.set(vm.obj, 'name', 'zhangsan')
源码中set方法的实现(关键点:调用了ob.dep.notify()
):
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
export function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
Vue.delete()
或vm.$delete()
删除对象的属性。如果对象时响应式的,确保删除能触发更新视图。这个方法主要用于避免Vue不能检测到属性被删除的限制。(其实开发者应该会很少使用到它)
注意:目标对象不能是一个Vue实例或Vue实例的根数据对象。
示例:vm.$delete(vm.obj, 'msg')
或Vue.delete(vm.obj, 'msg')
定义位置:同上的set。
源码中delete方法的实现(关键点:调用了ob.dep.notify()
):
/**
* Delete a property and trigger change if necessary.
*/
export function del (target: Array<any> | Object, key: any) {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}
vm.$nextTick(function () {/*操作DOM*/})
/ Vue.nextTick(function () {})
源码中nextTick方法的实现(关键点:调用timerFunc()
):
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
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
mounted中的更新数据是一个异步的过程,可以通过nextTick获取最新值,在nextTick的回调函数中,视图已经更新完毕,所以可以获取视图上的最新数据。
用户传入的回调函数会存到callbacks数组中,然后在timerFunc函数中以微任务的形式执行callbacks。微任务是在本次任务完成之后,才会去执行。nextTick是获取DOM上的最新数据,当微任务执行的时候,DOM元素还未渲染到浏览器上,但其实在nextTick中的回调函数执行之前,数据已经被改变了,当数据改变的时候,会通知watcher渲染视图,但在watcher里是先更新DOM树,而什么时候将DOM数据更新到浏览器上,是这次事件循环结束之后,才会执行DOM的渲染。nextTick中获取DOM数据是从DOM树上获取数据的,此时DOM还未渲染到浏览器中。nextTick中优先使用Promise执行微任务。在非IE浏览器中,使用了MutationObserver执行微任务。如果Promise和MutationObserver都不支持,则使用setImmediate,setImmediate只有IE浏览器和Nodejs支持。有的浏览器不支持微任务,则降级使用setTimeout
注:setImmediate的性能比setTimeout好,在定时时间为0的时候,setTimeout会延迟4ms才会执行,而setImmediate会立即执行。
模板编译的主要目的是将模板(template)转换为渲染函数(render)
<div>
<h1 @click="handler">
title
h1>
<p>
some content
p>
div>
渲染函数:
render (h) {
return h('div', [
h('h1', { on: { click: this.handler } }, 'title'),
h('p', 'some content')
])
}
_c()
src/core/instance/render.js
_m()/_v()/_s()
src/core/instance/render-helpers/index.js
(function anonymous() {
with (this) {
return _c(
"div",
{ attrs: { id: "app" } },
[
_m(0), // 静态标签
_v(" "), // 节点之间的空白字符
_c("p", [_v(_s(msg))]), // _s是转换成字符串
_v(" "),
_c("comp", { on: {myclick: handler } }), // 自定义模板
],
1 // 1表示如果是二维数组,会进行拍平
)
}
})
这是一个网页工具,是将HTML模板转换为render函数,
Vue2网址是:https://template-explorer.vuejs.org/
Vu3网址是:https://vue-next-template-explorer.netlify.app/
我们可以看到Vue对render函数做了优化,此外Vue2中的模板中尽量不要出现多余的空白,因为都会被转换到render函数中,Vue3的模板中的空白则不影响render函数
位置:src/compiler/create-compiler.js
createCompilerCreator函数返回了compile对象和compileToFunctions函数。
模板编译的入口函数 compileToFunctions() 中的 generate 函数的作用是把优化后的 AST 转换成代码
模板和插值表达式在编译的过程中都会被转换成对应的代码形式,不会出现在 render 函数中
位置:src/compiler/index.js
在createCompilerCreator函数中,首先用parse函数把模板转换成ast抽象语法树,然后使用optimize函数优化抽象语法树,再用generate函数把抽象语法书生成字符串形式的js代码
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 把模板转换成ast抽象语法树
// 抽象语法树,用来以树形的方式描述代码结构
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 优化抽象语法树
optimize(ast, options)
}
// 把抽象语法书生成字符串形式的js代码
const code = generate(ast, options)
return {
ast,
// 渲染函数
render: code.render,
// 静态渲染函数
staticRenderFns: code.staticRenderFns
}
})
AST 在线生成网址:https://astexplorer.net/
位置:src/compiler/optimizer.js
优化处理,跳过静态子树。staticRoot是静态根节点。指节点中只包含纯文本的节点,是静态节点,但不是静态根节点,因为此时优化的成本大于收益,Vue认为这种优化会带来负面的影响
静态根节点不会被重新渲染,patch 的过程中会跳过静态根节点。
generate() 函数返回的是字符串形式的代码,还需要 toFunctions() 转换成函数的形式
根据传入的选项创建组件的构造函数,组件的构造函数继承自Vue的构造函数。
先创建父组件,再创建子组件。
先挂载子组件,再挂载父组件。
在 createElement() 函数中调用 createComponent() 创建的是组件的 VNode。组件对象是在组件的 init 钩子函数中创建的,然后在 patch() --> createElm() --> createComponent() 中挂载组件
全局组件之所以可以在任意组件中使用是因为 Vue 构造函数的选项被合并到了 VueComponent 组件构造函数的选项中。
局部组件的使用范围被限制在当前组件内是因为,在创建当前组件的过程中传入的局部组件选项,其它位置无法访问