日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > vue >内容正文

vue

Vue源码探究笔记

發布時間:2024/7/5 vue 47 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Vue源码探究笔记 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

對于源代碼分析有一個基本原則:要找到它的最早期的版本,比如1.0版本。1.0版本奠定了一款框架的基礎結構,之后的版本迭代都是基于這套結構進行更新的。所以掌握了基礎結構,那也就掌握了這個框架。這個原則適用于世界上絕大多數事務:

  • 計算機基本組成結構
  • 汽車等各類交通工具的基本結構
  • Android等框架類的基本結構

所以基于以上原則,我在分析Vue源代碼時采用的是它的0.10版本,這是我能找到的最早的、也能順利運行的版本。

執行以下命令便可以得到0.10版本:

git clone https://github.com/vuejs/vue.gitgit checkout 0.10

之后便可以通過順手的IDE工具比如VS Code將這個項目加載,開始正式進入我們的解析過程。

本篇文章的目的

讀完這篇文章,你可以學到以下內容:

  • Vue對于JS文件的解析。
  • Vue對于DOM樹的解析。
  • 簡單的TEXT賦值更新事件的整個執行過程。

引用結構圖

一切從這張圖開始:
上面這張圖描述了Vue各個部分的引用關系,它有助于我們梳理Vue的主體結構。
從上圖中我們可以確認,compiler應當是Vue的核心部分。

分析所需要的環境

一切從我們熟悉的Vue用法開始說起,以下內容是摘自于項目中的./examples/commits文件夾:

// app.js var demo = new Vue({el: '#demo',data: {branch: 'master', title: 'tl'},created: function () {this.$watch('branch', function () {this.fetchData()})},filters: {truncate: function (v) {var newline = v.indexOf('\n')return newline > 0 ? v.slice(0, newline) : v},formatDate: function (v) {return v.replace(/T|Z/g, ' ')}},methods: {fetchData: function () {var xhr = new XMLHttpRequest(),self = thisxhr.open('GET', 'https://api.github.com/repos/yyx990803/vue/commits?per_page=3&sha=' + self.branch)xhr.onload = function () {self.commits = JSON.parse(xhr.responseText)}xhr.send()}} }) <!-- index.html --> <!DOCTYPE html><style>#demo {font-family: 'Helvetica', Arial, sans-serif;}a {text-decoration: none;color: #f66;}li {line-height: 1.5em;margin-bottom: 20px;}.author, .date {font-weight: bold;} </style><div id="demo"><h1>Latest Vue.js Commits</h1><p>{{title}}</p><input type="radio" id="master" name="branch" v-model="branch" value="master"><label for="master">master</label><br><input type="radio" id="dev" name="branch" v-model="branch" value="dev"><label for="dev">dev</label><ul><li v-repeat="commits"><a href="{{html_url}}" target="_blank" class="commit">{{sha.slice(0, 7)}}</a>- <span class="message">{{commit.message | truncate}}</span><br>by <span class="author">{{commit.author.name}}</span>at <span class="date">{{commit.author.date | formatDate}}</span></li></ul> </div><script src="../../dist/vue.js"></script> <script src="app.js"></script>

典型的Vue用法如上,那我們的分析就從new Vue()開始說起。

*注意:
如果要達到良好的學習效果,需要自己clone一份源代碼,跟著查看,反復查看。
為了節省篇幅,不影響主流程的代碼都以“…”代替。
不是核心的代碼,會直接略過。

Vue的入口

我們可以在Vue的源代碼中找到:

if (typeof exports == 'object') {module.exports = require('vue');} else if (typeof define == 'function' && define.amd) {define(function () { return require('vue'); });} else {window['Vue'] = require('vue');}

那也就是說我們在new Vue時,調用的構造方法應當是require('vue');方法所返回的。
經過一輪探尋(這個過程可自行探尋,這不是我們的關注的重點),可以找到Vue實際的入口為vue/src/main.js方法中所返回的內容:

require.register("vue/src/main.js", function (exports, require, module) {var config = require('./config');var ViewModel = require('./viewmodel');...module.exports = ViewModel});

