<<往期回顧>>
接下來一起學(xué)習(xí)下,runtime-core里面的方法,本期主要實(shí)現(xiàn)的內(nèi)容是,通過createApp方法,到mount最后把咋們的dom給掛載成功!,所有的源碼請查看
咋們需要使這個測試用例跑成功!,在圖中可以發(fā)現(xiàn),調(diào)用app傳入了一個render函數(shù),然后掛載,對比期望結(jié)果!
思考再三,先把這一節(jié)先說了,jest是怎么來測試dom的?
jest默認(rèn)的環(huán)境是node,在jest.config.js中可以看到
npm有在node中實(shí)現(xiàn)了瀏覽器環(huán)境的api的庫,jsdom、happy-dom 等,咋們這里就使用比較輕的happy-dom,但是happy-dom里面與jest結(jié)合是一個子包——@happy-dom/jest-environment,那就安裝一下
pnpm add @happy-dom/jest-environment -w -D
復(fù)制代碼
由于我項目示例使用的是monorepo,所以只需要在runtime-core中進(jìn)行以下操作即可:
在jest.config.js中修改環(huán)境
testEnvironment: '@happy-dom/jest-environment',
復(fù)制代碼
然后你就可以在當(dāng)前子包中使用正確運(yùn)行測試用例了。
針對第一個問題,在上一節(jié)vue3源碼分析——rollup打包monorepo中我們可以知道,在全局可以執(zhí)行packages中的每一個腳本,同理,我們做以下操作:
// 在全局的package.json中的test修改成這句話
"test": "pnpm -r --filter=./packages/** run test",
復(fù)制代碼
那么就可以執(zhí)行啦!
第二個問題,這個是vscode的插件問題,我們可以重jest插件的文檔入手,可以發(fā)現(xiàn)jest執(zhí)行的時候,可以自定義腳本,解決辦法如下:
意思是說,jest自動執(zhí)行的時候,直接執(zhí)行我們項目的test腳本,由于第一個問題的解決,第二個問題也是ok的哦!
在正文之前,希望您先看過本系列文章的 vue3 組件初始化流程,這里詳細(xì)介紹了組件的初始化流程,這里主要是實(shí)現(xiàn)掛載
describe('apiCreateApp', () => {
// 定義一個跟節(jié)點(diǎn)
let appElement: Element;
// 開始之前創(chuàng)建dom元素
beforeEach(() => {
appElement = document.createElement('div');
appElement.id = 'app';
document.body.appendChild(appElement);
});
// 執(zhí)行完測試后,情況html內(nèi)部的內(nèi)容
afterEach(() => {
document.body.innerHTML = '';
});
test('測試createApp,是否正確掛載', () => {
// 調(diào)用app方法,傳入render函數(shù)
const app = createApp({
render() {
return h('div', {}, '123');
}
});
const appDoc = document.querySelector('#app')
// 調(diào)用mount函數(shù)
app.mount(appDoc);
expect(document.body.innerHTML).toBe('<div id="app"><div>123</div></div>');
})
})
復(fù)制代碼
function createApp(rootComponent) {
const app = {
_component: rootComponent,
mount(container) {
const vnode = createVNode(rootComponent);
render(vnode, container);
}
};
return app;
}
復(fù)制代碼
function createVNode(type, props, children) {
// 一開始咋們就是這么簡單,vnode里面有一個type,props,children這幾個關(guān)鍵的函數(shù)
const vnode = {
type,
props: props || {},
children: children || []
};
return vnode;
}
復(fù)制代碼
function render(vnode, container) {
patch(vnode, container);
}
function patch(vnode, container) {
// patch需要判斷vnode的type,如果是對象,則是處理組件,如果是字符串div,p等,則是處理元素
if (isObj(vnode.type)) {
processComponent(null, vnode, container);
} else if (String(vnode.type).length > 0) {
processElement(null, vnode, container);
}
}
復(fù)制代碼
// n1 是老節(jié)點(diǎn),n2則是新節(jié)點(diǎn),container是掛載的容器
function processComponent(n1, n2, container) {
// 如果n1不存在,直接是掛載組件
if (!n1) {
mountComponent(n2, container);
}
}
復(fù)制代碼
function mountComponent(vnode, container) {
// 創(chuàng)建組件實(shí)例
const instance = createComponentInstance(vnode);
// 處理組件,初始化setup,slot,props, render等在實(shí)例的掛載
setupComponent(instance);
// 執(zhí)行render函數(shù)
setupRenderEffect(instance, vnode, container);
}
復(fù)制代碼
// 是不是組件實(shí)例很簡單,就只有一個vnode,props,
function createComponentInstance(vnode) {
const instance = {
vnode,
props: {},
type: vnode.type
};
return instance;
}
復(fù)制代碼
function setupComponent(instance) {
const { props } = instance;
// 初始化props
initProps(instance, props);
// 處理組件的render函數(shù)
setupStatefulComponent(instance);
}
function setupStatefulComponent(instance) {
const Component = instance.type;
const { setup } = Component;
// 是否存在setup
if (setup) {
const setupResult = setup();
// 處理setup的結(jié)果
handleSetupResult(instance, setupResult);
}
// 完成render在instance中
finishComponentSetup(instance);
}
function handleSetupResult(instance, setupResult) {
// 函數(shù)作為instance的render函數(shù)
if (isFunction(setupResult)) {
instance.render = setupResult;
} else if (isObj(setupResult)) {
instance.setupState = proxyRefs(setupResult);
}
finishComponentSetup(instance);
}
function finishComponentSetup(instance) {
const Component = instance.type;
// 如果沒有的話,直接使用Component的render
if (!instance.render) {
instance.render = Component.render;
}
}
復(fù)制代碼
function setupRenderEffect(instance, vnode, container) {
const subtree = instance.render();
patch(subtree, container);
}
復(fù)制代碼
// 這個方法和processComponent一樣
function processElement(n1, n2, container) {
// 需要判斷是更新還是掛載
if (n1) ; else {
mountElement(n2, container);
}
}
復(fù)制代碼
function mountElement(vnode, container) {
// 創(chuàng)建根節(jié)點(diǎn)
const el = document.createElement(vnode.type);
const { props } = vnode;
// 掛載屬性
for (let key in props) {
el.setAttribute(key, props[key]);
}
const children = vnode.children;
// 如果children是數(shù)組,繼續(xù)patch
if (Array.isArray(children)) {
children.forEach((child) => {
patch(child, el);
});
} else if (String(children).length > 0) {
el.innerHTML = children;
}
// 把元素掛載到根節(jié)點(diǎn)
container.appendChild(el);
}
復(fù)制代碼
恭喜,到這兒就完成本期的內(nèi)容,重頭看一下,vue組件的掛載分為兩種,處理組件和處理元素,最終回歸到處理元素上面,最后實(shí)現(xiàn)節(jié)點(diǎn)的掛載,該內(nèi)容是經(jīng)過非常多刪減,只是為了實(shí)現(xiàn)一個基本掛載,還有許多的邊界都沒有完善,后續(xù)繼續(xù)加油???
我們都聽過知其然知其所以然這句話
那么不知道大家是否思考過new Vue()這個過程中究竟做了些什么?
過程中是如何完成數(shù)據(jù)的綁定,又是如何將數(shù)據(jù)渲染到視圖的等等
首先找到vue的構(gòu)造函數(shù)
源碼位置:src\core\instance\index.js
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)
}
options是用戶傳遞過來的配置項,如data、methods等常用的方法
vue構(gòu)建函數(shù)調(diào)用_init方法,但我們發(fā)現(xiàn)本文件中并沒有此方法,但仔細(xì)可以看到文件下方定定義了很多初始化方法
initMixin(Vue); // 定義 _init
stateMixin(Vue); // 定義 $set $get $delete $watch 等
eventsMixin(Vue); // 定義事件 $on $once $off $emit
lifecycleMixin(Vue);// 定義 _update $forceUpdate $destroy
renderMixin(Vue); // 定義 _render 返回虛擬dom
首先可以看initMixin方法,發(fā)現(xiàn)該方法在Vue原型上定義了_init方法
源碼位置:src\core\instance\init.js
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-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
// 合并屬性,判斷初始化的是否是組件,這里合并主要是 mixins 或 extends 的方法
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 { // 合并vue屬性
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// 初始化proxy攔截器
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化組件生命周期標(biāo)志位
initLifecycle(vm)
// 初始化組件事件偵聽
initEvents(vm)
// 初始化渲染方法
initRender(vm)
callHook(vm, 'beforeCreate')
// 初始化依賴注入內(nèi)容,在初始化data、props之前
initInjections(vm) // resolve injections before data/props
// 初始化props/data/method/watch/methods
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(`vue ${vm._name} init`, startTag, endTag)
}
// 掛載元素
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
仔細(xì)閱讀上面的代碼,我們得到以下結(jié)論:
initState方法是完成props/data/method/watch/methods的初始化
源碼位置:src\core\instance\state.js
export function initState (vm: Component) {
// 初始化組件的watcher列表
vm._watchers = []
const opts = vm.$options
// 初始化props
if (opts.props) initProps(vm, opts.props)
// 初始化methods方法
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
// 初始化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)
}
}
我們和這里主要看初始化data的方法為initData,它與initState在同一文件上
function initData (vm: Component) {
let data = vm.$options.data
// 獲取到組件上的data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
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') {
// 屬性名不能與方法名重復(fù)
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
// 屬性名不能與state名稱重復(fù)
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.`,
vm
)
} else if (!isReserved(key)) { // 驗證key值的合法性
// 將_data中的數(shù)據(jù)掛載到組件vm上,這樣就可以通過this.xxx訪問到組件上的數(shù)據(jù)
proxy(vm, `_data`, key)
}
}
// observe data
// 響應(yīng)式監(jiān)聽data是數(shù)據(jù)的變化
observe(data, true /* asRootData */)
}
仔細(xì)閱讀上面的代碼,我們可以得到以下結(jié)論:
關(guān)于數(shù)據(jù)響應(yīng)式在這就不展開詳細(xì)說明
上文提到掛載方法是調(diào)用vm.$mount方法
源碼位置:
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 獲取或查詢元素
el = el && query(el)
/* istanbul ignore if */
// vue 不允許直接掛載到body或頁面文檔上
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
// 存在template模板,解析vue模板文件
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// 通過選擇器獲取元素內(nèi)容
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
/**
* 1.將temmplate解析ast tree
* 2.將ast tree轉(zhuǎn)換成render語法字符串
* 3.生成render方法
*/
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
閱讀上面代碼,我們能得到以下結(jié)論:
對template的解析步驟大致分為以下幾步:
生成render函數(shù),掛載到vm上后,會再次調(diào)用mount方法
源碼位置:src\platforms\web\runtime\index.js
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
// 渲染組件
return mountComponent(this, el, hydrating)
}
調(diào)用mountComponent渲染組件
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 如果沒有獲取解析的render函數(shù),則會拋出警告
// render是解析模板文件生成的
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
// 沒有獲取到vue的模板文件
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
// 執(zhí)行beforeMount鉤子
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
// 定義更新函數(shù)
updateComponent = () => {
// 實(shí)際調(diào)?是在lifeCycleMixin中定義的_update和renderMixin中定義的_render
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 監(jiān)聽當(dāng)前組件狀態(tài),當(dāng)有數(shù)據(jù)變化時,更新組件
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
// 數(shù)據(jù)更新引發(fā)的組件更新
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
閱讀上面代碼,我們得到以下結(jié)論:
updateComponent方法主要執(zhí)行在vue初始化時聲明的render,update方法
render的作用主要是生成vnode
源碼位置:src\core\instance\render.js
// 定義vue 原型上的render方法
Vue.prototype._render = function (): VNode {
const vm: Component = this
// render函數(shù)來自于組件的option
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
// 調(diào)用render方法,自己的獨(dú)特的render方法, 傳入createElement參數(shù),生成vNode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && 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
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
_update主要功能是調(diào)用patch,將vnode轉(zhuǎn)換為真實(shí)DOM,并且更新到頁面中
源碼位置:src\core\instance\lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
// 設(shè)置當(dāng)前激活的作用域
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
// 執(zhí)行具體的掛載邏輯
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
關(guān)注本頭條號,每天堅持更新原創(chuàng)干貨技術(shù)文章。
如需學(xué)習(xí)視頻,請在微信搜索公眾號“智傳網(wǎng)優(yōu)”直接開始自助視頻學(xué)習(xí)
1. 前言
ExFAT(Extended File Allocation Table擴(kuò)展文件分配表)是一種針對SD卡和USB閃存驅(qū)動器(U盤)等閃存設(shè)備進(jìn)行優(yōu)化的Microsoft專有文件系統(tǒng)。
它旨在取代舊的32位FAT32文件系統(tǒng),后者不能存儲大于4 GB的文件。
最新版本的Windows和MacOS操作系統(tǒng)支持exFAT文件系統(tǒng)。
CentOS與大多數(shù)其他主要Linux發(fā)行版一樣,默認(rèn)情況下不提供對專有exFAT文件系統(tǒng)的支持。
如果您使用CentOS作為操作系統(tǒng),在嘗試掛載exFAT格式的USB驅(qū)動器時可能會遇到問題。
本教程介紹如何在CentOS 7上啟用exFAT支持,以使CentOS7可以支持exFAT文件系統(tǒng),可以正常掛載exFAT的U盤。
centos掛載exfat u盤
2. CentOS系統(tǒng)怎么掛載exfat格式的U盤
為了能夠在CentOS上安裝exFAT文件系統(tǒng),您需要安裝免費(fèi)的FUSE exFAT模塊和工具,為類Unix系統(tǒng)提供全功能的exFAT文件系統(tǒng)管理。
在CentOS 7核心存儲庫中不提供exFAT軟件包。 您可以選擇從源構(gòu)建exFAT工具,或者使用Nux Dextop軟件倉庫中的yum進(jìn)行安裝。 我們將選擇第二種方案。
Nux軟件倉庫依賴于EPEL軟件倉庫。 如果系統(tǒng)上未啟用EPEL存儲庫,請鍵入以下命令啟用它:
sudo yum install epel-release
接下來,導(dǎo)入yum倉庫GPG密鑰并通過安裝rpm軟件包啟用Nux存儲庫:
sudo rpm -v --import http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro sudo rpm -Uvh http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-5.el7.nux.noarch.rpm
啟用軟件倉庫后,使用以下命令安裝exfat-fuse和exfat-utils軟件包:
sudo yum install exfat-utils fuse-exfat
完成了!您現(xiàn)在可以安裝exFAT格式的設(shè)備。
CentOS系統(tǒng)怎么掛載exfat格式的U盤
3. 結(jié)論
本文主要介紹了如何在CentOS 7計算機(jī)上啟用對exFAT文件系統(tǒng)的支持。 有些人將exFAT稱為FAT64。
插入時USB驅(qū)動器將自動安裝,但如果自動安裝失敗,則必須手動掛載exFAT格式的U盤。
centos7怎么掛載exfat格式U盤
本文已同步至博客站,尊重原創(chuàng),轉(zhuǎn)載時請在正文中附帶以下鏈接:
https://www.linuxrumen.com/rmxx/1224.html
點(diǎn)擊了解更多,快速查看更多的技術(shù)文章列表。
*請認(rèn)真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。