Skip to content

Instantly share code, notes, and snippets.

@koba04
Last active September 1, 2015 03:30
Show Gist options
  • Save koba04/c93aaf2fcd9cf0349772 to your computer and use it in GitHub Desktop.
Save koba04/c93aaf2fcd9cf0349772 to your computer and use it in GitHub Desktop.
How to Vue.js compile ViewModel

これがどうやってCompileされるのか追ってみる

vm = new Vue({
  el: "#content",
  data: {
    name: "taro",
  }
})

Constructor

src/viewmodel.js

こんな感じでViewModelのコンストラクタはCompilerに委譲されているだけ

function ViewModel (options) {
    // just compile. options are passed directly to compiler
    new Compiler(this, options)
}

Compiler

src/compiler.js

まずは渡されたオプションを処理している

var compiler = this,
    key, i

// default state
compiler.init       = true
compiler.destroyed  = false

// process and extend options
options = compiler.options = options || {}
utils.processOptions(options)

// copy compiler options
extend(compiler, options.compilerOptions)
// repeat indicates this is a v-repeat instance
compiler.repeat   = compiler.repeat || false
// expCache will be shared between v-repeat instances
compiler.expCache = compiler.expCache || {}

ここでelをセットしている

var el = compiler.el = compiler.setupElement(options)

Elementの作成

src/compiler.js setupElement

elが文字列ならquerySelectorに渡して、違ったらそのまま使って、ない場合はtagNameがあればそれでなければdivでcreateElementする

var el = typeof options.el === 'string'
  ? document.querySelector(options.el)
  : options.el || document.createElement(options.tagName || 'div')

var template = options.template,
  child, replacer, i, attr, attrs

templateが指定されている場合

if (template) {

elが子要素を持っている場合はthis.rawContentにdiv作ってその下に突っ込んでいく firstChildをappendChildで追加していくことで移動させている

// collect anything already in there
if (el.hasChildNodes()) {
  this.rawContent = document.createElement('div')
  /* jshint boss: true */
  while (child = el.firstChild) {
    this.rawContent.appendChild(child)
  }
}

replaceオプションがあってtemplateの子要素が一つだけの場合はelと置き換える

elの親要素がある場合はtemplateの子要素を追加してel自体はDOMから削除する

elが持っていた属性は置き換えた要素にコピーされる

上記にマッチせずに置き換えでない場合はelにtemplateを追加するだけ

// replace option: use the first node in
// the template directly
if (options.replace && template.firstChild === template.lastChild) {
  replacer = template.firstChild.cloneNode(true)
  if (el.parentNode) {
    el.parentNode.insertBefore(replacer, el)
    el.parentNode.removeChild(el)
}
// copy over attributes
if (el.hasAttributes()) {
  i = el.attributes.length
  while (i--) {
    attr = el.attributes[i]
    replacer.setAttribute(attr.name, attr.value)
  }
}
// replace
el = replacer
} else {
  el.appendChild(template.cloneNode(true))
}

後はid、className、attributesのオプションをセットして返す

// apply element options
if (options.id) el.id = options.id
if (options.className) el.className = options.className
attrs = options.attributes
if (attrs) {
  for (attr in attrs) {
    el.setAttribute(attr, attrs[attr])
  }
}

return el

プロパティの準備してる。vmとemitterもセットされている

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

methodsとcomputedのバインディングを生成している

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

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

binding.mdに続く...

methodsとcomputedのバインディングの生成

src/compiler.js createBinding

プロパティの準備

new Bindingsrc/binding.jsに定義されていて、コンストラクタではプロパティのセットしかしていないので省略

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)

isExp、isFnはdirectiveの時の処理なので今回は飛ばす TODO

