Vue源码最终是向外部抛出一个Vue的构造函数,见源码:css
function Vue (options) { this._init(options) }
在源码最开始,经过installGlobalAPI方法(见源码)向Vue构造函数添加全局方法,如Vue.extend、Vue.nextTick、Vue.delete等,主要初始化Vue一些全局使用的方法、变量和配置;html
export default function (Vue){ Vue.options = { ..... } Vue.extend = function (extendOptions){ ...... } Vue.use = function (plugin){ ...... } Vue.mixin = function (mixin){ ...... } Vue.extend = function (extendOptions){ ...... } }
当使用vue时,最基本使用方式以下:vue
var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } })
此时,会调用构造函数实例化一个vue对象,而在构造函数中只有这句代码this.init(options)
;而在init中(源码),主要进行一些变量的初始化、option重组、各类状态、事件初始化;以下:node
Vue.prototype._init = function (options) { options = options || {} this.$el = null this.$parent = options.parent this.$root = this.$parent ? this.$parent.$root : this this.$children = [] this.$refs = {} // child vm references this.$els = {} // element references this._watchers = [] // all watchers as an array this._directives = [] // all directives ...... // 更多见源码 options = this.$options = mergeOptions( this.constructor.options, options, this ) // set ref this._updateRef() // initialize data as empty object. // it will be filled up in _initData(). this._data = {} // call init hook this._callHook('init') // initialize data observation and scope inheritance. this._initState() // setup event system and option events. this._initEvents() // call created hook this._callHook('created') // if `el` option is passed, start compilation. if (options.el) { this.$mount(options.el) } }
在其中经过mergeOptions
方法,将全局this.constructor.options
与传入的options及实例化的对象进行合并;而this.constructor.options则是上面初始化vue时进行配置的,其中主要包括一些全局使用的指令、过滤器,如常用的"v-if"、"v-for"、"v-show"、"currency":git
this.constructor.options = { directives: { bind: {}, // v-bind cloak: {}, // v-cloak el: {}, // v-el for: {}, // v-for html: {}, // v-html if: {}, // v-if for: {}, // v-for text: {}, // v-text model: {}, // v-model on: {}, // v-on show: {} // v-show }, elementDirectives: { partial: {}, // <partial></partial> api: https://v1.vuejs.org/api/#partial slot: {} // <slot></slot> }, filters: { // api: https://v1.vuejs.org/api/#Filters capitalize: function() {}, // {{ msg | capitalize }} ‘abc’ => ‘Abc’ currency: funnction() {}, debounce: function() {}, filterBy: function() {}, json: function() {}, limitBy: function() {}, lowercase: function() {}, orderBy: function() {}, pluralize: function() {}, uppercase: function() {} } }
而后,会触发初始化一些状态、事件、触发init、create钩子;而后随后,会触发this.$mount(options.el)
;进行实例挂载,将dom添加到页面;而this.$mount()方法则包含了绝大部分页面渲染的代码量,包括模板的嵌入、编译、link、指令和watcher的生成、批处理的执行等等,后续会详细进行说明;github
在上面说了下,在Vue.prototype.$mount
完成了大部分工做,而在$mount方法里面,最主要的工做量由this._compile(el)
承担;其主要包括transclude(嵌入)、compileRoot(根节点编译)、compile(页面其余的编译);而在这儿主要说明transclude方法;express
经过对transclude进行网络翻译结果是"嵌入";其主要目的是将页面中自定义的节点转化为真实的html节点;如一个组件<hello></hello>
其实际dom为<div><h1>hello {{message}}</h1></div>
,源码; 当咱们使用时<div><hello></hello></div>
; 会经过transclude将其转化为<div><div><h1>hello {{message}}</h1></div></div>
,见源码注释;json
那transclude具体干了什么呢,咱们先看它的源码:api
export function transclude (el, options) { // extract container attributes to pass them down // to compiler, because they need to be compiled in // parent scope. we are mutating the options object here // assuming the same object will be used for compile // right after this. if (options) { // 把el(虚拟节点,如<hello></hello>)元素上的全部attributes抽取出来存放在了选项对象的_containerAttrs属性上 // 使用el.attributes 方法获取el上面,并使用toArray方法,将类数组转换为真实数组 options._containerAttrs = extractAttrs(el) } // for template tags, what we want is its content as // a documentFragment (for fragment instances) // 判断是否为 template 标签 if (isTemplate(el)) { // 获得一段存放在documentFragment里的真实dom el = parseTemplate(el) } if (options) { if (options._asComponent && !options.template) { options.template = '<slot></slot>' } if (options.template) { // 将el的内容(子元素和文本节点)抽取出来 options._content = extractContent(el) // 使用options.template 将虚拟节点转化为真实html, <hello></hello> => <div><h1>hello {{ msg }}</h1></div> // 但不包括未绑定数据, 则上面转化为 => <div><h1>hello</h1></div> el = transcludeTemplate(el, options) } } // isFragment: node is a DocumentFragment // 使用nodeType 为 11 进行判断是非为文档片断 if (isFragment(el)) { // anchors for fragment instance // passing in `persist: true` to avoid them being // discarded by IE during template cloning prepend(createAnchor('v-start', true), el) el.appendChild(createAnchor('v-end', true)) } return el }
首先先看以下代码:数组
if (options) { // 把el(虚拟节点,如<hello></hello>)元素上的全部attributes抽取出来存放在了选项对象的_containerAttrs属性上 // 使用el.attributes 方法获取el上面,并使用toArray方法,将类数组转换为真实数组 options._containerAttrs = extractAttrs(el) }
而extractAttrs方法以下,其主要根据元素nodeType去判断是否为元素节点,若是为元素节点,且元素有相关属性,则将属性值取出以后,再转为属性数组;最后将属性数组放到options._containerAttrs中,为何要这么作呢?由于如今的el可能不是真实的元素,而是诸如<hello class="test"></hello>
,在后面编译过程,须要将其替换为真实的html节点,因此,它上面的属性值都会先取出来预存起来,后面合并到真实html根节点的属性上面;
function extractAttrs (el) { // 只查找元素节点及有属性 if (el.nodeType === 1 && el.hasAttributes()) { // attributes 属性返回指定节点的属性集合,即 NamedNodeMap, 类数组 return toArray(el.attributes) } }
下一句,根据元素nodeName是否为“template”去判断是否为<template></template>元素;若是是,则走parseTemplate(el)方法,并覆盖当前el对象
if (isTemplate(el)) { // 获得一段存放在documentFragment里的真实dom el = parseTemplate(el) } function isTemplate (el) { return el.tagName && el.tagName.toLowerCase() === 'template' }
而parseTemplate
则主要是将传入内容生成一段存放在documentFragment里的真实dom;进入函数,首先判断传入是否已是一个文档片断,若是已是,则直接返回;不然,判断传入是否为字符串,若是为字符串, 先判断是不是"#test"这种选择器类型,若是是,经过document.getElementById
方法取出元素,若是文档中有此元素,将经过nodeToFragment
方式,将其放入一个新的节点片断中并赋给frag,最后返回到外面;若是不是选择器类型字符串,则使用stringToFragment
将其生成一个新的节点片断,并返回;若是传入非字符串而是节点(不论是什么节点,能够是元素节点、文本节点、甚至Comment节点等);则直接经过nodeToFragment
生成节点片断并返回;
export function parseTemplate (template, shouldClone, raw) { var node, frag // if the template is already a document fragment, // do nothing // 是否为文档片断, nodetype是否为11 // https://developer.mozilla.org/zh-CN/docs/Web/API/DocumentFragment // 判断传入是否已是一个文档片断,若是已是,则直接返回 if (isFragment(template)) { trimNode(template) return shouldClone ? cloneNode(template) : template } // 判断传入是否为字符串 if (typeof template === 'string') { // id selector if (!raw && template.charAt(0) === '#') { // id selector can be cached too frag = idSelectorCache.get(template) if (!frag) { node = document.getElementById(template.slice(1)) if (node) { frag = nodeToFragment(node) // save selector to cache idSelectorCache.put(template, frag) } } } else { // normal string template frag = stringToFragment(template, raw) } } else if (template.nodeType) { // a direct node frag = nodeToFragment(template) } return frag && shouldClone ? cloneNode(frag) : frag }
从上面可见,在parseTemplat
e里面最重要的是nodeToFragment和stringToFragment;那么,它们又是如何将传入内容转化为新的文档片断呢?首先看nodeToFragment:
function nodeToFragment (node) { // if its a template tag and the browser supports it, // its content is already a document fragment. However, iOS Safari has // bug when using directly cloned template content with touch // events and can cause crashes when the nodes are removed from DOM, so we // have to treat template elements as string templates. (#2805) /* istanbul ignore if */ // 是template元素或者documentFragment,使用stringToFragment转化并保存节点内容 if (isRealTemplate(node)) { return stringToFragment(node.innerHTML) } // script template if (node.tagName === 'SCRIPT') { return stringToFragment(node.textContent) } // normal node, clone it to avoid mutating the original var clonedNode = cloneNode(node) var frag = document.createDocumentFragment() var child /* eslint-disable no-cond-assign */ while (child = clonedNode.firstChild) { /* eslint-enable no-cond-assign */ frag.appendChild(child) } trimNode(frag) return frag }
其实看源码,很容易理解,首先判断传入内容是否为template元素或者documentFragment或者script标签,若是是,都直接走stringToFragment;后面就是先使用document.createDocumentFragment
建立一个文档片断,而后将节点进行循环appendChild到建立的文档片断中,并返回新的片断;
那么,stringToFragment呢?这个就相对复杂一点了,以下:
function stringToFragment (templateString, raw) { // try a cache hit first var cacheKey = raw ? templateString : templateString.trim() //trim() 方法会从一个字符串的两端删除空白字符 var hit = templateCache.get(cacheKey) if (hit) { return hit } // 建立一个文档片断 var frag = document.createDocumentFragment() // tagRE: /<([\w:-]+)/ // 匹配标签 // '<test v-if="ok"></test>'.match(/<([\w:-]+)/) => ["<test", "test", index: 0, input: "<test v-if="ok"></test>"] var tagMatch = templateString.match(tagRE) // entityRE: /&#?\w+?;/ var entityMatch = entityRE.test(templateString) // commentRE: /<!--/ // 匹配注释 var commentMatch = commentRE.test(templateString) if (!tagMatch && !entityMatch && !commentMatch) { // text only, return a single text node. // 若是都没匹配到,建立一个文本节点添加到文档片断 frag.appendChild( document.createTextNode(templateString) ) } else { var tag = tagMatch && tagMatch[1] // map, 对标签进行修正;如是td标签,则返回"<table><tbody><tr>" + templateString + "</tr></tbody></table>"; // map['td'] = [3, "<table><tbody><tr>", "</tr></tbody></table>"] var wrap = map[tag] || map.efault var depth = wrap[0] var prefix = wrap[1] var suffix = wrap[2] var node = document.createElement('div') node.innerHTML = prefix + templateString + suffix while (depth--) { node = node.lastChild } var child document.body.appendChild(node); /* eslint-disable no-cond-assign */ while (child = node.firstChild) { /* eslint-enable no-cond-assign */ frag.appendChild(child) } } if (!raw) { // 移除文档中空文本节点及注释节点 trimNode(frag) } templateCache.put(cacheKey, frag) return frag }
首先去缓存查看是否已经有,若是有,则直接取缓存数据,减小程序运行;然后,经过正则判断是否为元素文本,若是不是,则说明为正常的文字文本,直接建立文本节点,并放入新建的DocumentFragment中再放入缓存中,并返回最终生成的DocumentFragment;若是是节点文本,则首先对文本进行修正;好比若是传入的是<td></td>则须要在其外层添加tr、tbody、table后才能直接使用appendChild将节点添加到文档碎片中,而没法直接添加td元素到div元素中;在最后返回一个DocumentFragment;
以上就是parseTemplate及其里面nodeToFragment、stringToFragment的具体实现;而后咱们继续回到transclude;
在transclude后续中,重要就是transcludeTemplate方法,其主要就是经过此函数,根据option.template将自定义标签转化为真实内容的元素节点;如<hello></hello>
这个自定义标签,会根据此标签里面真实元素而转化为真实的dom结构;
// app.vue <hello></hello> // template: <div class="hello" _v-0480c730=""> <h1 _v-0480c730="">hello {{ msg }} welcome here</h1> <h3 v-if="show" _v-0480c730="">this is v-if</h3> </div>
函数首先会经过上述parseTemplate方法将模版数据转化为一个临时的DocumentFragment,而后根据是否将根元素进行替换,即option.replace是否为true进行对应处理,而若是须要替换,主要进行将替换元素上的属性值和模版根元素属性值进行合并,也就是将替换元素上面的属性合并并添加到根节点上面,若是两个上面都有此属性,则进行合并后的做为最终此属性值,若是模板根元素上没有此属性而自定义元素上有,则将其设置到根元素上,即:
options._replacerAttrs = extractAttrs(replacer) mergeAttrs(el, replacer)
因此,综上,在compile中,el = transclude(el, options)
主要是对元素进行处理,将一个简单的自定义标签根据它对应的template模板数据和option的一些配置,进行整合处理,最后返回整理后的元素数据;
前面,说了下vue在_compile
函数中,首先对el元素进行了处理,主要是处理了自定义标签元素;将自定义标签转化为真实html元素,并对元素属性和真实html根节点属性进行合并;
在这,主要说下对元素根节点的的编译过程,即var rootLinker = compileRoot(el, options, contextOptions)
,compileRoot会生成一个最终的linker函数;而最后经过执行生成的linker函数,完成全部编译过程;
而在源码,能够看到还有compile这个方法,也是对元素进行编译,并生成一个最终的linker函数,那这两个有什么区别呢?为何要分开处理呢?
根据个人理解,compileRoot主要对根节点进行编译,在这儿的根节点不只包括模板中的根节点,也包括自定义的标签;以下组件<hello></hello>
:
// hello.vue <template> <div class="hello"> <h1>hello {{ msg }} welcome here</h1> <h3 v-if="show" >this is v-if</h3> </div> </template> // app.vue <hello class="hello1" :class="{'selected': true}" @click.stop="hello"></hello>
经过compileRoot主要处理<hello>节点和<div class="hello"></div>节点;而compile主要处理整个元素及元素下面的子节点;也包括已经经过compileRoot处理过的节点,只是根节点若是已经处理,在compile中就不会再进行处理;
那为何会分开进行处理呢,由于咱们在前面说过,对于根节点,它也包含了自定义的标签节点,即上面的<hello></hello>,全部就分开进行了处理;
而在具体说明compileRoot如何处理以前,咱们先要知道一点,在vue中,基本上全部的dom操做都是经过指令(directive)的方式处理的;如dom属性的操做(修改class、style)、事件的添加、数据的添加、节点的生成等;而基本大部分的指令都是经过写在元素属性上面(如v-bind、v-if、v-show、v-for)等;因此在编译过程当中,主要是对元素的属性进行提取、根据不一样的属性而后生成对应的Derective的实例;而在执行最终编译生成的linker函数时,也就是对全部生成的指令实例执行bind;并对其添加响应式处理,也就是watcher;
下面,咱们主要说下具体compileRoot里面的代码解析:
// el(虚拟元素,如<hello></hello>)元素上的全部attributes // <hello @click.stop="hello" style="color: red" class="hello" :class="{'selected': true}"></hello> // ['@click.stop', 'style', 'class', ':class'] var containerAttrs = options._containerAttrs // 虚拟元素对应真实html根节点全部attributes // <div class="hello"> ... </div> // ['class', '_v-b9ed5d18'] var replacerAttrs = options._replacerAttrs
这两个主要保存着根元素的属性列表;包括自定义元素和其对应的模板根元素的属性;而它们在哪儿去提取的呢?就是咱们前面说的transclude方法里面,若是忘记了能够回到对应函数里面去查看;
// 2. container attributes if (containerAttrs && contextOptions) { contextLinkFn = compileDirectives(containerAttrs, contextOptions) } // 3. replacer attributes if (replacerAttrs) { replacerLinkFn = compileDirectives(replacerAttrs, options) }
compileDirectives主要对传入的attrs和options,经过正则,对一些属性指令初始化基础信息,并生成对应的处理函数并返回到外面,而最终处理的是
this._directives.push( new Directive(descriptor, this, node, host, scope, frag) )
也就是上面说的生成对应的指令实例化对象,并保存在this._directives中;
具体compileDirectives里面的详细代码,就不细说,这里取出一部分进行说下:
// event handlers // onRE: /^v-on:|^@/ 是否为事件相关属性,如“v-on:click”、"@click" if (onRE.test(name)) { arg = name.replace(onRE, '') pushDir('on', publicDirectives.on) }
这个是主要匹配属性名是不是v-on:类型的,也就是事件相关的,若是是,则取出对应的事件名,而后将其进行指令参数初始化,生成一个指令描述对象:
/** 指令描述对象,以v-bind:href.literal="mylink"为例: { arg:"href", attr:"v-bind:href.literal", def:Object,// v-bind指令的定义 expression:"mylink", // 表达式,若是是插值的话,那主要用到的是下面的interp字段 filters:undefined hasOneTime:undefined interp:undefined,// 存放插值token modifiers:Object, // literal修饰符的定义 name:"bind" //指令类型 raw:"mylink" //未处理前的原始属性值 } **/ dirs.push({ name: dirName, attr: rawName, raw: rawValue, def: def, arg: arg, modifiers: modifiers, // conversion from interpolation strings with one-time token // to expression is differed until directive bind time so that we // have access to the actual vm context for one-time bindings. expression: parsed && parsed.expression, filters: parsed && parsed.filters, interp: interpTokens, hasOneTime: hasOneTimeToken })
生成描述对象数组以后,经过下面函数去初始化指令实例化对象:
function makeNodeLinkFn (directives) { return function nodeLinkFn (vm, el, host, scope, frag) { // reverse apply because it's sorted low to high var i = directives.length while (i--) { vm._bindDir(directives[i], el, host, scope, frag) } } } Vue.prototype._bindDir = function (descriptor, node, host, scope, frag) { this._directives.push( new Directive(descriptor, this, node, host, scope, frag) ) // console.log(new Directive(descriptor, this, node, host, scope, frag)) }
那么,在生成指令数组以后,在哪进行指令的绑定呢?就是下面这儿,在compileRoot返回的最终函数中:
export function compileRoot (el, options, contextOptions) { // 指令的生成过程 ...... return function rootLinkFn (vm, el, scope) { // link context scope dirs var context = vm._context var contextDirs if (context && contextLinkFn) { contextDirs = linkAndCapture(function () { contextLinkFn(context, el, null, scope) }, context) } // link self var selfDirs = linkAndCapture(function () { if (replacerLinkFn) replacerLinkFn(vm, el) }, vm) // return the unlink function that tearsdown context // container directives. return makeUnlinkFn(vm, selfDirs, context, contextDirs) } } // link函数的执行过程会生成新的Directive实例,push到_directives数组中 // 而这些_directives并无创建对应的watcher,watcher也没有收集依赖, // 一切都还处于初始阶段,所以capture阶段须要找到这些新添加的directive, // 依次执行_bind,在_bind里会进行watcher生成,执行指令的bind和update,完成响应式构建 function linkAndCapture (linker, vm) { /* istanbul ignore if */ if (process.env.NODE_ENV === 'production') { // reset directives before every capture in production // mode, so that when unlinking we don't need to splice // them out (which turns out to be a perf hit). // they are kept in development mode because they are // useful for Vue's own tests. vm._directives = [] } // 先记录下数组里原先有多少元素,他们都是已经执行过_bind的,咱们只_bind新添加的directive var originalDirCount = vm._directives.length // 在生成的linker中,会对元素的属性进行指令化处理,并保存到_directives中 linker() // slice出新添加的指令们 var dirs = vm._directives.slice(originalDirCount) // 根据 priority 进行排序 // 对指令进行优先级排序,使得后面指令的bind过程是按优先级从高到低进行的 sortDirectives(dirs) for (var i = 0, l = dirs.length; i < l; i++) { dirs[i]._bind() } return dirs }
也就是经过这儿dirs[i]._bind()
进行绑定;也就是最终compileRoot生成的最终函数中,当执行此函数,首先会执行linkAndCapture
, 而这儿会先去执行传入的函数,也就是contextLinkFn和replacerLinkFn,经过上面两个方法,生成指令数组后,再执行循环,并进行_bind()处理;
而对于_bind()具体干了什么,会在后面详细进行说明;其实主要经过指令对元素进行初始化处理和对须要双向绑定的进行绑定处理;
在上面主要谈了下vue整个compile编译过程,其实最主要任务就是提取节点属性、根据属性建立成对应的指令directive实例并保存到this.directives
数组中,并在执行生成的linker
的时候,将this.directives
中新的指令进行初始化绑定_bind;那这儿主要谈下directive相关的知识;
在前面说过,自定义组件的渲染其实也是经过指令的方式完成的,那这儿就以组件渲染过程来进行说明,以下组件:
// hello.vue <template> <div class="hello"> <h1>hello, welcome here</h1> </div> </template> // app.vue <hello @click.stop="hello" style="color: red" class="hello1" :class="{'selected': true}"></hello>
对于自定义组件的整个编译过程,在前面已经说过了,在这就不说了,主要说下如何经过指令将真正的html添加到对应的文档中;
首先,new directive其实主要是对指令进行初始化配置,就很少谈;
主要说下其中this._bind方法,它是指令初始化后绑定到对应元素的方法;
// remove attribute if ( // 只要不是cloak指令那就从dom的attribute里移除 // 是cloak指令可是已经编译和link完成了的话,那也仍是能够移除的 // 如移出":class"、":style"等 (name !== 'cloak' || this.vm._isCompiled) && this.el && this.el.removeAttribute ) { var attr = descriptor.attr || ('v-' + name) this.el.removeAttribute(attr) }
这儿主要移出元素上添加的自定义指令,如v-if、v-show等;因此当咱们使用控制台去查看dom元素时,实际上是看不到写在代码中的自定义指令属性;可是不包括v-cloak,由于这个在css中须要使用;
// html <div v-cloak> {{ message }} </div> // css [v-cloak] { display: none; } // copy def properties // 不采用原型链继承,而是直接extend定义对象到this上,来扩展Directive实例 // 将不一样指令一些特殊的函数或熟悉合并到实例化的directive里 var def = descriptor.def if (typeof def === 'function') { this.update = def } else { extend(this, def) }
这儿主要说下extend(this, def),descriptor主要是指令的一些描述信息:
指令描述对象,以v-bind:href.literal="mylink"为例: { arg:"href", attr:"v-bind:href.literal", def:Object,// v-bind指令的定义 expression:"mylink", // 表达式,若是是插值的话,那主要用到的是下面的interp字段 filters:undefined hasOneTime:undefined interp:undefined,// 存放插值token modifiers:Object, // literal修饰符的定义 name:"bind" //指令类型 raw:"mylink" //未处理前的原始属性值 }
而,def其实就是指令对应的配置信息;也就是咱们在写指令时配置的数据,以下指令:
<template> <div class="hello"> <h1 v-demo="demo">hello {{ msg }} welcome here</h1> <!-- <h3 v-if="show" >this is v-if</h3> --> </div> </template> <script> export default { created() { setInterval(()=> { this.demo += 1; }, 1000) }, data () { return { msg: 'Hello World!', show: false, demo: 1 } }, directives: { demo: { bind: function() { this.el.setAttribute('style', 'color: green'); }, update: function(value) { if(value % 2) { this.el.setAttribute('style', 'color: green'); } else { this.el.setAttribute('style', 'color: red'); } } } } } </script>
它对应的descriptor就是:
descriptor = { arg: undefined, attr: "v-demo", def: { bind: function() {}, // 上面定义的bind update: function() {} // 上面定义的update }, expression:"demo", filters: undefined, modifiers: {}, name: 'demo' }
接着上面的,使用extend(this, def)
就将def中定义的方法或属性就复制到实例化指令对象上面;好供后面使用;
// initial bind if (this.bind) { this.bind() }
这就是执行上面刚刚保存的bind方法;当执行此方法时,上面就会执行
this.el.setAttribute('style', 'color: green');
将字体颜色改成绿色;
// 下面这些判断是由于许多指令好比slot component之类的并非响应式的, // 他们只须要在bind里处理好dom的分发和编译/link便可而后他们的使命就结束了,生成watcher和收集依赖等步骤根本没有 // 因此根本不用执行下面的处理 if (this.literal) { } else if ( (this.expression || this.modifiers) && (this.update || this.twoWay) && !this._checkStatement() ) { var watcher = this._watcher = new Watcher( this.vm, this.expression, this._update, // callback { filters: this.filters, twoWay: this.twoWay, deep: this.deep, preProcess: preProcess, postProcess: postProcess, scope: this._scope } ) }
而这儿就是对须要添加双向绑定的指令添加watcher
;对应watcher后面再进行详细说明; 能够从上看出,传入了this._update
方法,其实也就是当数据变化时,就会执行this._update
方法,而:
var dir = this if (this.update) { // 处理一下本来的update函数,加入lock判断 this._update = function (val, oldVal) { if (!dir._locked) { dir.update(val, oldVal) } } } else { this._update = function() {} }
其实也就是执行上面的descriptor.def.update
方法,因此当值变化时,会触发咱们自定义指令时定义的update方法,而发生颜色变化;
这是指令最主要的代码部分;其余的以下:
// 获取指令的参数, 对于一些指令, 指令的元素上可能存在其余的attr来做为指令运行的参数 // 好比v-for指令,那么元素上的attr: track-by="..." 就是参数 // 好比组件指令,那么元素上可能写了transition-mode="out-in", 诸如此类 this._setupParams(); // 当一个指令须要销毁时,对其进行销毁处理;此时,若是定义了unbind方法,也会在此刻调用 this._teardown(); 而对于每一个指令的处理原理,能够看其对应源码;如v-show源码: // src/directives/public/show.js import { getAttr, inDoc } from '../../util/index' import { applyTransition } from '../../transition/index' export default { bind () { // check else block var next = this.el.nextElementSibling if (next && getAttr(next, 'v-else') !== null) { this.elseEl = next } }, update (value) { this.apply(this.el, value) if (this.elseEl) { this.apply(this.elseEl, !value) } }, apply (el, value) { if (inDoc(el)) { applyTransition(el, value ? 1 : -1, toggle, this.vm) } else { toggle() } function toggle () { el.style.display = value ? '' : 'none' } } }
能够从上面看出在初始化页面绑定时,主要获取后面兄弟元素是否使用v-else
; 若是使用,将元素保存到this.elseEl
中,而当值变化执行update
时,主要执行了this.apply
;而最终只是执行了下面代码:
el.style.display = value ? '' : 'none'
从而达到隐藏或者展现元素的效果;
未完待续,后续会持续完善......