摘要:根據(jù)樹(shù)生成所需的內(nèi)部包含與首先會(huì)將模板進(jìn)行得到一個(gè)語(yǔ)法樹(shù),再通過(guò)做一些優(yōu)化,最后通過(guò)得到以及。會(huì)用正則等方式解析模板中的指令等數(shù)據(jù),形成語(yǔ)法樹(shù)。是將語(yǔ)法樹(shù)轉(zhuǎn)化成字符串的過(guò)程,得到結(jié)果是的字符串以及字符串。
寫(xiě)在前面
這篇文章算是對(duì)最近寫(xiě)的一系列Vue.js源碼的文章(https://github.com/answershuto/learnVue)的總結(jié)吧,在閱讀源碼的過(guò)程中也確實(shí)受益匪淺,希望自己的這些產(chǎn)出也會(huì)對(duì)同樣想要學(xué)習(xí)Vue.js源碼的小伙伴有所幫助。之前這篇文章同樣在我司(大搜車)的技術(shù)博客中發(fā)表過(guò),歡迎大家關(guān)注我司的技術(shù)博客,給個(gè)傳送門https://blog.souche.com/。
因?yàn)閷?duì)Vue.js很感興趣,而且平時(shí)工作的技術(shù)棧也是Vue.js,這幾個(gè)月花了些時(shí)間研究學(xué)習(xí)了一下Vue.js源碼,并做了總結(jié)與輸出。
文章的原地址:https://github.com/answershuto/learnVue。
在學(xué)習(xí)過(guò)程中,為Vue加上了中文的注釋https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以對(duì)其他想學(xué)習(xí)Vue源碼的小伙伴有所幫助。
可能會(huì)有理解存在偏差的地方,歡迎提issue指出,共同學(xué)習(xí),共同進(jìn)步。
從new一個(gè)Vue對(duì)象開(kāi)始let vm = new Vue({ el: "#app", /*some options*/ });
很多同學(xué)好奇,在new一個(gè)Vue對(duì)象的時(shí)候,內(nèi)部究竟發(fā)生了什么?
究竟Vue.js是如何將data中的數(shù)據(jù)渲染到真實(shí)的宿主環(huán)境環(huán)境中的?
又是如何通過(guò)“響應(yīng)式”修改數(shù)據(jù)的?
template是如何被編譯成真實(shí)環(huán)境中可用的HTML的?
Vue指令又是執(zhí)行的?
帶著這些疑問(wèn),我們從Vue的構(gòu)造類開(kāi)始看起。
Vue構(gòu)造類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) }
Vue的構(gòu)造類只做了一件事情,就是調(diào)用_init函數(shù)進(jìn)行
來(lái)看一下init的代碼
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-init:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to avoid this being observed /*一個(gè)防止vm實(shí)例自身被觀察的標(biāo)志位*/ vm._isVue = true // merge options 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 { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== "production") { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm /*初始化生命周期*/ initLifecycle(vm) /*初始化事件*/ initEvents(vm) /*初始化render*/ initRender(vm) /*調(diào)用beforeCreate鉤子函數(shù)并且觸發(fā)beforeCreate鉤子事件*/ callHook(vm, "beforeCreate") initInjections(vm) // resolve injections before data/props /*初始化props、methods、data、computed與watch*/ initState(vm) initProvide(vm) // resolve provide after data/props /*調(diào)用created鉤子函數(shù)并且觸發(fā)created鉤子事件*/ callHook(vm, "created") /* istanbul ignore if */ if (process.env.NODE_ENV !== "production" && config.performance && mark) { /*格式化組件名*/ vm._name = formatComponentName(vm, false) mark(endTag) measure(`${vm._name} init`, startTag, endTag) } if (vm.$options.el) { /*掛載組件*/ vm.$mount(vm.$options.el) } }
_init主要做了這兩件事:
1.初始化(包括生命周期、事件、render函數(shù)、state等)。
2.$mount組件。
在生命鉤子beforeCreate與created之間會(huì)初始化state,在此過(guò)程中,會(huì)依次初始化props、methods、data、computed與watch,這也就是Vue.js對(duì)options中的數(shù)據(jù)進(jìn)行“響應(yīng)式化”(即雙向綁定)的過(guò)程。對(duì)于Vue.js響應(yīng)式原理不了解的同學(xué)可以先看一下筆者的另一片文章《Vue.js響應(yīng)式原理》。
/*初始化props、methods、data、computed與watch*/ export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options /*初始化props*/ if (opts.props) initProps(vm, opts.props) /*初始化方法*/ if (opts.methods) initMethods(vm, opts.methods) /*初始化data*/ if (opts.data) { initData(vm) } else { /*該組件沒(méi)有data的時(shí)候綁定一個(gè)空對(duì)象*/ observe(vm._data = {}, true /* asRootData */) } /*初始化computed*/ if (opts.computed) initComputed(vm, opts.computed) /*初始化watchers*/ if (opts.watch) initWatch(vm, opts.watch) }雙向綁定
以initData為例,對(duì)option的data的數(shù)據(jù)進(jìn)行雙向綁定Oberver,其他option參數(shù)雙向綁定的核心原理是一致的。
function initData (vm: Component) { /*得到data數(shù)據(jù)*/ let data = vm.$options.data data = vm._data = typeof data === "function" ? getData(data, vm) : data || {} /*判斷是否是對(duì)象*/ if (!isPlainObject(data)) { data = {} process.env.NODE_ENV !== "production" && warn( "data functions should return an object: " + "https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function", vm ) } // proxy data on instance /*遍歷data對(duì)象*/ const keys = Object.keys(data) const props = vm.$options.props let i = keys.length //遍歷data中的數(shù)據(jù) while (i--) { /*保證data中的key不與props中的key重復(fù),props優(yōu)先,如果有沖突會(huì)產(chǎn)生warning*/ if (props && hasOwn(props, keys[i])) { process.env.NODE_ENV !== "production" && warn( `The data property "${keys[i]}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else if (!isReserved(keys[i])) { /*判斷是否是保留字段*/ /*這里是我們前面講過(guò)的代理,將data上面的屬性代理到了vm實(shí)例上*/ proxy(vm, `_data`, keys[i]) } } /*Github:https://github.com/answershuto*/ // observe data /*從這里開(kāi)始我們要observe了,開(kāi)始對(duì)數(shù)據(jù)進(jìn)行綁定,這里有尤大大的注釋asRootData,這步作為根數(shù)據(jù),下面會(huì)進(jìn)行遞歸observe進(jìn)行對(duì)深層對(duì)象的綁定。*/ observe(data, true /* asRootData */) }
observe會(huì)通過(guò)defineReactive對(duì)data中的對(duì)象進(jìn)行雙向綁定,最終通過(guò)Object.defineProperty對(duì)對(duì)象設(shè)置setter以及getter的方法。getter的方法主要用來(lái)進(jìn)行依賴收集,對(duì)于依賴收集不了解的同學(xué)可以參考筆者的另一篇文章《依賴收集》。setter方法會(huì)在對(duì)象被修改的時(shí)候觸發(fā)(不存在添加屬性的情況,添加屬性請(qǐng)用Vue.set),這時(shí)候setter會(huì)通知閉包中的Dep,Dep中有一些訂閱了這個(gè)對(duì)象改變的Watcher觀察者對(duì)象,Dep會(huì)通知Watcher對(duì)象更新視圖。
如果是修改一個(gè)數(shù)組的成員,該成員是一個(gè)對(duì)象,那只需要遞歸對(duì)數(shù)組的成員進(jìn)行雙向綁定即可。但這時(shí)候出現(xiàn)了一個(gè)問(wèn)題,?如果我們進(jìn)行pop、push等操作的時(shí)候,push進(jìn)去的對(duì)象根本沒(méi)有進(jìn)行過(guò)雙向綁定,更別說(shuō)pop了,那么我們?nèi)绾伪O(jiān)聽(tīng)數(shù)組的這些變化呢?
Vue.js提供的方法是重寫(xiě)push、pop、shift、unshift、splice、sort、reverse這七個(gè)數(shù)組方法。修改數(shù)組原型方法的代碼可以參考o(jì)bserver/array.js以及observer/index.js。
export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor (value: any) { //....... if (Array.isArray(value)) { /* 如果是數(shù)組,將修改后可以截獲響應(yīng)的數(shù)組方法替換掉該數(shù)組的原型中的原生方法,達(dá)到監(jiān)聽(tīng)數(shù)組數(shù)據(jù)變化響應(yīng)的效果。 這里如果當(dāng)前瀏覽器支持__proto__屬性,則直接覆蓋當(dāng)前數(shù)組對(duì)象原型上的原生數(shù)組方法,如果不支持該屬性,則直接覆蓋數(shù)組對(duì)象的原型。 */ const augment = hasProto ? protoAugment /*直接覆蓋原型的方法來(lái)修改目標(biāo)對(duì)象*/ : copyAugment /*定義(覆蓋)目標(biāo)對(duì)象或數(shù)組的某一個(gè)方法*/ augment(value, arrayMethods, arrayKeys) /*如果是數(shù)組則需要遍歷數(shù)組的每一個(gè)成員進(jìn)行observe*/ this.observeArray(value) } else { /*如果是對(duì)象則直接walk進(jìn)行綁定*/ this.walk(value) } } } /** * Augment an target Object or Array by intercepting * the prototype chain using __proto__ */ /*直接覆蓋原型的方法來(lái)修改目標(biāo)對(duì)象或數(shù)組*/ function protoAugment (target, src: Object) { /* eslint-disable no-proto */ target.__proto__ = src /* eslint-enable no-proto */ } /** * Augment an target Object or Array by defining * hidden properties. */ /* istanbul ignore next */ /*定義(覆蓋)目標(biāo)對(duì)象或數(shù)組的某一個(gè)方法*/ function copyAugment (target: Object, src: Object, keys: Array) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) } }
/* * not type checking this file because flow doesn"t play well with * dynamically accessing methods on Array prototype */ import { def } from "../util/index" /*取得原生數(shù)組的原型*/ const arrayProto = Array.prototype /*創(chuàng)建一個(gè)新的數(shù)組對(duì)象,修改該對(duì)象上的數(shù)組的七個(gè)方法,防止污染原生數(shù)組方法*/ export const arrayMethods = Object.create(arrayProto) /** * Intercept mutating methods and emit events */ /*這里重寫(xiě)了數(shù)組的這些方法,在保證不污染原生數(shù)組原型的情況下重寫(xiě)數(shù)組的這些方法,截獲數(shù)組的成員發(fā)生的變化,執(zhí)行原生數(shù)組操作的同時(shí)dep通知關(guān)聯(lián)的所有觀察者進(jìn)行響應(yīng)式處理*/ ;[ "push", "pop", "shift", "unshift", "splice", "sort", "reverse" ] .forEach(function (method) { // cache original method /*將數(shù)組的原生方法緩存起來(lái),后面要調(diào)用*/ const original = arrayProto[method] def(arrayMethods, method, function mutator () { // avoid leaking arguments: // http://jsperf.com/closure-with-arguments let i = arguments.length const args = new Array(i) while (i--) { args[i] = arguments[i] } /*調(diào)用原生的數(shù)組方法*/ const result = original.apply(this, args) /*數(shù)組新插入的元素需要重新進(jìn)行observe才能響應(yīng)式*/ const ob = this.__ob__ let inserted switch (method) { case "push": inserted = args break case "unshift": inserted = args break case "splice": inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change /*dep通知所有注冊(cè)的觀察者進(jìn)行響應(yīng)式處理*/ ob.dep.notify() return result }) })
從數(shù)組的原型新建一個(gè)Object.create(arrayProto)對(duì)象,通過(guò)修改此原型可以保證原生數(shù)組方法不被污染。如果當(dāng)前瀏覽器支持__proto__這個(gè)屬性的話就可以直接覆蓋該屬性則使數(shù)組對(duì)象具有了重寫(xiě)后的數(shù)組方法。如果沒(méi)有該屬性的瀏覽器,則必須通過(guò)遍歷def所有需要重寫(xiě)的數(shù)組方法,這種方法效率較低,所以優(yōu)先使用第一種。
在保證不污染不覆蓋數(shù)組原生方法添加監(jiān)聽(tīng),主要做了兩個(gè)操作,第一是通知所有注冊(cè)的觀察者進(jìn)行響應(yīng)式處理,第二是如果是添加成員的操作,需要對(duì)新成員進(jìn)行observe。
但是修改了數(shù)組的原生方法以后我們還是沒(méi)法像原生數(shù)組一樣直接通過(guò)數(shù)組的下標(biāo)或者設(shè)置length來(lái)修改數(shù)組,Vue.js提供了$set()及$remove()方法。
對(duì)于更具體的講解數(shù)據(jù)雙向綁定以及Dep、Watcher的實(shí)現(xiàn)可以參考筆者的文章《從源碼角度再看數(shù)據(jù)綁定》。
template編譯在$mount過(guò)程中,如果是獨(dú)立構(gòu)建構(gòu)建,則會(huì)在此過(guò)程中將template編譯成render function。當(dāng)然,你也可以采用運(yùn)行時(shí)構(gòu)建。具體參考運(yùn)行時(shí)-編譯器-vs-只包含運(yùn)行時(shí)。
template是如何被編譯成render function的呢?
function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { /*parse解析得到ast樹(shù)*/ const ast = parse(template.trim(), options) /* 將AST樹(shù)進(jìn)行優(yōu)化 優(yōu)化的目標(biāo):生成模板AST樹(shù),檢測(cè)不需要進(jìn)行DOM改變的靜態(tài)子樹(shù)。 一旦檢測(cè)到這些靜態(tài)樹(shù),我們就能做以下這些事情: 1.把它們變成常數(shù),這樣我們就再也不需要每次重新渲染時(shí)創(chuàng)建新的節(jié)點(diǎn)了。 2.在patch的過(guò)程中直接跳過(guò)。 */ optimize(ast, options) /*根據(jù)ast樹(shù)生成所需的code(內(nèi)部包含render與staticRenderFns)*/ const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } }
baseCompile首先會(huì)將模板template進(jìn)行parse得到一個(gè)AST語(yǔ)法樹(shù),再通過(guò)optimize做一些優(yōu)化,最后通過(guò)generate得到render以及staticRenderFns。
parseparse的源碼可以參見(jiàn)https://github.com/answershuto/learnVue/blob/master/vue-src/compiler/parser/index.js#L53。
parse會(huì)用正則等方式解析template模板中的指令、class、style等數(shù)據(jù),形成AST語(yǔ)法樹(shù)。
optimizeoptimize的主要作用是標(biāo)記static靜態(tài)節(jié)點(diǎn),這是Vue在編譯過(guò)程中的一處優(yōu)化,后面當(dāng)update更新界面時(shí),會(huì)有一個(gè)patch的過(guò)程,diff算法會(huì)直接跳過(guò)靜態(tài)節(jié)點(diǎn),從而減少了比較的過(guò)程,優(yōu)化了patch的性能。
generategenerate是將AST語(yǔ)法樹(shù)轉(zhuǎn)化成render funtion字符串的過(guò)程,得到結(jié)果是render的字符串以及staticRenderFns字符串。
具體的template編譯實(shí)現(xiàn)請(qǐng)參考《聊聊Vue.js的template編譯》。
Watcher到視圖Watcher對(duì)象會(huì)通過(guò)調(diào)用updateComponent方法來(lái)達(dá)到更新視圖的目的。這里提一下,其實(shí)Watcher并不是實(shí)時(shí)更新視圖的,Vue.js默認(rèn)會(huì)將Watcher對(duì)象存在一個(gè)隊(duì)列中,在下一個(gè)tick時(shí)更新異步更新視圖,完成了性能優(yōu)化。關(guān)于nextTick感興趣的小伙伴可以參考《Vue.js異步更新DOM策略及nextTick》。
updateComponent = () => { vm._update(vm._render(), hydrating) }
updateComponent就執(zhí)行一句話,_render函數(shù)會(huì)返回一個(gè)新的Vnode節(jié)點(diǎn),傳入_update中與舊的VNode對(duì)象進(jìn)行對(duì)比,經(jīng)過(guò)一個(gè)patch的過(guò)程得到兩個(gè)VNode節(jié)點(diǎn)的差異,最后將這些差異渲染到真實(shí)環(huán)境形成視圖。
什么是VNode?
VNode在刀耕火種的年代,我們需要在各個(gè)事件方法中直接操作DOM來(lái)達(dá)到修改視圖的目的。但是當(dāng)應(yīng)用一大就會(huì)變得難以維護(hù)。
那我們是不是可以把真實(shí)DOM樹(shù)抽象成一棵以JavaScript對(duì)象構(gòu)成的抽象樹(shù),在修改抽象樹(shù)數(shù)據(jù)后將抽象樹(shù)轉(zhuǎn)化成真實(shí)DOM重繪到頁(yè)面上呢?于是虛擬DOM出現(xiàn)了,它是真實(shí)DOM的一層抽象,用屬性描述真實(shí)DOM的各個(gè)特性。當(dāng)它發(fā)生變化的時(shí)候,就會(huì)去修改視圖。
但是這樣的JavaScript操作DOM進(jìn)行重繪整個(gè)視圖層是相當(dāng)消耗性能的,我們是不是可以每次只更新它的修改呢?所以Vue.js將DOM抽象成一個(gè)以JavaScript對(duì)象為節(jié)點(diǎn)的虛擬DOM樹(shù),以VNode節(jié)點(diǎn)模擬真實(shí)DOM,可以對(duì)這顆抽象樹(shù)進(jìn)行創(chuàng)建節(jié)點(diǎn)、刪除節(jié)點(diǎn)以及修改節(jié)點(diǎn)等操作,在這過(guò)程中都不需要操作真實(shí)DOM,只需要操作JavaScript對(duì)象,大大提升了性能。修改以后經(jīng)過(guò)diff算法得出一些需要修改的最小單位,再將這些小單位的視圖進(jìn)行更新。這樣做減少了很多不需要的DOM操作,大大提高了性能。
Vue就使用了這樣的抽象節(jié)點(diǎn)VNode,它是對(duì)真實(shí)DOM的一層抽象,而不依賴某個(gè)平臺(tái),它可以是瀏覽器平臺(tái),也可以是weex,甚至是node平臺(tái)也可以對(duì)這樣一棵抽象DOM樹(shù)進(jìn)行創(chuàng)建刪除修改等操作,這也為前后端同構(gòu)提供了可能。
先來(lái)看一下Vue.js源碼中對(duì)VNode類的定義。
export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component"s scope functionalContext: Component | void; // only for functional component root nodes key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? constructor ( tag?: string, data?: VNodeData, children?: ?Array , text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions ) { /*當(dāng)前節(jié)點(diǎn)的標(biāo)簽名*/ this.tag = tag /*當(dāng)前節(jié)點(diǎn)對(duì)應(yīng)的對(duì)象,包含了具體的一些數(shù)據(jù)信息,是一個(gè)VNodeData類型,可以參考VNodeData類型中的數(shù)據(jù)信息*/ this.data = data /*當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn),是一個(gè)數(shù)組*/ this.children = children /*當(dāng)前節(jié)點(diǎn)的文本*/ this.text = text /*當(dāng)前虛擬節(jié)點(diǎn)對(duì)應(yīng)的真實(shí)dom節(jié)點(diǎn)*/ this.elm = elm /*當(dāng)前節(jié)點(diǎn)的名字空間*/ this.ns = undefined /*編譯作用域*/ this.context = context /*函數(shù)化組件作用域*/ this.functionalContext = undefined /*節(jié)點(diǎn)的key屬性,被當(dāng)作節(jié)點(diǎn)的標(biāo)志,用以優(yōu)化*/ this.key = data && data.key /*組件的option選項(xiàng)*/ this.componentOptions = componentOptions /*當(dāng)前節(jié)點(diǎn)對(duì)應(yīng)的組件的實(shí)例*/ this.componentInstance = undefined /*當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)*/ this.parent = undefined /*簡(jiǎn)而言之就是是否為原生HTML或只是普通文本,innerHTML的時(shí)候?yàn)閠rue,textContent的時(shí)候?yàn)閒alse*/ this.raw = false /*靜態(tài)節(jié)點(diǎn)標(biāo)志*/ this.isStatic = false /*是否作為跟節(jié)點(diǎn)插入*/ this.isRootInsert = true /*是否為注釋節(jié)點(diǎn)*/ this.isComment = false /*是否為克隆節(jié)點(diǎn)*/ this.isCloned = false /*是否有v-once指令*/ this.isOnce = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } }
這是一個(gè)最基礎(chǔ)的VNode節(jié)點(diǎn),作為其他派生VNode類的基類,里面定義了下面這些數(shù)據(jù)。
tag: 當(dāng)前節(jié)點(diǎn)的標(biāo)簽名
data: 當(dāng)前節(jié)點(diǎn)對(duì)應(yīng)的對(duì)象,包含了具體的一些數(shù)據(jù)信息,是一個(gè)VNodeData類型,可以參考VNodeData類型中的數(shù)據(jù)信息
children: 當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn),是一個(gè)數(shù)組
text: 當(dāng)前節(jié)點(diǎn)的文本
elm: 當(dāng)前虛擬節(jié)點(diǎn)對(duì)應(yīng)的真實(shí)dom節(jié)點(diǎn)
ns: 當(dāng)前節(jié)點(diǎn)的名字空間
context: 當(dāng)前節(jié)點(diǎn)的編譯作用域
functionalContext: 函數(shù)化組件作用域
key: 節(jié)點(diǎn)的key屬性,被當(dāng)作節(jié)點(diǎn)的標(biāo)志,用以優(yōu)化
componentOptions: 組件的option選項(xiàng)
componentInstance: 當(dāng)前節(jié)點(diǎn)對(duì)應(yīng)的組件的實(shí)例
parent: 當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)
raw: 簡(jiǎn)而言之就是是否為原生HTML或只是普通文本,innerHTML的時(shí)候?yàn)閠rue,textContent的時(shí)候?yàn)閒alse
isStatic: 是否為靜態(tài)節(jié)點(diǎn)
isRootInsert: 是否作為跟節(jié)點(diǎn)插入
isComment: 是否為注釋節(jié)點(diǎn)
isCloned: 是否為克隆節(jié)點(diǎn)
isOnce: 是否有v-once指令
打個(gè)比方,比如說(shuō)我現(xiàn)在有這么一個(gè)VNode樹(shù)
{ tag: "div" data: { class: "test" }, children: [ { tag: "span", data: { class: "demo" } text: "hello,VNode" } ] }
渲染之后的結(jié)果就是這樣的
hello,VNode
更多操作VNode的方法,請(qǐng)參考《VNode節(jié)點(diǎn)》。
patch最后_update會(huì)將新舊兩個(gè)VNode進(jìn)行一次patch的過(guò)程,得出兩個(gè)VNode最小的差異,然后將這些差異渲染到視圖上。
首先說(shuō)一下patch的核心diff算法,diff算法是通過(guò)同層的樹(shù)節(jié)點(diǎn)進(jìn)行比較而非對(duì)樹(shù)進(jìn)行逐層搜索遍歷的方式,所以時(shí)間復(fù)雜度只有O(n),是一種相當(dāng)高效的算法。
這兩張圖代表舊的VNode與新VNode進(jìn)行patch的過(guò)程,他們只是在同層級(jí)的VNode之間進(jìn)行比較得到變化(第二張圖中相同顏色的方塊代表互相進(jìn)行比較的VNode節(jié)點(diǎn)),然后修改變化的視圖,所以十分高效。
在patch的過(guò)程中,如果兩個(gè)VNode被認(rèn)為是同一個(gè)VNode(sameVnode),則會(huì)進(jìn)行深度的比較,得出最小差異,否則直接刪除舊有DOM節(jié)點(diǎn),創(chuàng)建新的DOM節(jié)點(diǎn)。
什么是sameVnode?
我們來(lái)看一下sameVnode的實(shí)現(xiàn)。
/* 判斷兩個(gè)VNode節(jié)點(diǎn)是否是同一個(gè)節(jié)點(diǎn),需要滿足以下條件 key相同 tag(當(dāng)前節(jié)點(diǎn)的標(biāo)簽名)相同 isComment(是否為注釋節(jié)點(diǎn))相同 是否data(當(dāng)前節(jié)點(diǎn)對(duì)應(yīng)的對(duì)象,包含了具體的一些數(shù)據(jù)信息,是一個(gè)VNodeData類型,可以參考VNodeData類型中的數(shù)據(jù)信息)都有定義 當(dāng)標(biāo)簽是的時(shí)候,type必須相同 */ function sameVnode (a, b) { return ( a.key === b.key && a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) } // Some browsers do not support dynamically changing type for // so they need to be treated as different nodes /* 判斷當(dāng)標(biāo)簽是的時(shí)候,type是否相同 某些瀏覽器不支持動(dòng)態(tài)修改類型,所以他們被視為不同類型 */ function sameInputType (a, b) { if (a.tag !== "input") return true let i const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type return typeA === typeB }
當(dāng)兩個(gè)VNode的tag、key、isComment都相同,并且同時(shí)定義或未定義data的時(shí)候,且如果標(biāo)簽為input則type必須相同。這時(shí)候這兩個(gè)VNode則算sameVnode,可以直接進(jìn)行patchVnode操作。
patchVnode的規(guī)則是這樣的:
1.如果新舊VNode都是靜態(tài)的,同時(shí)它們的key相同(代表同一節(jié)點(diǎn)),并且新的VNode是clone或者是標(biāo)記了once(標(biāo)記v-once屬性,只渲染一次),那么只需要替換elm以及componentInstance即可。
2.新老節(jié)點(diǎn)均有children子節(jié)點(diǎn),則對(duì)子節(jié)點(diǎn)進(jìn)行diff操作,調(diào)用updateChildren,這個(gè)updateChildren也是diff的核心。
3.如果老節(jié)點(diǎn)沒(méi)有子節(jié)點(diǎn)而新節(jié)點(diǎn)存在子節(jié)點(diǎn),先清空老節(jié)點(diǎn)DOM的文本內(nèi)容,然后為當(dāng)前DOM節(jié)點(diǎn)加入子節(jié)點(diǎn)。
4.當(dāng)新節(jié)點(diǎn)沒(méi)有子節(jié)點(diǎn)而老節(jié)點(diǎn)有子節(jié)點(diǎn)的時(shí)候,則移除該DOM節(jié)點(diǎn)的所有子節(jié)點(diǎn)。
5.當(dāng)新老節(jié)點(diǎn)都無(wú)子節(jié)點(diǎn)的時(shí)候,只是文本的替換。
updateChildrenfunction updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { 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 // removeOnly is a special flag used only by// to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { /*前四種情況其實(shí)是指定key的時(shí)候,判定為同一個(gè)VNode,則直接patchVnode即可,分別比較oldCh以及newCh的兩頭節(jié)點(diǎn)2*2=4種情況*/ 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)) { // Vnode moved right 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)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { /* 生成一個(gè)key與舊VNode的key對(duì)應(yīng)的哈希表(只有第一次進(jìn)來(lái)undefined的時(shí)候會(huì)生成,也為后面檢測(cè)重復(fù)的key值做鋪墊) 比如childre是這樣的 [{xx: xx, key: "key0"}, {xx: xx, key: "key1"}, {xx: xx, key: "key2"}] beginIdx = 0 endIdx = 2 結(jié)果生成{key0: 0, key1: 1, key2: 2} */ if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) /*如果newStartVnode新的VNode節(jié)點(diǎn)存在key并且這個(gè)key在oldVnode中能找到則返回這個(gè)節(jié)點(diǎn)的idxInOld(即第幾個(gè)節(jié)點(diǎn),下標(biāo))*/ idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null if (isUndef(idxInOld)) { // New element /*newStartVnode沒(méi)有key或者是該key沒(méi)有在老節(jié)點(diǎn)中找到則創(chuàng)建一個(gè)新的節(jié)點(diǎn)*/ createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { /*獲取同key的老節(jié)點(diǎn)*/ elmToMove = oldCh[idxInOld] /* istanbul ignore if */ if (process.env.NODE_ENV !== "production" && !elmToMove) { /*如果elmToMove不存在說(shuō)明之前已經(jīng)有新節(jié)點(diǎn)放入過(guò)這個(gè)key的DOM中,提示可能存在重復(fù)的key,確保v-for的時(shí)候item有唯一的key值*/ warn( "It seems there are duplicate keys that is causing an update error. " + "Make sure each v-for item has a unique key." ) } if (sameVnode(elmToMove, newStartVnode)) { /*Github:https://github.com/answershuto*/ /*如果新VNode與得到的有相同key的節(jié)點(diǎn)是同一個(gè)VNode則進(jìn)行patchVnode*/ patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) /*因?yàn)橐呀?jīng)patchVnode進(jìn)去了,所以將這個(gè)老節(jié)點(diǎn)賦值undefined,之后如果還有新節(jié)點(diǎn)與該節(jié)點(diǎn)key相同可以檢測(cè)出來(lái)提示已有重復(fù)的key*/ oldCh[idxInOld] = undefined /*當(dāng)有標(biāo)識(shí)位canMove實(shí)可以直接插入oldStartVnode對(duì)應(yīng)的真實(shí)DOM節(jié)點(diǎn)前面*/ canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { // same key but different element. treat as new element /*當(dāng)新的VNode與找到的同樣key的VNode不是sameVNode的時(shí)候(比如說(shuō)tag不一樣或者是有不一樣type的input標(biāo)簽),創(chuàng)建一個(gè)新的節(jié)點(diǎn)*/ createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } } } } if (oldStartIdx > oldEndIdx) { /*全部比較完成以后,發(fā)現(xiàn)oldStartIdx > oldEndIdx的話,說(shuō)明老節(jié)點(diǎn)已經(jīng)遍歷完了,新節(jié)點(diǎn)比老節(jié)點(diǎn)多,所以這時(shí)候多出來(lái)的新節(jié)點(diǎn)需要一個(gè)一個(gè)創(chuàng)建出來(lái)加入到真實(shí)DOM中*/ refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { /*如果全部比較完成以后發(fā)現(xiàn)newStartIdx > newEndIdx,則說(shuō)明新節(jié)點(diǎn)已經(jīng)遍歷完了,老節(jié)點(diǎn)多余新節(jié)點(diǎn),這個(gè)時(shí)候需要將多余的老節(jié)點(diǎn)從真實(shí)DOM中移除*/ removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
直接看源碼可能比較難以捋清其中的關(guān)系,我們通過(guò)圖來(lái)看一下。
首先,在新老兩個(gè)VNode節(jié)點(diǎn)的左右頭尾兩側(cè)都有一個(gè)變量標(biāo)記,在遍歷過(guò)程中這幾個(gè)變量都會(huì)向中間靠攏。當(dāng)oldStartIdx <= oldEndIdx或者newStartIdx <= newEndIdx時(shí)結(jié)束循環(huán)。
索引與VNode節(jié)點(diǎn)的對(duì)應(yīng)關(guān)系:
oldStartIdx => oldStartVnode
oldEndIdx => oldEndVnode
newStartIdx => newStartVnode
newEndIdx => newEndVnode
在遍歷中,如果存在key,并且滿足sameVnode,會(huì)將該DOM節(jié)點(diǎn)進(jìn)行復(fù)用,否則則會(huì)創(chuàng)建一個(gè)新的DOM節(jié)點(diǎn)。
首先,oldStartVnode、oldEndVnode與newStartVnode、newEndVnode兩兩比較一共有2*2=4種比較方法。
當(dāng)新老VNode節(jié)點(diǎn)的start或者end滿足sameVnode時(shí),也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接將該VNode節(jié)點(diǎn)進(jìn)行patchVnode即可。
如果oldStartVnode與newEndVnode滿足sameVnode,即sameVnode(oldStartVnode, newEndVnode)。
這時(shí)候說(shuō)明oldStartVnode已經(jīng)跑到了oldEndVnode后面去了,進(jìn)行patchVnode的同時(shí)還需要將真實(shí)DOM節(jié)點(diǎn)移動(dòng)到oldEndVnode的后面。
如果oldEndVnode與newStartVnode滿足sameVnode,即sameVnode(oldEndVnode, newStartVnode)。
這說(shuō)明oldEndVnode跑到了oldStartVnode的前面,進(jìn)行patchVnode的同時(shí)真實(shí)的DOM節(jié)點(diǎn)移動(dòng)到了oldStartVnode的前面。
如果以上情況均不符合,則通過(guò)createKeyToOldIdx會(huì)得到一個(gè)oldKeyToIdx,里面存放了一個(gè)key為舊的VNode,value為對(duì)應(yīng)index序列的哈希表。從這個(gè)哈希表中可以找到是否有與newStartVnode一致key的舊的VNode節(jié)點(diǎn),如果同時(shí)滿足sameVnode,patchVnode的同時(shí)會(huì)將這個(gè)真實(shí)DOM(elmToMove)移動(dòng)到oldStartVnode對(duì)應(yīng)的真實(shí)DOM的前面。
當(dāng)然也有可能newStartVnode在舊的VNode節(jié)點(diǎn)找不到一致的key,或者是即便key相同卻不是sameVnode,這個(gè)時(shí)候會(huì)調(diào)用createElm創(chuàng)建一個(gè)新的DOM節(jié)點(diǎn)。
到這里循環(huán)已經(jīng)結(jié)束了,那么剩下我們還需要處理多余或者不夠的真實(shí)DOM節(jié)點(diǎn)。
1.當(dāng)結(jié)束時(shí)oldStartIdx > oldEndIdx,這個(gè)時(shí)候老的VNode節(jié)點(diǎn)已經(jīng)遍歷完了,但是新的節(jié)點(diǎn)還沒(méi)有。說(shuō)明了新的VNode節(jié)點(diǎn)實(shí)際上比老的VNode節(jié)點(diǎn)多,也就是比真實(shí)DOM多,需要將剩下的(也就是新增的)VNode節(jié)點(diǎn)插入到真實(shí)DOM節(jié)點(diǎn)中去,此時(shí)調(diào)用addVnodes(批量調(diào)用createElm的接口將這些節(jié)點(diǎn)加入到真實(shí)DOM中去)。
2。同理,當(dāng)newStartIdx > newEndIdx時(shí),新的VNode節(jié)點(diǎn)已經(jīng)遍歷完了,但是老的節(jié)點(diǎn)還有剩余,說(shuō)明真實(shí)DOM節(jié)點(diǎn)多余了,需要從文檔中刪除,這時(shí)候調(diào)用removeVnodes將這些多余的真實(shí)DOM刪除。
更詳細(xì)的diff實(shí)現(xiàn)參考筆者的文章VirtualDOM與diff(Vue.js實(shí)現(xiàn)).MarkDown)。
映射到真實(shí)DOM由于Vue使用了虛擬DOM,所以虛擬DOM可以在任何支持JavaScript語(yǔ)言的平臺(tái)上操作,譬如說(shuō)目前Vue支持的瀏覽器平臺(tái)或是weex,在虛擬DOM的實(shí)現(xiàn)上是一致的。那么最后虛擬DOM如何映射到真實(shí)的DOM節(jié)點(diǎn)上呢?
Vue為平臺(tái)做了一層適配層,瀏覽器平臺(tái)見(jiàn)/platforms/web/runtime/node-ops.js以及weex平臺(tái)見(jiàn)/platforms/weex/runtime/node-ops.js。不同平臺(tái)之間通過(guò)適配層對(duì)外提供相同的接口,虛擬DOM進(jìn)行操作真實(shí)DOM節(jié)點(diǎn)的時(shí)候,只需要調(diào)用這些適配層的接口即可,而內(nèi)部實(shí)現(xiàn)則不需要關(guān)心,它會(huì)根據(jù)平臺(tái)的改變而改變。
現(xiàn)在又出現(xiàn)了一個(gè)問(wèn)題,我們只是將虛擬DOM映射成了真實(shí)的DOM。那如何給這些DOM加入attr、class、style等DOM屬性呢?
這要依賴于虛擬DOM的生命鉤子。虛擬DOM提供了如下的鉤子函數(shù),分別在不同的時(shí)期會(huì)進(jìn)行調(diào)用。
const hooks = ["create", "activate", "update", "remove", "destroy"] /*構(gòu)建cbs回調(diào)函數(shù),web平臺(tái)上見(jiàn)/platforms/web/runtime/modules*/ for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { cbs[hooks[i]].push(modules[j][hooks[i]]) } } }
同理,也會(huì)根據(jù)不同平臺(tái)有自己不同的實(shí)現(xiàn),我們這里以Web平臺(tái)為例。Web平臺(tái)的鉤子函數(shù)見(jiàn)/platforms/web/runtime/modules。里面有對(duì)attr、class、props、events、style以及transition(過(guò)渡狀態(tài))的DOM屬性進(jìn)行操作。
以attr為例,代碼很簡(jiǎn)單。
/* @flow */ import { isIE9 } from "core/util/env" import { extend, isDef, isUndef } from "shared/util" import { isXlink, xlinkNS, getXlinkProp, isBooleanAttr, isEnumeratedAttr, isFalsyAttrValue } from "web/util/index" /*更新attr*/ function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) { /*如果舊的以及新的VNode節(jié)點(diǎn)均沒(méi)有attr屬性,則直接返回*/ if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) { return } let key, cur, old /*VNode節(jié)點(diǎn)對(duì)應(yīng)的Dom實(shí)例*/ const elm = vnode.elm /*舊VNode節(jié)點(diǎn)的attr*/ const oldAttrs = oldVnode.data.attrs || {} /*新VNode節(jié)點(diǎn)的attr*/ let attrs: any = vnode.data.attrs || {} // clone observed objects, as the user probably wants to mutate it /*如果新的VNode的attr已經(jīng)有__ob__(代表已經(jīng)被Observe處理過(guò)了), 進(jìn)行深拷貝*/ if (isDef(attrs.__ob__)) { attrs = vnode.data.attrs = extend({}, attrs) } /*遍歷attr,不一致則替換*/ for (key in attrs) { cur = attrs[key] old = oldAttrs[key] if (old !== cur) { setAttr(elm, key, cur) } } // #4391: in IE9, setting type can reset value for input[type=radio] /* istanbul ignore if */ if (isIE9 && attrs.value !== oldAttrs.value) { setAttr(elm, "value", attrs.value) } for (key in oldAttrs) { if (isUndef(attrs[key])) { if (isXlink(key)) { elm.removeAttributeNS(xlinkNS, getXlinkProp(key)) } else if (!isEnumeratedAttr(key)) { elm.removeAttribute(key) } } } } /*設(shè)置attr*/ function setAttr (el: Element, key: string, value: any) { if (isBooleanAttr(key)) { // set attribute for blank value // e.g. if (isFalsyAttrValue(value)) { el.removeAttribute(key) } else { el.setAttribute(key, key) } } else if (isEnumeratedAttr(key)) { el.setAttribute(key, isFalsyAttrValue(value) || value === "false" ? "false" : "true") } else if (isXlink(key)) { if (isFalsyAttrValue(value)) { el.removeAttributeNS(xlinkNS, getXlinkProp(key)) } else { el.setAttributeNS(xlinkNS, key, value) } } else { if (isFalsyAttrValue(value)) { el.removeAttribute(key) } else { el.setAttribute(key, value) } } } export default { create: updateAttrs, update: updateAttrs }
attr只需要在create以及update鉤子被調(diào)用時(shí)更新DOM的attr屬性即可。
最后至此,我們已經(jīng)從template到真實(shí)DOM的整個(gè)過(guò)程梳理完了?,F(xiàn)在再去看這張圖,是不是更清晰了呢?
關(guān)于作者:染陌
Email:[email protected] or [email protected]
Github: https://github.com/answershuto
Blog:http://answershuto.github.io/
知乎主頁(yè):https://www.zhihu.com/people/cao-yang-49/activities
知乎專欄:https://zhuanlan.zhihu.com/ranmo
掘金: https://juejin.im/user/58f87ae844d9040069ca7507
osChina:https://my.oschina.net/u/3161824/blog
轉(zhuǎn)載請(qǐng)注明出處,謝謝。
歡迎關(guān)注我的公眾號(hào)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/89080.html
摘要:實(shí)際上,我在看代碼的過(guò)程中順手提交了這個(gè),作者眼明手快,當(dāng)天就進(jìn)行了修復(fù),現(xiàn)在最新的代碼里已經(jīng)不是這個(gè)樣子了而且狀態(tài)機(jī)標(biāo)識(shí)由字符串換成了數(shù)字常量,解析更準(zhǔn)確的同時(shí)執(zhí)行效率也會(huì)更高。 最近饒有興致的又把最新版?Vue.js?的源碼學(xué)習(xí)了一下,覺(jué)得真心不錯(cuò),個(gè)人覺(jué)得 Vue.js 的代碼非常之優(yōu)雅而且精辟,作者本身可能無(wú) (bu) 意 (xie) 提及這些。那么,就讓我來(lái)吧:) 程序結(jié)構(gòu)梳...
摘要:實(shí)際上,我在看代碼的過(guò)程中順手提交了這個(gè),作者眼明手快,當(dāng)天就進(jìn)行了修復(fù),現(xiàn)在最新的代碼里已經(jīng)不是這個(gè)樣子了而且狀態(tài)機(jī)標(biāo)識(shí)由字符串換成了數(shù)字常量,解析更準(zhǔn)確的同時(shí)執(zhí)行效率也會(huì)更高。 最近饒有興致的又把最新版?Vue.js?的源碼學(xué)習(xí)了一下,覺(jué)得真心不錯(cuò),個(gè)人覺(jué)得 Vue.js 的代碼非常之優(yōu)雅而且精辟,作者本身可能無(wú) (bu) 意 (xie) 提及這些。那么,就讓我來(lái)吧:) 程序結(jié)構(gòu)梳...
摘要:圖在中應(yīng)用三數(shù)據(jù)渲染過(guò)程數(shù)據(jù)綁定實(shí)現(xiàn)邏輯本節(jié)正式分析從到數(shù)據(jù)渲染到頁(yè)面的過(guò)程,在中定義了一個(gè)的構(gòu)造函數(shù)。一、概述 vue已是目前國(guó)內(nèi)前端web端三分天下之一,也是工作中主要技術(shù)棧之一。在日常使用中知其然也好奇著所以然,因此嘗試閱讀vue源碼并進(jìn)行總結(jié)。本文旨在梳理初始化頁(yè)面時(shí)data中的數(shù)據(jù)是如何渲染到頁(yè)面上的。本文將帶著這個(gè)疑問(wèn)一點(diǎn)點(diǎn)追究vue的思路。總體來(lái)說(shuō)vue模版渲染大致流程如圖1所...
摘要:當(dāng)我們的視圖和數(shù)據(jù)任何一方發(fā)生變化的時(shí)候,我們希望能夠通知對(duì)方也更新,這就是所謂的數(shù)據(jù)雙向綁定。返回值返回傳入函數(shù)的對(duì)象,即第一個(gè)參數(shù)該方法重點(diǎn)是描述,對(duì)象里目前存在的屬性描述符有兩種主要形式數(shù)據(jù)描述符和存取描述符。 前言 談起當(dāng)前前端最熱門的 js 框架,必少不了 Vue、React、Angular,對(duì)于大多數(shù)人來(lái)說(shuō),我們更多的是在使用框架,對(duì)于框架解決痛點(diǎn)背后使用的基本原理往往關(guān)注...
閱讀 1590·2021-11-17 09:33
閱讀 1140·2021-11-12 10:36
閱讀 2445·2019-08-30 15:54
閱讀 2462·2019-08-30 13:14
閱讀 2949·2019-08-26 14:05
閱讀 3318·2019-08-26 11:32
閱讀 3033·2019-08-26 10:09
閱讀 3026·2019-08-26 10:09