所以我們真正的入口便是ViewModel的構造方法。

真正的入口ViewModel()

數據的執行入口:

/*** ViewModel exposed to the user that holds data,* computed properties, event handlers* and a few reserved methods*/function ViewModel(options) {//對外暴露的入口console.info(options);// compile if options passed, if false return. options are passed directly to compilerif (options === false) returnnew Compiler(this, options)}

而后開始進入Compiler構造方法:

/*** The DOM compiler* scans a DOM node and compile bindings for a ViewModel* options: custom data.*/function Compiler(vm, options) {...}

最開始processOptions內部會對自定義的四種類型做初步處理:components,partials,template,filters,我們沒有定義,也不是核心流程,直接跳過。

/*** convert certain option values to the desired format.*/processOptions:(options);

接下來將自定義編譯選項與主編譯器合并:

// copy compiler optionsextend(compiler, options.compilerOptions);

通過setupElement方法查找el所定義的元素,其內部使用了document.querySelector()方法,參數為id選擇器的值#demo。

// initialize elementvar el = compiler.el = compiler.setupElement(options);

這里的el就代表了整個根節點。接下來的操作都圍繞著這個根節點進行操作。

接下來給compiler添加了一些屬性,這些屬性為接下來做鋪墊:

// set other compiler propertiescompiler.vm = el.vue_vm = vmcompiler.bindings = utils.hash()compiler.dirs = []compiler.deferred = []compiler.computed = []compiler.children = []compiler.emitter = new Emitter(vm)

上面給el賦了一個屬性:el.vue_vm。
vue_vm擁有以下屬性:

vm.$ = {}vm.$el = elvm.$options = optionsvm.$compiler = compilervm.$event = nullvm.$root = getRoot(compiler).vm

其中這些為循環引用,需要注意:

vue_vm.el = vm.el = elcompiler.options = vm.$options = optionsvm.$compiler = compiler,而compiler.vm = el.vue_vm = vm

接下來我們需要進入compiler.setupObserver()方法一探究竟,這是個關鍵的地方。

CompilerProto.setupObserver = function () {var compiler = this,bindings = compiler.bindings,options = compiler.options,observer = compiler.observer = new Emitter(compiler.vm)...// add own listeners which trigger binding updatesobserver.on('get', onGet).on('set', onSet).on('mutate', onSet)// register hooks// 對自定義的鉤子方法做處理hooks = ['created', 'ready','beforeDestroy', 'afterDestroy','attached', 'detached']var i = hooks.length, j, hook, fnswhile (i--) {hook = hooks[i]fns = options[hook]if (Array.isArray(fns)) {j = fns.length// since hooks were merged with child at head,// we loop reversely.while (j--) {registerHook(hook, fns[j])}} else if (fns) {registerHook(hook, fns)}}// broadcast attached/detached hooksobserver.on('hook:attached', function () {broadcast(1)}).on('hook:detached', function () {broadcast(0)})function onGet(key) {check(key)DepsParser.catcher.emit('get', bindings[key])}function onSet(key, val, mutation) {observer.emit('change:' + key, val, mutation)check(key)bindings[key].update(val)}function registerHook(hook, fn) {observer.on('hook:' + hook, function () {fn.call(compiler.vm)})}function broadcast(event) {...}...}

上面做了這么幾件重要的事情:

  • compiler.observer初始化,其中compiler.observer是一個Emitter對象的實例。
  • 給compiler.observer注冊需要觀察的事件,需要觀察的事件包含:get、set、mutate、hook:attached、hook:detached。其中后兩項會在事件被觸發時,將事件廣播出去。
  • 將自定義生命周期方法與生命周期事件掛鉤。

observer.on方法實現如下,它用來注冊事件與回調的關系。是一對多的關系。

EmitterProto.on = function (event, fn) {this._cbs = this._cbs || {};(this._cbs[event] = this._cbs[event] || []).push(fn)return this}

通過setupObserver方法的執行,我們可知如下對應關系:

compiler.observer._cbs.get = ['onGet'] compiler.observer._cbs.set = ['onSet'] compiler.observer._cbs.mutate = ['onSet'] compiler.observer._cbs.hook:attached = ['broadcast function'] compiler.observer._cbs.hook:detached = ['broadcast function'] ... 自定義生命周期觀察者,如果有的話

以上對分析最重要的就是onSet的回調,在這里先有個印象,后面很關鍵。onSet實現如下:

function onSet(key, val, mutation) {observer.emit('change:' + key, val, mutation)check(key)bindings[key].update(val)}

到這里跳出setupObserver方法,回到Compiler(vm, options)構造方法內繼續往下:

接下來對自定義方法處理,我們的示例中有自定義方法fetchData:

// create bindings for computed propertiesif (options.methods) {for (key in options.methods) {compiler.createBinding(key)}}

內部實現如下:

CompilerProto.createBinding = function (key, directive) {...var compiler = this,methods = compiler.options.methods,isExp = directive && directive.isExp,isFn = (directive && directive.isFn) || (methods && methods[key]),bindings = compiler.bindings,computed = compiler.options.computed,binding = new Binding(compiler, key, isExp, isFn)if (isExp) {...} else if (isFn) {bindings[key] = bindingcompiler.defineVmProp(key, binding, methods[key])} else {bindings[key] = binding...}return binding}

這里的key是fetchData,它是一個方法,所以isFn = true。然后將這些關鍵的信息生成了一個Binding對象。Binding通過類似的建造者模式將所有的關鍵信息維護在一起。現在這個binding對象是專門為fetchData方法所產生的。

然后代碼進入isFn條件繼續執行,便產生了如下關系:

compiler.bindings.fetchData = new Binding(compiler, 'fetchData', false, true);

然后繼續執行:

compiler.defineVmProp('fetchData', binding, fetchDataFunc);//fetchDataFunc為fetchData所對應的自定義方法。

方法內部如下:

CompilerProto.defineVmProp = function (key, binding, value) {var ob = this.observerbinding.value = valuedef(this.vm, key, {get: function () {if (Observer.shouldGet) ob.emit('get', key)return binding.value},set: function (val) {ob.emit('set', key, val)}})}

經過 defineVmProp代碼的執行,可以得出以下結論:

compiler.vm.fetchData有了代理get/set方法,后期對于自定義方法的讀取或者賦值都需要經過這一層代理。binding.value也指向了用戶自定義的方法。當讀取vm.fetchData時就會得到自定義的方法。

我們跳出defineVmProp方法,然后繼續向下執行,createBinding方法執行完畢,我們返回到createBinding方法調用處,也就是Compiler的構造方內,繼續向下執行。

我們的示例中沒有computed的相關定義,這里跳過。

接下來對defaultData做處理,我們沒有定義,跳過。

也沒有對paramAttributes的定義,跳過。

走到這里:

// copy data properties to vm// so user can access them in the created hookextend(vm, data)vm.$data = data

這里將data里面的屬性全部賦值給了vm。并且vm.$data屬性也指向data。

// extend方法的實現如下:extend: function (obj, ext) {for (var key in ext) {if (obj[key] !== ext[key]) {obj[key] = ext[key]}}return obj}

extend方法將第二個參數的所有屬性全部賦值給了第一個參數。對于示例會產生如下關系:

vm.branch = 'master' vm.title = 'tl' vm.$data = data

接著向下,觸發created生命周期方法:

// beforeCompile hookcompiler.execHook('created')

我們沒有定義created生命周期方法,然后繼續。

對于自定義數據的事件監聽

略過中間的數據處理,到達這里:

// now we can observe the data.// this will convert data properties to getter/setters// and emit the first batch of set events, which will// in turn create the corresponding bindings.compiler.observeData(data)

observeData方法內部如下:

CompilerProto.observeData = function (data) {var compiler = this,observer = compiler.observer// recursively observe nested propertiesObserver.observe(data, '', observer)...}

observeData方法中比較重要的地方是:

Observer.observe(data, '', observer)

然后是observe方法內部:

...// 第一次執行alreadyConverted = falseif (alreadyConverted) {// for objects that have already been converted,// emit set events for everything insideemitSet(obj)} else {watch(obj)}

所以第一次走的是watch方法:

/*** Watch target based on its type*/ function watch (obj) {if (isArray(obj)) {watchArray(obj)} else {watchObject(obj)} }

watch方法對對象做了一個初步的分揀。示例的代碼不是Array,走watchObject:

/*** Watch an Object, recursive.*/ function watchObject (obj) {// 用戶給對象添加$add/$delete兩個屬性augment(obj, ObjProxy)for (var key in obj) {convertKey(obj, key)} }

我們到這里稍微等一下,這里的obj還是:

data: {branch: 'master', title: 'tl'}

watchObject對對象的每個屬性進行遍歷,而convertKey方法內做了比較重要的事情:

function convertKey(obj, key, propagate) {var keyPrefix = key.charAt(0)// 初步對以$開頭的、以_開頭的做過濾if (keyPrefix === '$' || keyPrefix === '_') {return}...// 重要之所在oDef(obj, key, {enumerable: true,configurable: true,get: function () {var value = values[key]// only emit get on tip valuesif (pub.shouldGet) {emitter.emit('get', key)}return value},set: function (newVal) {var oldVal = values[key]unobserve(oldVal, key, emitter)copyPaths(newVal, oldVal)// an immediate property should notify its parent// to emit set for itself tooinit(newVal, true)}})...}

convertKey方法中比較重要的就是這里了,這里對new Vue()時傳入的對象的data對象中的每個屬性添加相應的get/set方法,也就是說在給某個屬性賦值時,就會觸發這里。如果給branch/title賦予新值,就會觸發上面提到的set方法。到這里我們有理由相信,set方法中的init方法是用來更新界面的。

好了,到了這里convertKey方法就分析完了,我們再一路往回:convertKey -> watchObject -> watch -> observe -> observeData。回到observeData方法內,接下的代碼是對compiler.vm.$data添加觀察事件,它暫時不是我們關心的內容,observeData返回調用處,并接著向下:

// before compiling, resolve content insertion pointsif (options.template) {this.resolveContent()}

上面這段代碼我們沒有定義template,略過。

對于DOM樹的解析

向下到了又一個很關鍵的地方:

// now parse the DOM and bind directives.// During this stage, we will also create bindings for// encountered keypaths that don't have a binding yet.compiler.compile(el, true)

compile內部實現:

CompilerProto.compile = function (node, root) {var nodeType = node.nodeTypeif (nodeType === 1 && node.tagName !== 'SCRIPT') { // a normal nodethis.compileElement(node, root)} else if (nodeType === 3 && config.interpolate) {this.compileTextNode(node)}}

執行到這里el使我們的根節點demo,其中node = demoNode, root = true。上面的分發會進入compileElement:

CompilerProto.compileElement = function (node, root) {// textarea is pretty annoying// because its value creates childNodes which// we don't want to compile.if (node.tagName === 'TEXTAREA' && node.value) {node.value = this.eval(node.value)}// only compile if this element has attributes// or its tagName contains a hyphen (which means it could// potentially be a custom element)if (node.hasAttributes() || node.tagName.indexOf('-') > -1) {...}// recursively compile childNodesif (node.hasChildNodes()) {slice.call(node.childNodes).forEach(this.compile, this)}}

compileElement方法內部細節比較多也比較長。

先來說說compileElement方法的作用,compileElement方法用來對dom樹的所有節點進行遍歷,會處理所有的屬性節點與文本節點。其中就會遇到v-model等指令以及{{value}}這樣的占位符。

compileElement方法內分為幾大塊:

  • 1.對TEXTAREA的處理:if (node.tagName === 'TEXTAREA' && node.value)
  • 2.對用于屬性的或者tag的名稱中包含’-'的處理:if (node.hasAttributes() || node.tagName.indexOf('-') > -1) {
  • 3.如果不符合1或2的條件,則對其子節點進行處理。

子節點的處理會進一步進行遞歸,走compile方法。compile方法繼續進行分發,如果是元素節點則走compileElement,如果是文本節點,則走compileTextNode。這個過程直到將整顆DOM樹遍歷完畢。

CompilerProto.compile = function (node, root) {var nodeType = node.nodeTypeif (nodeType === 1 && node.tagName !== 'SCRIPT') { // a normal nodethis.compileElement(node, root)} else if (nodeType === 3 && config.interpolate) {this.compileTextNode(node)}}

以下代碼從index.html摘除,它有利于我們的繼續分析:

<p>{{title}}</p>

如果渲染以上內容,那么它的處理就會被分發到compileTextNode方法中:

CompilerProto.compileTextNode = function (node) {var tokens = TextParser.parse(node.nodeValue)if (!tokens) returnvar el, token, directivefor (var i = 0, l = tokens.length; i < l; i++) {token = tokens[i]directive = nullif (token.key) { // a bindingif (token.key.charAt(0) === '>') { // a partialel = document.createComment('ref')directive = this.parseDirective('partial', token.key.slice(1), el)} else {if (!token.html) { // text binding// 示例中,會在這里處理{{title}}的邏輯,并綁定與之對應的directive處理函數。el = document.createTextNode('')directive = this.parseDirective('text', token.key, el)} else { // html bindingel = document.createComment(config.prefix + '-html')directive = this.parseDirective('html', token.key, el)}}} else { // a plain stringel = document.createTextNode(token)}// insert nodenode.parentNode.insertBefore(el, node)// bind directivethis.bindDirective(directive)}node.parentNode.removeChild(node)}

上面方法中的TextParser.parse(node.nodeValue)的實現細節不去了解了,它是用來匹配各種占位符和表達式的,純算法型代碼。
對于<p>{{title}}</p>這種類型的處理會進入:

el = document.createTextNode('') directive = this.parseDirective('text', token.key, el)

其中token.key = ‘title’, el為剛剛創建好的新文本節點。parseDirective方法內:

CompilerProto.parseDirective = function (name, value, el, multiple) {var compiler = this,definition = compiler.getOption('directives', name)if (definition) {// parse into AST-like objectsvar asts = Directive.parse(value)return multiple? asts.map(build): build(asts[0])}function build(ast) {return new Directive(name, ast, definition, compiler, el)}}

上面代碼最為核心的調用是getOption,其中type = ‘directives’, id = ‘text’, silent = undefined:

CompilerProto.getOption = function (type, id, silent) {var opts = this.options,parent = this.parent,globalAssets = config.globalAssets,res = (opts[type] && opts[type][id]) || (parent? parent.getOption(type, id, silent): globalAssets[type] && globalAssets[type][id])if (!res && !silent && typeof id === 'string') {utils.warn('Unknown ' + type.slice(0, -1) + ': ' + id)}return res}

其中globalAssets存儲了vue所支持類型的所有對應關系:

然后getOption返回的就是處理類型與處理方法的對應關系對象。最后parseDirective方法返回一個新的Directive對象。這個對象包含了處理類型與處理方法的相關關系。這是很重要的一點。

對于text類型的,它的Directive對象則是:

directives.text = {bind: function () {this.attr = this.el.nodeType === 3? 'nodeValue': 'textContent'},update: function (value) {this.el[this.attr] = utils.guard(value)}}

回到compileTextNode方法繼續向下執行:

CompilerProto.bindDirective = function (directive, bindingOwner) {if (!directive) return...if (directive.isExp) {// expression bindings are always created on current compilerbinding = compiler.createBinding(key, directive)} else {// recursively locate which compiler owns the binding...compiler = compiler || thisbinding = compiler.bindings[key] || compiler.createBinding(key)}binding.dirs.push(directive)...}

上面又執行了compiler.createBinding(key),這里的key = ‘title’。

經過bindDirective方法的執行,最后會產生如下關系(這里很重要):

compiler.bindings.title = new Binding(compiler, 'ttile', false, false); compiler.bindings.title.binding.dirs = [directive]; // 這里存放的是title對應的處理方法

執行到了這里就可以返回至compileTextNode方法的調用處。compileTextNode的初始化到這里就算完成了一步。

到這里可以返回至function Compiler(vm, options)方法處,繼續向下。中間略過一些非核心的內容:

// done!compiler.init = false// post compile / ready hookcompiler.execHook('ready')

到這里初始化就算完成,并通過ready方法告知Vue已經準備好了。

事件的執行

接下來如果執行demo.title = 'Hello',就會觸發set方法的內部的init方法,而init方法內部有這樣的關鍵:

function init(val, propagate) {values[key] = val/重要/ emitter.emit('set', key, val, propagate)/重要/ if (isArray(val)) {emitter.emit('set', key + '.length', val.length, propagate)}observe(val, key, emitter)}

能看到上面的emitter.emit('set', key, val, propagate)方法被執行,我們就根據這個set查看它是怎么執行的:

EmitterProto.emit = function (event, a, b, c) {this._cbs = this._cbs || {}var callbacks = this._cbs[event]if (callbacks) {callbacks = callbacks.slice(0)for (var i = 0, len = callbacks.length; i < len; i++) {callbacks[i].call(this._ctx, a, b, c)}}return this}

上面這段代碼通過event獲取到對應的callbacks并進行回調,我們在上面已經得知set所對應的callbacks是onSet方法,我們再來回顧一下onSet:

function onSet(key, val, mutation) {observer.emit('change:' + key, val, mutation)check(key)compiler.bindings[key].update(val)}

而compiler.bindings的屬性添加是在createBinding中進行的,這個我們上面就有提到。執行到這里key = ‘title’。

于是這里執行的便是:

BindingProto.update = function (value) {if (!this.isComputed || this.isFn) {this.value = value}if (this.dirs.length || this.subs.length) {var self = thisbindingBatcher.push({id: this.id,execute: function () {if (!self.unbound) {self._update()}}})}}

以下是bindingBatcher.push的實現細節:

BatcherProto.push = function (job) {if (!job.id || !this.has[job.id]) {this.queue.push(job)this.has[job.id] = jobif (!this.waiting) {this.waiting = trueutils.nextTick(utils.bind(this.flush, this))}} else if (job.override) {var oldJob = this.has[job.id]oldJob.cancelled = truethis.queue.push(job)this.has[job.id] = job} }

bindingBatcher.push方法會將參數對象經過包裝交給:

/*** used to defer batch updates*/nextTick: function (cb) {defer(cb, 0)},

而這里的defer為requestAnimationFrame方法,requestAnimationFrame會在下一次瀏覽器繪制時,觸發cb回調方法。

其中的cb回調對象是由這個bind方法生成的:

/*** Most simple bind* enough for the usecase and fast than native bind()*/bind: function (fn, ctx) {return function (arg) {return fn.call(ctx, arg)}},

這里的fn是:

BatcherProto.flush = function () {// before flush hookif (this._preFlush) this._preFlush()// do not cache length because more jobs might be pushed// as we execute existing jobsfor (var i = 0; i < this.queue.length; i++) {var job = this.queue[i]if (!job.cancelled) {job.execute()}}this.reset() }

也就說緊接著flush方法會被requestAnimationFrame方法調用:

flush方法的核心是:

job.execute()

而這里的job對象就是剛剛被Push進去的:

{id: this.id,execute: function () {if (!self.unbound) {self._update()}}}

這里會執行self._update():

/*** Actually update the directives.*/ BindingProto._update = function () {var i = this.dirs.length,value = this.val()while (i--) {this.dirs[i].$update(value)}this.pub() }

可以理解為這是一個事件分發過程。

這里從dirs中取出是一個與text相關的directive對象,這里執行的是directive對象的$update方法:

DirProto.$update = function (value, init) {if (this.$lock) returnif (init || value !== this.value || (value && typeof value === 'object')) {this.value = valueif (this.update) {this.update(this.filters && !this.computeFilters? this.$applyFilters(value): value,init)}}}

上面的this對應的是之前提到的與text對應的處理器:

directives.text = {bind: function () {this.attr = this.el.nodeType === 3? 'nodeValue': 'textContent'},update: function (value) {this.el[this.attr] = utils.guard(value)}}

而這里的update則是執行整個text更新的核心所在,通過對相應元素的nodeValue賦值便達到的更新值的效果。

以上內容僅僅是更新data值的粗略過程。vue還包括其它內容:如列表渲染、條件渲染、生命周期方法等等。

對于列表渲染和條件渲染它們分別有對應的處理器,對于它們的執行過程也和text的過程是一致的。


零散的記錄一下:

emitter是vue引擎的核心,負責各種事件的分發。

它含有兩個關鍵的方法:

// 注冊觀察者方法,每個event可以理解為觀察者,fn為觀察者對應的事件回調對象集合。EmitterProto.on = function (event, fn) {this._cbs = this._cbs || {};(this._cbs[event] = this._cbs[event] || []).push(fn)return this}// 通知觀察者,針對于觀察的事件進行事件的分發處理EmitterProto.emit = function (event, a, b, c) {this._cbs = this._cbs || {}var callbacks = this._cbs[event]if (callbacks) {callbacks = callbacks.slice(0)for (var i = 0, len = callbacks.length; i < len; i++) {callbacks[i].call(this._ctx, a, b, c)}}return this}

其中在vue中注冊的觀察者為:

compiler.observer.on('get', onGet).on('set', onSet).on('mutate', onSet).on('hook:attached', function () {broadcast(1)}).on('hook:detached', function () {broadcast(0)}).on('created', '自定義生命周期方法').on('ready', '自定義生命周期方法').on('beforeDestroy', '自定義生命周期方法').on('afterDestroy', '自定義生命周期方法').on('attached', '自定義生命周期方法').on('detached', '自定義生命周期方法').on('set', function (key) {if (key !== '$data') update()}).on('mutate', function (key) {if (key !== '$data') update()})......

當某個Key所對應的事件被觸發時,它所對應的回調就會被觸發并執行。

總結

所以到此為止,我們搞清楚了Vue的主體框架。上文中有些亂,我們來梳理一下:

  • 最開始new Vue = new ViewModel = new Compiler
  • Compiler執行了對于自定義數據、自定義方法、自定義生命周期、自定義模板等等的處理。我們的示例演示了如何為自定義數據添加觀察者方法。
  • Compiler解析了整顆DOM樹,為樹里面定義的占位符、v-指令、自定義組件做了處理。示例中演示了如何對占位符中的值進行解析以及添加觀察者。
  • Compiler.bindings中存放了所有需要觀察對象的綁定關系Binding對象。Binding中的dirs存放了相關key的處理對象Directive。
  • Emitter負責關鍵中轉事件的注冊與分發。
  • Batcher負責更新事件的提交。它將事件交給瀏覽器,由瀏覽器觸發事件的執行。
  • Directives中存放了所有的指令。包括:if,repeat,on,model,with等等。
  • TextParser負責文本的萃取,解析。
  • Directive負責單個事件的觸發,通過directive使更新執行。
  • Observer用于添加觀察者。
  • Binding用于維護一些運行時的關鍵信息。
  • Utils中提供了一些非常棒的基礎工具。
  • Config提供了一些可配的配置信息。
  • main.js是整個程序的執行入口,負責一些模塊的加載和組裝。

額外學習到的內容

除了摸清楚Vue的基礎框架之外,我從代碼中讀到了以下信息:

  • 代碼非常整潔,注釋全面,結構合理、清晰。無額外注釋和冗余代碼。
  • 對于日志的輸出做了控制,這也是一個優秀程序員所必備的。
  • 對于JS語言針對于類的使用值得借鑒。
  • 一些非常奇妙的用法。

良好的日志管控無處不在:

function enableDebug() {/*** log for debugging*/utils.log = function (msg) {if (config.debug && console) {console.log(msg)}}/*** warnings, traces by default* can be suppressed by `silent` option.*/utils.warn = function (msg) {if (!config.silent && console) {console.warn(msg)if (config.debug && console.trace) {console.trace()}}}}

很多地方會看到這種寫法:

slice.call(node.childNodes).forEach(this.compile, this);

slice方法在這里的作用是拷貝了一個副本出來,對于副本的操作不會引起原型的變動。這個對于拷貝數組副本的用法很妙。


以上。

創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎

總結

以上是生活随笔為你收集整理的Vue源码探究笔记的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。