if (isExp) {
  // expression bindings are anonymous
  compiler.defineExp(key, binding, directive)
} else if (isFn) {
  bindings[key] = binding
  binding.value = compiler.vm[key] = methods[key]

root要素の場合、今回はVMのmethodsなのでroot

computedなプロパティの場合はdefineComputedを呼んで、$から始まるvue.jsが定義しているような値の場合はdefineMetaを読んでそれ以外の通常の値の場合はdefinePropを呼ぶ

bindings[key] = binding
if (binding.root) {
  // this is a root level binding. we need to define getter/setters for it.
  if (computed && computed[key]) {
    // computed property
    compiler.defineComputed(key, binding, computed[key])
  } else if (key.charAt(0) !== '$') {
    // normal property
    compiler.defineProp(key, binding)
  } else {
    compiler.defineMeta(key, binding)
  }

defineComputed

defっていうのはObject.definePropertyのエイリアスなのでmarkComputedしたあとにgetter, setterをセットしている

/**
 *  Define a computed property on the VM
 */
CompilerProto.defineComputed = function (key, binding, value) {
    this.markComputed(binding, value)
    def(this.vm, key, {
        get: binding.value.$get,
        set: binding.value.$set
    })
}

markedComputed

Computed Propertyは値に関数を定義するとgetterとして処理されるのでその処理をしつつ$getter, $setterを作る

Computed Propertyのパースはこの時点では行われなくてただcomputedにpushdされる

utils.bindはESのbindみたいなもの

/**
 *  Process a computed property binding
 *  so its getter/setter are bound to proper context
 */
CompilerProto.markComputed = function (binding, value) {
    binding.isComputed = true
    // bind the accessors to the vm
    if (binding.isFn) {
        binding.value = value
    } else {
        if (typeof value === 'function') {
            value = { $get: value }
        }
        binding.value = {
            $get: utils.bind(value.$get, this.vm),
            $set: value.$set
                ? utils.bind(value.$set, this.vm)
                : undefined
        }
    }
    // keep track for dep parsing later
    this.computed.push(binding)
}

defineProp

dataの値に対してのobserverを設定しつつ、dataの値に対してのaliasをvmにつくる

/**
 *  Define the getter/setter for a root-level property on the VM
 *  and observe the initial value
 */
CompilerProto.defineProp = function (key, binding) {
    var compiler = this,
        data     = compiler.data,
        ob       = data.__emitter__

    // make sure the key is present in data
    // so it can be observed
    if (!(hasOwn.call(data, key))) {
        data[key] = undefined
    }

    // if the data object is already observed, but the key
    // is not observed, we need to add it to the observed keys.
    if (ob && !(hasOwn.call(ob.values, key))) {
        Observer.convertKey(data, key)
    }

    binding.value = data[key]

    def(compiler.vm, key, {
        get: function () {
            return compiler.data[key]
        },
        set: function (val) {
            compiler.data[key] = val
        }
    })
}

Observer.convertKey

$と_で始まっているkeyは無視する

/**
 *  Define accessors for a property on an Object
 *  so it emits get/set events.
 *  Then watch the value itself.
 */
function convertKey (obj, key, propagate) {
    var keyPrefix = key.charAt(0)
    if (keyPrefix === '$' || keyPrefix === '_') {
        return
    }
    // emit set on bind
    // this means when an object is observed it will emit
    // a first batch of set events.
    var emitter = obj.__emitter__,
        values  = emitter.values

initでは"set"のイベントをkey, valueを渡して発行してるのと配列の場合は{key}.lengthと配列のlengthを渡してイベントをさらに発行している(observerはこの後見る)

definePropertyでgetterでは、"get"のイベントを発行して、setterではobserverを一旦解除して、initを読んでobserverをセットしている

    init(obj[key], propagate)

    oDef(obj, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            var value = values[key]
            // only emit get on tip values
            if (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 too
            init(newVal, true)
        }
    })

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

Observer.observer

get、set、mutateイベントに対してイベントを登録している

/**
 *  Observe an object with a given path,
 *  and proxy get/set/mutate events to the provided observer.
 */
function observe (obj, rawPath, observer) {

    if (!isWatchable(obj)) return

    var path = rawPath ? rawPath + '.' : '',
        alreadyConverted = convert(obj),
        emitter = obj.__emitter__

    // setup proxy listeners on the parent observer.
    // we need to keep reference to them so that they
    // can be removed when the object is un-observed.
    observer.proxies = observer.proxies || {}
    var proxies = observer.proxies[path] = {
        get: function (key) {
            observer.emit('get', path + key)
        },
        set: function (key, val, propagate) {
            if (key) observer.emit('set', path + key, val)
            // also notify observer that the object itself changed
            // but only do so when it's a immediate property. this
            // avoids duplicate event firing.
            if (rawPath && propagate) {
                observer.emit('set', rawPath, obj, true)
            }
        },
        mutate: function (key, val, mutation) {
            // if the Array is a root value
            // the key will be null
            var fixedPath = key ? path + key : rawPath
            observer.emit('mutate', fixedPath, val, mutation)
            // also emit set for Array's length when it mutates
            var m = mutation.method
            if (m !== 'sort' && m !== 'reverse') {
                observer.emit('set', fixedPath + '.length', val.length)
            }
        }
    }

    // attach the listeners to the child observer.
    // now all the events will propagate upwards.
    emitter
        .on('get', proxies.get)
        .on('set', proxies.set)
        .on('mutate', proxies.mutate)

    if (alreadyConverted) {
        // for objects that have already been converted,
        // emit set events for everything inside
        emitSet(obj)
    } else {
        watch(obj)
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment