從Vue源碼中學到的28個編程好習慣

長城內外
長城內外

筆者最近在讀Vue2.6.11的源碼,在閱讀過程當中,不只體會到Vue組件化及數據響應式的設計之美,也感嘆於尤大擼碼的規範、優雅。因此這裏一一將其總結羅列出來,保證新手看了,寫代碼更老練。老人看了,更進一步。html

友情提示:注意漢語註釋前端

  • 單行註釋
/* 聲明變量位於聲明以後 */ 
vm._vnode = null // the root of the child tree vm._staticTrees = null // v-once cached trees  /* 函數中使用註釋 注意縮進 */ export function initRender (vm){  // bind the createElement fn to this instance  // so that we get proper render context inside it.  // args order: tag, data, children, normalizationType, alwaysNormalize  // internal version is used by render functions compiled from templates  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) }  /* if...else... if 以前說明if做用,if裏面表示經過檢測 */ // if the returned array contains only a single node, allow it  if (Array.isArray(vnode) && vnode.length === 1) {  vnode = vnode[0] }  if (res.iterator2) {  // (if以後)  // (item, key, index) for object iteration  // is this even supported?  addRawAttr(el, 'index', res.iterator2) } else if (res.iterator1) {  addRawAttr(el, 'index', res.iterator1) }  /* 變量或者參數後註釋 說明其做用 */ isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function const _target = target // save current target element in closure 複製代碼
  • 塊註釋
