Vue早期源码阅读(二)

2016-06-09  本文已影响157人  lizhihua

Vue.js 早期源码阅读(版本号154861f71d4886251e0057c74f07c786f5262081),关于ViewModel的一点分析,这一次代码来的比上一次复杂的多,完全不谦虚的说,以我的能力可谓是勉强往下看。总体感觉代码写的非常好(和自己平时写的一坨代码相比,完全不是一个档次),悟性太低看了只有一点点小收获而已。代码太长粘在最下面。

一、依赖关系
main.js
require('./directive')
require('./directives')

directive
require('./directives')
require('./filters')

二、导出函数和作用域内函数

main.js
1.module.exports.create
  1)Seed.constructor
  2)Seed.prototype.compileNode
  3)Seed.prototype.bind
  4)Seed.prototype.createBinding
  5)Seed.prototype.dump
  6)Seed.prototype.destroy
  7)scope.cloneAttributes
2.module.exports.directive
3.module.exports.filter

directive.js
1.module.exports.parse
  1)Directive.constructor
  2)Directive.prototype.update
  3)Directive.prototype.applyFilters

directives.js
1.module.exports.text
2.module.exports.show
3.module.exports.class
4.module.exports.on
  1)module.exports.on.update
  2)module.exports.on.unbind
5.module.exports.each
6.scope.augmentArray

filters.js
1.module.exports.capitalize
2.module.exports.uppercase
3.module.exports.delegate

三、执行分析
1.Seed.create(此Seed是变量名)传入参数创建Seed实例(此Seed是main.js内部的函数名)
2.Seed做了3件事
以下root指#test,select指[sd-text],[sd-show],[sd-class],[sd-on],[sd-each]
1)遍历具有指令的元素(root.querySelectorAll(selector)),调用compileNode解析节点,此时的生命周期处于beforeCompile(按照官网文档参考,命名不一定准确)
2)对根节点root,调用compileNode解析节点
3)上面已经为劫持了data(绑定了getter和setter),并将具有指令的元素和setter建立了映射关系

四、函数分析

dev.html

<style type="text/css">
  .red {
    color: red;
  }
</style>
<body>
    <div id="test" sd-on-click="changeMessage | delegate .button">
          <p sd-text="msg.wow | capitalize" sd-on-click="remove"></p>
          <p sd-text="msg.wow | uppercase" class="button"></p>
          <p sd-class-red="error" sd-text="hello"></p>
          <div sd-each="todos">
            <span sd-text="text"></span>
          </div>
      </div>
  <script>
    var Seed = require('seed')
    var app = Seed.create({
        id: 'test',
        // template
        scope: {
            'msg.wow': 'wow',
            hello: 'hello',
            changeMessage: function () {
                app.scope['msg.wow'] = 'hola'
            },
            remove: function () {
                app.destroy()
            },
            todos: [
              {
                title: 'make this shit work',
                done: false
              },
              {
                title: 'make this shit kinda work',
                done: true
              }
            ]
        }
    })
  </script>
</body>

main.js

var prefix      = 'sd',
    Directive   = require('./directive'),
    Directives  = require('./directives'),
    selector    = Object.keys(Directives).map(function (d) {
        return '[' + prefix + '-' + d + ']'
    }).join()

function Seed (opts) {

    var self = this,
        root = this.el = document.getElementById(opts.id),
        els  = root.querySelectorAll(selector)

    self.bindings = {}
    self.scope = {}

    // process nodes for directives
    ;[].forEach.call(els, this.compileNode.bind(this))
    this.compileNode(root)

    // initialize all variables by invoking setters
    for (var key in self.bindings) {
        self.scope[key] = opts.scope[key]
    }

}

Seed.prototype.compileNode = function (node) {
    var self = this
    cloneAttributes(node.attributes).forEach(function (attr) {
        var directive = Directive.parse(attr, prefix)
        if (directive) {
            self.bind(node, directive)
        }
    })
}

Seed.prototype.bind = function (node, directive) {

    directive.el = node
    node.removeAttribute(directive.attr.name)

    var key      = directive.key,
        binding  = this.bindings[key] || this.createBinding(key)

    // add directive to this binding
    binding.directives.push(directive)

    // invoke bind hook if exists
    if (directive.bind) {
        directive.bind(node, binding.value)
    }

}

Seed.prototype.createBinding = function (key) {

    var binding = {
        value: undefined,
        directives: []
    }

    this.bindings[key] = binding

    // bind accessor triggers to scope
    Object.defineProperty(this.scope, key, {
        get: function () {
            return binding.value
        },
        set: function (value) {
            binding.value = value
            binding.directives.forEach(function (directive) {
                directive.update(value)
            })
        }
    })

    return binding
}

