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方法在这里的作用是拷贝了一个副本出来,对于副本的操作不会引起原型的变动。这个对于拷贝数组副本的用法很妙。


以上。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/478685.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Jarvis:一个值得关注的多模态端到端人机对话框架,针对所有行业适配

说到应用级的人机对话框架&#xff0c;很多人可能首先想到的是RASA开源项目。不过&#xff0c;今天跟大家简要分享一个功能更为丰富、性能更为强劲的多模对话框架——Jarvis&#xff0c;非常值得对话系统从业者关注一下。Jarvis是英伟达于2019年发布的人机对话服务&#xff0c;…

技术动态 | 知识图谱构建的研究已走入下半场,但大规模落地应用仍需时间

本文转载自公众号&#xff1a;AI前线。作者 | 李冬梅 采访嘉宾 | 唐杰知识图谱是近年来人工智能技术蓬勃发展的核心驱动力之一&#xff0c;已广泛应用在金融、电商、医疗、政务等众多领域&#xff0c;经过短短几年的发展&#xff0c;热度依旧不减&#xff0c;未来&#xff0c;知…

LeetCode 542. 01 矩阵(BFS DP)

文章目录1. 题目2. 解题2.1 BFS2.2 DP动态规划1. 题目 给定一个由 0 和 1 组成的矩阵&#xff0c;找出每个元素到最近的 0 的距离。 两个相邻元素间的距离为 1 。 示例 1: 输入: 0 0 0 0 1 0 0 0 0 输出: 0 0 0 0 1 0 0 0 0示例 2: 输入: 0 0 0 0 1 0 1 1 1 输出: 0 0 0 0…

美团旅行销售绩效系统研发实践

背景 O2O是目前互联网竞争最激烈的领域之一&#xff0c;其重要的业务特征是有大规模的线下业务团队&#xff0c;他们分布在五湖四海&#xff0c;直接服务着数以百万的商家&#xff0c;责任很重&#xff0c;管理的难度巨大。能否通过技术手段&#xff0c;打造高效的线下团队&…

深入解析Node.js setTimeout方法的执行过程

深入了解setTimeout源码之前&#xff0c;本有两个选择。一是通过chromium源码分析&#xff0c;二是通过Node.js源码分析。后来发现第一种方案的源码获取成本太大&#xff0c;于是从Node官网获取了几十兆的代码用来了解。 当前的Node版本为&#xff1a;v10.16.0 setTimeout方法定…

别只关注GPT3!细如发丝的模型更具现实杀伤力!

这个世界上有两种极具难度的工程&#xff1a;第一种是把很平常的东西做到最大&#xff0c;例如把语言模型扩大成能够写诗写文写代码的GPT-3&#xff1b;而另一种恰恰相反&#xff0c;是把很平常的东西做到最小。 ----王苏语录GPT3自从诞生以来&#xff0c;便受到…

论文浅尝 - EMNLP2020 | 基于分组式对比学习的神经对话生成

论文笔记整理&#xff1a;叶宏彬&#xff0c;浙江大学计算机博士生。论文地址&#xff1a;https://arxiv.org/abs/2009.07543摘要&#xff1a;近年来&#xff0c;神经对话问答的产生已广受欢迎。现有对话模型学习中广泛采用了最大似然估计目标&#xff08;MLE&#xff09;。但是…

LeetCode 841. 钥匙和房间(DFS/BFS)

文章目录1. 题目2. 解题2.1 DFS2.2 BFS1. 题目 有 N 个房间&#xff0c;开始时你位于 0 号房间。每个房间有不同的号码&#xff1a;0&#xff0c;1&#xff0c;2&#xff0c;…&#xff0c;N-1&#xff0c;并且房间里可能有一些钥匙能使你进入下一个房间。 在形式上&#xff…

容器中用uwsgi协议部署注意的问题以及用flask部署

1 表示当前容器名 一般通过 docker exec -it 容器名 /bin/bash , 进入容器内部进行操作&#xff0c;比如安装环境&#xff0c;传输文件 2 4888 容器内部访问端口地址&#xff0c;如果对外开放api&#xff0c;需要对其进行映射&#xff0c;比如映射成11022,11022是对外访问的端…

美团点评金融平台Web前端技术体系

背景 随着美团点评金融业务的高速发展&#xff0c;前端研发数量从 2015 年的 1 个人&#xff0c;扩张到了现在横跨北上两地 8 个事业部的将近 150 人。业务新&#xff0c;团队新&#xff0c;前端领域框架技术又层出不穷&#xff0c;各个业务的研发团队在技术选择上没有明确的指…

参会邀请 - CCKS2020 | 2020全国知识图谱与语义计算大会(CCKS2020)明日开幕

本文转载自公众号&#xff1a; 中国中文信息学会。第十四届全国知识图谱与语义计算大会将于2020年11月12日-15日在南昌召开。会议由中国中文信息学会语言与知识计算专业委员会主办&#xff0c;由江西师范大学承办&#xff0c;智源社区提供社区支持。本次会议讲习班采用线上举行…

Linux 程 序 员 失 业 警 告

文 | 小戏有多少人期待过像贾维斯一样的强人工智能&#xff1f;尽管老实说看当下的技术离这一期待还很遥远&#xff0c;但用用类似 GPT-3 这样的技术去实现些朴素的愿望似乎并没有那么困难。就在昨天&#xff0c;来自 Facebook 的 Elvis 在推特上发布了一个借助 OpenAI 的 GPT-…

LeetCode 707. 设计链表(List)

文章目录1. 设计一个单链表2. 双向链表1. 设计一个单链表 在链表类中实现这些功能&#xff1a; get(index)&#xff1a;获取链表中第 index 个节点的值。如果索引无效&#xff0c;则返回-1。 addAtHead(val)&#xff1a;在链表的第一个元素之前添加一个值为 val 的节点。插入…

用Vue.js开发微信小程序:开源框架mpvue解析

前言 mpvue 是一款使用 Vue.js 开发微信小程序的前端框架。使用此框架&#xff0c;开发者将得到完整的 Vue.js 开发体验&#xff0c;同时为 H5 和小程序提供了代码复用的能力。如果想将 H5 项目改造为小程序&#xff0c;或开发小程序后希望将其转换为 H5&#xff0c;mpvue 将是…

对比学习有多火?文本聚类都被刷爆了…

文 | 花小花Posy大家好&#xff0c;我是小花。对比学习的大火???? 越来越旺了&#xff0c;已然从CV蔓延到NLP了。今天给大家介绍的正是一篇将对比学习应用到文本聚类上的工作&#xff0c;NAACL21新鲜出炉的paper——《Supporting Clustering with Contrastive Learning》。…

论文浅尝 - WWW2020 | 生成多跳推理问题以改善机器阅读理解能力

论文笔记整理&#xff1a;谭亦鸣&#xff0c;东南大学博士生。来源&#xff1a;WWW 2020链接&#xff1a;https://dl.acm.org/doi/pdf/10.1145/3366423.3380114概述这篇论文关注的任务是&#xff1a;基于给定文本的“多跳问题生成”&#xff08;多关系问题&#xff09;。作者提…

记一次Vue框架升级

框架升级背景 公司目前业务迭代很快&#xff0c;且大部分的流量都在公众号上。然而我们公众号所使用的框架却是3年前的Vue 1.0.16。面对Vue这3年来带来的无数新特性&#xff0c;我们只能望洋兴叹&#xff1a;看得见&#xff0c;摸不着&#xff0c;因为升级这事看起来太难了。 …

谈谈NLP下一个主战场:万亿参数的预训练模型!

自从BERT诞生以来&#xff0c;各大互联网巨头之间就展开了预训练语言模型军备竞赛&#xff0c;XLNet、ERNIE、RoBERTa、T5、GPT-3....但当事情进展到号称自己是zero-shot learner的GPT-3时&#xff0c;批判的声音变得明显多了。这么大&#xff0c;能用吗&#xff1f;真的能做到…

人物志 | 美团女技术总监任登君:不要给自己的人生设限

在我们美团技术团队超过6000名工程师中&#xff0c;有众多的女同学&#xff0c;她们是支撑中国领先的生活服务电子商务平台不可或缺的力量。3月8日女神节&#xff0c;我们专访了她们的代表——美团广告平台技术负责人任登君。登君也是我们团队里目前职位最高的女性技术Leader&a…

论文浅尝 - ISWC2020 | KnowlyBERT: 知识图谱结合语言模型补全图谱查询

论文笔记整理&#xff1a;胡楠&#xff0c;东南大学博士。来源&#xff1a;ISWC 2020动机像Wikidata这样的现代知识图已经捕获了数十亿个RDF三元组&#xff0c;但是它们仍然缺乏对大多数关系的良好覆盖。同时在NLP研究的最新进展表明&#xff0c;可以轻松地查询神经语言模型以获…