2023.1【若川视野 x 源码共读】第2期 | vue3 工具函数

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

大家好,我是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管理项目


1675166067940.png

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)
1675215780474.png

类型判断: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}))
1675216914513.png

合并两个对象的属性,相同的属性以后面的为准


1675216997164.png
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会转换后再判断

isNaN MDN

getGlobalThis

在 Web 中,可以通过 windowself 或者 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源码学习思路,可以按照模块进行学习,各模块之间不冲突
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容