Seed.prototype.dump = function () {
    var data = {}
    for (var key in this._bindings) {
        data[key] = this._bindings[key].value
    }
    return data
}

Seed.prototype.destroy = function () {
    for (var key in this._bindings) {
        this._bindings[key].directives.forEach(unbind)
    }
    this.el.parentNode.remove(this.el)
    function unbind (directive) {
        if (directive.unbind) {
            directive.unbind()
        }
    }
}

// clone attributes so they don't change
function cloneAttributes (attributes) {
    return [].map.call(attributes, function (attr) {
        return {
            name: attr.name,
            value: attr.value
        }
    })
}

module.exports = {
    create: function (opts) {
        return new Seed(opts)
    },
    directive: function () {
        // create dir
    },
    filter: function () {
        // create filter
    }
}

directive.js

var Directives = require('./directives'),
    Filters    = require('./filters')

var KEY_RE = /^[^\|]+/,
    FILTERS_RE = /\|[^\|]+/g

function Directive (def, attr, arg, key) {

    if (typeof def === 'function') {
        this._update = def
    } else {
        for (var prop in def) {
            if (prop === 'update') {
                this['_update'] = def.update
                continue
            }
            this[prop] = def[prop]
        }
    }

    this.attr = attr
    this.arg  = arg
    this.key  = key
    
    var filters = attr.value.match(FILTERS_RE)
    if (filters) {
        this.filters = filters.map(function (filter) {
            // TODO test performance against regex
            var tokens = filter.replace('|', '').trim().split(/\s+/)
            return {
                apply: Filters[tokens[0]],
                args: tokens.length > 1 ? tokens.slice(1) : null
            }
        })
    }
}

Directive.prototype.update = function (value) {
    // apply filters
    if (this.filters) {
        value = this.applyFilters(value)
    }
    this._update(value)
}

Directive.prototype.applyFilters = function (value) {
    var filtered = value
    this.filters.forEach(function (filter) {
        filtered = filter.apply(filtered, filter.args)
    })
    return filtered
}

module.exports = {

    // make sure the directive and value is valid
    parse: function (attr, prefix) {
        
        if (attr.name.indexOf(prefix) === -1) return null

        var noprefix = attr.name.slice(prefix.length + 1),
            argIndex = noprefix.indexOf('-'),
            arg = argIndex === -1
                ? null
                : noprefix.slice(argIndex + 1),
            name = arg
                ? noprefix.slice(0, argIndex)
                : noprefix,
            def = Directives[name]

        var key = attr.value.match(KEY_RE)

        return def && key
            ? new Directive(def, attr, arg, key[0].trim())
            : null
    }
}

directives.js

module.exports = {

    text: function (value) {
        this.el.textContent = value || ''
    },

    show: function (value) {
        this.el.style.display = value ? '' : 'none'
    },

    class: function (value) {
        this.el.classList[value ? 'add' : 'remove'](this.arg)
    },

    on: {
        update: function (handler) {
            var event = this.arg
            if (!this.handlers) {
                this.handlers = {}
            }
            var handlers = this.handlers
            if (handlers[event]) {
                this.el.removeEventListener(event, handlers[event])
            }
            if (handler) {
                handler = handler.bind(this.el)
                this.el.addEventListener(event, handler)
                handlers[event] = handler
            }
        },
        unbind: function () {
            var event = this.arg
            if (this.handlers) {
                this.el.removeEventListener(event, this.handlers[event])
            }
        }
    },

    each: {
        update: function (collection) {
            augmentArray(collection, this)
        },
        mutate: function (mutation) {
            console.log(mutation)
        }
    }

}

var push = [].push,
    slice = [].slice

function augmentArray (collection, directive) {
    collection.push = function (element) {
        push.call(this, arguments)
        directive.mutate({
            event: 'push',
            elements: slice.call(arguments),
            collection: collection
        })
    }
}

filters.js

module.exports = {

    capitalize: function (value) {
        value = value.toString()
        return value.charAt(0).toUpperCase() + value.slice(1)
    },

    uppercase: function (value) {
        return value.toUpperCase()
    },

    delegate: function (handler, selectors) {
        return function (e) {
            var match = selectors.every(function (selector) {
                return e.target.webkitMatchesSelector(selector)
            })
            if (match) handler.apply(this, arguments)
        }
    }

}
上一篇下一篇

猜你喜欢

热点阅读