/* 導出模塊以前 */ 
/**  * Map the following syntax to corresponding attrs:  *  * <recycle-list for="(item, i) in longList" switch="cellType">  * <cell-slot case="A"> ... </cell-slot>  * <cell-slot case="B"> ... </cell-slot>  * </recycle-list>  */ export function preTransformRecycleList (  el: ASTElement,  options: WeexCompilerOptions ) {  // 省略的代碼 }  /* 函數聲明以前 */ /**  * Convert an input value to a number for persistence.  * If the conversion fails, return original string.  */ function toNumber (val) {  // 省略的代碼 } 複製代碼
  • 注意換行
/* 每行最多在75個字節左右 */
 /* 單行註釋換行 */ // 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 vnode = render.call(vm._renderProxy, vm.$createElement)   /* 多行註釋換行 */ /**  * Collect dependencies on array elements when the array is touched, since  * we cannot intercept array element access like property getters.  */ function dependArray (value) {  // 省略的代碼 } 複製代碼
  • 字符串拼接換行,注意 + 位置
warn(
 `Property or method "${key}" is not defined on the instance but ` +  'referenced during render. Make sure that this property is reactive, ' +  'either in the data option, or for class-based components, by ' +  'initializing the property. ' +  'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',  target ) 複製代碼
  • 多個三目運算符使用
/* better */
function mergeHook (...) {  return childVal  ? parentVal  ? parentVal.concat(childVal)  : Array.isArray(childVal)  ? childVal  : [childVal]  : parentVal }  /* bad */ function mergeHook (...) {  return childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal } 複製代碼
  • || 、&& 代替 ...? ... : ...
function getComponentName (opts: ?VNodeComponentOptions): ?string {  return opts && (opts.Ctor.options.name || opts.tag) }  /* 至關於 */ function getComponentName (opts: ?VNodeComponentOptions): ?string {  return opts ? (opts.Ctor.options.name ? opts.Ctor.options.name : opts.tag) : opts } // 哪一個更簡潔 複製代碼
  • 使用 !! 將值轉爲 boolean
this.deep = !!options.deep
this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync 複製代碼
  • 文件命名
/* 命名時,文件內的代碼應與其功能或者業務模塊相關 */
 ├── scripts ------------------------------- 構建相關的文件 │ ├── git-hooks ------------------------- 存放git鉤子的目錄 │ ├── alias.js -------------------------- 別名配置 │ ├── config.js ------------------------- 生成rollup配置的文件 │ ├── build.js -------------------------- 對 config.js 中全部的rollup配置進行構建 │ ├── ci.sh ----------------------------- 持續集成運行的腳本 │ ├── release.sh ------------------------ 用於自動發佈新版本的腳本 ├── src ----------------------------------- 重要部分 │ ├── compiler -------------------------- 編譯模板 │ ├── core ------------------------------ 存放通用的,與平臺無關的代碼 │ │ ├── observer ---------------------- 觀測數據 │ │ ├── vdom -------------------------- 虛擬DOM │ │ ├── instance ---------------------- 構造函數 │ │ ├── global-api -------------------- Vue全局API │ │ ├── components -------------------- 通用組件 │ ├── server ---------------------------- 服務端渲染(server-side rendering) │ ├── platforms ------------------------- 包含平臺特有的相關代碼, │ │ ├── web --------------------------- web平臺 │ │ │ ├── entry-runtime.js ---------- 運行時構建的入口 │ │ │ ├── entry-runtime-with-compiler.js -- 獨立構建版本的入口,帶編譯 │ │ │ ├── entry-compiler.js --------- vue-template-compiler 包的入口文件 │ │ │ ├── entry-server-renderer.js -- vue-server-renderer 包的入口文件 │ │ │ ├── entry-server-basic-renderer.js -- 輸出 packages/vue-server-renderer/basic.js 文件 │ │ ├── weex -------------------------- 混合應用 │ ├── sfc ------------------------------- 包含單文件組件(.vue文件)的解析邏輯,用於vue-template-compiler包 │ ├── shared ---------------------------- 項目通用代碼 複製代碼
  • 合理使用 index文件,將當前業務文件夾中模塊導入導出
/* util/index.js */
export * from './attrs' export * from './class' export * from './element'  /* src/core/util/index.js */ export * from 'shared/util' export * from './lang' export * from './env' export * from './options' export * from './debug' export * from './props' export * from './error' export * from './next-tick' export { defineReactive } from '../observer/index' 複製代碼
  • 變量及函數命名
判斷詞前綴 含義 栗子
is 是否 isDefisTrueisFalse
has 有沒有 hasOwnPropertyhasProxy
static 是否靜態 staticRootstaticInForstaticProcessed
should 應不該該 shouldDecodeTagsshouldDecodeNewlinesshouldDecodeNewlinesForHref
dynamic 是否動態 dynamicAttrs
動詞前綴 含義 栗子
init 初始化 initMixin
merge 合併 mergeOptions
compile 編譯 compileToFunctions
validate 校驗 validateProp
handle 處理 handleError
update 更新 updateListeners
create 建立 createOnceHandler
  • 變量聲明
/* const 代替 let */
/* better */ const opts const parentVnode const vnodeComponentOptions const superOptions const cachedSuperOptions  /* bad*/ let opts let parentVnode let vnodeComponentOptions let superOptions let cachedSuperOptions 複製代碼
  • 合理使用變量前綴 _$
// Vue中通常以:
// _ 開頭表私有屬性或方法 // 以 $ 開頭表全局屬性或方法 declare interface Component {   // public properties (表全局屬性)  $el: any; // so that we can attach __vue__ to it  $data: Object;  $props: Object;  $options: ComponentOptions;  $parent: Component | void;  $root: Component;  // ...省略一部分   // public methods (表全局方法)  $mount: (el?: Element | string, hydrating?: boolean) => Component;  $forceUpdate: () => void;  $destroy: () => void;  $set: <T>(target: Object | Array<T>, key: string | number, val: T) => T;  $delete: <T>(target: Object | Array<T>, key: string | number) => void;  // ...省略一部分   // private properties (表私有屬性)  _uid: number | string;  _name: string; // this only exists in dev mode  _isVue: true;  _self: Component;  _renderProxy: Component;  // ...省略一部分 };  複製代碼
  • if...else...
/* 閉合{} */
if (!valid) {  warn(  getInvalidTypeMessage(name, value, expectedTypes),  vm  )  return } 複製代碼
  • for循環
/* 優先聲明key */
/* 緣由就一點:高效 */ var key; for (key in parent) {  // 省略 } for (key in child) {  // 省略 }   /* 同時聲明 i,length */ function processAttrs (el) {  var i, l,  for (i = 0, l = list.length; i < l; i++) {  // 省略  } } export function getAndRemoveAttr (): {  const list = el.attrsList  for (let i = 0, l = list.length; i < l; i++) {  // 省略  } } 複製代碼
  • for...in...循環中使用 hasOwnProperty
/* for...in...會遍歷對象原型鏈中的屬性或者方法 */
oldClassList.forEach(name => {  const style = stylesheet[name]  for (const key in style) {  if (!result.hasOwnProperty(key)) {  result[key] = ''  }  } }) 複製代碼
  • 檢測 undefined或者 null
export function isUndef (v: any): boolean %checks {
 return v === undefined || v === null } 複製代碼
  • 檢測原始類型
export function isPrimitive (value: any): boolean %checks {
 return (  typeof value === 'string' ||  typeof value === 'number' ||  // $flow-disable-line  typeof value === 'symbol' ||  typeof value === 'boolean'  ) } 複製代碼
  • 檢測對象
export function isObject (obj: mixed): boolean %checks {
 return obj !== null && typeof obj === 'object' } 複製代碼
  • 檢測純對象
export function isPlainObject (obj: any): boolean {
 return _toString.call(obj) === '[object Object]' } 複製代碼
  • ===代替 ==
/* 注意換行處邏輯運算符的位置 */
while (  (lastNode = el.children[el.children.length - 1]) &&  lastNode.type === 3 &&  lastNode.text === ' '  ) {  // 省略 }  function isPrimitive (value) {  return (  typeof value === 'string' ||  typeof value === 'number' ||  // $flow-disable-line  typeof value === 'symbol' ||  typeof value === 'boolean'  ) } 複製代碼
  • 公共常量進行抽離
/* shared/contants */
export const SSR_ATTR = 'data-server-rendered' export const ASSET_TYPES = [  'component',  'directive',  'filter' ] export const LIFECYCLE_HOOKS = [  'beforeCreate',  'created',  'beforeMount',  'mounted',  'beforeUpdate',  'updated',  'beforeDestroy',  'destroyed',  'activated',  'deactivated',  'errorCaptured',  'serverPrefetch' ]  複製代碼
  • 利用開關
/* 開關called保證代碼只會執行一次 */
export function once (fn: Function): Function {  let called = false  return function () {  if (!called) {  called = true  fn.apply(this, arguments)  }  } } 複製代碼
  • 檢測是否是對象的原有屬性
/**  * Check whether an object has the property.  */ const hasOwnProperty = Object.prototype.hasOwnProperty export function hasOwn (obj: Object | Array<*>, key: string): boolean {  return hasOwnProperty.call(obj, key) } 複製代碼
  • 使用 userAgent進行瀏覽器嗅探,能夠判斷當前設備環境,而進行合理的優化,兼容
/* src/core/util/env.js */
UA = inBrowser && window.navigator.userAgent.toLowerCase() 複製代碼
  • 保持 Javascript對象原有屬性、方法(不是你的對象不要動)
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)  const methodsToPatch = [  'push',  'pop',  'shift',  'unshift',  'splice',  'sort',  'reverse' ]  /**  * Intercept mutating methods and emit events  */ methodsToPatch.forEach(function (method) {  // cache original method  const original = arrayProto[method]  def(arrayMethods, method, function mutator (...args) {  // 注意這裏並無改變數組原有方法,只不過改變的是this指向  const result = original.apply(this, args)  const ob = this.__ob__  let inserted  switch (method) {  // ...省略的代碼  }  return result  }) }) 複製代碼
  • 利用類型轉換
/* 下面的filter會過濾掉轉爲false*/
export function pluckModuleFunction (  modules  key ){  return modules  ? modules.map(m => m[key]).filter(_ => _)  : [] } 複製代碼
  • 利用 Javascript閉包及柯里化
/** * Vue的compiler利用了函數柯里化及閉包的原理,實現將公共配置緩存, * 根據不一樣平臺須要而進行compiler的功能, * 感興趣的能夠閱讀源碼,體驗Vue設計之美 */  /* src/compile/index.js */ /* 源碼註釋說明能夠根據不一樣環境進行編譯 */ // `createCompilerCreator` allows creating compilers that use alternative // parser/optimizer/codegen, e.g the SSR optimizing compiler. // Here we just export a default compiler using the default parts. export const createCompiler = createCompilerCreator(function baseCompile (  template: string,  options: CompilerOptions ) {  // 省略的代碼 })  /* src/compile/create-compiler.js */  import { createCompileToFunctionFn } from './to-function' export function createCompilerCreator (baseCompile: Function): Function {  return function createCompiler (baseOptions: CompilerOptions) {  function compile (  template: string,  options?: CompilerOptions  ): CompiledResult {  //... 省略的代碼  const compiled = baseCompile(template.trim(), finalOptions)  return {  compile,  compileToFunctions: createCompileToFunctionFn(compile)  }  } }  /* src/compile/to-function.js */ export function createCompileToFunctionFn (compile: Function): Function {  const cache = Object.create(null)   return function compileToFunctions (  template: string,  options?: CompilerOptions,  vm?: Component  ): CompiledFunctionResult {  // ... 省略的代碼  } }  複製代碼
  • 函數保持單一職責,解耦

Javascript中函數爲一等公民,能夠說一箇中初級前端程序員在僅使用函數的狀況下就可完成平常開發任務。可見函數在Javascript中有多麼強大。可是函數使用也須要規範。在閱讀Vue源碼的過程當中,筆者就體會到尤大對函數解耦,單一職責,封裝使用的美妙之處。就拿Vue初始化舉例:vue

// 一目瞭然, 保證你本身都看的明白
/* src/core/instance/init.js */ 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') 複製代碼

PS: 源碼中的精髓之處不止筆者羅列的這些,筆者後期還會慢慢補充,羅列。node

人的一輩子中關鍵的就那麼幾步,特別是在年輕的時候。——路遙react

參考資料:git

  • vue2.6.11
  • 《編寫可維護的Javascript》
  • 《重構:改善既有代碼的設計》

本文使用 mdnice 排版程序員

相關文章
相關標籤/搜索