本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
大家好,我是wo不是黄蓉,今年学习目标从源码共读开始,希望能跟着若川大佬学习源码的思路学到更多的东西。
学习目标:
1.学习vue-next项目架构思路
2.学习打包和源码调试技巧
3.学习vue3 shared模块中的工具函数和js基础知识
环境准备
clone项目到本地,可以使用若川提供的vue-next-analysis或者直接clone vue3源码,我本地使用的是第一种。
// clone项目
git clone https://github.com/lxchuan12/vue-next-analysis.git
//进入到vue-next目录
cd vue-next
//已经安装过yarn的跳过
npm install --global yarn
//安装依赖
yarn
//生成打包文件
yarn build
yarn build后在vue-next\packages\shared\dist生成shared.esm-bundler.js文件,打包成js代码能看的更直观。
vue-next项目架构
Vue-next使用monorepo管理项目
packages下的每个模块都是一个单独的项目都有package.json文件,执行yarn build后从这里我们也可以发现,可以单独引用vue3某快功能的api,只需要将所需的打包好的js引入即可,每个模块都是单独的一个子项目,配置工作目录是在package.json中workspaces配置的
"workspaces": [
"packages/*"
],
vue-next\scripts可执行脚本,和package.json中scripts的脚本中配置的内容是对应的,例如:dev配置实际执行的是node scripts/dev.js
源码调试
开启sourcemap
在vue-next\package.json中追加"dev:sourcemap": "node scripts/dev.js --sourcemap"
执行yarn dev:sourcemap
或者yarn build
控制台会输出bundles D:\workspace\Learn\vue-next-analysis\vue-next\packages\vue\src\index.ts → packages\vue\dist\vue.global.js..
vue-next\packages\vue\dist\vue.global.js.map
就是生成的sourcemap
文件了
找到vue-next\examples\index.html
引入打包好的packages\vue\dist\vue.global.js
就可以直接使用Vue3语法了
然后再新建一个终端,执行yarn serve
启动一个服务,在浏览器打开http://localhost:5000/examples/就可以启动源码调试了
工具函数
判断类型的
Object.freeze()
冻结对象,只能冻结第一层,如需深度冻结需要结合递归
var person = {
name: '我不是黄蓉',
age: 18,
others: {
hobby: '游泳'
}
}
function isObject(value) {
return (
Object.prototype.toString.call(value).indexOf('[object Object]') != -1
)
}
function deepFreeze(obj) {
if (!obj) return
var cloneObj = {}
for (let key in obj) {
if (isObject(obj[key])) {
cloneObj[key] = Object.freeze(deepFreeze(obj[key]))
} else {
cloneObj[key] = obj[key]
}
}
return Object.freeze(cloneObj)
}
const clonePerson = deepFreeze(person)
//更新hobby的值没有被修改
clonePerson.others.hobby = '打球'
console.log(clonePerson)
类型判断:Object.prototype.toString.call(value)
因为像数组,boolean的toString方法被重写过,所以需要调用Object的toString方法来判断类型,call将this指向传入的对象
基础类型可以通过typeof来判断
cacheStringFunction 缓存
const cacheStringFunction = fn => {
const cache = Object.create(null)
return str => {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}
}
接收一个函数,将结果变为类似map结构的数据缓存函数
isOn
const onRE = /^on[^a-z]/
//on后首字母是不是大写
const isOn = key => onRE.test(key)
isModelListener
//判断是否是opUpdate开头的
const isModelListener = key => key.startsWith('onUpdate:')
extend
//继承、扩展
const extend = Object.assign
console.log(extend({name:"黄蓉",age:28},{other:{hobby:"游泳"}}))
console.log(extend({name:"黄蓉",age:28},{age:30}))
合并两个对象的属性,相同的属性以后面的为准
remove
//移除数组的某一项,耗费性能,可以将要移除项的结果转为false,不执行false的代码即可
const remove = (arr, el) => {
const i = arr.indexOf(el)
if (i > -1) {
arr.splice(i, 1)
}
}
hasOwnProperty
//是否有自身的属性
const hasOwnProperty = Object.prototype.hasOwnProperty
hasOwn
//判断Key是否是对象自身的属性
const hasOwn = (val, key) => hasOwnProperty.call(val, key)
isPromise
//判断对象类型
const isObject$1 = val => val !== null && typeof val === 'object'
//判断函数对象
const isFunction$1 = val => typeof val === 'function'
//判断promise类型
const isPromise = val => {
return isObject$1(val) && isFunction$1(val.then) && isFunction$1(val.catch)
}
toRawType 只返回类型,之前是返回[object xxx],只截取后面类型部分,返回Object
const toRawType = value => {
// extract "RawType" from strings like "[object RawType]"
return toTypeString$1(value).slice(8, -1)
}
//使用
console.log(toRawType('hp'))
//返回 String
isPlainObject 判断空对象
const isPlainObject$1 = val => toTypeString$1(val) === '[object Object]'
isIntegerKey 判断Key是否为数字
const isIntegerKey = key =>
isString$1(key) &&
key !== 'NaN' &&
key[0] !== '-' &&
'' + parseInt(key, 10) === key
isReservedProp 按照,分割,并将不存在的key保存在map中,检查保留属性
function makeMap(str, expectsLowerCase) {
const map = Object.create(null)
const list = str.split(',')
for (let i = 0; i < list.length; i++) {
map[list[i]] = true
}
return expectsLowerCase ? val => !!map[val.toLowerCase()] : val => !!map[val]
}
const isReservedProp = /*#__PURE__*/ makeMap(
// the leading comma is intentional so empty string "" is also included
',key,ref,' +
'onVnodeBeforeMount,onVnodeMounted,' +
'onVnodeBeforeUpdate,onVnodeUpdated,' +
'onVnodeBeforeUnmount,onVnodeUnmounted'
)
camelize 按照-将分割后的单词首字母大写
//\w查找数字、字母及下划线 -查找单个字符,除了换行和行结束符
const camelizeRE = /-(\w)/g
/**
* @private
*/
const camelize = cacheStringFunction$1(str => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
})
//使用
console.log( camelize('1-click'), camelize('on-click'),camelize('A-click'),camelize('_-click'))
//返回 1Click onClick AClick _Click
按照-分割将分割后的第一个单词的首字母大写
hyphenate 按照大写字母分割并以-连接
const hyphenate$1 = cacheStringFunction$1(str =>
str.replace(hyphenateRE$1, '-$1').toLowerCase()
)
//使用
console.log(hyphenate('CLICK'))
//返回c-l-i-c-k
capitalize 将单词首字母大写
const capitalize = cacheStringFunction$1(
str => str.charAt(0).toUpperCase() + str.slice(1)
)
//使用
console.log(`on${capitalize('click')}`)
//返回onClick
想在examples/index.html里面调用packages/shared/dist/shared.esm-bundler.js
里面得函数,这个文件里面用的es的模块化规范
,html
支持es模块化
得方式,在script
标签加上type="module"
属性,在使用的地方使用import
引入
<script
src="../packages/shared/dist/shared.esm-bundler.js"
type="module"
></script>
<script type="module">
import { capitalize } from '../packages/shared/dist/shared.esm-bundler.js'
console.log(`on${capitalize('click')}`)
</script>
toHandlerKey
const cacheStringFunction$1 = fn => {
const cache = Object.create(null)
return str => {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}
}
const toHandlerKey = cacheStringFunction$1(str =>
str ? `on${capitalize(str)}` : ``
)
传入update,会返回onUpdate
hasChanged 判断对象是否更改
//Object.is +0和-0 返回false NaN=NaN 返回true
const hasChanged = (value, oldValue) => !Object.is(value, oldValue)
invokeArrayFns 执行数组中的方法
const invokeArrayFns = (fns, arg) => {
for (let i = 0; i < fns.length; i++) {
fns[i](arg)
}
}
def 定义对象属性和值
const def = (obj, key, value) => {
Object.defineProperty(obj, key, {
configurable: true,
enumerable: false,
value
})
}
定义对象的属性,利用configurable
为true属性描述符可以被修改、enumerable
为true可以被枚举、writable
为true可以被修改
toNumber
const toNumber = val => {
const n = parseFloat(val)
return isNaN(n) ? val : n
}
console.log(NaN == NaN, NaN === NaN)
//false,false
isNaN
通过Number方法把值转换成数字类型,如果转换成功,则返回false,反之返回true,只能用来判断参数是否能转成数字,不能严格用来判断NaN
isNaN和Number.isNaN区别,Number.isNaN不会自动做类型提升,isNaN会转换后再判断
getGlobalThis
在 Web 中,可以通过 window
、self
或者 frames
取到全局对象,但是在 Web Workers 中,只有 self
可以。在 Node.js 中,它们都无法获取,必须使用 global
在松散模式下,可以在函数中返回 this
来获取全局对象,但是在严格模式和模块环境下,this
会返回 undefined
首次进入_globalThis
返回undefined
,后面如果是web worker
进程self
代表的是子进程,web worker
中没有window
,可以通过self
访问到worker
环境中的全局对象,参考阮一峰web-worker 、参考 MDN globalThis
window
浏览器环境
global node
运行环境
let _globalThis
const getGlobalThis = () => {
return (
_globalThis ||
(_globalThis =
typeof globalThis !== 'undefined'
? globalThis
: typeof self !== 'undefined'
? self
: typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: {})
)
}
收获
- 工具函数封装思路,工具函数不一定是实现某个特定功能的,可以是封装后操作方便简单的。例如,之前我们使用的时候会封装防抖、节流函数,我们也可以在原有api的基础上进行封装,使得使用简单。
- 结合深拷贝实现深度冻结对象
- html中使用模块引入的方法
- vue3源码学习思路,可以按照模块进行学习,各模块之间不冲突