久久精品中文字幕免费_91香蕉国产亚洲一区二区三区_国产精品巨作无遮拦_亚洲人成电影

    <center id="oy65s"><ol id="oy65s"></ol></center>

  • <menu id="oy65s"></menu>
    當(dāng)前位置:首頁(yè) > 足球資訊 > 正文內(nèi)容

    Vue(v2.6.11)萬(wàn)行源碼生啃,就硬剛

    杏彩體育2年前 (2023-01-28)足球資訊45

    前言

    源碼閱讀可能會(huì)遲到,但是一定不會(huì)缺席!

    眾所周知,以下代碼就是 vue 的一種直接上手方式。通過(guò) cdn 可以在線打開 vue.js。一個(gè)文件,一萬(wàn)行源碼,是萬(wàn)千開發(fā)者賴以生存的利器,它究竟做了什么?讓人品味。

    <html> <head></head> <body> <div id="app"> {{ message }} </div> </body> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> var app = new Vue({ el: #app, data: { message: See Vue again! }, }) </script> </html>

    源碼cdn地址:

    https://cdn.jsdelivr.net/npm/vue/dist/vue.js,當(dāng)下版本:v2.6.11。

    本瓜選擇生啃的原因是,可以更自主地選擇代碼段分輕重來(lái)閱讀,一方面測(cè)試自己的掌握程度,一方面追求更直觀的源碼閱讀。

    當(dāng)然你也可以選擇在

    https://github.com/vuejs/vue/tree/dev/src 分模塊的閱讀,也可以看各路大神的歸類整理。

    其實(shí)由于本次任務(wù)量并不算小,為了能堅(jiān)持下來(lái),本瓜將源碼盡量按 500 行作為一個(gè)模塊來(lái)形成一個(gè) md 文件記錄(分解版本共 24 篇感興趣可移步),結(jié)合注釋、自己的理解、以及附上對(duì)應(yīng)查詢鏈接來(lái)逐行細(xì)讀源碼,此篇為合并版本。

    目的:自我梳理,分享交流。

    最佳閱讀方式推薦:先點(diǎn)贊再閱讀,靴靴靴靴

    正文

    第 1 行至第 10 行

    // init

    ( function (global, factory) { typeof exports === object && typeof module !== undefined ? module.exports = factory() : typeof define === function && define.amd ? define(factory) : (global = global || self, global.Vue = factory()); }( this, function () { use strict; //...核心代碼... } ) ); // 變形 if (typeof exports === object && typeof module !== undefined) { // 檢查 CommonJS module.exports = factory() } else { if (typeof define === function && define.amd) { // AMD 異步模塊定義 檢查JavaScript依賴管理庫(kù) require.js 的存在 [link](https://stackoverflow.com/questions/30953589/what-is-typeof-define-function-defineamd-used-for) define(factory) } else { (global = global || self, global.Vue = factory()); } } // 等價(jià)于 window.Vue=factory() // factory 是個(gè)匿名函數(shù),該匿名函數(shù)并沒自執(zhí)行 設(shè)計(jì)參數(shù) window,并傳入window對(duì)象。不污染全局變量,也不會(huì)被別的代碼污染

    第 11 行至第 111 行

    // 工具代碼

    var emptyObject = Object.freeze({});// 凍結(jié)的對(duì)象無(wú)法再更改 [link](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)

    // 接下來(lái)是一些封裝用來(lái)判斷基本類型、引用類型、類型轉(zhuǎn)換的方法

    isUndef//判斷未定義isDef// 判斷已定義isTrue// 判斷為 trueisFalse// 判斷為 falseisPrimitive// 判斷為原始類型isObject// 判斷為 objtoRawType // 切割引用類型得到后面的基本類型,例如:[object RegExp] 得到的就是 RegExpisPlainObject// 判斷純粹的對(duì)象:"純粹的對(duì)象",就是通過(guò) { }、new Object()、Object.create(null) 創(chuàng)建的對(duì)象isRegExp// 判斷原生引用類型isValidArrayIndex// 檢查val是否是一個(gè)有效的數(shù)組索引,其實(shí)就是驗(yàn)證是否是一個(gè)非無(wú)窮大的正整數(shù)isPromise// 判斷是否是 PromisetoString// 類型轉(zhuǎn)成 StringtoNumber// 類型轉(zhuǎn)成 Number

    第 113 行至第 354 行

    makeMap// makeMap 方法將字符串切割,放到map中,用于校驗(yàn)其中的某個(gè)字符串是否存在(區(qū)分大小寫)于map中

    e.g.

    var isBuiltInTag = makeMap(slot,component, true);// 是否為內(nèi)置標(biāo)簽 isBuiltInTag(slot); //true isBuiltInTag(slot1); //undefined var isReservedAttribute = makeMap(key,ref,slot,slot-scope,is);// 是否為保留屬性 remove// 數(shù)組移除元素方法hasOwn// 判斷對(duì)象是否含有某個(gè)屬性cached// ※高級(jí)函數(shù) cached函數(shù),輸入?yún)?shù)為函數(shù),返回值為函數(shù)。同時(shí)使用了閉包,其會(huì)將該傳入的函數(shù)的運(yùn)行結(jié)果緩存,創(chuàng)建一個(gè)cache對(duì)象用于緩存運(yùn)行fn的運(yùn)行結(jié)果。function cached(fn) { var cache = Object.create(null);// 創(chuàng)建一個(gè)空對(duì)象 return (function cachedFn(str) {// 獲取緩存對(duì)象str屬性的值,如果該值存在,直接返回,不存在調(diào)用一次fn,然后將結(jié)果存放到緩存對(duì)象中 var hit = cache[str]; return hit || (cache[str] = fn(str)) }) } camelize// 駝峰化一個(gè)連字符連接的字符串capitalize// 對(duì)一個(gè)字符串首字母大寫hyphenateRE// 用字符號(hào)連接一個(gè)駝峰的字符串polyfillBind// ※高級(jí)函數(shù)Function.prototype.bind() toArray// 將像數(shù)組的轉(zhuǎn)為真數(shù)組extend// 將多個(gè)屬性插入目標(biāo)的對(duì)象toObject// 將對(duì)象數(shù)組合并為單個(gè)對(duì)象。

    e.g.

    console.log(toObject(["bilibli"])) //{0: "b", 1: "i", 2: "l", 3: "i", 4: "b", 5: "l", 6: "i", encodeHTML: ?} no// 任何情況都返回falseidentity // 返回自身genStaticKeys// 從編譯器模塊生成包含靜態(tài)鍵的字符串。TODO:demolooseEqual//※高級(jí)函數(shù) 對(duì)對(duì)象的淺相等進(jìn)行判斷

    //有贊、頭條面試題

    function looseEqual(a, b) { if (a === b) return true const isObjectA = isObject(a) const isObjectB = isObject(b) if(isObjectA && isObjectB) { try { const isArrayA = Array.isArray(a) const isArrayB = Array.isArray(b) if(isArrayA && isArrayB) { return a.length === b.length && a.every((e, i) => { return looseEqual(e, b[i]) }) }else if(!isArrayA && !isArrayB) { const keysA = Object.keys(a) const keysB = Object.keys(b) return keysA.length === keysB.length && keysA.every(function (key) { return looseEqual(a[key], b[key]) }) }else { return false } } catch(e) { return false } }else if(!isObjectA && !isObjectB) { return String(a) === String(b) }else { return false } } looseIndexOf// 返回索引,如果沒找到返回-1,否則執(zhí)行l(wèi)ooseEqual()once// 確保函數(shù)只被調(diào)用一次,用到閉包

    階段小結(jié)

    cachedpolyfillBindlooseEqual

    這三個(gè)函數(shù)要重點(diǎn)細(xì)品!主要的點(diǎn)是:閉包、類型判斷,函數(shù)之間的互相調(diào)用。也即是這部分工具函數(shù)的精華!

    第 356 行 至 第 612 行

    // 定義常量和配置

    SSR_ATTR// 服務(wù)端渲染ASSET_TYPES// 全局函數(shù) component、directive、filterLIFECYCLE_HOOKS// 生命周期,無(wú)需多言config // 全局配置 unicodeRegExp//用于解析html標(biāo)記、組件名稱和屬性pat的unicode字母isReserved// 檢查變量的開頭是 $ 或 _def// 在一個(gè)對(duì)象上定義一個(gè)屬性的構(gòu)造函數(shù),其中 !!enumerable 強(qiáng)制轉(zhuǎn)換 booleanparsePath// 解析一個(gè)簡(jiǎn)單路徑 TODO:userAgent// 瀏覽器識(shí)別inBrowser_isServer//檢測(cè) vue的服務(wù)器渲染是否存在, 而且避免webpack去填充processisNative //這里判斷 函數(shù)是否是系統(tǒng)函數(shù), 比如 Function Object ExpReg window document 等等, 這些函數(shù)應(yīng)該使用c/c++實(shí)現(xiàn)的。這樣可以區(qū)分 Symbol是系統(tǒng)函數(shù), 還是用戶自定義了一個(gè)SymbolhasSymbol//這里使用了ES6的Reflect方法, 使用這個(gè)對(duì)象的目的是, 為了保證訪問(wèn)的是系統(tǒng)的原型方法, ownKeys 保證key的輸出順序, 先數(shù)組 后字符串_Set// 設(shè)置一個(gè)Set

    第 616 行至第 706 行

    //設(shè)置warn,tip等全局變量 TODO:

    warntipgenerateComponentTrace// 生成組件跟蹤路徑(組件數(shù)規(guī)則)formatComponentName// 格式化組件名

    第 710 行至第 763 行

    Vue核心:數(shù)據(jù)監(jiān)聽最重要之一的 Dep

    // Dep是訂閱者Watcher對(duì)應(yīng)的數(shù)據(jù)依賴 var Dep = function Dep () { //每個(gè)Dep都有唯一的ID this.id = uid++; //subs用于存放依賴 this.subs = []; }; //向subs數(shù)組添加依賴 Dep.prototype.addSub = function addSub (sub) { this.subs.push(sub); }; //移除依賴 Dep.prototype.removeSub = function removeSub (sub) { remove(this.subs, sub); }; //設(shè)置某個(gè)Watcher的依賴 //這里添加了Dep.target是否存在的判斷,目的是判斷是不是Watcher的構(gòu)造函數(shù)調(diào)用 //也就是說(shuō)判斷他是Watcher的this.get調(diào)用的,而不是普通調(diào)用 Dep.prototype.depend = function depend () { if (Dep.target) { Dep.target.addDep(this); } }; Dep.prototype.notify = function notify () { var subs = this.subs.slice(); //通知所有綁定 Watcher。調(diào)用watcher的update() for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } };

    Dep 相當(dāng)于把 Observe 監(jiān)聽到的信號(hào)做一個(gè)收集(collect dependencies),然后通過(guò)dep.notify()再通知到對(duì)應(yīng) Watcher ,從而進(jìn)行視圖更新。

    第 767 行至第 900 行

    Vue核心:視圖更新最重要的 VNode( Virtual DOM)

    VNodecreateEmptyVNodecreateTextVNodecloneVNode

    把你的 template 模板 描述成 VNode,然后一系列操作之后通過(guò) VNode 形成真實(shí)DOM進(jìn)行掛載

    更新的時(shí)候?qū)Ρ扰f的VNode和新的VNode,只更新有變化的那一部分,提高視圖更新速度。

    e.g.

    <div class="parent" style="height:0" href="2222"> 111111 </div> //轉(zhuǎn)成Vnode { tag: div, data: { attrs:{href:"2222"} staticClass: "parent", staticStyle: { height: "0" } }, children: [{ tag: undefined, text: "111111" }] } methodsToPatch

    將數(shù)組的基本操作方法拓展,實(shí)現(xiàn)響應(yīng)式,視圖更新。

    因?yàn)椋簩?duì)于對(duì)象的修改是可以直接觸發(fā)響應(yīng)式的,但是對(duì)數(shù)組直接賦值,是無(wú)法觸發(fā)的,但是用到這里經(jīng)過(guò)改造的方法。我們可以明顯的看到 ob.dep.notify() 這一核心。

    階段小結(jié)

    這一 part 最重要的,毋庸置疑是:Dep 和 VNode,需重點(diǎn)突破?。?!

    第 904 行至第 1073 行

    Vue核心:數(shù)據(jù)監(jiān)聽最重要之一的 Observer

    核心的核心!Observer(發(fā)布者) => Dep(訂閱器) => Watcher(訂閱者)

    類比一個(gè)生活場(chǎng)景:報(bào)社將各種時(shí)下熱點(diǎn)的新聞收集,然后制成各類報(bào)刊,發(fā)送到每家門口的郵箱里,訂閱報(bào)刊人們看到了新聞,對(duì)新聞作出評(píng)論。

    在這個(gè)場(chǎng)景里,報(bào)社==發(fā)布者,新聞==數(shù)據(jù),郵箱==訂閱器,訂閱報(bào)刊的人==訂閱者,對(duì)新聞評(píng)論==視圖更新

    Observer//Observer的調(diào)用過(guò)程:initState()-->observe(data)-->new Observer()var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, __ob__, this); if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); } }; ※※ defineReactive 函數(shù),定義一個(gè)響應(yīng)式對(duì)象,給對(duì)象動(dòng)態(tài)添加 getter 和 setter ,用于依賴收集和派發(fā)更新。function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep()// 1. 為屬性創(chuàng)建一個(gè)發(fā)布者 const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters const getter = property && property.get // 依賴收集 const setter = property && property.set // 派發(fā)更新 if ((!getter || setter) && arguments.length === 2) { val = obj[key] } let childOb = !shallow && observe(val)// 2. 獲取屬性值的__ob__屬性 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend()// 3. 添加 Dep if (childOb) { childOb.dep.depend()//4. 也為屬性值添加同樣的 Dep if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== production && customSetter) { customSetter() } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } }) }

    第 4 步非常重要。為對(duì)象的屬性添加 dep.depend(),達(dá)到監(jiān)聽對(duì)象(引用的值)屬性的目的

    重點(diǎn)備注

    Vue對(duì)數(shù)組的處理跟對(duì)象還是有挺大的不同,length是數(shù)組的一個(gè)很重要的屬性,無(wú)論數(shù)組增加元素或者刪除元素(通過(guò)splice,push等方法操作)length的值必定會(huì)更新,為什么不直接操作監(jiān)聽length呢?而需要攔截splice,push等方法進(jìn)行數(shù)組的狀態(tài)更新?

    原因是:在數(shù)組length屬性上用defineProperty攔截的時(shí)候,會(huì)報(bào)錯(cuò)。

    Uncaught TypeError: Cannot redefine property: length

    再用

    Object.getOwnPropertyDescriptor(arr, length)查看一下://(

    Object.getOwnPropertyDescriptor用于返回defineProperty.descriptor)

    { configurable: false enumerable: false value: 0 writable: true } configurable為false,且MDN上也說(shuō)重定義數(shù)組的length屬性在不同瀏覽器上表現(xiàn)也是不一致的,所以還是老老實(shí)實(shí)攔截splice,push等方法,或者使用ES6的Proxy。

    第 1075 行至第 1153 行

    set //在對(duì)象上設(shè)置一個(gè)屬性。如果是新的屬性就會(huì)觸發(fā)更改通知(舊屬性也會(huì)觸發(fā)更新通知,因?yàn)榈谝粋€(gè)添加的時(shí)候已經(jīng)監(jiān)聽了,之后自動(dòng)觸發(fā),不再手動(dòng)觸發(fā))del //刪除一個(gè)屬性,如果必要觸發(fā)通知dependArray // 收集數(shù)組的依賴

    第 1157 行至第 1568 行

    // 配置選項(xiàng)合并策略

    ar strats = config.optionMergeStrategies; mergeDatastrats.datamergeDataOrFnmergeHookmergeAssetsstrats.watchstrats.computeddefaultStratcheckComponentsvalidateComponentNamenormalizePropsnormalizeInjectnormalizeDirectivesassertObjectTypemergeOptions

    這一部分代碼寫的就是父子組件配置項(xiàng)的合并策略,包括:默認(rèn)的合并策略、鉤子函數(shù)的合并策略、filters/props、data合并策略,且包括標(biāo)準(zhǔn)的組件名、props寫法有一個(gè)統(tǒng)一化規(guī)范要求。

    一圖以蔽之

    階段小結(jié)

    這一部分最重要的就是 Observer(觀察者) ,這也是 Vue 核心中的核心!其次是 mergeOptions(組件配置項(xiàng)的合并策略),但是通常在用的過(guò)程中,就已經(jīng)了解到了大部分的策略規(guī)則。

    第 1570 行至第 1754 行

    resolveAsset// resolveAsset 全局注冊(cè)組件用到

    e.g.

    我們的調(diào)用 resolveAsset(context.options,′components′,tag),即拿vm.options.components[tag],這樣我們就可以在 resolveAsset 的時(shí)候拿到這個(gè)組件的構(gòu)造函數(shù),并作為 createComponent 的鉤子的參數(shù)。

    validateProp// prop的格式校驗(yàn)

    校驗(yàn)prop:

    prop為Boolean類型時(shí)做特殊處理prop的值為空時(shí),獲取默認(rèn)值,并創(chuàng)建觀察者對(duì)象prop驗(yàn)證getPropDefaultValue// 獲取默認(rèn) prop 值

    獲取 prop 的默認(rèn)值 && 創(chuàng)建觀察者對(duì)象

    @param {*} vm vm 實(shí)例@param {*} prop 定義選項(xiàng)@param {*} vmkey prop 的 key

    // 在非生產(chǎn)環(huán)境下(除去 Weex 的某種情況),將對(duì)prop進(jìn)行驗(yàn)證,包括驗(yàn)證required、type和自定義驗(yàn)證函數(shù)。

    assertProp //驗(yàn)證 prop

    Assert whether a prop is valid.

    case 1: 驗(yàn)證 required 屬性 case 1.1: prop 定義時(shí)是 required,但是調(diào)用組件時(shí)沒有傳遞該值(警告) case 1.2: prop 定義時(shí)是非 required 的,且 value === null || value === undefined(符合要求,返回) case 2: 驗(yàn)證 type 屬性-- value 的類型必須是 type 數(shù)組里的其中之一 case 3: 驗(yàn)證自定義驗(yàn)證函數(shù) assertType`assertType`函數(shù),驗(yàn)證`prop`的值符合指定的`type`類型,分為三類: - 第一類:通過(guò)`typeof`判斷的類型,如`String`、`Number``Boolean`、`Function`、`Symbol` - 第二類:通過(guò)`Object.prototype.toString`判斷`Object`/`Array` - 第三類:通過(guò)`instanceof`判斷自定義的引用類型

    第 1756 行至第 1823 行

    // 輔助函數(shù):檢測(cè)內(nèi)置類型

    getTypeisSameTypegetTypeIndexgetInvalidTypeMessagestyleValueisExplicableisBoolean

    第 1827 行至第 1901 行

    // 輔助函數(shù):處理錯(cuò)誤、錯(cuò)誤打印

    handleErrorinvokeWithErrorHandlingglobalHandleErrorlogError

    第 1905 行至第 2007 行

    flushCallbacks// flushCallbacks 挨個(gè)同步執(zhí)行callbacks中回調(diào)MutationObservernextTick// 把傳入的 cb 回調(diào)函數(shù)用 try-catch 包裹后放在一個(gè)匿名函數(shù)中推入callbacks數(shù)組中,這么做是因?yàn)榉乐箚蝹€(gè) cb 如果執(zhí)行錯(cuò)誤不至于讓整個(gè)JS線程掛掉,每個(gè) cb 都包裹是防止這些回調(diào)函數(shù)如果執(zhí)行錯(cuò)誤不會(huì)相互影響,比如前一個(gè)拋錯(cuò)了后一個(gè)仍然可以執(zhí)行。

    精髓中的精髓 —— nextTick

    這里有一段很重要的注釋

    // Here we have async deferring wrappers using microtasks. // In 2.5 we used (macro) tasks (in combination with microtasks). // However, it has subtle problems when state is changed right before repaint // (e.g. #6813, out-in transitions). // Also, using (macro) tasks in event handler would cause some weird behaviors // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109). // So we now use microtasks everywhere, again. // A major drawback of this tradeoff is that there are some scenarios // where microtasks have too high a priority and fire in between supposedly // sequential events (e.g. #4521, #6690, which have workarounds) // or even between bubbling of the same event (#6566). 在vue2.5之前的版本中,nextTick基本上基于 micro task 來(lái)實(shí)現(xiàn)的,但是在某些情況下 micro task 具有太高的優(yōu)先級(jí),并且可能在連續(xù)順序事件之間(例如#4521,#6690)或者甚至在同一事件的事件冒泡過(guò)程中之間觸發(fā)(#6566)。但是如果全部都改成 macro task,對(duì)一些有重繪和動(dòng)畫的場(chǎng)景也會(huì)有性能影響,如 issue #6813。vue2.5之后版本提供的解決辦法是默認(rèn)使用 micro task,但在需要時(shí)(例如在v-on附加的事件處理程序中)強(qiáng)制使用 macro task。

    什么意思呢?分析下面這段代碼。

    <span id=name ref=name>{{ name }}</span> <button @click=change>change name</button> methods: { change() { this.$nextTick(() => console.log(setter前: + this.$refs.name.innerHTML)) this.name = vue3 console.log(同步方式: + this.$refs.name.innerHTML) setTimeout(() => this.console("setTimeout方式:" + this.$refs.name.innerHTML)) this.$nextTick(() => console.log(setter后: + this.$refs.name.innerHTML)) this.$nextTick().then(() => console.log(Promise方式: + this.$refs.name.innerHTML)) } } //同步方式:vue2 //setter前:vue2 //setter后: vue3 //Promise方式: vue3 //setTimeout方式: vue3 同步方式: 當(dāng)把data中的name修改之后,此時(shí)會(huì)觸發(fā)name的 setter 中的 dep.notify 通知依賴本data的render watcher去 update,update 會(huì)把 flushSchedulerQueue 函數(shù)傳遞給 nextTick,render watcher在 flushSchedulerQueue 函數(shù)運(yùn)行時(shí) watcher.run 再走 diff -> patch 那一套重渲染 re-render 視圖,這個(gè)過(guò)程中會(huì)重新依賴收集,這個(gè)過(guò)程是異步的;所以當(dāng)我們直接修改了name之后打印,這時(shí)異步的改動(dòng)還沒有被 patch 到視圖上,所以獲取視圖上的DOM元素還是原來(lái)的內(nèi)容。setter前: setter前為什么還打印原來(lái)的是原來(lái)內(nèi)容呢,是因?yàn)?nextTick 在被調(diào)用的時(shí)候把回調(diào)挨個(gè)push進(jìn)callbacks數(shù)組,之后執(zhí)行的時(shí)候也是 for 循環(huán)出來(lái)挨個(gè)執(zhí)行,所以是類似于隊(duì)列這樣一個(gè)概念,先入先出;在修改name之后,觸發(fā)把render watcher填入 schedulerQueue 隊(duì)列并把他的執(zhí)行函數(shù) flushSchedulerQueue 傳遞給 nextTick ,此時(shí)callbacks隊(duì)列中已經(jīng)有了 setter前函數(shù) 了,因?yàn)檫@個(gè) cb 是在 setter前函數(shù) 之后被push進(jìn)callbacks隊(duì)列的,那么先入先出的執(zhí)行callbacks中回調(diào)的時(shí)候先執(zhí)行 setter前函數(shù),這時(shí)并未執(zhí)行render watcher的 watcher.run,所以打印DOM元素仍然是原來(lái)的內(nèi)容。setter后: setter后這時(shí)已經(jīng)執(zhí)行完 flushSchedulerQueue,這時(shí)render watcher已經(jīng)把改動(dòng) patch 到視圖上,所以此時(shí)獲取DOM是改過(guò)之后的內(nèi)容。Promise方式: 相當(dāng)于 Promise.then 的方式執(zhí)行這個(gè)函數(shù),此時(shí)DOM已經(jīng)更改。setTimeout方式: 最后執(zhí)行macro task的任務(wù),此時(shí)DOM已經(jīng)更改。

    備注:前文提過(guò),在依賴收集原理的響應(yīng)式化方法 defineReactive 中的 setter 訪問(wèn)器中有派發(fā)更新 dep.notify() 方法,這個(gè)方法會(huì)挨個(gè)通知在 dep 的 subs 中收集的訂閱自己變動(dòng)的 watchers 執(zhí)行 update。

    0 行 至 2000 行小結(jié)

    0 至 2000 行主要的內(nèi)容是:

    工具代碼數(shù)據(jù)監(jiān)聽:Obeserve,DepVnodenextTick

    第 2011 行至第 2232 行

    perf// performanceinitProxy// 代理對(duì)象是es6的新特性,它主要用來(lái)自定義對(duì)象一些基本操作(如查找,賦值,枚舉等)。

    //proxy是一個(gè)強(qiáng)大的特性,為我們提供了很多"元編程"能力。

    const handler = { get: function(obj, prop) { return prop in obj ? obj[prop] : 37; } }; const p = new Proxy({}, handler); p.a = 1; p.b = undefined; console.log(p.a, p.b); // 1, undefined console.log(c in p, p.c); // false, 37 traverse// 遍歷:_traverse 深度遍歷,用于

    traverse 對(duì)一個(gè)對(duì)象做深層遞歸遍歷,因?yàn)楸闅v過(guò)程中就是對(duì)一個(gè)子對(duì)象的訪問(wèn),會(huì)觸發(fā)它們的 getter 過(guò)程,這樣就可以收集到依賴,也就是訂閱它們變化的 watcher,且遍歷過(guò)程中會(huì)把子響應(yīng)式對(duì)象通過(guò)它們的 dep id 記錄到 seenObjects,避免以后重復(fù)訪問(wèn)。

    normalizeEvent// normalizeEvents是針對(duì)v-model的處理,例如在IE下不支持change事件,只能用input事件代替。createFnInvoker// 在初始構(gòu)建實(shí)例時(shí),舊節(jié)點(diǎn)是不存在的,此時(shí)會(huì)調(diào)用createFnInvoker函數(shù)對(duì)事件回調(diào)函數(shù)做一層封裝,由于單個(gè)事件的回調(diào)可以有多個(gè),因此createFnInvoker的作用是對(duì)單個(gè),多個(gè)回調(diào)事件統(tǒng)一封裝處理,返回一個(gè)當(dāng)事件觸發(fā)時(shí)真正執(zhí)行的匿名函數(shù)。updateListeners// updateListeners的邏輯也很簡(jiǎn)單,它會(huì)遍歷on事件對(duì)新節(jié)點(diǎn)事件綁定注冊(cè)事件,對(duì)舊節(jié)點(diǎn)移除事件監(jiān)聽,它即要處理原生DOM事件的添加和移除,也要處理自定義事件的添加和移除,

    階段小結(jié)

    Vue 的事件機(jī)制

    第 2236 行至第 2422 行

    mergeVNodeHook// 重點(diǎn) 合并 VNode

    // 把 hook 函數(shù)合并到 def.data.hook[hookey] 中,生成新的 invoker,createFnInvoker 方法

    // vnode 原本定義了 init、prepatch、insert、destroy 四個(gè)鉤子函數(shù),而 mergeVNodeHook 函數(shù)就是把一些新的鉤子函數(shù)合并進(jìn)來(lái),例如在 transition 過(guò)程中合并的 insert 鉤子函數(shù),就會(huì)合并到組件 vnode 的 insert 鉤子函數(shù)中,這樣當(dāng)組件插入后,就會(huì)執(zhí)行我們定義的 enterHook 了。

    extractPropsFromVNodeData// 抽取相應(yīng)的從父組件上的propcheckProp// 校驗(yàn) Prop // The template compiler attempts to minimize the need for normalization by // statically analyzing the template at compile time. // 模板編譯器嘗試用最小的需求去規(guī)范:在編譯時(shí),靜態(tài)分析模板 // For plain HTML markup, normalization can be completely skipped because the // generated render function is guaranteed to return Array<VNode>. There are // two cases where extra normalization is needed: // 對(duì)于純 HTML 標(biāo)簽,可跳過(guò)標(biāo)準(zhǔn)化,因?yàn)樯射秩竞瘮?shù)一定會(huì)會(huì)返回 Vnode Array.有兩種情況,需要額外去規(guī)范 // 1. When the children contains components - because a functional component // may return an Array instead of a single root. In this case, just a simple // normalization is needed - if any child is an Array, we flatten the whole // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep // because functional components already normalize their own children. // 當(dāng)子級(jí)包含組件時(shí)-因?yàn)楣δ芙M件可能會(huì)返回Array而不是單個(gè)根。在這種情況下,需要規(guī)范化-如果任何子級(jí)是Array,我們將整個(gè)具有Array.prototype.concat的東西。保證只有1級(jí)深度,因?yàn)楣δ芙M件已經(jīng)規(guī)范了自己的子代。 // 2. When the children contains constructs that always generated nested Arrays, // e.g. <template>, <slot>, v-for, or when the children is provided by user // with hand-written render functions / JSX. In such cases a full normalization // is needed to cater to all possible types of children values. // 當(dāng)子級(jí)包含始終生成嵌套數(shù)組的構(gòu)造時(shí),例如<template>,<slot>,v-for或用戶提供子代時(shí),具有手寫的渲染功能/ JSX。在這種情況下,完全歸一化,才能滿足所有可能類型的子代值。

    Q:這一段話說(shuō)的是什么意思呢?

    A:歸一化操作其實(shí)就是將多維的數(shù)組,合并轉(zhuǎn)換成一個(gè)一維的數(shù)組。在 Vue 中歸一化分為三個(gè)級(jí)別,

    不需要進(jìn)行歸一化只需要簡(jiǎn)單的歸一化處理,將數(shù)組打平一層完全歸一化,將一個(gè) N 層的 children 完全打平為一維數(shù)組

    利用遞歸來(lái)處理的,同時(shí)處理了一些邊界情況。

    第 2426 行至第 2490 行

    initProvideinitInjectionsresolveInject

    第 2497 行至第 2958 行

    resolveSlots// Runtime helper for resolving raw children VNodes into a slot object.isWhitespacenormalizeScopedSlotsnormalizeScopedSlotproxyNormalSlotrenderList// Runtime helper for rendering v-for lists.renderSlot// Runtime helper for rendering <slot>resolveFilter// Runtime helper for resolving filterscheckKeyCodes// Runtime helper for checking keyCodes from config.bindObjectProps// Runtime helper for merging v-bind="object" into a VNodes data.renderStatic// Runtime helper for rendering static trees.markOnce// Runtime helper for v-once.

    這一部分講的是輔助程序 —— Vue 的各類渲染方法,從字面意思中可以知道一些方法的用途,這些方法用在Vue生成的渲染函數(shù)中。

    installRenderHelpers// installRenderHelpers 用于執(zhí)行以上。

    第 2962 行至第 3515 行

    FunctionalRenderContext// 創(chuàng)建一個(gè)包含渲染要素的函數(shù)createFunctionalComponent

    函數(shù)式組件的實(shí)現(xiàn)

    Ctor, //Ctro:組件的構(gòu)造對(duì)象(Vue.extend()里的那個(gè)Sub函數(shù)) propsData, //propsData:父組件傳遞過(guò)來(lái)的數(shù)據(jù)(還未驗(yàn)證) data, //data:組件的數(shù)據(jù) contextVm, //contextVm:Vue實(shí)例 children //children:引用該組件時(shí)定義的子節(jié)點(diǎn)

    // createFunctionalComponent 最后會(huì)執(zhí)行我們的 render 函數(shù)

    特注:Vue 組件是 Vue 的核心之一

    組件分為:異步組件和函數(shù)式組件

    這里就是函數(shù)式組件相關(guān)

    Vue提供了一種可以讓組件變?yōu)闊o(wú)狀態(tài)、無(wú)實(shí)例的函數(shù)化組件。從原理上說(shuō),一般子組件都會(huì)經(jīng)過(guò)實(shí)例化的過(guò)程,而單純的函數(shù)組件并沒有這個(gè)過(guò)程,它可以簡(jiǎn)單理解為一個(gè)中間層,只處理數(shù)據(jù),不創(chuàng)建實(shí)例,也是由于這個(gè)行為,它的渲染開銷會(huì)低很多。實(shí)際的應(yīng)用場(chǎng)景是,當(dāng)我們需要在多個(gè)組件中選擇一個(gè)來(lái)代為渲染,或者在將children,props,data等數(shù)據(jù)傳遞給子組件前進(jìn)行數(shù)據(jù)處理時(shí),我們都可以用函數(shù)式組件來(lái)完成,它本質(zhì)上也是對(duì)組件的一個(gè)外部包裝。

    函數(shù)式組件會(huì)在組件的對(duì)象定義中,將functional屬性設(shè)置為true,這個(gè)屬性是區(qū)別普通組件和函數(shù)式組件的關(guān)鍵。同樣的在遇到子組件占位符時(shí),會(huì)進(jìn)入createComponent進(jìn)行子組件Vnode的創(chuàng)建。**由于functional屬性的存在,代碼會(huì)進(jìn)入函數(shù)式組件的分支中,并返回createFunctionalComponent調(diào)用的結(jié)果。**注意,執(zhí)行完createFunctionalComponent后,后續(xù)創(chuàng)建子Vnode的邏輯不會(huì)執(zhí)行,這也是之后在創(chuàng)建真實(shí)節(jié)點(diǎn)過(guò)程中不會(huì)有子Vnode去實(shí)例化子組件的原因。(無(wú)實(shí)例)

    官方說(shuō)明

    cloneAndMarkFunctionalResultmergePropscomponentVNodeHookscreateComponent

    // createComponent 方法創(chuàng)建一個(gè)組件的 VNode。這 createComponent 是創(chuàng)建子組件的關(guān)鍵

    // 創(chuàng)建組件的 VNode 時(shí),若組件是函數(shù)式組件,則其 VNode 的創(chuàng)建過(guò)程將與普通組件有所區(qū)別。

    createComponentInstanceForVnode // linkinstallComponentHooks // installComponentHooks就是把 componentVNodeHooks的鉤子函數(shù)合并到data.hook中,,在合并過(guò)程中,如果某個(gè)時(shí)機(jī)的鉤子已經(jīng)存在data.hook中,那么通過(guò)執(zhí)行mergeHook函數(shù)做合并勾子。mergeHook$1transformModelcreateElement// 創(chuàng)建元素_createElementapplyNSregisterDeepBindingsinitRender // 初識(shí)渲染

    階段小結(jié)

    這一部分主要是圍繞 Vue 的組件的創(chuàng)建。Vue 將頁(yè)面劃分成各類的組件,組件思想是 Vue 的精髓之一。

    第 3517 行至第 3894 行

    renderMixin // 引入視圖渲染混合函數(shù)ensureCtorcreateAsyncPlaceholderresolveAsyncComponentisAsyncPlaceholdergetFirstComponentChildinitEvents// 初始化事件addremove$1createOnceHandlerupdateComponentListenerseventsMixin // 掛載事件響應(yīng)相關(guān)方法

    第 3898 行至第 4227 行

    setActiveInstanceinitLifecyclelifecycleMixin// 掛載生命周期相關(guān)方法mountComponentupdateChildComponentisInInactiveTreeactivateChildComponentdeactivateChildComponentcallHook

    幾乎所有JS框架或插件的編寫都有一個(gè)類似的模式,即向全局輸出一個(gè)類或者說(shuō)構(gòu)造函數(shù),通過(guò)創(chuàng)建實(shí)例來(lái)使用這個(gè)類的公開方法,或者使用類的靜態(tài)全局方法輔助實(shí)現(xiàn)功能。相信精通Jquery或編寫過(guò)Jquery插件的開發(fā)者會(huì)對(duì)這個(gè)模式非常熟悉。Vue.js也如出一轍,只是一開始接觸這個(gè)框架的時(shí)候?qū)λ軐?shí)現(xiàn)的功能的感嘆蓋過(guò)了它也不過(guò)是一個(gè)內(nèi)容較為豐富和精致的大型類的本質(zhì)。

    階段小結(jié)

    這里要對(duì) js 的繼承有一個(gè)深刻的理解。

    類繼承function Animal(){ this.live=true; } function Dog(name){ this.name=name } Dog.prototype=new Animal() var dog1=new Dog("wangcai") console.log(dog1)// Dog {name: "wangcai"} console.log(dog1.live)// true 構(gòu)造繼承function Animal(name,color){ this.name=name; this.color=color;} function Dog(){ Animal.apply(this,arguments) } var dog1=new Dog("wangcai","balck") console.log(dog1)// Dog {name: "wangcai", color: "balck"} 組合繼承(類繼承 + 構(gòu)造繼承)function Animal(name,color){ this.name=name; this.color=color; this.live=true; } function Dog(){ Animal.apply(this, arguments); } Dog.prototype=new Animal() var dog1=new Dog("wangcai","black") console.log(dog1)// Dog {name: "wangcai", color: "black", live: true} 寄生組合式繼承extend繼承

    Vue 同 Jquery 一樣,本質(zhì)也是一個(gè)大型的類庫(kù)。

    // 定義Vue構(gòu)造函數(shù),形參options

    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) }

    // 功能函數(shù)

    // 引入初始化混合函數(shù) import { initMixin } from ./init // 引入狀態(tài)混合函數(shù) import { stateMixin } from ./state // 引入視圖渲染混合函數(shù) import { renderMixin } from ./render // 引入事件混合函數(shù) import { eventsMixin } from ./events // 引入生命周期混合函數(shù) import { lifecycleMixin } from ./lifecycle // 引入warn控制臺(tái)錯(cuò)誤提示函數(shù) import { warn } from ../util/index ... // 掛載初始化方法 initMixin(Vue) // 掛載狀態(tài)處理相關(guān)方法 stateMixin(Vue) // 掛載事件響應(yīng)相關(guān)方法 eventsMixin(Vue) // 掛載生命周期相關(guān)方法 lifecycleMixin(Vue) // 掛載視圖渲染方法 renderMixin(Vue)

    第 4231 行至第 4406 行

    resetSchedulerState // 重置狀態(tài)flushSchedulerQueue// 據(jù)變化最終會(huì)把flushSchedulerQueue傳入到nextTick中執(zhí)行flushSchedulerQueue函數(shù)會(huì)遍歷執(zhí)行watcher.run()方法,watcher.run()方法最終會(huì)完成視圖更新

    vue中dom的更像并不是實(shí)時(shí)的,當(dāng)數(shù)據(jù)改變后,vue會(huì)把渲染watcher添加到異步隊(duì)列,異步執(zhí)行,同步代碼執(zhí)行完成后再統(tǒng)一修改dom。

    callUpdatedHooksqueueActivatedComponentcallActivatedHooksqueueWatcher

    第 4412 行至第 4614 行

    Watcher// !important 重中之重的重點(diǎn)

    這一 part 在 Watcher 的原型鏈上定義了get、addDep、cleanupDeps、update、run、evaluate、depend、teardown 方法,即 Watcher 的具體實(shí)現(xiàn)的一些方法,比如新增依賴、清除、更新試圖等。

    每個(gè)Vue組件都有一個(gè)對(duì)應(yīng)的watcher,這個(gè)watcher將會(huì)在組件render的時(shí)候收集組件所依賴的數(shù)據(jù),并在依賴有更新的時(shí)候,觸發(fā)組件重新渲染。

    第 4618 行至第 5071 行

    export function initMixin (Vue: Class<Component>) { 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) } // 如果是Vue的實(shí)例,則不需要被observe // a flag to avoid this being observed vm._isVue = true // merge options // 第一步: options參數(shù)的處理 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 { // mergeOptions接下來(lái)我們會(huì)詳細(xì)講哦~ vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } // 第二步: renderProxy /* istanbul ignore else */ if (process.env.NODE_ENV !== production) { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm // 第三步: vm的生命周期相關(guān)變量初始化 initLifecycle(vm) // 第四步: vm的事件監(jiān)聽初始化 initEvents(vm) // 第五步: vm的編譯render初始化 initRender(vm) // 第六步: vm的beforeCreate生命鉤子的回調(diào) callHook(vm, beforeCreate) // 第七步: vm在data/props初始化之前要進(jìn)行綁定 initInjections(vm) // resolve injections before data/props // 第八步: vm的sate狀態(tài)初始化 initState(vm) // 第九步: vm在data/props之后要進(jìn)行提供 initProvide(vm) // resolve provide after data/props // 第十步: vm的created生命鉤子的回調(diào) 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) } // 第十一步:render & mount if (vm.$options.el) { vm.$mount(vm.$options.el) } } }

    主要是為我們的Vue原型上定義一個(gè)方法_init。然后當(dāng)我們執(zhí)行new Vue(options) 的時(shí)候,會(huì)調(diào)用這個(gè)方法。而這個(gè)_init方法的實(shí)現(xiàn),便是我們需要關(guān)注的地方。 前面定義vm實(shí)例都挺好理解的,主要我們來(lái)看一下mergeOptions這個(gè)方法,其實(shí)Vue在實(shí)例化的過(guò)程中,會(huì)在代碼運(yùn)行后增加很多新的東西進(jìn)去。我們把我們傳入的這個(gè)對(duì)象叫options,實(shí)例中我們可以通過(guò)vm.$options訪問(wèn)到。

    0 至 5000 行 總結(jié)

    從 0 至 5000 行我們可以清晰看到 Vue 模板編譯的輪廓了。

    筆者將這一部分出現(xiàn)的關(guān)鍵詞進(jìn)行按順序羅列:function (global, factory)工具函數(shù)DepObserveVNodenextTick事件機(jī)制RendercomponentsWatcher

    我們可以總結(jié):Vue 的核心就是 VDOM !對(duì) DOM 對(duì)象的操作調(diào)整為操作 VNode 對(duì)象,采用 diff 算法比較差異,一次 patch。

    render 的流程是:

    Vue使用HTML的Parser將HTML模板解析為ASTfunction render(){}Virtual DOMwatcher將會(huì)在組件render的時(shí)候收集組件所依賴的數(shù)據(jù),并在依賴有更新的時(shí)候,觸發(fā)組件重新渲染

    第 5073 行至第 5446 行

    // 定義 Vue 構(gòu)造函數(shù) function Vue (options) { if (!(this instanceof Vue) ) { warn(Vue is a constructor and should be called with the `new` keyword); } this._init(options); } // 將 Vue 作為參數(shù)傳遞給導(dǎo)入的五個(gè)方法 initMixin(Vue);// 初始化 Mixin stateMixin(Vue);// 狀態(tài) Mixin eventsMixin(Vue);// 事件 Mixin lifecycleMixin(Vue);// 生命周期 Mixin renderMixin(Vue);// 渲染 Mixin

    這一部分就是初始化函數(shù)的調(diào)用。

    // Object.defineProperty(Vue.prototype, $isServer, { get: isServerRendering });

    為什么這么寫?

    Object.defineProperty能保護(hù)引入的庫(kù)不被重新賦值,如果你嘗試重寫,程序會(huì)拋出“TypeError: Cannot assign to read only property”的錯(cuò)誤。

    【譯】Vue框架引入JS庫(kù)的正確姿勢(shì)

    // 版本 Vue.version = 2.6.11;

    階段小結(jié)

    這一部分是 Vue index.js 的內(nèi)容,包括 Vue 的整個(gè)掛在過(guò)程

    先進(jìn)入 initMixin(Vue),在prototype上掛載Vue.prototype._init = function (options) {} 進(jìn)入 stateMixin(Vue),在prototype上掛載 Vue.prototype.$dataVue.prototype.$props Vue.prototype.$set = set Vue.prototype.$delete = del Vue.prototype.$watch = function(){} 進(jìn)入eventsMixin(Vue),在prototype上掛載Vue.prototype.$on Vue.prototype.$once Vue.prototype.$off Vue.prototype.$emit 進(jìn)入lifecycleMixin(Vue),在prototype上掛載Vue.prototype._update Vue.prototype.$forceUpdate Vue.prototype.$destroy 最后進(jìn)入renderMixin(Vue),在prototype上掛載 Vue.prototype.$nextTickVue.prototype._render Vue.prototype._o = markOnce Vue.prototype._n = toNumber Vue.prototype._s = toString Vue.prototype._l = renderList Vue.prototype._t = renderSlot Vue.prototype._q = looseEqual Vue.prototype._i = looseIndexOf Vue.prototype._m = renderStatic Vue.prototype._f = resolveFilter Vue.prototype._k = checkKeyCodes Vue.prototype._b = bindObjectProps Vue.prototype._v = createTextVNode Vue.prototype._e = createEmptyVNode Vue.prototype._u = resolveScopedSlots Vue.prototype._g = bindObjectListeners

    mergeOptions使用策略模式合并傳入的options和Vue.options合并后的代碼結(jié)構(gòu),

    可以看到通過(guò)合并策略components,directives,filters繼承了全局的, 這就是為什么全局注冊(cè)的可以在任何地方使用,因?yàn)槊總€(gè)實(shí)例都繼承了全局的, 所以都能找到。

    new 一個(gè) Vue 對(duì)象發(fā)生了什么:

    第 5452 行至第 5655 行

    // these are reserved for web because they are directly compiled away // during template compilation // 這些是為web保留的,因?yàn)樗鼈兪侵苯泳幾g掉的 // 在模板編譯期間 isBooleanAttrgenClassForVnode// class 轉(zhuǎn)碼獲取vonde 中的staticClass 靜態(tài)class 和class動(dòng)態(tài)class轉(zhuǎn)義成真實(shí)dom需要的class格式。然后返回class字符串mergeClassData// mergeClassDatarenderClass// 渲染calss 這里獲取到已經(jīng)轉(zhuǎn)碼的calssstringifyClass// 轉(zhuǎn)碼 class,把數(shù)組格式,對(duì)象格式的calss 全部轉(zhuǎn)化成 字符串格式stringifyArray// 數(shù)組字符串變成字符串,然后用空格 隔開 拼接 起來(lái)變成字符串stringifyObject// 對(duì)象字符串變成字符串,然后用空格 隔開 拼接 起來(lái)變成字符串namespaceMapisHTMLTagisSVG// 判斷svg 標(biāo)簽isUnknownElement// 檢查dom 節(jié)點(diǎn)的tag標(biāo)簽 類型 是否是VPre 標(biāo)簽 或者是判斷是否是瀏覽器自帶原有的標(biāo)簽isTextInputType // //匹配text,number,password,search,email,tel,url

    這一 part 沒有特別要說(shuō)的,主要是對(duì) class 的轉(zhuǎn)碼、合并和其他二次封裝的工具函數(shù)。實(shí)際上我們?cè)?Vue 源碼很多地方看到了這樣的封裝,在平常的開發(fā)中,我們也得要求自己封裝基本的函數(shù)。如果能形成自己習(xí)慣用的函數(shù)的庫(kù),會(huì)方便很多,且對(duì)自己能力也是一個(gè)提升。

    第 5659 行至第 5792 行

    createElement // 創(chuàng)建元素,實(shí)例化 VNodecreateElementNScreateTextNodecreateCommentinsertBeforeremoveChildappendChildparentNodenextSiblingtagNamesetTextContentsetStyleScopenodeOps// nodeOps: createElement: createElement$1, //創(chuàng)建一個(gè)真實(shí)的dom createElementNS: createElementNS, //創(chuàng)建一個(gè)真實(shí)的dom svg方式 createTextNode: createTextNode, // 創(chuàng)建文本節(jié)點(diǎn) createComment: createComment, // 創(chuàng)建一個(gè)注釋節(jié)點(diǎn) insertBefore: insertBefore, //插入節(jié)點(diǎn) 在xxx dom 前面插入一個(gè)節(jié)點(diǎn) removeChild: removeChild, //刪除子節(jié)點(diǎn) appendChild: appendChild, //添加子節(jié)點(diǎn) 尾部 parentNode: parentNode, //獲取父親子節(jié)點(diǎn)dom nextSibling: nextSibling, //獲取下一個(gè)兄弟節(jié)點(diǎn) tagName: tagName, //獲取dom標(biāo)簽名稱 setTextContent: setTextContent, // //設(shè)置dom 文本 setStyleScope: setStyleScope //設(shè)置組建樣式的作用域 refregisterRef

    // 注冊(cè)ref或者刪除ref。比如標(biāo)簽上面設(shè)置了ref=abc 那么該函數(shù)就是為this.$refs.abc 注冊(cè)ref 把真實(shí)的dom存進(jìn)去

    階段小結(jié)

    這里的重點(diǎn)想必就是 “ref” 了

    在絕大多數(shù)情況下,我們最好不要觸達(dá)另一個(gè)組件實(shí)例內(nèi)部或手動(dòng)操作 DOM 元素。不過(guò)也確實(shí)在一些情況下做這些事情是合適的。ref 為我們提供了解決途徑。

    ref屬性不是一個(gè)標(biāo)準(zhǔn)的HTML屬性,只是Vue中的一個(gè)屬性。

    第 5794 行至第 6006 行

    Virtual DOM !

    沒錯(cuò),這里就是 虛擬 dom 生成的源碼相關(guān)。

    sameVnodesameInputTypecreateKeyToOldIdxcreatePatchFunction // !important:patch 把 vonde 渲染成真實(shí)的 domemptyNodeAtcreateRmCbremoveNodeisUnknownElement?1createElm // 創(chuàng)造 dom 節(jié)點(diǎn)createComponent // 創(chuàng)建組件,并且判斷它是否實(shí)例化過(guò)initComponent

    createElement方法接收一個(gè)tag參數(shù),在內(nèi)部會(huì)去判斷tag標(biāo)簽的類型,從而去決定是創(chuàng)建一個(gè)普通的VNode還是一個(gè)組件類VNode;

    createComponent 的實(shí)現(xiàn),在渲染一個(gè)組件的時(shí)候的 3 個(gè)關(guān)鍵邏輯:

    構(gòu)造子類構(gòu)造函數(shù),安裝組件鉤子函數(shù)實(shí)例化 vnode。createComponent 后返回的是組件 vnode,它也一樣走到 vm._update 方法

    我們傳入的 vnode 是組件渲染的 vnode,也就是我們之前說(shuō)的 vm._vnode,如果組件的根節(jié)點(diǎn)是個(gè)普通元素,那么 vm._vnode 也是普通的 vnode,這里 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值是 false。接下來(lái)的過(guò)程就系列一的步驟一樣了,先創(chuàng)建一個(gè)父節(jié)點(diǎn)占位符,然后再遍歷所有子 VNode 遞歸調(diào)用 createElm,在遍歷的過(guò)程中,如果遇到子 VNode 是一個(gè)組件的 VNode,則重復(fù)過(guò)程,這樣通過(guò)一個(gè)遞歸的方式就可以完整地構(gòu)建了整個(gè)組件樹。

    initComponent 初始化組建,如果沒有tag標(biāo)簽則去更新真實(shí)dom的屬性,如果有tag標(biāo)簽,則注冊(cè)或者刪除ref 然后為insertedVnodeQueue.push(vnode);

    第 6008 行至第 6252 行

    reactivateComponentinsertcreateChildrenisPatchableinvokeCreateHookssetScopeaddVnodes // 添加 VnodesinvokeDestroyHookremoveVnodes // 移除 VnodesremoveAndInvokeRemoveHookupdateChildren // 在patchVnode中提到,如果新老節(jié)點(diǎn)都有子節(jié)點(diǎn),但是不相同的時(shí)候就會(huì)調(diào)用 updateChildren,這個(gè)函數(shù)通過(guò)diff算法盡可能的復(fù)用先前的DOM節(jié)點(diǎn)。

    // diff 算法就在這里辣!

    function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, elmToMove, refElm while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null if (isUndef(idxInOld)) { createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { elmToMove = oldCh[idxInOld] if (sameVnode(elmToMove, newStartVnode)) { patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } } } } if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } } checkDuplicateKeysfindIdxInOld

    reactivateComponent 承接上文 createComponent

    第 6259 行至第 6561 行

    patchVnode // 如果符合sameVnode,就不會(huì)渲染vnode重新創(chuàng)建DOM節(jié)點(diǎn),而是在原有的DOM節(jié)點(diǎn)上進(jìn)行修補(bǔ),盡可能復(fù)用原有的DOM節(jié)點(diǎn)。invokeInsertHookisRenderedModulehydrateassertNodeMatchpatch // !important: patch的本質(zhì)是將新舊vnode進(jìn)行比較,創(chuàng)建、刪除或者更新DOM節(jié)點(diǎn)/組件實(shí)例

    階段小結(jié)

    Vue 的核心思想:組件化。

    這一部分是關(guān)于構(gòu)建組件樹,形成虛擬 dom ,以及非常重要的 patch 方法。

    再來(lái)億遍:

    原因:當(dāng)修改某條數(shù)據(jù)的時(shí)候,這時(shí)候js會(huì)將整個(gè)DOM Tree進(jìn)行替換,這種操作是相當(dāng)消耗性能的。所以在Vue中引入了Vnode的概念:Vnode是對(duì)真實(shí)DOM節(jié)點(diǎn)的模擬,可以對(duì)Vnode Tree進(jìn)行增加節(jié)點(diǎn)、刪除節(jié)點(diǎn)和修改節(jié)點(diǎn)操作。這些過(guò)程都只需要操作VNode Tree,不需要操作真實(shí)的DOM,大大的提升了性能。修改之后使用diff算法計(jì)算出修改的最小單位,在將這些小單位的視圖進(jìn)行更新。原理:data中定義了一個(gè)變量a,并且模板中也使用了它,那么這里生成的Watcher就會(huì)加入到a的訂閱者列表中。當(dāng)a發(fā)生改變時(shí),對(duì)應(yīng)的訂閱者收到變動(dòng)信息,這時(shí)候就會(huì)觸發(fā)Watcher的update方法,實(shí)際update最后調(diào)用的就是在這里聲明的updateComponent。

    當(dāng)數(shù)據(jù)發(fā)生改變時(shí)會(huì)觸發(fā)回調(diào)函數(shù)updateComponent,updateComponent是對(duì)patch過(guò)程的封裝。patch的本質(zhì)是將新舊vnode進(jìn)行比較,創(chuàng)建、刪除或者更新DOM節(jié)點(diǎn)/組件實(shí)例。

    聯(lián)系前后QA

    Q:vue.js 同時(shí)多個(gè)賦值是一次性渲染還是多次渲染DOM?

    A:官網(wǎng)已給出答案:

    https://cn.vuejs.org/v2/guide/reactivity.html

    可能你還沒有注意到,Vue 在更新 DOM 時(shí)是異步執(zhí)行的。只要偵聽到數(shù)據(jù)變化,Vue 將開啟一個(gè)隊(duì)列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更。如果同一個(gè) watcher 被多次觸發(fā),只會(huì)被推入到隊(duì)列中一次。這種在緩沖時(shí)去除重復(fù)數(shù)據(jù)對(duì)于避免不必要的計(jì)算和 DOM 操作是非常重要的。然后,在下一個(gè)的事件循環(huán)“tick”中,Vue 刷新隊(duì)列并執(zhí)行實(shí)際 (已去重的) 工作。Vue 在內(nèi)部對(duì)異步隊(duì)列嘗試使用原生的 Promise.then、MutationObserver 和 setImmediate,如果執(zhí)行環(huán)境不支持,則會(huì)采用 setTimeout(fn, 0) 代替。

    例如,當(dāng)你設(shè)置 vm.someData = new value,該組件不會(huì)立即重新渲染。當(dāng)刷新隊(duì)列時(shí),組件會(huì)在下一個(gè)事件循環(huán)“tick”中更新。多數(shù)情況我們不需要關(guān)心這個(gè)過(guò)程,但是如果你想基于更新后的 DOM 狀態(tài)來(lái)做點(diǎn)什么,這就可能會(huì)有些棘手。雖然 Vue.js 通常鼓勵(lì)開發(fā)人員使用“數(shù)據(jù)驅(qū)動(dòng)”的方式思考,避免直接接觸 DOM,但是有時(shí)我們必須要這么做。為了在數(shù)據(jù)變化之后等待 Vue 完成更新 DOM,可以在數(shù)據(jù)變化之后立即使用 Vue.nextTick(callback)。這樣回調(diào)函數(shù)將在 DOM 更新完成后被調(diào)用。

    這樣是不是有種前后連貫起來(lái)的感覺,原來(lái) nextTick 是這樣子的。

    第 6566 行至第 7069 行

    directives // 官網(wǎng):https://cn.vuejs.org/v2/guide/custom-directive.htmlupdateDirectives // 更新指令_updatenormalizeDirectives // 統(tǒng)一directives的格式getRawDirName // 返回指令名稱 或者屬性name名稱+修飾符callHook$1 //觸發(fā)指令鉤子函數(shù)updateAttrs // 更新屬性setAttr // 設(shè)置屬性baseSetAttrupdateClass // 更新樣式klassparseFilters // 處理value 解析成正確的value,把過(guò)濾器 轉(zhuǎn)換成 vue 虛擬dom的解析方法函數(shù) 比如把過(guò)濾器 ab | c | d 轉(zhuǎn)換成 _f("d")(_f("c")(ab))wrapFilter // 轉(zhuǎn)換過(guò)濾器格式baseWarn // 基礎(chǔ)警告pluckModuleFunction //循環(huán)過(guò)濾數(shù)組或者對(duì)象的值,根據(jù)key循環(huán) 過(guò)濾對(duì)象或者數(shù)組[key]值,如果不存在則丟棄,如果有相同多個(gè)的key值,返回多個(gè)值的數(shù)組addProp //在虛擬dom中添加prop屬性addAttr //添加attrs屬性addRawAttr //添加原始attr(在預(yù)轉(zhuǎn)換中使用)addDirective //為虛擬dom 添加一個(gè) 指令directives屬性 對(duì)象addHandler // 為虛擬dom添加events 事件對(duì)象屬性

    前面圍繞“指令”和“過(guò)濾器”的一些基礎(chǔ)工具函數(shù)。

    后面圍繞為虛擬 dom 添加屬性、事件等具體實(shí)現(xiàn)函數(shù)。

    第 7071 行至第 7298 行

    getRawBindingAttrgetBindingAttr // 獲取 :屬性 或者v-bind:屬性,或者獲取屬性 移除傳進(jìn)來(lái)的屬性name,并且返回獲取到 屬性的值getAndRemoveAttr // 移除傳進(jìn)來(lái)的屬性name,并且返回獲取到 屬性的值getAndRemoveAttrByRegexrangeSetItemgenComponentModel // 為虛擬dom添加model屬性 /* * Parse a v-model expression into a base path and a final key segment. * Handles both dot-path and possible square brackets. * 將 v-model 表達(dá)式解析為基路徑和最后一個(gè)鍵段。 * 處理點(diǎn)路徑和可能的方括號(hào)。 */ parseModel //轉(zhuǎn)義字符串對(duì)象拆分字符串對(duì)象 把后一位key分離出來(lái)

    // 如果數(shù)據(jù)是object.info.name的情況下 則返回是 {exp: "object.info",key: "name"} // 如果數(shù)據(jù)是object[info][name]的情況下 則返回是 {exp: "object[info]",key: "name"}

    nexteofparseBracket //檢測(cè) 匹配[] 一對(duì)這樣的=括號(hào)parseString // 循環(huán)匹配一對(duì)或者""符號(hào)

    這一部分包括:原生指令 v-bind 和為虛擬 dom 添加 model 屬性,以及格式校驗(yàn)工具函數(shù)。

    第 7300 行至第 7473 行

    modelgenCheckboxModel // 為input type="checkbox" 虛擬dom添加 change 函數(shù) ,根據(jù)v-model是否是數(shù)組,調(diào)用change函數(shù),調(diào)用 set 去更新 checked選中數(shù)據(jù)的值genRadioModel // 為虛擬dom inpu標(biāo)簽 type === radio 添加change 事件 更新值genSelect // 為虛擬dom添加change 函數(shù) ,change 函數(shù)調(diào)用 set 去更新 select 選中數(shù)據(jù)的值genDefaultModel // 如果虛擬dom標(biāo)簽是 input 類型不是checkbox,radio 或者是textarea 標(biāo)簽的時(shí)候,獲取真實(shí)的dom的value值調(diào)用 change或者input方法執(zhí)行set方法更新數(shù)據(jù)

    階段小結(jié)

    v-bind、v-model

    區(qū)別:

    v-bind 用來(lái)綁定數(shù)據(jù)和屬性以及表達(dá)式,縮寫為:v-model 使用在表單中,實(shí)現(xiàn)雙向數(shù)據(jù)綁定的,在表單元素外使用不起作用

    Q:你知道v-model的原理嗎?說(shuō)說(shuō)看

    A: v-model本質(zhì)上是語(yǔ)法糖,即利用v-model綁定數(shù)據(jù),其實(shí)就是既綁定了數(shù)據(jù),又添加了一個(gè)input事件監(jiān)聽

    自定義指令鉤子函數(shù)

    一個(gè)指令定義對(duì)象可以提供如下幾個(gè)鉤子函數(shù) (均為可選):

    1. bind:只調(diào)用一次,指令第一次綁定到元素時(shí)調(diào)用。在這里可以進(jìn)行一次性的初始化設(shè)置。 2. inserted:被綁定元素插入父節(jié)點(diǎn)時(shí)調(diào)用 (僅保證父節(jié)點(diǎn)存在,但不一定已被插入文檔中)。 3. update:所在組件的 VNode 更新時(shí)調(diào)用,但是可能發(fā)生在其子 VNode 更新之前。指令的值可能發(fā)生了改變,也可能沒有。但是你可以通過(guò)比較更新前后的值來(lái)忽略不必要的模板更新 (詳細(xì)的鉤子函數(shù)參數(shù)見下)。 4. componentUpdated:指令所在組件的 VNode 及其子 VNode 全部更新后調(diào)用。 5. unbind:只調(diào)用一次,指令與元素解綁時(shí)調(diào)用。 指令鉤子函數(shù)會(huì)被傳入以下參數(shù):1. el:指令所綁定的元素,可以用來(lái)直接操作 DOM 。 2. binding:一個(gè)對(duì)象,包含以下屬性: name:指令名,不包括 v- 前綴。 value:指令的綁定值,例如:v-my-directive="1 + 1" 中,綁定值為 2。 oldValue:指令綁定的前一個(gè)值,僅在 update 和 componentUpdated 鉤子中可用。無(wú)論值是否改變都可用。 expression:字符串形式的指令表達(dá)式。例如 v-my-directive="1 + 1" 中,表達(dá)式為 "1 + 1"。 arg:傳給指令的參數(shù),可選。例如 v-my-directive:foo 中,參數(shù)為 "foo"。 modifiers:一個(gè)包含修飾符的對(duì)象。例如:v-my-directive.foo.bar 中,修飾符對(duì)象為 { foo: true, bar: true }。 3. vnode:Vue 編譯生成的虛擬節(jié)點(diǎn)。移步 VNode API 來(lái)了解更多詳情。 4. oldVnode:上一個(gè)虛擬節(jié)點(diǎn),僅在 update componentUpdated 鉤子中可用。

    除了 el 之外,其它參數(shù)都應(yīng)該是只讀的,切勿進(jìn)行修改。如果需要在鉤子之間共享數(shù)據(jù),建議通過(guò)元素的 dataset 來(lái)進(jìn)行。

    【譯】vue 自定義指令的魅力

    第 7473 行至第 7697 行

    normalizeEvents // 為事件 多添加 change 或者input 事件加進(jìn)去createOnceHandler$1add$1 // 為真實(shí)的dom添加事件remove$2updateDOMListeners // 更新dom事件updateDOMProps // 更新真實(shí)dom的props屬性shouldUpdateValue // 判斷是否需要更新valueisNotInFocusAndDirtyisDirtyWithModifiers // 判斷臟數(shù)據(jù)修改

    臟數(shù)據(jù)概念

    第 7699 行至第 7797 行

    domPropsparseStyleText // 把style 字符串 轉(zhuǎn)換成對(duì)象normalizeStyleData // 在同一個(gè)vnode上合并靜態(tài)和動(dòng)態(tài)樣式數(shù)據(jù)normalizeStyleBinding // 將可能的數(shù)組/字符串值規(guī)范化為對(duì)象getStyle /** * parent component style should be after childs * so that parent components style could override it * 父組件樣式應(yīng)該在子組件樣式之后 * 這樣父組件的樣式就可以覆蓋它 * 循環(huán)子組件和組件的樣式,把它全部合并到一個(gè)樣式對(duì)象中返回 樣式對(duì)象 如{width:100px,height:200px} 返回該字符串。 */ setProp // 設(shè)置 prop

    第 7799 行至第 7995 行

    normalize // 給css加前綴。解決瀏覽器兼用性問(wèn)題,加前綴updateStyle // 將vonde虛擬dom的css 轉(zhuǎn)義成并且渲染到真實(shí)dom的csszhongaddClass // 為真實(shí)dom 元素添加class類removeClass // 刪除真實(shí)dom的css類名resolveTransition // 解析vonde中的transition的name屬性獲取到一個(gè)css過(guò)度對(duì)象類autoCssTransition // 通過(guò) name 屬性獲取過(guò)渡 CSS 類名 比如標(biāo)簽上面定義name是 fade css就要定義 .fade-enter-active,.fade-leave-active,.fade-enter,.fade-leave-to 這樣的classnextFrame // 下一幀

    第 7997 行至第 8093 行

    addTransitionClass // 獲取 真實(shí)dom addTransitionClass 記錄calss類removeTransitionClass // 刪除vonde的class類和刪除真實(shí)dom的class類whenTransitionEnds // 獲取動(dòng)畫的信息,執(zhí)行動(dòng)畫。getTransitionInfo // 獲取transition,或者animation 動(dòng)畫的類型,動(dòng)畫個(gè)數(shù),動(dòng)畫執(zhí)行時(shí)間

    這一部分關(guān)于:對(duì)真實(shí) dom 的操作,包括樣式的增刪、事件的增刪、動(dòng)畫類等。

    回過(guò)頭再理一下宏觀上的東西,再來(lái)億遍-虛擬DOM:模板 → 渲染函數(shù) → 虛擬DOM樹 → 真實(shí)DOM

    那么這一部分則處在“虛擬DOM樹 → 真實(shí)DOM”這個(gè)階段

    第 8093 行至第 8518 行

    getTimeout// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers // in a locale-dependent way, using a comma instead of a dot. // If comma is not replaced with a dot, the input will be rounded down (i.e. acting // as a floor function) causing unexpected behaviors // 根據(jù)本地的依賴方式,Chromium 的舊版本(低于61.0.3163.100)格式化浮點(diǎn)數(shù)字,使用逗號(hào)而不是點(diǎn)。如果逗號(hào)未用點(diǎn)代替,則輸入將被四舍五入而導(dǎo)致意外行為 toMs

    // update toMs function. fix #4894

    enter// activeInstance will always be the <transition> component managing this // transition. One edge case to check is when the <transition> is placed // as the root node of a child component. In that case we need to check // <transition>s parent for appear check. // activeInstance 將一直作為<transition>的組件來(lái)管理 transition。要檢查的一種邊緣情況:<transition> 作為子組件的根節(jié)點(diǎn)時(shí)。在這種情況下,我們需要檢查 <transition> 的父項(xiàng)的展現(xiàn)。 leave // 離開動(dòng)畫performLeavecheckDuration // only used in dev mode : 檢測(cè) val 必需是數(shù)字isValidDurationgetHookArgumentsLength // 檢測(cè)鉤子函數(shù) fns 的長(zhǎng)度_entercreatePatchFunction // path 把vonde 渲染成真實(shí)的dom:創(chuàng)建虛擬 dom - 函數(shù)體在 5845 行directive // 生命指令:包括 插入 和 組件更新

    更新指令 比較 oldVnode 和 vnode,根據(jù)oldVnode和vnode的情況 觸發(fā)指令鉤子函數(shù)bind,update,inserted,insert,componentUpdated,unbind鉤子函數(shù)

    此節(jié)前部分是 transition 動(dòng)畫相關(guān)工具函數(shù),后部分關(guān)于虛擬 Dom patch、指令的更新。

    第 8520 行至第 8584 行

    setSelected // 設(shè)置選擇 - 指令更新的工具函數(shù)actuallySetSelected // 實(shí)際選擇,在 setSelected() 里調(diào)用hasNoMatchingOption // 沒有匹配項(xiàng) - 指令組件更新工具函數(shù)getValue // 獲取 option.valueonCompositionStart // 組成開始 - 指令插入工具函數(shù)onCompositionEnd // 組成結(jié)束-指令插入工具函數(shù):防止無(wú)故觸發(fā)輸入事件trigger // 觸發(fā)事件

    第 8592 行至第 8728 行

    // 定義在組件根內(nèi)部遞歸搜索可能存在的 transition

    locateNodeshow // 控制 el 的 display 屬性platformDirectives // 平臺(tái)指令transitionProps // 過(guò)渡Props對(duì)象 // in case the child is also an abstract component, e.g. <keep-alive> // we want to recursively retrieve the real component to be rendered // 如果子對(duì)象也是抽象組件,例如<keep-alive> // 我們要遞歸地檢索要渲染的實(shí)際組件 getRealChildextractTransitionData // 提取 TransitionDataplaceholder // 占位提示hasParentTransition // 判斷是否有 ParentTransitionisSameChild // 判斷子對(duì)象是否相同

    第 8730 行至第 9020 行

    Transition // !important

    前部分以及此部分大部分圍繞 Transition 這個(gè)關(guān)鍵對(duì)象。即迎合官網(wǎng) “過(guò)渡 & 動(dòng)畫” 這一節(jié),是我們需要關(guān)注的重點(diǎn)!

    Vue 在插入、更新或者移除 DOM 時(shí),提供多種不同方式的應(yīng)用過(guò)渡效果。包括以下工具:

    在 CSS 過(guò)渡和動(dòng)畫中自動(dòng)應(yīng)用 class可以配合使用第三方 CSS 動(dòng)畫庫(kù),如 Animate.css在過(guò)渡鉤子函數(shù)中使用 JavaScript 直接操作 DOM可以配合使用第三方 JavaScript 動(dòng)畫庫(kù),如 Velocity.js

    在這里,我們只會(huì)講到進(jìn)入、離開和列表的過(guò)渡,你也可以看下一節(jié)的管理過(guò)渡狀態(tài)。

    vue - transition 里面大有東西,這里有一篇“細(xì)談”推薦閱讀。

    propsTransitionGroup // TransitionGroupcallPendingCbs // Pending 回調(diào)recordPosition // 記錄位置applyTranslation // 應(yīng)用動(dòng)畫 - TransitionGroup.updated 調(diào)用 // we divide the work into three loops to avoid mixing DOM reads and writes // in each iteration - which helps prevent layout thrashing. //我們將工作分為三個(gè) loops,以避免將 DOM 讀取和寫入混合在一起 //在每次迭代中-有助于防止布局沖撞。 platformComponents // 平臺(tái)組件// 安裝平臺(tái)運(yùn)行時(shí)指令和組件 extend(Vue.options.directives, platformDirectives); extend(Vue.options.components, platformComponents);

    Q: vue自帶的內(nèi)置組件有什么?

    A: Vue中內(nèi)置的組件有以下幾種:

    component

    component組件:有兩個(gè)屬性---is inline-template

    渲染一個(gè)‘元組件’為動(dòng)態(tài)組件,按照is特性的值來(lái)渲染成那個(gè)組件

    transition

    transition組件:為組件的載入和切換提供動(dòng)畫效果,具有非常強(qiáng)的可定制性,支持16個(gè)屬性和12個(gè)事件

    transition-group

    transition-group:作為多個(gè)元素/組件的過(guò)渡效果

    keep-alive

    keep-alive:包裹動(dòng)態(tài)組件時(shí),會(huì)緩存不活動(dòng)的組件實(shí)例,而不是銷毀它們

    slot

    slot:作為組件模板之中的內(nèi)容分發(fā)插槽,slot元素自身將被替換

    第 9024 行至第 9207 行

    // install platform specific utils // 安裝平臺(tái)特定的工具

    Vue.config.xVue.config.mustUseProp = mustUseProp; Vue.config.isReservedTag = isReservedTag; Vue.config.isReservedAttr = isReservedAttr; Vue.config.getTagNamespace = getTagNamespace; Vue.config.isUnknownElement = isUnknownElement; Vue.prototype.$mount

    // public mount method 安裝方法 實(shí)例方法掛載 vm

    // public mount method Vue.prototype.$mount = function ( el, // 真實(shí)dom 或者是 string hydrating //新的虛擬dom vonde ) { el = el && inBrowser ? query(el) : undefined; return mountComponent(this, el, hydrating) };

    devtools global hook // 開發(fā)環(huán)境全局 hook Tip

    buildRegex // 構(gòu)建的正則匹配parseText // 匹配view 指令,并且把他轉(zhuǎn)換成 虛擬dom vonde 需要渲染的函數(shù),比如指令{{name}}轉(zhuǎn)換成 _s(name)transformNode // 獲取 class 屬性和:class或者v-bind的動(dòng)態(tài)屬性值,并且轉(zhuǎn)化成字符串 添加到staticClass和classBinding 屬性中genData // 初始化擴(kuò)展指令 baseDirectives,on,bind,cloak方法,dataGenFns 獲取到一個(gè)數(shù)組,數(shù)組中有兩個(gè)函數(shù) genData(轉(zhuǎn)換 class) 和 genData$1(轉(zhuǎn)換 style),transformNode1//transformNode1 獲取 style屬性和:style或者v-bind的動(dòng)態(tài)屬性值,并且轉(zhuǎn)化成字符串 添加到staticStyle和styleBinding屬性中genData$1 // 參見 genDatastyle$1 // 包含 staticKeys、transformNode、genData 屬性

    第 9211 行至第 9537 行

    heisUnaryTag // 工具函數(shù)canBeLeftOpenTag // 工具函數(shù)isNonPhrasingTag // 工具函數(shù)

    Regular Expressions

    parseHTML // 解析成 HTML !important

    parseHTML 這個(gè)函數(shù)實(shí)現(xiàn)大概兩百多行,是一個(gè)比較大的函數(shù)體了。

    parseHTML 中的方法用于處理HTML開始和結(jié)束標(biāo)簽。

    parseHTML 方法的整體邏輯是用正則判斷各種情況,進(jìn)行不同的處理。其中調(diào)用到了 options 中的自定義方法。

    options 中的自定義方法用于處理AST語(yǔ)法樹,最終返回出整個(gè)AST語(yǔ)法樹對(duì)象。

    貼一下源碼,有興趣可自行感受一二。附一篇詳解Vue.js HTML解析細(xì)節(jié)學(xué)習(xí)

    function parseHTML(html, options) { var stack = []; var expectHTML = options.expectHTML; var isUnaryTag?1 = options.isUnaryTag || no; var canBeLeftOpenTag?1 = options.canBeLeftOpenTag || no; var index = 0; var last, lastTag; while (html) { last = html; // 確保我們不在像腳本/樣式這樣的純文本內(nèi)容元素中 if (!lastTag || !isPlainTextElement(lastTag)) { var textEnd = html.indexOf(<); if (textEnd === 0) { // Comment: if (comment.test(html)) { var commentEnd = html.indexOf(-->); if (commentEnd >= 0) { if (options.shouldKeepComment) { options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3); } advance(commentEnd + 3); continue } } // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment if (conditionalComment.test(html)) { var conditionalEnd = html.indexOf(]>); if (conditionalEnd >= 0) { advance(conditionalEnd + 2); continue } } // Doctype: // 匹配 html 的頭文件 var doctypeMatch = html.match(doctype); if (doctypeMatch) { advance(doctypeMatch[0].length); continue } // End tag: var endTagMatch = html.match(endTag); if (endTagMatch) { var curIndex = index; advance(endTagMatch[0].length); parseEndTag(endTagMatch[1], curIndex, index); continue } // Start tag: // 解析開始標(biāo)記 var startTagMatch = parseStartTag(); if (startTagMatch) { handleStartTag(startTagMatch); if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) { advance(1); } continue } } var text = (void 0), rest = (void 0), next = (void 0); if (textEnd >= 0) { rest = html.slice(textEnd); while ( !endTag.test(rest) && !startTagOpen.test(rest) && !comment.test(rest) && !conditionalComment.test(rest) ) { // < in plain text, be forgiving and treat it as text next = rest.indexOf(<, 1); if (next < 0) { break } textEnd += next; rest = html.slice(textEnd); } text = html.substring(0, textEnd); } if (textEnd < 0) { text = html; } if (text) { advance(text.length); } if (options.chars && text) { options.chars(text, index - text.length, index); } } else { // 處理是script,style,textarea var endTagLength = 0; var stackedTag = lastTag.toLowerCase(); var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp(([\\s\\S]*?)(</ + stackedTag + [^>]*>), i)); var rest$1 = html.replace(reStackedTag, function (all, text, endTag) { endTagLength = endTag.length; if (!isPlainTextElement(stackedTag) && stackedTag !== noscript) { text = text .replace(/<!\--([\s\S]*?)-->/g, $1) // #7298 .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, $1); } if (shouldIgnoreFirstNewline(stackedTag, text)) { text = text.slice(1); } if (options.chars) { options.chars(text); } return }); index += html.length - rest$1.length; html = rest$1; parseEndTag(stackedTag, index - endTagLength, index); } if (html === last) { options.chars && options.chars(html); if (!stack.length && options.warn) { options.warn(("Mal-formatted tag at end of template: \"" + html + "\""), { start: index + html.length }); } break } } // Clean up any remaining tags parseEndTag(); function advance(n) { index += n; html = html.substring(n); } function parseStartTag() { var start = html.match(startTagOpen); if (start) { var match = { tagName: start[1], attrs: [], start: index }; advance(start[0].length); var end, attr; while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) { attr.start = index; advance(attr[0].length); attr.end = index; match.attrs.push(attr); } if (end) { match.unarySlash = end[1]; advance(end[0].length); match.end = index; return match } } } function handleStartTag(match) { var tagName = match.tagName; var unarySlash = match.unarySlash; if (expectHTML) { if (lastTag === p && isNonPhrasingTag(tagName)) { parseEndTag(lastTag); } if (canBeLeftOpenTag?1(tagName) && lastTag === tagName) { parseEndTag(tagName); } } var unary = isUnaryTag?1(tagName) || !!unarySlash; var l = match.attrs.length; var attrs = new Array(l); for (var i = 0; i < l; i++) { var args = match.attrs[i]; var value = args[3] || args[4] || args[5] || ; var shouldDecodeNewlines = tagName === a && args[1] === href ? options.shouldDecodeNewlinesForHref : options.shouldDecodeNewlines; attrs[i] = { name: args[1], value: decodeAttr(value, shouldDecodeNewlines) }; if (options.outputSourceRange) { attrs[i].start = args.start + args[0].match(/^\s*/).length; attrs[i].end = args.end; } } if (!unary) { stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end }); lastTag = tagName; } if (options.start) { options.start(tagName, attrs, unary, match.start, match.end); } } function parseEndTag(tagName, start, end) { var pos, lowerCasedTagName; if (start == null) { start = index; } if (end == null) { end = index; } // Find the closest opened tag of the same type if (tagName) { lowerCasedTagName = tagName.toLowerCase(); for (pos = stack.length - 1; pos >= 0; pos--) { if (stack[pos].lowerCasedTag === lowerCasedTagName) { break } } } else { // If no tag name is provided, clean shop pos = 0; } if (pos >= 0) { // Close all the open elements, up the stack for (var i = stack.length - 1; i >= pos; i--) { if (i > pos || !tagName && options.warn ) { options.warn( ("tag <" + (stack[i].tag) + "> has no matching end tag."), { start: stack[i].start, end: stack[i].end } ); } if (options.end) { options.end(stack[i].tag, start, end); } } // Remove the open elements from the stack stack.length = pos; lastTag = pos && stack[pos - 1].tag; } else if (lowerCasedTagName === br) { if (options.start) { options.start(tagName, [], true, start, end); } } else if (lowerCasedTagName === p) { if (options.start) { options.start(tagName, [], false, start, end); } if (options.end) { options.end(tagName, start, end); } } } }

    第 9541 行至第 9914 行

    Regular Expressions // 相關(guān)正則

    createASTElement

    // Convert HTML string to AST.

    parse // !important

    parse 函數(shù)從 9593 行至 9914 行,共三百多行。核心嗎?當(dāng)然核心!

    引自 wikipedia:

    在計(jì)算機(jī)科學(xué)和語(yǔ)言學(xué)中,語(yǔ)法分析(英語(yǔ):syntactic analysis,也叫 parsing)是根據(jù)某種給定的形式文法對(duì)由單詞序列(如英語(yǔ)單詞序列)構(gòu)成的輸入文本進(jìn)行分析并確定其語(yǔ)法結(jié)構(gòu)的一種過(guò)程。

    語(yǔ)法分析器(parser)通常是作為編譯器或解釋器的組件出現(xiàn)的,它的作用是進(jìn)行語(yǔ)法檢查、并構(gòu)建由輸入的單詞組成的數(shù)據(jù)結(jié)構(gòu)(一般是語(yǔ)法分析樹、抽象語(yǔ)法樹等層次化的數(shù)據(jù)結(jié)構(gòu))。語(yǔ)法分析器通常使用一個(gè)獨(dú)立的詞法分析器從輸入字符流中分離出一個(gè)個(gè)的“單詞”,并將單詞流作為其輸入。實(shí)際開發(fā)中,語(yǔ)法分析器可以手工編寫,也可以使用工具(半)自動(dòng)生成。

    parse 的整體流程實(shí)際上就是先處理了一些傳入的options,然后執(zhí)行了parseHTML 函數(shù),傳入了template,options和相關(guān)鉤子。

    具體實(shí)現(xiàn)這里盜一個(gè)圖:

    parse中的語(yǔ)法分析可以看這一篇這一節(jié)

    startcharcommentend

    parse、optimize、codegen的核心思想解讀可以看這一篇這一節(jié)

    這里實(shí)現(xiàn)的細(xì)節(jié)還真不少!

    階段小結(jié)(重點(diǎn))

    噫噓唏!來(lái)到第 20 篇的小結(jié)!來(lái)個(gè)圖鎮(zhèn)一下先!

    還記得官方這樣的一句話嗎?

    下圖展示了實(shí)例的生命周期。你不需要立馬弄明白所有的東西,不過(guò)隨著你的不斷學(xué)習(xí)和使用,它的參考價(jià)值會(huì)越來(lái)越高。

    看了這么多,我們?cè)倩仡^看看注釋版。

    上圖值得一提的是:Has "template" option? 這個(gè)邏輯的細(xì)化

    碰到是否有 template 選項(xiàng)時(shí),會(huì)詢問(wèn)是否要對(duì) template 進(jìn)行編譯:即模板通過(guò)編譯生成 AST,再由 AST 生成 Vue 的渲染函數(shù),渲染函數(shù)結(jié)合數(shù)據(jù)生成 Virtual DOM 樹,對(duì) Virtual DOM 進(jìn)行 diff 和 patch 后生成新的UI。

    如圖(此圖前文也有提到,見 0 至 5000 行總結(jié)):

    將 Vue 的源碼的“數(shù)據(jù)監(jiān)聽”、“虛擬 DOM”、“Render 函數(shù)”、“組件編譯”、結(jié)合好,則算是融會(huì)貫通了!

    一圖勝萬(wàn)言

    好好把上面的三張圖看懂,便能做到“成竹在胸”,走遍天下的 VUE 原理面試都不用慌了。框架就在這里,細(xì)化的東西就需要多多記憶了!

    第 9916 行至第 10435 行

    到 1w 行了,自我慶祝一下!

    processRawAttrs // parse 方法里用到的工具函數(shù) 用于將特性保存到AST對(duì)象的attrs屬性上processElement// parse 方法工具函數(shù) 元素填充export function processElement ( element: ASTElement, options: CompilerOptions ) { processKey(element) // determine whether this is a plain element after // removing structural attributes element.plain = ( !element.key && !element.scopedSlots && !element.attrsList.length ) processRef(element) processSlotContent(element) processSlotOutlet(element) processComponent(element) for (let i = 0; i < transforms.length; i++) { element = transforms[i](element, options) || element } processAttrs(element) return element }

    可以看到主要函數(shù)包括:processKey、processRef、processSlotContent、processSlotOutlet、processComponent、processAttrs 和最后遍歷執(zhí)行的transforms。

    processElement完成的slotTarget的賦值,這里則是將所有的slot創(chuàng)建的astElement以對(duì)象的形式賦值給currentParent的scopedSlots。以便后期組件內(nèi)部實(shí)例話的時(shí)候可以方便去使用vm.?slot。

    processKeyprocessRef首先最為簡(jiǎn)單的是processKey和processRef,在這兩個(gè)函數(shù)處理之前,我們的key屬性和ref屬性都是保存在astElement上面的attrs和attrsMap,經(jīng)過(guò)這兩個(gè)函數(shù)之后,attrs里面的key和ref會(huì)被干掉,變成astElement的直屬屬性。探討一下slot的處理方式,我們知道的是,slot的具體位置是在組件中定義的,而需要替換的內(nèi)容又是組件外面嵌套的代碼,Vue對(duì)這兩塊的處理是分開的。

    先說(shuō)組件內(nèi)的屬性摘取,主要是slot標(biāo)簽的name屬性,這是processSlotOutLet完成的。

    processForparseForprocessIfprocessIfConditionsfindPrevElementaddIfConditionprocessOnceprocessSlotContentgetSlotNameprocessSlotOutlet// handle <slot/> outlets function processSlotOutlet (el) { if (el.tag === slot) { el.slotName = getBindingAttr(el, name) // 就是這一句了。 if (process.env.NODE_ENV !== production && el.key) { warn( `\`key\` does not work on <slot> because slots are abstract outlets ` + `and can possibly expand into multiple elements. ` + `Use the key on a wrapping element instead.`, getRawBindingAttr(el, key) ) } } } // 其次是摘取需要替換的內(nèi)容,也就是 processSlotContent,這是是處理展示在組件內(nèi)部的slot,但是在這個(gè)地方只是簡(jiǎn)單的將給el添加兩個(gè)屬性作用域插槽的slotScope和 slotTarget,也就是目標(biāo)slot。 processComponent // processComponent 并不是處理component,而是摘取動(dòng)態(tài)組件的is屬性。 processAttrs是獲取所有的屬性和動(dòng)態(tài)屬性。processAttrscheckInForparseModifiersmakeAttrsMap

    這一部分仍是銜接這 parse function 里的具體實(shí)現(xiàn):start、end、comment、chars四大函數(shù)。

    流程再回顧一下:

    一、普通標(biāo)簽處理流程描述

    識(shí)別開始標(biāo)簽,生成匹配結(jié)構(gòu)match。

    const match = { // 匹配startTag的數(shù)據(jù)結(jié)構(gòu) tagName: div, attrs: [ { id="xxx",id,=,xxx }, ... ], start: index, end: xxx } 復(fù)制代碼 2. 處理attrs,將數(shù)組處理成 {name:xxx,value:xxx} 3. 生成astElement,處理for,if和once的標(biāo)簽。 4. 識(shí)別結(jié)束標(biāo)簽,將沒有閉合標(biāo)簽的元素一起處理。 5. 建立父子關(guān)系,最后再對(duì)astElement做所有跟Vue 屬性相關(guān)對(duì)處理。slot、component等等。

    二、文本或表達(dá)式的處理流程描述。

    截取符號(hào)<之前的字符串,這里一定是所有的匹配規(guī)則都沒有匹配上,只可能是文本了。使用chars函數(shù)處理該字符串。判斷字符串是否含有delimiters,默認(rèn)也就是${},有的話創(chuàng)建type為2的節(jié)點(diǎn),否則type為3.

    三、注釋流程描述

    匹配注釋符號(hào)。使用comment函數(shù)處理。直接創(chuàng)建type為3的節(jié)點(diǎn)。

    階段小結(jié)

    parseHTML() 和 parse() 這兩個(gè)函數(shù)占了很大的篇幅,值得重點(diǎn)去看看。的確也很多細(xì)節(jié),一些正則的匹配,字符串的操作等。從宏觀上把握從 template 到 vnode 的 parse 流程也無(wú)大問(wèn)題。

    第 10437 行至第 10605 行

    isTextTag // function chars() 里的工具函數(shù)isForbiddenTag // function parseHTML() 用到的工具函數(shù)用于檢查元素標(biāo)簽是否合法(不是保留命名)guardIESVGBug // parse start() 中用到的工具函數(shù)checkForAliasModel // checkForAliasModel用于檢查v-model的參數(shù)是否是v-for的迭代對(duì)象preTransformNode // preTransformNode 方法對(duì)el進(jìn)行預(yù)處理,便于后續(xù)對(duì)標(biāo)簽上的指令和屬性進(jìn)行處理,然后進(jìn)行樹結(jié)構(gòu)的構(gòu)建,確定el的root, parent, children等屬性??偨Y(jié)下來(lái)就是生成樹節(jié)點(diǎn),構(gòu)建樹結(jié)構(gòu)(關(guān)聯(lián)樹節(jié)點(diǎn))。cloneASTElement // 轉(zhuǎn)換屬性,把數(shù)組屬性轉(zhuǎn)換成對(duì)象屬性,返回對(duì)象 AST元素text // 為虛擬dom添加textContent 屬性html // 為虛擬dom添加innerHTML 屬性baseOptionsvar baseOptions = { expectHTML: true, //標(biāo)志 是html modules: modules$1, //為虛擬dom添加staticClass,classBinding,staticStyle,styleBinding,for, //alias,iterator1,iterator2,addRawAttr ,type ,key, ref,slotName //或者slotScope或者slot,component或者inlineTemplate ,plain,if ,else,elseif 屬性 directives: directives$1, //根據(jù)判斷虛擬dom的標(biāo)簽類型是什么?給相應(yīng)的標(biāo)簽綁定 相應(yīng)的 v-model 雙數(shù)據(jù)綁定代碼函數(shù), //為虛擬dom添加textContent 屬性,為虛擬dom添加innerHTML 屬性 isPreTag: isPreTag, // 判斷標(biāo)簽是否是 pre isUnaryTag: isUnaryTag, // 匹配標(biāo)簽是否是area,base,br,col,embed,frame,hr,img,input, // isindex,keygen, link,meta,param,source,track,wbr mustUseProp: mustUseProp, canBeLeftOpenTag: canBeLeftOpenTag,// 判斷標(biāo)簽是否是 colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source isReservedTag: isReservedTag, // 保留標(biāo)簽 判斷是不是真的是 html 原有的標(biāo)簽 或者svg標(biāo)簽 getTagNamespace: getTagNamespace, // 判斷 tag 是否是svg或者math 標(biāo)簽 staticKeys: genStaticKeys(modules$1) // 把數(shù)組對(duì)象 [{ staticKeys:1},{staticKeys:2},{staticKeys:3}]連接數(shù)組對(duì)象中的 staticKeys key值,連接成一個(gè)字符串 str=‘1,2,3’ }; genStaticKeysCached

    第 10607 行至第 10731 行

    /** * Goal of the optimizer: walk the generated template AST tree * and detect sub-trees that are purely static, i.e. parts of * the DOM that never needs to change. * * Once we detect these sub-trees, we can: * * 1. Hoist them into constants, so that we no longer need to * create fresh nodes for them on each re-render; * 2. Completely skip them in the patching process. */ // 優(yōu)化器的目標(biāo):遍歷生成的模板AST樹檢測(cè)純靜態(tài)的子樹,即永遠(yuǎn)不需要更改的DOM。 // 一旦我們檢測(cè)到這些子樹,我們可以: // 1。把它們變成常數(shù),這樣我們就不需要了 // 在每次重新渲染時(shí)為它們創(chuàng)建新的節(jié)點(diǎn); // 2。在修補(bǔ)過(guò)程中完全跳過(guò)它們。 optimize // !important:過(guò) parse 過(guò)程后,會(huì)輸出生成 AST 樹,接下來(lái)需要對(duì)這顆樹做優(yōu)化。即這里的 optimize // 循環(huán)遞歸虛擬node,標(biāo)記是不是靜態(tài)節(jié)點(diǎn) // 根據(jù)node.static或者 node.once 標(biāo)記staticRoot的狀態(tài)genStaticKeys$1markStatic$1 // 標(biāo)準(zhǔn)靜態(tài)節(jié)點(diǎn)markStaticRoots // 標(biāo)注靜態(tài)根(重要)isStatic // isBuiltInTag(即tag為component 和slot)的節(jié)點(diǎn)不會(huì)被標(biāo)注為靜態(tài)節(jié)點(diǎn),isPlatformReservedTag(即平臺(tái)原生標(biāo)簽,web 端如 h1 、div標(biāo)簽等)也不會(huì)被標(biāo)注為靜態(tài)節(jié)點(diǎn)。isDirectChildOfTemplateFor

    階段小結(jié)

    簡(jiǎn)單來(lái)說(shuō):整個(gè) optimize 的過(guò)程實(shí)際上就干 2 件事情,markStatic(root) 標(biāo)記靜態(tài)節(jié)點(diǎn) ,markStaticRoots(root, false) 標(biāo)記靜態(tài)根節(jié)點(diǎn)。

    那么被判斷為靜態(tài)根節(jié)點(diǎn)的條件是什么?

    該節(jié)點(diǎn)的所有子孫節(jié)點(diǎn)都是靜態(tài)節(jié)點(diǎn)(判斷為靜態(tài)節(jié)點(diǎn)要滿足 7 個(gè)判斷,詳見)必須存在子節(jié)點(diǎn)子節(jié)點(diǎn)不能只有一個(gè)純文本節(jié)點(diǎn)

    其實(shí),markStaticRoots()方法針對(duì)的都是普通標(biāo)簽節(jié)點(diǎn)。表達(dá)式節(jié)點(diǎn)與純文本節(jié)點(diǎn)都不在考慮范圍內(nèi)。

    markStatic()得出的static屬性,在該方法中用上了。將每個(gè)節(jié)點(diǎn)都判斷了一遍static屬性之后,就可以更快地確定靜態(tài)根節(jié)點(diǎn):通過(guò)判斷對(duì)應(yīng)節(jié)點(diǎn)是否是靜態(tài)節(jié)點(diǎn) 且 內(nèi)部有子元素 且 單一子節(jié)點(diǎn)的元素類型不是文本類型。

    只有純文本子節(jié)點(diǎn)時(shí),他是靜態(tài)節(jié)點(diǎn),但不是靜態(tài)根節(jié)點(diǎn)。靜態(tài)根節(jié)點(diǎn)是 optimize 優(yōu)化的條件,沒有靜態(tài)根節(jié)點(diǎn),說(shuō)明這部分不會(huì)被優(yōu)化。

    Q:為什么子節(jié)點(diǎn)的元素類型是靜態(tài)文本類型,就會(huì)給 optimize 過(guò)程加大成本呢?

    A:optimize 過(guò)程中做這個(gè)靜態(tài)根節(jié)點(diǎn)的優(yōu)化目是:在 patch 過(guò)程中,減少不必要的比對(duì)過(guò)程,加速更新。但是需要以下成本

    維護(hù)靜態(tài)模板的存儲(chǔ)對(duì)象

    一開始的時(shí)候,所有的靜態(tài)根節(jié)點(diǎn) 都會(huì)被解析生成 VNode,并且被存在一個(gè)緩存對(duì)象中,就在 Vue.proto._staticTree 中。 隨著靜態(tài)根節(jié)點(diǎn)的增加,這個(gè)存儲(chǔ)對(duì)象也會(huì)越來(lái)越大,那么占用的內(nèi)存就會(huì)越來(lái)越多 勢(shì)必要減少一些不必要的存儲(chǔ),所有只有純文本的靜態(tài)根節(jié)點(diǎn)就被排除了

    多層render函數(shù)調(diào)用

    這個(gè)過(guò)程涉及到實(shí)際操作更新的過(guò)程。在實(shí)際render 的過(guò)程中,針對(duì)靜態(tài)節(jié)點(diǎn)的操作也需要調(diào)用對(duì)應(yīng)的靜態(tài)節(jié)點(diǎn)渲染函數(shù),做一定的判斷邏輯。這里需要一定的消耗。

    純文本直接對(duì)比即可,不進(jìn)行 optimize 將會(huì)更高效。

    第 10733 行至第 10915 行

    // KeyboardEvent.keyCode aliases

    keyCodes // 內(nèi)置按鍵keyNamesgenGuard // genGuard = condition => if(${condition})return null;modifierCode //m odifierCode生成內(nèi)置修飾符的處理genHandlersgenHandler // 調(diào)用genHandler處理events[name],events[name]可能是數(shù)組也可能是獨(dú)立對(duì)象,取決于name是否有多個(gè)處理函數(shù)。genKeyFilter // genKeyFilter用于生成一段過(guò)濾的字符串:genFilterCode // 在 genKeyFilter 里被調(diào)用onbind$1baseDirectives // CodegenState 里的工具函數(shù)

    不管是組件還是普通標(biāo)簽,事件處理代碼都在genData的過(guò)程中,和之前分析原生事件一致,genHandlers用來(lái)處理事件對(duì)象并拼接成字符串。

    第 10921 行至第 11460 行

    // generate(ast, options)

    export function generate ( ast: ASTElement | void, options: CompilerOptions ): CodegenResult { const state = new CodegenState(options) const code = ast ? genElement(ast, state) : _c("div") return { render: `with(this){return ${code}}`, staticRenderFns: state.staticRenderFns } } CodegenStategenerate // !importantgenElementexport function genElement (el: ASTElement, state: CodegenState): string { if (el.parent) { el.pre = el.pre || el.parent.pre } if (el.staticRoot && !el.staticProcessed) { // 如果是一個(gè)靜態(tài)的樹, 如 <div id="app">123</div> // 生成_m()方法 // 靜態(tài)的渲染函數(shù)被保存至staticRenderFns屬性中 return genStatic(el, state) } else if (el.once && !el.onceProcessed) { // v-once 轉(zhuǎn)化為_o()方法 return genOnce(el, state) } else if (el.for && !el.forProcessed) { // _l() return genFor(el, state) } else if (el.if && !el.ifProcessed) { // v-if 會(huì)轉(zhuǎn)換為表達(dá)式 return genIf(el, state) } else if (el.tag === template && !el.slotTarget && !state.pre) { // 如果是template,處理子節(jié)點(diǎn) return genChildren(el, state) || void 0 } else if (el.tag === slot) { // 如果是插槽,處理slot return genSlot(el, state) } else { // component or element let code // 如果是組件,處理組件 if (el.component) { code = genComponent(el.component, el, state) } else { let data if (!el.plain || (el.pre && state.maybeComponent(el))) { data = genData(el, state) } const children = el.inlineTemplate ? null : genChildren(el, state, true) code = `_c(${el.tag}${ data ? `,${data}` : // data }${ children ? `,${children}` : // children })` } // module transforms for (let i = 0; i < state.transforms.length; i++) { code = state.transforms[i](el, code) } return code } } genStatic // genStatic會(huì)將ast轉(zhuǎn)化為_m()方法genOnce // 如果v-once在v-for中,那么就會(huì)生成_o()方法, 否則將其視為靜態(tài)節(jié)點(diǎn)genIf // genIf會(huì)將v-if轉(zhuǎn)換為表達(dá)式,示例如下genIfConditionsgenFor // v-for會(huì)轉(zhuǎn)換為_l()genData$2genDirectives // genData() 里調(diào)用genInlineTemplate // genData() 里調(diào)用genScopedSlots // genData() 里調(diào)用genScopedSlotgenChildren // 處理子節(jié)點(diǎn)getNormalizationType // 用于判斷是否需要規(guī)范化genNode // 處理 NodegenText // 處理 TextgenCommentgenSlot // 處理插槽genComponent // 處理組件genProps // 處理 propstransformSpecialNewlines

    這里面的邏輯、細(xì)節(jié)太多了,不做贅述,有興趣了解的童鞋可以去看推薦閱讀

    階段小結(jié)

    generate方法內(nèi)部邏輯還是很復(fù)雜的,但僅做了一件事情,就是將ast轉(zhuǎn)化為render函數(shù)的字符串,形成一個(gè)嵌套結(jié)構(gòu)的方法,模版編譯生成的_c(),_m(),_l等等其實(shí)都是生成vnode的方法,在執(zhí)行vue.$mount方法的時(shí)候,會(huì)調(diào)用vm._update(vm._render(), hydrating)方法,此時(shí)_render()中方法會(huì)執(zhí)行生成的render()函數(shù),執(zhí)行后會(huì)生成vnode,也就是虛擬dom節(jié)點(diǎn)。

    第 11466 行至第 11965 行

    prohibitedKeywordRE // 正則校驗(yàn):禁止關(guān)鍵字unaryOperatorsRE // 正則校驗(yàn):一元表達(dá)式操作stripStringRE // 正則校驗(yàn):腳本字符串detectErrors // 檢測(cè)錯(cuò)誤工具函數(shù)checkNode // 檢查 NodecheckEvent // 檢查 EventcheckFor // 檢查 For 循環(huán)checkIdentifier // 檢查 IdentifiercheckExpression // 檢查表達(dá)式checkFunctionParameterExpression // 檢查函數(shù)表達(dá)式generateCodeFramerepeat$1createFunction // 構(gòu)建函數(shù)createCompileToFunctionFn // 構(gòu)建編譯函數(shù)compile // !importantreturn function createCompiler (baseOptions) { function compile ( template, options ) { var finalOptions = Object.create(baseOptions); var errors = []; var tips = []; var warn = function (msg, range, tip) { (tip ? tips : errors).push(msg); }; if (options) { if (options.outputSourceRange) { // $flow-disable-line var leadingSpaceLength = template.match(/^\s*/)[0].length; warn = function (msg, range, tip) { var data = { msg: msg }; if (range) { if (range.start != null) { data.start = range.start + leadingSpaceLength; } if (range.end != null) { data.end = range.end + leadingSpaceLength; } } (tip ? tips : errors).push(data); }; } // merge custom modules if (options.modules) { finalOptions.modules = (baseOptions.modules || []).concat(options.modules); } // merge custom directives if (options.directives) { finalOptions.directives = extend( Object.create(baseOptions.directives || null), options.directives ); } // copy other options for (var key in options) { if (key !== modules && key !== directives) { finalOptions[key] = options[key]; } } } finalOptions.warn = warn; var compiled = baseCompile(template.trim(), finalOptions); { detectErrors(compiled.ast, warn); } compiled.errors = errors; compiled.tips = tips; return compiled }

    再看這張圖,對(duì)于“模板編譯”是不是有一種新的感覺了。

    compileToFunctions

    // 最后的最后

    return Vue;

    哇!歷時(shí)一個(gè)月左右,我終于完成啦?。。?/span>

    完結(jié)撒花!激動(dòng) + 釋然 + 感恩 + 小滿足 + ...... ??ヽ(°▽°)ノ?

    這生啃給我牙齒都啃酸了??!

    總結(jié)

    emmm,本來(lái)打算再多修補(bǔ)一下,但是看到 vue3 的源碼解析已有版本出來(lái)啦(扶朕起來(lái),朕還能學(xué)),時(shí)不我待,Vue3 奧利給,干就完了!

    后續(xù)仍會(huì)完善此文,您的點(diǎn)贊是我最大的動(dòng)力!也望大家不吝賜教,不吝贊美~

    最最最最后,還是那句老話,與君共勉:

    紙上得來(lái)終覺淺 絕知此事要躬行

    我是掘金安東尼: 一名人氣前端技術(shù)博主(文章 100w+ 閱讀量)

    終身寫作者(INFP 寫作人格)???

    堅(jiān)持與熱愛(簡(jiǎn)書打卡 1000 日)

    陪你一起度過(guò)漫長(zhǎng)技術(shù)歲月吧(以夢(mèng)為馬)

    覺得不錯(cuò),點(diǎn)個(gè)贊吧(您的三連,我最大的動(dòng)力 )

    掃描二維碼推送至手機(jī)訪問(wèn)。

    版權(quán)聲明:本文由財(cái)神資訊-領(lǐng)先的體育資訊互動(dòng)媒體轉(zhuǎn)載發(fā)布,如需刪除請(qǐng)聯(lián)系。

    本文鏈接:http://www.daniuzhishi.com/?id=44539

    “Vue(v2.6.11)萬(wàn)行源碼生啃,就硬剛” 的相關(guān)文章

    足彩小白看懂此文帶你快速了解足彩賠率的意義

    最近上網(wǎng)發(fā)現(xiàn)網(wǎng)上的足彩干貨五花八門 但是看下去會(huì)發(fā)現(xiàn),各種不適應(yīng),感覺無(wú)從下手,一臉懵逼,根本不知道如何下手 我個(gè)人認(rèn)為 自己在足彩方面還是有點(diǎn)水平的 兜兜轉(zhuǎn)轉(zhuǎn)10多年 亞盤到歐賠 起起落落數(shù)百次 雖然不算是個(gè)高手 但是相信比大多數(shù)人更有資格分享干貨,有說(shuō)的不對(duì)的地方也別急著抨擊我...

    2020年世界足球聯(lián)賽排名:中超聯(lián)賽世界排名第43名,亞洲第4名

    2020年世界足球聯(lián)賽排名:中超聯(lián)賽世界排名第43名,亞洲第4名

      記得以前有許多球迷調(diào)侃:中超聯(lián)賽是世界第六大聯(lián)賽,這也反映出了當(dāng)年中超隨著大牌球星的加盟,中超聯(lián)賽的知名度與關(guān)注度大幅度提升,但是由于足協(xié)出臺(tái)了投資帽與工資帽,所以說(shuō)許多大牌球星出走,這導(dǎo)致中超的知名度、比賽觀賞性、球迷數(shù)量等下降,中超聯(lián)賽在世界足球聯(lián)賽的排名肯定會(huì)受影響的。...

    德國(guó)vs西班牙:拜仁打巴薩,西班牙隊(duì)能否狠狠踩老對(duì)手一腳?

    德國(guó)vs西班牙:拜仁打巴薩,西班牙隊(duì)能否狠狠踩老對(duì)手一腳?

    北京時(shí)間11月27日凌晨3點(diǎn),世界杯E組第二輪比賽,首輪落敗的德國(guó)隊(duì)將迎戰(zhàn)西班牙隊(duì)。本場(chǎng)比賽是德國(guó)隊(duì)的生死大戰(zhàn),如果不能取勝他們很可能繼上屆世界杯之后再次小組出局。 誰(shuí)來(lái)拯救德國(guó)隊(duì)? 德國(guó)隊(duì)在小組賽首輪中1 比2負(fù)于日本隊(duì),可以說(shuō)是爆了本屆世界杯...

    (體育·國(guó)際足球)西甲綜合:皇馬2:0勝赫塔菲逼近西甲冠軍寶座

    新華社馬德里4月9日電(謝宇智)2021-2022賽季西班牙足球甲級(jí)聯(lián)賽第31輪9日展開,皇家馬德里主場(chǎng)2:0擊敗赫塔菲,向著西甲冠軍再邁進(jìn)一步。 皇馬本輪坐鎮(zhèn)伯納烏迎戰(zhàn)赫塔菲,卡塞米羅和盧卡斯·巴斯克斯各入一球,助球隊(duì)在主場(chǎng)拿到3分。本場(chǎng)勝利后,皇馬在西甲積分榜上以22勝6平3負(fù)...

    看球指南:關(guān)于足球場(chǎng)的這些小知識(shí) 你都知道嗎?

    看球指南:關(guān)于足球場(chǎng)的這些小知識(shí) 你都知道嗎?

    世界杯馬上開始了,經(jīng)??辞虻哪慊蛘哒郎?zhǔn)備看世界杯的你,知道多少關(guān)于足球場(chǎng)的小知識(shí)?你知道世界杯的球場(chǎng)是多大的嗎?你知道國(guó)際比賽的足球場(chǎng)有哪些要求嗎?你知道點(diǎn)球點(diǎn)有多遠(yuǎn)嗎?一起來(lái)看逗逗逗侃球與您分享關(guān)于足球場(chǎng)的小知識(shí)。 足球場(chǎng)大?。?標(biāo)準(zhǔn)的11人制足球場(chǎng):長(zhǎng)度90~120米,...

    10個(gè)國(guó)際足聯(lián)標(biāo)準(zhǔn)球場(chǎng)建成試運(yùn)營(yíng),就在珠海這里……

    10個(gè)國(guó)際足聯(lián)標(biāo)準(zhǔn)球場(chǎng)建成試運(yùn)營(yíng),就在珠海這里……

    在金灣區(qū)平沙鎮(zhèn)南新大道旁,一片片開闊平坦的綠茵相連,引人注目。這里就是珠海市索卡體育文旅綜合產(chǎn)業(yè)基地一期工程(索卡體育訓(xùn)練基地),近日,該項(xiàng)目首期建設(shè)的13個(gè)足球場(chǎng)中,目前已有10個(gè)建成投入試運(yùn)營(yíng)。 首期13個(gè)足球場(chǎng)均為11人制標(biāo)準(zhǔn)足球場(chǎng)。 該基地的足球訓(xùn)練場(chǎng)按照國(guó)際足聯(lián)A...

    ?