/vue-learning-notes

Vue.js source code learning notes

MIT LicenseMIT

Vue learing notes

My note of learning Vue.js

初始设置

把依赖啥的都安装好,component 这个全局库不能使用最新版本否则执行grunt task时会报错

$ npm i -g grunt 
$ npm i -g component@0.16.3
$ npm i
$ component install component/emitter
$ grunt (--force)


PS:每个commit的标题,当然不能复制粘贴手写... chrome snippet来一套

(function () {
    let href = window.location.href;
    let key = window.location.pathname.match(/(commit\/)[\w]+/g)[0].split('/')[1];
    let shortKey = key.slice(0,7);
    let commitTitle = document.querySelector('.commit-title').innerText;
    let $commitDesc = document.querySelector('.commit-desc');
    let commitDesc = $commitDesc ? $commitDesc.innerText : '';
    let commitText = (commitTitle + commitDesc).replace(//g, '');
    let result = `#### ${commitText} [${shortKey}](${href})`;
    console.log(result);    
})();

initial setup 83fac01

Vue 最初的名字叫 element


rename 871ed91

改名叫 seed ,初期思路的尝试。

Mustache语法 {{}} 做一个replace,替换掉原来的innerHTML

<p>{{msg}}</p>
// parse后:
<p><span data-element-binding="msg"></span></p>

同时维护一个 bindings 变量,存储所有 {{}} 中的绑定变量,如 msg

msg 维护一个els的属性,使用 document.queryselectAll("[data-element-binding=*]") 拿到所有DOM,存入。然后删除 data-element-binding 这个属性。

使用 Object.defineProperty 中的 set 钩子完成值 -> dom单向绑定

Object.defineProperty(data, variable, {
    set: function (newVal) {
        // 赋值时设定binding, 改动dom
        [].forEach.call(bindings[variable].els, function (e) {
            bindings[variable].value = e.textContent = newVal
        })
    },
    get: function () {
        return bindings[variable].value
    }
})

初期的实现,作者当时ms在参考 Rivets.js

filters.js

filters 来对绑定变量做特殊处理,这里仅是一个 capitalize

directive.js

directives 一些内置指令,是 vue 使用者很熟悉的指令:

  • text: el.textContent
  • show: el.style.display
  • class: add/remove className
  • on: update, unbind, customFilter

main.js

new Seed(opts)做初始化,参数opts是根dom。

在根dom里面把为具有sdprefix,且满足在当前 内置directives 中条件的dom 找出来。比如

<div id="test" sd-on-click="changeMessage | .button">
    <p sd-text="msg | capitalize"></p>
    <p sd-show="something">YOYOYO</p>
    <p class="button" sd-text="msg"></p>
    <p sd-class-red="error" sd-text="hello"></p>
</div>

整个流程如下

graph TB
A(取出所有内置指令)-->B(获取root);
B-->C(解析并parse该内置指令,设置update);
C-->D(在seed scope中绑定所有key);
D-->E(对所有key做get/set处理)

同时,在 set 函数执行时, 由于内置指令的预定义函数绑定了directive的update。(见函数bindAccessors), 所以统一调用 directive.update 方法。 不同的内置指令对应不同的行为,如:

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

最后,从TODO中可以了解作者的思路:

  • nested levels by parsing dot syntax (which means nested getter/setters...)

例如在传

new Seed(dom, {
    'msg.wow': 'hello'
})

的情况下,即有nested getter/setter

  • repeat directive by watching an array: 这个应该是 v-for 之类的遍历数组了
  • parse textNodes:对TextNode进行特殊处理,比如{{hello}},转化成dom attribute效率还是比较低
  • make Seeds compositable:可以mixin?
  • formatter arguments: 不知道要干嘛
  • Seed.extend(): 创建一个新实例
  • options to pass in templates to Seed.create():new Seed(arguments)

filter value should not be written 3eb7f6f

dump, destroy, fix filters ec39439

修了个bug,就是用户原始传入的值被filter处理后,是需要保持原值的,filter处理的是值的副本。

Seed构造函数加入 dumpdestroy


refrator cf1732b

把内置指令的parse/具体方法划分为 directive.jsdirectives.js

做了 this 的绑定

classrepeat 暂时还没有实现


augmentArray seems to work 154861f

覆写了Array.prototype.push方法,push时进行mutate,做到数组长度改变时的监听

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

再看这一段

<div id="test" sd-on-click="changeMessage | delegate .button">
...
</div>

filter.js里面使用了Element.prototype.matches 来进行事件代理 ,但是这里有bug, e.target !== e.currentTarget 时需要往parentNode上继续检测的嘛,而且matches有兼容性问题,待后续看解决方案是polyfill还是其他。

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

PS: Element.matches() 支持情况, 原则上是可以加前缀通用的, 基本都已经支持。即使作polyfill也没啥。

Feature Chrome Firefox (Gecko) Internet Explorer Opera Safari (WebKit)
Original support with a non-standard name Yes 3.6 (1.9.2) 9.0 11.5 / 15.0 5.0
Specified version 34 34 ? 21.0 7.1

此数据来自 mozilla developer


抽离设置到config.js filter.js 当filter函数没定义时throw Error

数组的监听方面,监听了几个常用的方法, 做本职工作的同时加入callback

var proto = Array.prototype,
    slice = proto.slice,
    mutatorMethods = [
        'pop',
        'push',
        'reverse',
        'shift',
        'unshift',
        'splice',
        'sort'
    ]

module.exports = function (arr, callback) {

    mutatorMethods.forEach(function (method) {
        arr[method] = function () {
            proto[method].apply(this, arguments)
            callback({
                event: method,
                args: slice.call(arguments),
                array: arr
            })
        }
    })

}

main.js 只作为一个入口点,主要逻辑移到了seed.js


refractor 5ce3b82

sd-repeat --> sd-each

引入Seed.extend 替换原来的 Seed.seed(ms原来这种命名感觉会绕晕),通过原型继承创建子组件

作者打算写一个todo , 毕竟是MVVM的标准Demo...


kinda working now 952ab43

作者在todo.md里面做的思考:

  • ? separate scope data and prototype methods // think about this

MVVM 里面不可避免的会碰上 nested scope 。 如:

<parent>
  <child>
    <button sd-on-click="change" sd-text="hello">
  </child>
</parent>

那么问题是:比如parent 和 child 都有change方法,data中都有hello,应该用哪个?遍历dom树解析的时候如何解析?

假设我们希望hello是只读取child的,比较合情理。

那么有N个child 绑定 change事件呢?我们又希望能够做delegate绑到parent上去.....

那么如何实现这样的功能,甚至是一个 isolated scope

这个任何一个MVVM框架都会做而且必须做。


sd-each-* works 3149839

实现了数组循环绑定。

<li sd-each-todo="todos">
    <span class="todo" sd-text="todo.title" sd-class-done="todo.done"></span>   
</li>

要实现循环绑定,那么按上面例子就需要插入n个span. 这里引入了一个childSeed ,当长度大于0时进行插入:

function buildItem (data, index, collection) {
    var node = this.el.cloneNode(true),
        spore = new Seed(node, data, {
            eachPrefixRE: this.prefixRE,
            parentScope: this.seed.scope
        })
    this.container.insertBefore(node, this.marker)
    collection[index] = spore.scope
    return spore
}

Seed.prototype._compileNode中对textnode进行处理, _compileTextNode暂还未实现:

if (node.nodeType === 3) {
    // text node
    self._compileTextNode(node)
}

big refactor.. again 23a2bde

重构狂魔的作者又搞出来一个controller的概念。。是否也受了ng的影响。。

controller 主要就是解决 scope nesting的问题,这个版本还没实现

实际就是创建一个Seed新实例, 避免混淆

Seed.controller = function (id, extensions) {
    if (controllers[id]) {
        console.warn('controller "' + id + '" was already registered and has been overwritten.')
    }
    var c = controllers[id] = Seed.extend(extensions)
    return c
}

原先的 directive.jsdirectives.js 显然看名字有点晕,所以改成了binding.js

前面提到的:

filter.js里面使用了Element.prototype.matches 来进行事件代理 ,但是这里有bug, e.target !== e.currentTarget 时需要往parentNode上继续检测的嘛,而且matches有兼容性问题,待后续看解决方案是polyfill还是其他。

filter.js中的deletegate方法做了一个delegateCheck ,做冒泡遍历,修掉之前bug。

Seed.bootstrap 对于controller做一个自检测,但是还未加入流程。


kinda work now except for scope nesting 62b75d4

almost there! scope nesting 15ffaa4

_compileTextNode 方法做了实现:先match, 如果自己没有,去找parentSeed

<ul sd-show="todos">
    <li class="todo"
        sd-controller="Todo"
        sd-each="todo:todos"
        sd-class="done:todo.done"
        sd-on="click:changeMessage, click:todo.toggle"
        sd-text="msg"
    ></li>
</ul>

里面对于nested scope的实现:

var key = bindingInstance.key,
        epr = this._options.eachPrefixRE,
        isEachKey = epr && epr.test(key),
        scopeOwner = this
    // TODO make scope chain work on nested controllers
    if (isEachKey) {
        key = key.replace(epr, '')
    } else if (epr) {
        scopeOwner = this._options.parentSeed
    }

感觉现在这样的设计用起来还是有点囧的...必须要标注一下自己是属于哪个scope的


finally workssssss 08e7992

还在解决 nested scope的问题。

milestone reached, update todo 83665f9

一个基本的MVVM已经实现了。但是原来的nested scope实现被作者自己否了。

else if (ctrlExp && !root) { // nested controllers
    // TODO need to be clever here!
} 

todo.md

  • nested controllers - but how to inherit scope? 作者在思考作用域继承的问题。
  • improve arrayWatcher
  • parse textNodes
  • computed properties : computed properties的萌芽,天才的想法~

emitter 1d7a94e

用到了component/emitter, 让它作为Seed.prototypemixin, 用来做事件传递

seed.js中:

Emitter(Seed.prototype)
Seed.controller('Todo', function (scope, seed) {
    scope.toggle = function () {
        scope.done = !scope.done
        seed.parentSeed.emit('toggle', seed)
    }
})

todo demo kinda works a6b7257

Binding 统一改回 Directive

directives.js 内置指令中加入了 sd-checked

这个demo还不完整,directives.js中的mutate 中还未加相关逻辑,当数组长度改变时(如添加一个todo)并不能反馈到dom上。

update: function (collection) {
    if (this.childSeeds.length) {
        this.childSeeds.forEach(function (child) {
            child.destroy()
        })
        this.childSeeds = []
    }
    watchArray(collection, this.mutate.bind(this))
    var self = this
    collection.forEach(function (item, i) {
        self.childSeeds.push(self.buildItem(item, i, collection))
    })
},
mutate: function (mutation) {
    console.log(mutation)
}

awesome fcd9544

加入了内置指令 sd-class


nested controllers f1ed54b

作者现在思考在框架中应该包含的内容:

  • Template
  • Controller
    • Nested Controllers and accessing parent scope
    • Controller inheritance
  • Data
  • Data Binding
  • Filters
  • Computed Properties
  • Custom Filter
  • Custom Directive

接下来要做的内容:

  • complete arrayWatcher

  • computed properties (through invoking functions, need to rework the setter triggering mechanism using emitter)

    • (computed properties 基于一个已声明的函数,这里呢就需要在setter里面计算依赖啊啥的)
  • the data object passed in should become an absolute source of truth, so multiple controllers can bind to the same data (i.e. second seed using it should insert dependency instead of overwriting it)

    • (原始的data保留副本,可以让多个实例来用)
  • nested properties in scope (kinda hard but try)

    • (nested properties 的确是比较tough的东西)
  • parse textNodes

作者做了一下 nested controllers 的尝试

<div sd-controller="Grandpa">
    <p sd-text="name"></p>

    <div sd-controller="Dad">
        <p><span sd-text="name"></span>, son of <span sd-text="^name"></span></p>

        <div sd-controller="Son">
            <p><span sd-text="name"></span>, son of <span sd-text="^name"></span></p>

            <div sd-controller="Baby">
                <p><span sd-text="name"></span>, son of <span sd-text="^name"></span>, grandson of <span sd-text="^^name"></span> and great-grandson of <span sd-text="$name"></span></p>

            </div>
        </div>
    </div>
</div>

作者定义的逻辑是这样的:

  • name -> 自己的scope
  • ^name -> 父亲的scope
  • ^^name -> 父亲的父亲的scope
  • $name -> root scope

在这里,先定义了两个正则:

var ancestorKeyRE = /\^/g,
    rootKeyRE = /^\$/

这里就是定义个while循环去match例如 ^^name 这样的东西,找到最终的 scopeOwner

if (snr && !isEachKey) {
    scopeOwner = this.parentSeed
} else {
    var ancestors = key.match(ancestorKeyRE),
        root      = key.match(rootKeyRE)
    if (ancestors) {
        key = key.replace(ancestorKeyRE, '')
        var levels = ancestors.length
        while (scopeOwner.parentSeed && levels--) {
            scopeOwner = scopeOwner.parentSeed
        }
    } else if (root) {
        key = key.replace(rootKeyRE, '')
        while (scopeOwner.parentSeed) {
            scopeOwner = scopeOwner.parentSeed
        }
    }
}

solved the setter init invoke 14d0cef

干掉了统一初始化scope_dataCopy属性相关代码,

var binding = {
    value: this.scope[key], // 原先的value是null
    instances: []
}             
// set initial value
  if (binding.value) {
      directive.update(binding.value) // 初始化时使用这个value
  }

better unbind/destroy 6d81bff

调用unbind(true)destroy, 调用unbind()即为unbind

unbind: function (rm) {
    if (this.childSeeds.length) {
        var fn = rm ? 'destroy' : 'unbind'
        this.childSeeds.forEach(function (child) {
            child[fn]()
        })
    }
}

改进了unbind, 现在是完备的逻辑了

Seed.prototype.unbind = function () {
    var unbind = function (instance) {
        if (instance.unbind) {
            instance.unbind()
        }
    }
    for (var key in this._bindings) {
        this._bindings[key].instances.forEach(unbind)
    }
    this.childSeeds.forEach(function (child) {
        child.unbind()
    })
}

作者同时在思考delegate的问题。

// TODO probably there's a better way.
// Angular doesn't support delegation either
// any real performance gain?
delegate: function (handler, args) {
    var selector = args[0]
    return function (e) {
        var oe = e.originalEvent,
            target = delegateCheck(oe.target, oe.currentTarget, selector)
        if (target) {
            e.el = target
            e.seed = target.seed
            handler.call(this, e)
        }
    }
}

fix sd-on 8f79a10

怒了,把delegate整个注掉了。


computed property progress 3d33458

要实现computed property就一定需要有依赖解析

显然,依赖解析最简单的方法是声明依赖

like this:

Total: <span sd-text="total < todos"></span> |
Remaining: <span sd-text="remaining"></span> |
Completed: <span sd-text="completed < remaining total"></span>

// computed properties
scope.total = function () {
    return scope.todos.length
}

scope.completed = function () {
    return scope.todos.length - scope.remaining
}

上述这种声明方式,声明了total依赖todos, completed 依赖 remainingtotal

但是这样对用户来说成本略高。

这个版本中也还未实现调用total, completed这些声明函数去计算的逻辑。


event delegation in sd-each 5227248

搞回delegate, 实现一个*matchSelector 的 polyfill

directives.js 里面的 on,即指令sd-on 有以下逻辑

  • bind时,如果是sd-each说明是循环,那么需要用delegate;定义当前的selectordelegator(父元素)
  • update时,通过delegateCheck找到delegator中的handler并绑定eventListener
bind: function (handler) {
    if (this.seed.each) {
        this.selector = '[' + this.directiveName + '*="' + this.expression + '"]'
        this.delegator = this.seed.el.parentNode
    }
},

但是现在的实现ms是有问题的,因为上面对this.delegator赋值的时候,dom还没有渲染完成,这时是没有parentNode

换句话说,this.delegator始终为null, 绑定的时机不太对。


break directives into individual files ca62c95

拆分了directives到不同的文件


arrayWatcher 88513c0

如前所述,对于数组的mutation需要做处理。这里定义了一个 mutationHandlers,做以下处理

  • push: self.buildItem --> self.container.insertBefore
  • pop: pop 拿出来的元素,当然是直接$destroy
  • unshift: self.buildItem --> self.container.insertBefore --> self.reorder 很形象
  • shift: $destroy --> self.reorder
  • splice: $destroy --> self.container.insertBefore
  • sort: selft.container.insertBefore

push为例来梳理一下arrayWatcher的实现过程:

  • 在scope中执行了push, 进入被watchArray覆写的push方法,执行push
  • 覆写的push方法带着执行mutationHandler, 接着执行mutationHandler[push],传入三个参数:method,args,result
  • 执行 self.buildItem , 通过 cloneNode 创建一个node
  • 执行 new Seed() 创建一个新实例 spore, 绑定这个node
  • 把这个spore 放到 负责sd-eachdirective中的collection属性数组中, 这里当然不能再使用push了
  • 执行dom操作 self.container.insertBefore

对于 push 等不同的函数 apply 处理之后,得到的 result 显然是不一样的;在mutationHandlers里面也对 result 进行量身处理。

Object.keys(mutationHandlers).forEach(function (method) {
    arr[method] = function () {
        var result = Array.prototype[method].apply(this, arguments)
        callback({
            method: method,
            args: Array.prototype.slice.call(arguments),
            result: result
        })
    }
})

总之这些arrayWatcher实现还是挺优雅的。

应该存在的问题是:

  • Array不止这些方法,是否全搞一次?
  • 性能问题

PS:作者的起名还是挺有意思的, Seed , plantspore ...


allow overwritting mutationHandlers 343ea29

mutationHandlers可以被覆写。

这是打算弄成api? 按目前的写法显然用户层是不能定义mutationHandlers的。而且显然不应该由用户来定义才对啊。。


better dep parsing c4f31a5

检测一个directive是否有依赖其它scope data;

在这里引入了一个parseKey 函数来解析

指令key的写法有几种,比如:

  • done:todo.done (sd-class)
  • change:toggleTodo (checkbox)
  • completed < remaining total (数据的关联计算)

显然现在的声明式依赖只是用<符号来声明;于是便有:

var DEPS_RE = /<[^<\|]+/g

这里需要避开连续两个<符号和声明filter|

completed < remaining total 这个指令,最终这一段的执行重复利用了parseKey, 很优雅:

var depExp = expression.match(DEPS_RE)
    this.deps = depExp
        ? depExp[0].slice(1).trim().split(/\s+/).map(parseKey)
        : null
// depExp === ["< remaining total"]
// depExp[0].slice(1).trim().split(/\s+/)  --> ["remaining", "total"]
// map(parseKey)之后得到this.deps

被解析后的结果:

this.deps = [{
    arg: null,
    key: 'remaining',
    nesting: false,
    root: false
    }, {
        arg: null,
        key: 'total',
        nesting: false,
        root: false
    }
]

接下来是肯定要 do something的:

// computed properties
 if (directive.deps) {
        directive.deps.forEach(function (dep) {
            console.log(dep)
        })
    }

PS:由用户来声明依赖的方式显然是需要重构的。慢慢看作者的思路。


computed properties!!! 5acc8a2

parseKey函数中加入了nestingroot的判断

当依赖(即directive.deps)发生变化时调用 Directive.prototype.refresh

// called when a dependency has changed
Directive.prototype.refresh = function () {
    if (this.value) {
        this._update(this.value.call(this.seed.scope))
    }
    if (this.binding.refreshDependents) {
        this.binding.refreshDependents()
    }
}
// computed properties
if (directive.deps) {
    directive.deps.forEach(function (dep) {
        var depScope = determinScope(dep, scope),
            depBinding =
                depScope._bindings[dep.key] ||
                depScope._createBinding(dep.key)
        if (!depBinding.dependents) {
            depBinding.dependents = []
            // 每个依赖发生变化时均会触发refresh()
            depBinding.refreshDependents = function () {
                depBinding.dependents.forEach(function (dept) {
                    dept.refresh()
                })
            }
        }
        depBinding.dependents.push(directive)
    })
}

对于watchArray, 对数组加入了两个辅助方法replaceremove


todos dc04a1a

  • parse textNodes
  • more directives / filters
  • nested properties in scope (kinda hard, maybe later)

complete todo example 7d12612

开始战斗力爆表...

照例还是先看todo

  • parse textNodes?
  • method invoke with arguments
  • more directives / filters
    • sd-if (终于出来了)
    • sd-route (这是啥)
  • nested properties in scope (kinda hard, maybe later)

todos demo 大改动,js,css啥的抽出来, 不得不说,css写得还是很有逼格的。。

布局和todomvc差不多。

默认指令添加了 hide, focus, value

filters添加了常用的keyCode, 加入了currencykey 两个filter


todo f19e6c3

  • limited set of expressions (e.g. ternary operator)

考虑在指令中支持更多的expression三目运算符

thoughts c1c0b3a

todo.md中的思考:

  • getter setter should emit events
  • auto dependency extraction for computed properties (evaluate, record triggered getters)
  • use descriptor for computed properties

gettersetter中做emit有啥用?暂时没有想到。。

果然在考虑自动收集依赖。。

另外descriptor(描述符)是个什么鬼。。

今天先这样吧


use emitters 9a4e5d0

gettersetter中做emit有啥用?暂时没有想到。。

恩,在这个commit里面得到了解释,就是使用Emmiter来做事件监听啦。以这种事件驱动的方式来实现。

例如:

    var seed = this
    Object.defineProperty(this.scope, key, {
        get: function () {
            seed.emit('get', key)
            return binding.value
        },
        set: function (value) {
            if (value === binding.value) return
            seed.emit('set', key, value)
        }
    })

    ...

 // update bindings when a property is set
    this.on('set', this._updateBinding.bind(this))

sourceURLs for dev, reverse value f6d6bba

grunt的工作,uglify & source mapping

增加对 ! 取非操作的逻辑。


add simple example & manual refresh of computed properties 67ff344

惯例先来看todo.md :

  • sd-width
  • sd-visible
  • sd-style

弄了一个最简的hello world example

加了一个sd-data进来,有什么用?只能后面再看了

dataSlt = '[' + config.prefix + '-data]'

auto parse dependency for computed properties!!!!! 7a0172d

作者用了这么多感叹号,想必是很爽的。。实现了依赖的自动计算

这个commit对computed properties做了较大的重构,需要梳理一下:

以下面这个total属性为例:

scope.total = {get: function () {
    return scope.todos.length
}}
  • total依赖于todos
  • 把依赖计算写到了get函数里。(这里规定了这样的格式)

执行了上面的赋值后,必然触发set;

 // add event listener to update corresponding binding
// when a property is set
this.on('set', this._updateBinding.bind(this))

由这句,去触发_updateBinding, 有以下逻辑:

if (type === 'Object') {
    if (value.get) { // computed property
        this._computed.push(binding)
        binding.isComputed = true
        value = value.get
    } else { // normal object
        // TODO watchObject
    }
} 

可以看出:

  • 如果value.get有定义,那么它就是一个computed property (isComputed = true)
  • pushSeed_computed属性中

然后开始:

    // update all instances
    binding.instances.forEach(function (instance) {
        instance.update(value)
    })

    // 接着执行
    if (typeof value === 'function' && !this.fn) {
        // 这里就是开始执行scope.todo.length 了
        value = value()
    }

那么在这里又会对Scope.todoget操作......

Seed.prototype._createBinding = function (key) {
    ...
    Object.defineProperty(this.scope, key, {
        get: function () {
            if (parsingDeps) {
                // 每当get时,就会触发`get`事件
                depsObserver.emit('get', binding)
            }
            seed.emit('get', key)
            return binding.isComputed
                ? binding.value()
                : binding.value
        },
        ...
    })

    return binding
}

// 接着执行 

    this._computed.forEach(this._parseDeps.bind(this))

// 接着执行 

Seed.prototype._parseDeps = function (binding) {
    // 如上, 触发了`get`事件后,这里的dependents就会收集依赖。
    depsObserver.on('get', function (dep) {
        if (!dep.dependents) {
            dep.dependents = []
        }
        dep.dependents.push.apply(dep.dependents, binding.instances)
    })
    binding.value()
    // 收集完依赖,干掉 
    depsObserver.off('get')
}

这个流程就结束了

如果设置了新值,流程如下:

update --> refresh --> emitChange --> 所有依赖refresh

// called when a new value is set
Directive.prototype.update = function (value) {
    ...
    if (this.binding.isComputed) {
        this.refresh()
    }
}

// called when a dependency has changed
Directive.prototype.refresh = function () {
    ...
    this.binding.emitChange()
}

 Binding.prototype.emitChange = function () {
     this.dependents.forEach(function (dept) {
         dept.refresh()
     })
 }

fix _dump() dada181

no longer need $refresh faf0557

clean up, trying to fix delegation after array reset c0a65dd

结合前面的get做的改动。 还在和delegation做斗争, 使用each指令时, 来操作parentNode :

那么前面一直存在的各种matchSelector就木用了

bind: function () {
    ...
    this.delegator = this.el.parentNode;
}

 unbind: function (rm) {
     ...
        var delegator = this.delegator
        if (!delegator) return
        var handlers = delegator.sdDelegationHandlers
        for (var key in handlers) {
            console.log('remove: ' + key)
            delegator.removeEventListener(handlers[key].event, handlers[key])
        }
        delete delegator.sdDelegationHandlers
    }

separate binding into its own file 832e975

clean up binding 60e246e

把负责绑定和数组监听的部分拆分到 binding.js,部分变量重命名


sd-if 60a3e46

加入sd-if指令。

'if': {
    bind: function () {
        this.parent = this.el.parentNode
        this.ref = this.el.nextSibling
    },
    update: function (value) {
        if (!value) {
            if (this.el.parentNode) {
                this.parent.removeChild(this.el)
            }
        } else {
            if (!this.el.parentNode) {
                // insertBefore时需要考虑原始位置。
                this.parent.insertBefore(this.el, this.ref)
            }
        }
    }
}

这里insertBefore是需要维护原来的位置的,所以要维护一个参考节点nextSibling


sd-style 646b790

style指令。

现在的写法,以borderColor为例:

dom : sd-style="borderColor" scope: borderColor = '#000'

是以-做分隔符,比如borderColor这个style,写成border-color

额,一个初级版本吧。


remove unused 62a7ebe

嗯,把各种matchSelector干掉了


remove redundant dependencies for computed properties 76ee306

原先的依赖解析提取出来, 单独作为流程的一步,这样清晰多了。

this._computed.forEach(parseDeps)
this._computed.forEach(injectDeps)

同时,原来的依赖解析有重复依赖的问题,每次get都会去push,要修一下

function parseDeps (binding) {
    depsObserver.on('get', function (dep) {
         if (!dep.dependents) {
             dep.dependents = []
             }
        dep.dependents.push.apply(dep.dependents, binding.instances)
    })
}

放在了binding.dependencies里面:

/*
 *  Auto-extract the dependencies of a computed property
 *  by recording the getters triggered when evaluating it.
 *
 *  However, the first pass will contain duplicate dependencies
 *  for computed properties. It is therefore necessary to do a
 *  second pass in injectDeps()
 */
function parseDeps (binding) {
    binding.dependencies = []
    depsObserver.on('get', function (dep) {
        binding.dependencies.push(dep)
    })
    binding.value.get()
    depsObserver.off('get')
}

修掉,原因作者说得很清楚了:

/*
 *  The second pass of dependency extraction.
 *  Only include dependencies that don't have dependencies themselves.
 */
function injectDeps (binding) {
    binding.dependencies.forEach(function (dep) {
        if (!dep.dependencies || !dep.dependencies.length) {
            dep.dependents.push.apply(dep.dependents, binding.instances)
        }
    })
}

html and attr directives f9077cf

加了attrhtml的指令


text parser started, features, optional oneway binding for input 7fd557c

todo.md 中可以看到在思考组件化的问题

开始搞text-parser, 原来是sd-text

现在使用{{}}语法

单次绑定需要加一个-oneway标记,如sd-checked-oneway

内置指令对这个oneway会做处理。

text parser 5541b97

在完善text-parser, parse完成后返回一个token数组

还未到可用状态

finish text parser b5f0227

OK, 是时候梳理一下text-parser

api.bootstrap --> buildRegex,即创建一个{{(.+?)}}Regexp

初始化创建Seed实例 --> _compileNode --> 如果是textNode, 进入_compileTextNode

textNode 两种内置指令: {{}}sd-text

如果match了{{}}的方式,就按sd-text的方式去parse再绑定:

tokens.forEach(function (token) {
    // 这里是bug. createTextNode() 方法要求有1个参数
    var el = document.createTextNode()
    // token.key,如{{label}} --> label 
    if (token.key) {
        var directive = Directive.parse(config.prefix + '-text', token.key)
        if (directive) {
            directive.el = el
            seed._bind(directive)
        }
    } else { // else的情况,那就是纯文本,如 <i>text:{{label}}</i> 中的"text:"部分
        el.nodeValue = token
    }
    node.parentNode.insertBefore(el, node)
})

chinese readme 8c8a07d

directive.js可以自定义directive的相关函数

var prop,
        definition = directives[directiveName]
    // mix in properties from the directive definition
    if (typeof definition === 'function') {
        this._update = definition
    } else {
        // 这里就是自定义的
        this._update = definition.update
        for (prop in definition) {
            if (prop !== 'update') {
                this[prop] = definition[prop]
            }
        }
    }

directive目录下的内置指令,均有bind, update, unbind的钩子, 已经有现在的custom directive的味道了:

module.exports = {
    ...
    bind: function () {
        ...
    },
    update: function (handler) {
        ...
    },
    unbind: function () {
        ...
    }
}

minor updates 0e91e50

卖点:

  • gzip后5kb大小
  • 基于DOM的动态模版,精确到TextNode的DOM操作
  • 管道过滤函数 (filter piping)
  • 自定义绑定函数 (directive) 和过滤函数 (filter)
  • Model就是原生JS对象,不需要繁琐的get()或set()。操作对象自动更新DOM
  • 自动抓取需要计算的属性 (computed properties) 的依赖
  • 在数组重复的元素上添加listener的时候自动代理事件 (event delegation)
  • 基于Component,遵循CommonJS模块标准,也可独立使用
  • 移除时自动解绑所有listener

另外做了一些cleanup.


move defineProperty() into Binding, add setter for computed property 52645e3

对于computed property 加入了setter

scope.allDone = {
    get: function () {
        return scope.remaining === 0
    },
    set: function (value) {
        scope.todos.forEach(function (todo) {
            todo.done = value
        })
        scope.remaining = value ? 0 : scope.total
    }
}

对于这个allDone, 加入了set; 有set属性的也说明是一个computed property

/*
 *  Define getter/setter for this binding on scope
 */
Binding.prototype.defineAccessors = function (seed, key) {
    var self = this
    Object.defineProperty(seed.scope, key, {
        get: function () {
            ...
        },
        set: function (value) {
            // 这里不再使用emit('set')
            if (self.isComputed && self.value.set) {
                self.value.set(value)
            } else if (value !== self.value) {
                self.value = value
                self.update(value)
            }
        }
    })
}

separate deps-parser e762cc7

deps-parser抽离,职责更清晰


clean up, add comments 7ad304e

只在sd-each指令update时来定义delegator

不再单独定义delegator, 放在了this.container.sd_dHandlers (parentNode)中


parse key path in directive parser c2faf1c

key path的作用显然就是用来做nested property的。。

if (key.indexOf('.') > 0) {
    var path = key.split('.')
    key = path[path.length - 1]
    this.path = path.slice(0, -1)
}

举例:如sd-text="a.b.c"

this.path = ['a', 'b']

。。。


nested properties 4126f41

update nested props example bf71151

todo.md: 已经在思考插件(或生态)的问题。。

  • sd-with
  • standarized way to reuse components (sd-component?)
  • plugins: seed-touch, seed-storage, seed-router

这两个commit一起看,搞定了nested properties

结合examples/nested_props.html做为示例测试:

<h1>a.b.c : {{a.b.c}}</h1>
<h2>a.c : {{a.c}}</h2>
<h3>Computed property that concats the two: {{d}}</h3>
<button sd-on="click:one">one</button>
<script>
    var Seed = require('seed')
    Seed.controller('test', function (scope) {
        scope.one = function () {
            scope.a = {
                c: 1,
                b: {
                    c: 'one'
                }
            }
        }

        // computed properties also works!!!!
        scope.d = {get: function () {
            return (scope.a.b.c + scope.a.c) || ''
        }}
    })
    var app = Seed.bootstrap()
</script>

当key为a.b.c时,显然, 是需要对cgettersetter, 原先的defineAccessors已经不适用, 所以对于这个绑定表达式,需要判定当前的path有多长 递归地去做defineAccessors, 直到对c建立gettersetter

Binding.prototype.defineAccessors = function (scope, path) {
    ...
    if (path.length === 1) {
        // 像就是a这样的expression,直接defineProperty
    } else {
        // 创建一个过渡的(中间的) subscope
        // 递归地defineAccessor, 直到path.length === 1

         // we are not there yet!!!
        // create an intermediate subscope
        // which also has its own getter/setters
        var subScope = scope[key]
        if (!subScope) {
            subScope = {}
            Object.defineProperty(scope, key, {
                get: function () {
                    return subScope
                },
                set: function (value) {
                    // when the subScope is given a new value,
                    // copy everything over to trigger the setters
                    for (var prop in value) {
                        subScope[prop] = value[prop]
                    }
                }
            })
        }
        this.defineAccessors(subScope, path.slice(1))
    }

}

增加了一个getValue函数, 也有这样的递归**:

function getValue (scope, path) {
    if (path.length === 1) return scope[path[0]]
    var i = 0
    /* jshint boss: true */
    while (scope[path[i]]) {
        scope = scope[path[i]]
        i++
    }
    return i === path.length ? scope : undefined
}

shorten some function names since they cant be mangled fa45383

重命名,压缩用

  • defineAccessors --> def
  • dependents --> subs
  • denpendencies --> deps
  • emitChange --> pub
  • getScopeOwner --> trace

bootstrap returns single seep if that's the only one f5995a5

sd-controller="a" 出现多个时返回数组,仅出现一个返回Seed实例


debug option ad1cc3b

增加debug模式,将一些常见报错console.warn()打印


restructure todomvc, add $watch/$unwatch 5200951

增加一个scope.js 加入$watchunwatch

new Seed()时会创建Scope实例:

var scope = this.scope = new Scope(this, options)

Scope包含的内容:

function Scope (seed, options) {
    this.$seed     = seed
    this.$el       = seed.el
    this.$index    = options.index
    this.$parent   = options.parentSeed && options.parentSeed.scope
    this.$watchers = {}
}

常用的一些自定义函数抛到了utils.js


0.1.0 5f5aa8f

打包完成, 0.1.0 done

梳理一下:

├── binding.js // 绑定逻辑
├── config.js // 设置
├── deps-parser.js // computed property依赖解析
├── directive-parser.js // Expression解析器,同时负责创建Directive实例作为dom属性
├── directives // 内部指令
   ├── each.js // sd-each数组指令,包括一系列mutationHandlers
   ├── index.js // 内部指令入口点及一系列常用的directive
   └── on.js // 事件监听
├── filters.js // filter处理
├── main.js  // 入口点
├── scope.js // Scope class,包含了seed实例,dom,$watchers,$parent等信息
├── seed.js // main ViewModel 
├── text-parser.js // textNode parser
└── utils.js // utols, 一些公用函数

里面最复杂的部分应该是:

  • _compileNode 节点解析
  • computed properties自动计算

过一下_compileNode:

根据节点类型做不同处理:

  • textNode --> 解析纯文本节点_compileTextNode(node)
  • nested controllers --> 直接创建新实例new Seed(), parent是当前Seed
  • normal node -->
    • 【 遍历attributes --> DirectiveParser.parse(attr.name, exp)得到一个Directive新实例(d) --> 创建或使用现有binding, 把d添加到正确的binding.instancesSeedProto._bind(d)
    • --> 如果有childNode则递归

过一下computed properties的实现:

  • 核心是利用Emiiter的观察者模式,在scope上定义的属性,当其存在getter时, 收集依赖
  • 依赖只能依赖没有依赖的依赖, 举例:
// 说明remaining依赖于total和completed
scope.remaining = {
    get: function () {return scope.total - scope.completed}
}

// 说明total依赖于todos
scope.total = {
    get: function () {return scope.todos.length}
}

这样,remaining 经过deps-parser后,实际依赖是 todosremaining, total由于有其他依赖,被干掉

依赖改变时,通过接收到get事件,调用自己的get来更新。


PS:改了grunt打包之后,整个seed在一个立即执行function里面,原来的Demo应该都不能用了。。。

Uncaught ReferenceError: require is not defined

这当然了,因为都没有暴露到window里面去嘛......

把第一行和最后一行注掉即可

0.1.0 (branch 0.10)done

computed properties now have access to context scope and element 8eedfea

computed properties 有访问domscope的能力

值得关注的改动点是src/deps-parser.js

function catchDeps (binding) {
    ...
    binding.value.get({
        scope: createDummyScope(binding.value.get),
        el: dummyEl
    })
    ...
}

这个createDummyScope 是在搞啥呢?

看这段注释:

/*
 *  We need to invoke each binding's getter for dependency parsing,
 *  but we don't know what sub-scope properties the user might try
 *  to access in that getter. To avoid thowing an error or forcing
 *  the user to guard against an undefined argument, we staticly
 *  analyze the function to extract any possible nested properties
 *  the user expects the target scope to possess. They are all assigned
 *  a noop function so they can be invoked with no real harm.
 */

首先,如果要让computed property访问scopedom,就得在get时传入参数。: 举例,这个参数是e

e = {$el: xxx, $scope:xxx}

假设用户这么去写

scope.a = {}
scope.compute_a = {
    get: function (e) {
      return  e.scope.a.b.c + 1
    }
}

那这样肯定是会报错的。

但是却不能约束用户不去这么写。比如我是用户,我需要一个promise回来后,再赋值scope.a = {b:{c:'hello'}},这时我也要求这个compute_a 能计算啊。这个要求合情理。

作者的实现是:(这里的e也可以是其他啦,一样的)

  • get函数转成字符串,匹配function (e) {xxx}部分
  • 取出xxx部分,找匹配e.scope.a.b.c
  • 解析成path, ['a', 'b', 'c']
  • 看看a,b,c都有没有值咯,有值传值,没值将值赋成空函数noop(function(){})

如果a,b,c都没值的话,最后解析的结果:

e.scope.a = function(){}
e.scope.a.b = function(){}
e.scope.a.b.c = function(){}

avoid duplicate context dependencies a5727bd

catchDeps --> parseContextDependency

避免deps重复的情况。


use for loops instead of forEach whenever possible 2d448ea

forEach 都改写成普通的forwhile循环,应该是性能方面的考虑。


add pluralize filter 9546965

【复数】形式的filter, 例如数量为1时显示item, 大于1时是items 这个好像必然是要干掉的 -- 复数后缀是es的咋办 = =


separate storage in todos example, expose some utils methods cfc27d8

todos里面的内容存在localStorage

把下列函数放在了新建文件 utils.js

  • typeOf
  • dump
  • serialize
  • getNestedValue
  • watchArray

createTextNode in FF requires argument 8a004e7

document.createTextNode() ==> document.createTextNode('')

作者终于把这个bug修掉了... 之前的commit都要手动去改这里。。

奇怪的是,难道作者当时在用IE测试吗。。。

查了一下资料,包括Mozilla Developer都似乎并没有说参数是必选的。

IE全系可以不带参数。ChromeFF是必带的。各浏览器实现不同吧。。


stricter check for computed properties 5bfb4b5

if (type === 'Object') {
    // 原来的判断 
    // if (value.get || value.set) { self.isComputed = true}
    if (value.get) {
        var l = Object.keys(value).length
        if (l === 1 || (l === 2 && value.set)) {
            self.isComputed = true // computed property
        }
    }
}

规定了必须有get 才能是computed property


add Seed.broadcast() and scope.$on() 2658486

利用Emitter 实现事件系统, 绑定到seed._listener

onoff来绑定和解绑


put properties on the scope prototype! cc64365

computed propertiesgetset搞到了scope的原型上

if (l === 1 || (l === 2 && value.set)) {
    self.isComputed = true // computed property
    value.get = value.get.bind(self.scope)
    if (value.set) value.set = value.set.bind(self.scope)
}

api.controller中引入了一个ExtendedScope, 注释如下

// create a subclass of Scope that has the extension methods mixed-in

var ExtendedScope = function () {
        Scope.apply(this, arguments)
    }
    var p = ExtendedScope.prototype = Object.create(Scope.prototype)
    p.constructor = ExtendedScope
    for (var prop in properties) {
        if (prop !== 'init') {
            p[prop] = properties[prop]
        }
    }

典型的原型继承, 这说明controller实际上就是scope的子类

seed.js中;如果有ExtendedScopeConstructor则使用,否则用原生的

// create the scope object

// if the controller has an extended scope contructor, use it;

// otherwise, use the original scope constructor.

为什么要引入这个东西,要思考下。。这里存疑


rename internal, optimize memory usage for GC d78df31

really fix GC a85453c

  • seed.js 改名为 complier.js
  • scope.js 改名为 viewmodel.js

这个命名比原来的赞多了...

scope -> vm 容易让人想到angularcontroller as...

干掉了好多self换成了this感觉清爽许多,当然一些地方需要配合Function.prototype.bind

加入了BindingProto.unbind

GC:

this.vm = this.compiler = this.pubs = this.subs = this.instances = this.deps = null

binding should only have access to compiler f071f87

binding直接使用complier中的vm 而不是自己定义一个;


new api WIP 761b643

开发模式大改变,隐隐有现在的味道了:

Seed.ViewModel.extend({
    template: 'xxx', //optional
    el: 'xxx', //optional
    initialize: function () {...},
    properties: {
        // 属性
        total: {get: function (){...}},
        // 方法。
        addTodo: function () {...}
    },

})

对比原来:

Seed.controller('todos', {
    // initializer, reserved
    init: function () {
       ...
    },
    // computed properties ----------------------------------------------------
    total: {get: function () {
        ...
    }},
    // event handlers ---------------------------------------------------------
    addTodo: function () {
        ...
    },
})

Seed.bootstrap()

对比原来的好处:

  • 结构更清晰
  • 开发者理解更加简单:不用去写sd-controller, 不用去调用Seed.bootstrap()
  • 不用去理解controllerscope这些概念名词,传el即可
  • 更易复用和组件化,整个viewmodel即是一个object

MVVM 模式下:

写html(绑定propertiesmethods) -> 写viewModel(定义propertiesmethods)

properties 改变时通知html改变视图; html改变时触发properties改变或触发method

这么做的代价并不高,但是带来的开发体验感觉是颠覆式的~

俺个人就很不喜欢controllerscope这两个词儿 = =


working, but with memory issue; c98c8a6

each.js

var config = require('../config'),
    ViewModel // lazy def to avoid circular dependency

buildItem: function (ref, data, index) {
    ...
    ViewModel = ViewModel || require('../viewmodel')
    ...
},

ViewModel.js 需要require compiler.js

compiler.js 需要 require directive-parser

directive-parser 需要 require directives/index.js

directives/index.js 需要 require each.js

each.js又需要requireViewModel.js....

绕了半天就形成了一个circular dependency


new api fixed dddb255

0.2.0 253c26d

少量修正,编译0.2.0


0.2.0 (branch 0.10)done

总结一下0.1.0到0.2.0的升级改进:

  • computed properties 可以访问 scope(viewmodel)dom
  • 基于Emitter引入了事件pub/sub系统
  • 引入了viewmodel概念,开发范式接近现在的Vue
  • 性能优化类:循环优化,GC等

optimize array watch method hijacking a104afb

把Array相关的Mutation方法提取出来了


fix dump 174a19f

照例看todo.md

  • ability to register a ViewModel so it can be auto-compiled as a nested vm

  • literals, function arguments, simple logic comparisons

  • let user specify which keys are data/state (i.e. available in $dump())

  • 声明一个 viewmodel 时自动编译啦

  • 字面量,函数参数,简单的逻辑对比

  • 用户指定哪些是data哪些是state (这的state是类react的概念?)

学英文: contextual binding 结合上下文的

var ARGS_RE = /^function\s*?\((.+?)[\),]/,

args = str.match(ARGS_RE)
if (!args) return null
binding.isContextual = true

这里意味着:

  • 这个RegExp匹配的是形如 function (abc) 这样 带参数的
  • computed property 需要用到当前上下文的时候, isContextual = true
  • 这种情况下,dump()时会跳过

举例:

// dynamic context computed property using info from target viewmodel
filterSelected: {get: function (ctx) {
        return this.filter === ctx.el.textContent.toLowerCase()
    }},

support nested VMs and update examples to use new API bf01a14

todo.md 已经在考虑生态问题。。。

  • sd-with?
  • plugins
    • seed-touch (e.g. sd-drag="onDrag" sd-swipe="onSwipe")
    • seed-storage (RESTful sync)
    • seed-router (express style)
    • sd-validation (e.g. sd-validate="type:email, max:100, required:true")

为当前的viewmodel模式支持了 nested VMsa.b.c


simply api; 6f0eca4

开发者体验至上的理念,赞~


todos 0afd700

  • add a few util methods, e.g. extend, inherits

  • fix architecture: objects as single source of truth...

  • prototypal scope/binding inheritance (Object.create)

  • 加些util方法

  • 改架构,对象作为单一数据源~~

  • 原型继承~


watcher e028262

external observe b89092b

Array => arrayMutators

Object => Emitter

通过Object.defineProperty定义了几个enumerable:false(不可枚举)configurable:false(不可删改) 的变量:

  • __path__ 路径信息如a.b.c
  • __value__ 即值的副本
  • __observer__ 用于事件emit

最后,这个watcher的作用就是:

当变量get|set|mutate时,触发事件,结合现在的watch,很好理解

另外,Arraypush进来一个新数据的时候要添加watcher,作者的思考是否是放在each里面搞,不单独放到watcher.js


小重构

Object.defineProperty 相关被从binding.js移入complier.js

binding.js 做的事情现在只有:

  • update
  • refresh
  • unbind
  • pub

做的事情更纯粹,即在数据变化时做一个「执行者」角色

watchArray相关被移出util.js做为observer.js的一部分,这样从概念上感觉更职责清晰

arrayWatcher 只在observer时使用,并不是一个太公用的方法。


make dep parsing work ea792ba

computed properties(依赖get function中的成员)和已经有__observer__(依赖下层级成员)的属性们,你们已经不干净了!不纯洁了!

所以不需要emit了。

Object.defineProperty(this.vm, key, {
    enumerable: true,
    get: function () {
        if (!binding.isComputed && !binding.value.__observer__) {
            // only emit non-computed, non-observed values
            // because these are the cleanest dependencies
            compiler.observer.emit('get', key)
        }
        return binding.isComputed
            ? binding.value.get({
                el: compiler.el,
                vm: compiler.vm
            })
            : binding.value
    },
    set: function (value) {
        if (binding.isComputed) {
            if (binding.value.set) {
                binding.value.set(value)
            }
        } else if (value !== binding.value) {
            compiler.observer.emit('set', key, value)
            observe(value, key, compiler.observer)
        }
    }
})
})

add asyn compilation ($wait/$ready) beded7d

为异步加载的数据引入$wait$ready

如果调用了vm.$wait() 那么暂时不进行compile

在用户执行vm.$ready() 的时候,使用Emitter派发事件,进行compile


todo 0f56b85

  • $watch / $unwatch & $destroy(now much easier)
  • add a few util methods, e.g. extend, inherits

最后,这个watcher的作用就是:

当变量get|set|mutate时,触发事件,结合现在的watch,很好理解

嗯,如前面的笔记,显然watcher是为 $watch,$unwatch,$destroy 服务的


unobserve when setting new values caed31f

作者在这个commit里面做了很多替换,多次访问属性的全搞出来,细微的性能优化也要优化~追求极致啊

if (this.contextBindings.length) this.bindContexts(this.contextBindings)

to:

var contextBindings = this.contextBindings

if (contextBindings.length) thisbindContexts(contextBindings)

当value改变时,这些old value已经不需要再watch了。emit get/set/mutate全置nullGC之~~

set: function (value) {
    ...
    else if (value !== binding.value) {
        // unwatch the old value!
        Observer.unobserve(binding.value, key, compiler.observer)
        // now watch the new one instead
        Observer.observe(value, key, compiler.observer)
        binding.value = value
        compiler.observer.emit('set', key, value)
    }
}

templates d2779aa

额咋又把api.bootstrap搞回来了。。

要支持template这玩意儿,作者当前想法是在script标签里面搞:

<script type="text/sd-template" sd-template-id="test">
    <p>{{hi}}!</p>
</script>

拿到innerHTML, 给它一个id: test, 存起来,取的时候用这个id取

搞完之后把这个script干掉


VMProto.$set 用于处理在value change时类似a.b.c的赋值;

如: sd-checked = "a.b.c"

这里实现很优雅,学习一下

如果没有"." 那么 vm.a = value;

如果有那么一直往下搞。。 最后是 vm.a.b.c.xxx = value

VMProto.$set = function (key, value) {
    var path = key.split('.'),
        level = 0,
        target = this
    while (level < path.length - 1) {
        target = target[path[level]]
        level++
    }
    target[path[level]] = value
}

trying to fix nested props 6ec70eb

  • auto add .length binding for Arrays

嗯这个是必要的。

src/deps-parser.js:

filterDeps 完全干掉:由于emit的时候跳过computed property, 这玩意儿已经木有用了嘛。。

src/compiler.js:

createBinding()时维护了一个数组observables,用于收集非computed的vm属性

遍历完vm后,observables也就收集完了,此时再进行observe

for (key in vm) {
    if (vm.hasOwnProperty(key) &&
        key.charAt(0) !== '$' &&
        !this.bindings[key])
    {
        this.createBinding(key)
    }
}

...

if (type === 'Object') {
    if (value.get) {// computed property
        ...
    } else {
        // observe objects later, becase there might be more keys
        // to be added to it
        this.observables.push(binding)
    }
} 

...

// define root keys
var i = observables.length, binding
while (i--) {
    binding = observables[i]
    Observer.observe(binding.value, binding.key, this.observer)
}

fix root key setter trigger order 1dd9c0e

学英文。。忽略我俗俗的翻译水平

/*

  • Sometimes when a binding is found in the template, the value might
  • have not been set on the VM yet. To ensure computed properties and
  • dependency extraction can work, we have to create a dummy value for
  • any given path. */

有时在模板中找到一个绑定变量时,值还没设哪(比如异步),那么为了让「计算属性」和「依赖提取」好使,需要为所有给定路径创建一个“虚拟变量”

举个例子吧:比如定义一个sd-checked但又没有初始化a.b, 通过一个异步请求搞到并赋值

<input type="checkbox" sd-checked="a.b">

vm.checked = {get: function (){return !!a.b}}
vm.a = {} 

fetch(url).then(function(res) {
  if (res.ok) {
    res.json().then(function(data) {
      vm.a.b = true
    });
  }
});

那么在CompilerProto.ensurePath中会先给a.b搞成undefined


allow multiple VMs sharing one data object 75a4850

新场景 : 一份data被2个(或以上)vm使用

是判断有没有__observer__ , 有的话再触发一次


small cleanups 0a45bb8

经常性地做一些cleanup,包括todo.md的编写对代码质量提升很有好处。 向尤大学习科学的coding素养!


directly use array length 77ce1fc

这个实现有点暴力了 = =

咋搞出个this.array专门做array length处理。。 画风突然变了的赶脚。。。。


fix each.js for new architecture c6c5fdb

果然果断把上面的实现干掉了。。

Array就是observables, 直接emitSet即可

each.js里面按照vm的模式搞了一遍


todo 1979aea

尤大当时居然在看avalon..

这里面的实现搞进去看到了artTemplate

模板引擎搞起来没个边啊(parser? 调试? 性能?。。。)

对于MVVM框架这个的「边际」是一个很微妙的事情,做多了有些喧宾夺主,做少了也不行

我理解支持常见运算符就能满足大部分需求了:

  • 比较运算符,如"> === <"
  • 算术运算符,如%
  • 逻辑运算符,如&& ! ||
  • 条件运算符(即三目)

比如位运算符,一些关键字像delete, typeof, void,instanceof这些,感觉支持了也没有什么卵用...没啥使用率啊

后面看作者是怎么思考这个边界的...


fix init value for Directives 9c4b62f

/*
 *  called when a new value is set 
 *  for computed properties, this will only be called once
 *  during initialization.
 */
DirProto.update = function (value, init) {
    if (!init && value === this.value) return
    this.value = value
    this.apply(value)
}

加了个参数init,但是好像没看到哪里有这么调的?这有啥用?

存疑


add utils.extend e1ce623

    extend: function (obj, ext) {
         for (var key in ext) {
             obj[key] = ext[key]
         }
     },

expression parsing 9d0d211

src/exp-parser.js 加入了expression parsing

参考的artTemplate 实现

算法采用过滤的思路实现:

  1. 删除注释、字符串、方法名,这一步是为了排除干扰
  2. 删除可组成变量的字符,只保留字母、数字、美元符号与下划线,并进行分组
  3. 删除分组中的 js 关键字与保留字成员
  4. 删除分组中的数字成员
    REMOVE_RE   = /\/\*(?:.|\n)*?\*\/|\/\/[^\n]*\n|\/\/[^\n]*$|'[^']*'|"[^"]*"|[\s\t\n]*\.[\s\t\n]*[$\w\.]+/g,

最长的这个RE拆开就没啥啦

/*(?:.|\n)?*/|//[^\n]\n -> 多行注释

//[^\n]*$ -> 单行注释

'[^']'|"[^"]" -> 字符串

[\s\t\n].[\s\t\n][$\w.]+ 排除a.b.c干扰(.前面可能有\s\t\n)

举例:

<p sd-text="one + ' ' + two + '!'"></p>

getVariables(expresion) => ['one', 'two']

搞出函数表达式args =>

args = ['two=this.$get('two')', 'one=this.$get('one')']

args拼装 => var two=this.$get('two'), one=this.$get('one');return one + ' ' + two + '!'

new Function(args), 得到Expression的值

这里最后把这个Expression当作一个computed成员来处理,思路很清楚


0.3.0 d4beb35

0.3.0版本

0.3.0 (branch 0.10) done


make it cleaner b379274

果然大神都有的特点:强迫症,洁癖,完美主义


$watch/$unwatch 874afe2

$watch和$unwatch直接代理到了observer上, 监听change事件~

干净利落


comments d7f753e

各种没注释的都加了很清晰的注释...


get ready for tests c6903e0

这个大阵仗,要开始写测试了。。。

vue的测试覆盖率是恐怖的100%...


0.3.2 - make it actually work for Browserify 498778e

赶时髦兼容browserify了... 对于13年来说,browserify的确是非常时髦的。。。


readme, todos 87f603f

  • ability to create custom tags

组件化的第一步!


unit tests for binding.js df21257

unit tests for directive.js b23c790

fix directive test 5685f68

use strictEqual 7fdb1b2

单元测试,撸码如有神


decouple compiler and exp-parser 1ef6571

需要从exp-parser中获得parse后的变量来创建binding


unit test for Expression Parser d80b5ff

unit test for Dependency Parser's internal methods 2433a3a

unit test for TextParser 1c85e86

...

make all unit tests run in real browsers 6bc19e6

暂时先mark吧...这写测试的速度...已跪


use proto interception for array methods 75dcb03

拦截了__proto__ 干掉了原来的arrayMutators

but...__proto__作为原型访问器的特性,IE11以下是不支持的,确定要这样吗。。

var ArrayProxy = Object.create(Array.prototype)
ArrayProxy.remove = function () {...}
ArrayProxy.replace = function () {...}
ArrayProxy.mutateFilter = function () {...}


arr.__proto__ = ArrayProxy

simplify template API c94ff6b

// determine el
var el  = typeof options.el === 'string'
    ? document.querySelector(options.el)
    : options.el
        ? options.el
        : options.template
            ? utils.makeTemplateNode(options)
            : vm.templateNode
                ? vm.templateNode.cloneNode(true)
                : null

原来的方式是传这个ID:

<script type="text/sd-template" sd-template-id="test"></script>

如果有options.el且是string => 搞这个dom

如果有options.el是dom => 搞这个dom

如果没有options.el, 有options.template(string): 通过utils.makeTemplateNode创建一个dom

没有options.template,有vm.templateNode => 复制

都没有 = = 那就木办法了

这样做的好处:template可以任意传string, 原来的约束较大。


fix observer mechanism 5ff47a8

之前通过emitSet递归,对于nested properties(如a.b.c)有可能有重复emit的现象

现在observe寄存到了各级属性里,直接emit即可

if (alreadyConverted) {
                emitSet(obj, ob, rawPath)
            }
function emitSet (obj, observer) {
    if (typeOf(obj) === 'Array') {
        observer.emit('set', 'length', obj.length)
    } else {
        emit(obj.__values__)
    }
    function emit (values, path) {
        var val
        path = path ? path + '.' : ''
        for (var key in values) {
            val = values[key]
            observer.emit('set', path + key, val)
            if (typeOf(val) === 'Object') {
                emit(val, key)
            }
        }
    }
}

template changes again + allow further extend() for VMs 949e6e1

抽离出来一个src/template.js

template还是恢复到允许定义一个template name


sd-if and minor fixes 4209e27

todo.md

  • sd-partial
  • transition effects
  • component examples

完成了sd-if


implement new API per spec 4003fe2

new init/extend options API 8a94192

template => documentFragment

src/template.js 又干掉了


directive interface change 9297042

template api的改动带来的directive改动

directive = Directive.parse(eachAttr, eachExp)
=>
directive = Directive.parse(eachAttr, eachExp, compiler, node)

private directives and filters eef0dc6

用户的自定义directive和filter


ViewModel.extend() should also extend Object options ec86b9f

ViewModel.extend()

function extend (options) {
    var ParentVM = this
    // inherit options
    options = inheritOptions(options, ParentVM.options, true)
    ...
}

inheritOptions是一个自定义的深拷贝,除去el和props:

  • props不需要拷贝因为已经在prototype上了
  • el只允许是一个instance option,所以也不需要拷贝

$index for each items 2232cf2

sd-each 支持$index, 在有mutation时,需要updateIndexes()


array methods should be inenumerable 331f03b

嗯,array methods自己的扩展,并不是广义性的,用户也很难会去用。


detach container before batch DOM updates for sd-each 98d1108

avoid no parent detach 61e897e

批量更新

类似domfragment的**,性能优化


implement $ event methods, optimize for minification a21e890

src/compiler.js 中template解析部分提取出来成为compiler.setupElement(options)


TODOs 81c705c

  • change the name to one that no one has used before...
  • change the exposed var to a single one (capitalized constructor)
  • change the default prefix to a single letter (depending on new name)
  • change sd-each to s-repeat
  • put all API methods on Seed
  • add a method to observe additional data properties
  • add lazy option
  • add directive for all form elements, and make it consistent as s-model
  • add escape: {{{ things in here should not be parsed }}}
  • rename props option to proto
  • properties that start with _ should also be ignored and used as a convention: $ is library properties, _ is user properties and non-prefixed properties are data.
  • 开始思考名字的问题了...
  • sd-each to s-repeat
  • additional data properties是处理data属性的么?
  • lazy = lazyload? async load?
  • v-model前身
  • escape {{{}}} 这些不会被解析
  • 最后一点特性很给力,也是现在的vue有的特性:$开头的是库属性/方法;_开头是用户属性/方法;这两种开头的都不会被observe

move all API methods to Seed 65faa95

这是... Find and replace吗...

--

rename prop option to scope 99b25b2

data => scope

剧透的赶脚。。反正会改回来的。。


implement sd-model 1e90903

sd-model b8781c5

sd-model统一代替了sd-valuesd-checked

  • input:checkbox
  • input:radio
  • input:text, select (及其他)

这里对于select也这么玩?IE应该是不支持直接设select的value的

update: function () {
    this.el.checked = value == this.el.value
}

add sd-pre b121a00

sd-pre这个directive的innerHTML也是会skip complilation的


IE9 compatibility 7b17f80

这里对于select也这么玩?IE应该是不支持直接设select的value的

果然,注释笑喷

else if (el.tagName === 'SELECT') {
    // you cannot set <select>'s value in stupid IE9
    ...
    o.selectedIndex = index
} 

remove option to change interpolate tags b2c4882

强势要求语法是{{}},这种语法已经被广泛认同不需要再用户自定义,

换言之,这里不需要过度设计


remove context binding related stuff af342b7

是要把 get(ctx) {//do something to ctx} 这种东西干掉的节奏


0.4.0 b5bc232

总结一下0.3.0 => 0.4.0

  • 各种测试覆盖
  • template API改进, 以及其他代码配合
  • 修复observer重复emit问题
  • 用户自定义directive和filter
  • each的性能优化,批量dom更新后再插入
  • each(repeat)加入$index
  • Seed.ViewModel.extend() => Seed.extend()
  • sd-model

0.4.0 (branch 0.10) done


should ignore keys starting with $ or _ when observing an external ob… 3700d2c

之前没有修改这里,补上


simple no-data directive 094a1fe

对于no-data directive,即是expression为空的directive 比如一个用户自定义的directive, 如v-btnloading代表一个在点击时会有loading效果的button

<button v-btnloading></button>

只要在bind或update时做相应的调用即可


sd-id 5fa908d

对于viewmodel定义一个id (又是剧透感,会被干掉的...)


fix sd-repeat + sd-viewmodel fb6b4ea

examples/repeated-vms.html 里的这种用法感觉有点奇怪...


fix dependency tracking for nested values db22a2a

complier.js 会对复杂的binding做处理,调用exp-parser.js,返回getter和vars,利用vars进行createBinding

return {
            getter: new Function(args),
            vars: Object.keys(hash)
        }
 while (i--) {
    v = result.vars[i]
    if (!bindings[v]) {
        compiler.rootCompiler.createBinding(v)
    }
}

但作者可能是在上个commit里面发现了vars返回的问题

item.title + msg => vars=['item', 'msg']

而实际应该需要: path=['item.title', 'msg']

于是实现就是把vars再搞一遍匹配

function getPaths (code, vars) {
    // code: 'item.title + msg'
    var pathRE = new RegExp("\\b(" + vars.join('|') + ")[$\\w\\.]*\\b", 'g')
    // 上面例子就是/\b(item|msg)[$\w\.]*\b/g
    return code.match(pathRE)
    // ['item.title','msg']
}

restructure for functional tests, use CasperJS 412873f

函数测试使用了CasperJS

这个并没有听说过= =后续有时间可以了解下


use Object.create(null) for better hash 91d8528

注释说得很清楚了: Object.create(null)的结果是prototype-less

/**
 *  Create a prototype-less object
 *  which is a better hash/map
 */
function makeHash () {
    return Object.create(null)
}

这个问题我之前有看过...

作为一个哈希or字典来说,显然是Object.create的结果好;因为它不inherit anything, 更加纯净

举例:

var a = {}
var b = Object.create(null)

这样,如果你使用a.toString => function toString() { [native code]}

看起来,toString这个key像已经被原型方法占用了, a作为哈希或字典就有点不纯洁

定义var a = {}, 实际相当于:

var a = Object.create(Object.prototype)

change model event to input, and try to fix it for IE9 (not tested) dd68ea2

对IE9的鄙视之情跃然纸上

// fix shit for IE9
// since it doesn't fire input on backspace / del / cut
if (isIE) {
    el.addEventListener('cut', self.set)
    el.addEventListener('keydown', function (e) {
        if (e.keyCode === 46 || e.keyCode === 8) {
            self.set()
        }
    })
}

add teardown option; tests for $destroy() 5ffa301

teardown这个option,如果传入了,就在vm.$destroy的时候调用


move $index to VM instead of item, add $parent & $collection db284ff

设置完parent vm的时候把child vm就搞到parent vm上去,而不是搞到整个vm


test implementation 8ba58dd

transition working with sd-if + sd-repeat (TODO: $destroy) 36da0a7

内置directives中的show, visible,if等dom操作统一到了transition.js

changeState: enter/leave的时候调用的函数

这个API设计看起来有些别扭。。

/**
  *  stage:
  *  1 = enter
  *  2 = leave
  */
module.exports = function (el, stage, changeState, init) {
    ...
    if (stage > 0) { // enter
        ...
    } else { // leave
        cl.add(className)
        el.addEventListener(endEvent, onEnd)
    }
}

这里endEvent='transitionEnd',but这个事件stupid IE9是不支持的哇 = =

目测这种设计也会干掉...


meta 23c98cb

看来是考虑了这一点了。。


sniff transitionend event cd80fe4

兼容性处理

function sniffTransitionEndEvent () {
    var el = document.createElement('div'),
        defaultEvent = 'transitionend',
        events = {
            'transition'       : defaultEvent,
            'MozTransition'    : defaultEvent,
            'WebkitTransition' : 'webkitTransitionEnd'
        }
    for (var name in events) {
        if (el.style[name] !== undefined) {
            return events[name]
        }
    }
}

change sd-viewmodel to sd-component, allow direct use of Objects in S… 5ad8ede

<div class="vm" sd-component="vm-test">{{vmMsg}}</div>

var T = Seed.extend({
    components: {
        'vm-test': Seed.extend({
            scope: {
                vmMsg: 'component works'
            }
        })
    },
    ...
})

sd-on can now execute expressions 9dc45ea

sd-on="click: this.a = 'b'"

travis f584c84

使用了[travis ci](#### travis f584c84)


0.5.0 8ade1f4

0.5.0 (branch 0.10) done

0.4.0 => 0.5.0

  • 主要加入的功能是transition
  • 引入Casper.js和travis ci做持续集成

split multiple expressions by unquoted commas 88ebff6

不带引号的逗号

'ffsef + "fse,fsef"' 带引号的

'fsef,fsf:fsefsef' 不带引号的,需要split

这种逗号的场景举例:

sd-class="
        completed : todo.completed,
        editing   : todo == editedTodo
    "

fix sd-model selection position issue 3eba564

 // if this directive has filters
// we need to let the vm.$set trigger
// update() so filters are applied.
// therefore we have to record cursor position
// so that after vm.$set changes the input
// value we can put the cursor back at where it is

如果有filter,那是需要执行filter方法的即update()

鼠标位置这个东西也要修。。这个修的意义是?暂时没明白

如果没有filter, lock = true时,model.js中的update()就不会执行了

function () {
    // no filters, don't let it trigger update()
    self.lock = true
    self.vm.$set(self.key, el[attr])
    self.lock = false
}

only emit get events during deps parsing - improves perf 23bd49f

只在做computed properties的依赖分析时用到emit,优化性能

dep-parser.js

parse: function (bindings) {
        ...
        observer.active = true
        bindings.forEach(catchDeps)
        observer.active = false
        ...
    }

defineProperty时就会去看这个标志位active,不需要频繁emit


New ExpParser implementation bc0fd37

exp-parser.js中新增getRel()

例如对于sd-checked="a.b"

这个函数的作用是找到一个vm property(a)的真正owner vm

同时,如果没有当前的path(a.b)则通过compiler.createBinding(path) 创建一个


fix input event handler for Chinese input methods 4e6ad72

这个真是闻所未闻,hack原理又是啥?。。。没弄明白

先抄下来吧

try {
    cursorPos = el.selectionStart
} catch (e) {}
// `input` event has weird updating issue with
// International (e.g. Chinese) input methods,
// have to use a Timeout to hack around it...
setTimeout(function () {
    self.vm.$set(self.key, el[attr])
    if (cursorPos !== undefined) {
        el.setSelectionRange(cursorPos, cursorPos)
    }
}, 0)

add replace option & tests 0419b05

如果传入的option里设置replace=true,原来在dom上的声明式标签会被完全替换掉


一堆fixes & tests...


0.6.0 - rename to VueJS 218557c

终于取了个never used before的名字,感觉是0.5.0~0.6.0最大的改动 = =

0.6.0 (branch 0.10) done


lifecycle hooks e04553a

绝赞~!

生命周期钩子出现~

类似:

if (ready) {
    ready.call(vm, options)
}

vm和options作为arguments可以进行操作。

  • beforeCompile / created
  • afterCompile / ready
  • beforeDestroy
  • afterDestroy

component refactor 628c42c

src/directives/component.js 专门用于处理组件化

原来的自定义elements被干掉

test/functional/fixtures/encapsulation.html 来看,现在的组件化方式还不太友好。


DOM convenience methods d132fdc

  • $appendTo
  • $remove
  • $before
  • $after
  • query (querySelector)

enteredView/leftView hooks & test 9a132a9

增加一对transition的钩子:

  • enterView
  • leftView

v-model for content editable 9ec0259

v-model现在可以附加到textarea


dom method callbacks should be async. 070d5c0

出现了utils.nextTick()利用requestAnimationFrame 加载dom method的callback


observer rewrite 216e398

defineProperty的set中先做了unoserve oldVal,再observe newVal

set: function (newVal) {
    var oldVal = values[key]
    unobserve(oldVal, key, observer)
    values[key] = newVal
    ensurePaths('', newVal, oldVal)
    observer.emit('set', key, newVal)
    observe(newVal, key, observer)
}

add benchmark for todomvc example dc17a4e

加入了一个todomvc的简单benchmark


update benchmarks, remove attach/detach in v-repeat as its actually expensive in common use cases f29be01

remove attach/detach in v-repeat as its actually expensive in common use cases

attach和detach不是为了批量更新dom而生的吗?为啥又干掉了?

实际使用场景里面很少有大批量更新dom的情况?。。。


compiler rewrite - observe scope directly and proxy access through vm 14d8ce2

complier.js做了比较大的改动

scope现在可以依附在complier身上了

先拷贝scope到vm里

在hook后user可能改变了vm, 需要再搞一遍vm到scope

然后直接Observer.observe(scope, '', complier.observer)

即进行path,def,emit等事情

 // init scope
var scope = compiler.scope = options.scope || {}
utils.extend(vm, scope, true)

...

// beforeCompile hook
compiler.execHook('beforeCompile', 'created')
// the user might have set some props on the vm 
// so copy it back to the scope...
utils.extend(scope, vm)
// observe the scope
Observer.observe(scope, '', compiler.observer)

simplify v-repeat syntax cd90e64

simplify v-component usage c75aa42

改了之后更难用的即视感。。。


API change: split scope into data and methods 7c1d196

开发者的大事,大快所有人心的大好事


"Release v0.7.0" 56fbe04

0.7.0 (branch 0.10) done


async batch update - first pass 5c73a37

异步的批量update, batch.js实现得很简洁

binding.js在 update or refresh时均会使用batch.queue()

另外,viewmodel.js中transition的异步callback部分

把requestAnimationFrame 换成了 setTimeout(func, 0)

猜测可能是更快的异步执行?

因为requestAnimationFrame每秒只能触发60以下次(60FPS)

setTimeout(func, 0)可以触发200+次 (最小间隔约是4ms~10ms)。


nextTick phantomjs fix, unit tests for batcher, config() api addition c7b2d9c

搞回了nextTick, 并在batcher.js里面换回了nextTick

仍不知道作者使用setTimeout和requestAnimationFrame之间的标准。。


avoid duplicate Observer.convert() 331bcc6

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

比如在data和methods里面都没有定义的key出现了,那么要使用Observer.convert

如:

{
    data:{},
    methods: {
        a: function () {
            // this.b没有被定义过
            var value = this.b && this.b;
            if (!value) value = this.b = 'c'
            return value
        }
    }
}

如果不加入这个条件,因为data/methods中的key都已经observed,会重复observe


test using gulp plugins instead c7f8a68

部分test用上了gulp, 时髦啊...


Async fixes c71a908

vm.$watch 现在支持异步 v-model不支持异步是个原先的bug. 这里修复了


perfectly resolve Chinese input methods issue with composition events 1e58721

利用compositionstartcompositionEnd事件解决中文输入问题

不知道有多少人跟我反应一样“卧槽居然还有这事件”

学习了一下compositionEvents

mark~


meta 74e2f38

作者的开发**

Technically, it provides the ViewModel layer of the MVVM pattern, which connects the View (the actual HTML that the user sees) and the Model (JSON-compliant plain JavaScript objects) via two way data-bindings.

Philosophically, the goal is to allow the developer to embrace an extremely minimal mental model when dealing with interfaces - there's only one type of object you need to worry about: the ViewModel. It is where all the view logic happens, and manipulating the ViewModel will automatically keep the View and the Model in sync. Actuall DOM manipulations and output formatting are abstracted away into Directives and Filters.


vm.$emit() is now self only, propagating events now triggered by $dispatch() 354f559

新增$emit用于触发自己的事件,$dispatch用来触发冒泡事件,和现在一致了


add support for interpolation in html attributes a2e287f

允许在正常的html属性里面带上{{}}

// non directive attribute, check interpolation tags

like this:

<img src="{{url}}"></img>

make exp-parser deal with cases where strings contain variable names 212990a

exp: "'"test"' + test + "'hi'" + hi"

我觉得这算是一种edge case了, 真这么用的人...


Component Change 04249e3

  • v-component now takes only a string value (the component id)
  • Vue.component() now also registers the component id as a custom element
  • add new directive: v-with, which can be used in combination with v-component or standalone

Fix inline partial parentNode problemDirectives inside inline partials would get the fragment as the parentNodeif they are compiled before the partial is appended to the correct parentNode.Therefore keep the reference of the fragment's childNodes, append it,then compile afterwards fixes the problem. 5f21eed

对于partial的处理,在append fragment之后再进行compile

否则其children的parentNode会不对


bindings that are not found are now created on current compiler instead of root 1fb885b

当一个baseKey(如a.b的a)不在当前vm的时候,之前是去rootComplier.createBinding,

现在是递归地找complier.parentComplier, 就近地createBinding


clean up get binding owner compiler logic 9a45f63

compiler.bindings = makeHash() 这样初始化

后面在function check(key) 中就不用再去检查hasOwnProperty


should lock during composition in all cases 61e4dd1

以前是仅当Expression中带有filter时才进行composition lock

现在是所有的情况均进行lock


update() is now optional when creating a directive d3ebd42

当进行一个自定义directive时,有可能只是初始化一次(once)之后就不再变化了

所以这个update是optional的


simplify binding mechanism, remove refresh() f139f92

原来对于computed properties,在数据更新时用refresh()方法;

现在用一个eval()把所有的properties都合并到一块统一使用_update()

/**
 *  Return the valuated value regardless
 *  of whether it is computed or not
 */
BindingProto.eval = function () {
    return this.isComputed && !this.isFn
        ? this.value.$get() // is computed
        : this.value // not computed
}

do not modify original computed property getter/setters 62fd49a

修改发生在CompilerProto.define即说明是个root-level binding (即非expression, 不是a.b,或带$parent/$root) 的

对于这种binding, Object.defineProperty的时候不设置enumerable即默认为false

compiler.data即是原始用户传入的data部分

Object.defineProperty(vm, key, {
    get: binding.isComputed
        ? function () {
            // 原先是compiler.data[key].$get
            return binding.value.$get()
        }
        : function () {
            return compiler.data[key]
        },
    set: binding.isComputed
        ? function (val) {
            // 原先是compiler.data[key].$set
            if (binding.value.$set) {
                binding.value.$set(val)
            }
        }
        : function (val) {
            compiler.data[key] = val
        }
})

separate computed properties into computed option 7a6169c

很牛逼的举动,

  • 即方便了用户(后续还可以不用去死板地写$get function), 也可以和data/method区分开来,方便代码组织

  • 也方便了自己(computed中的东西必须是computed property)


refactor Compiler.createBinding 50a7881

vm中加入了computed之后, createBinding做一次重构

看下面这个代码多么的清晰!

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 {
        // normal property
        compiler.defineProp(key, binding)
    }
} 

CompilerProto.define => CompilerProto.defineProp + CompilerProto.defineExp

分而治之,代码也更加清晰易读,赞~


Release-v0.8.0 882c16c

0.8.0 (branch 0.10) done


defer child components compilation 3cb7027

把child components延迟compile,这样在child components进行compile时,其parent已经收集了所有的binding


Fix #65A

computed property can depend on properties on repeated items.When new items are added to the Array, the dependency collection has already been done so their properties are not collected by the computed property on the parent. We need to re-parse the dependencies in such situation, namely push, unshift, splice and when the entireArray is swapped. Also included casper test case by @daines. a7dfb3f

针对这样的case:

<body>
    <form id="form">
        <p v-repeat="items">
            <input type="text" name="text{{$index}}" v-model="text">
        </p>
        <button v-on="click: add" id="add">Add</button>
        <p id="texts">{{texts}}</p>
    </form>
    <script>
        var app = new Vue({
            el: '#form',
            data: {
                items: [
                    { text: "a" },
                    { text: "b" }
                ]
            },
            methods: {
                add: function(e) {
                    this.items.push({ text: "c" })
                    e.preventDefault()
                }
            },
            computed: {
                texts: function () {
                    return this.items.map(function(item) {
                        return item.text
                    }).join(",")
                }
            }
        })
    </script>
</body>

当点击add button时,input会多加一个:

input:a input:b input:c

但是在input:c中输入文字的时候,{{texts}}没有改变

原因作者说得很清楚了。在这个例子里面texts是一个computed property

当array push的时候,依赖收集已经完成,只对input:a和input:b有双向绑定

那么在push完成之后,需要对input:c也有双向绑定

对于 push/unshift/splice 这些改变数组长度的行为 :

mutationListener => changed()里面再进行一次parseDeps()

elf.mutationListener = function (path, arr, mutation) {
    ...
    if (method === 'push' || method === 'unshift' || method === 'splice') {
        self.changed()
    }
}

...

/**
    *  Notify parent compiler that new items
    *  have been added to the collection, it needs
    *  to re-calculate computed property dependencies.
    *  Batched to ensure it's called only once every event loop.
    */
changed: function () {
    var self = this
    if (self.queued) return
    self.queued = true
    setTimeout(function () {
        self.compiler.parseDeps()
        self.queued = false
    }, 0)
},

text-parser deal with triple mustache 1c6dacf

{{{ }}} tags for unescaped html 58dd07e

这两个commit解决{{{xxx}}}的问题,如果expression match则里面的xxx当做html来解析


rewrite lifecycle hook system d75ee62

每个hook就叫一个名字比较好...


make sure a vm and its data are properly removed from Array when $destroy is called directly efcaad1

当$destroy的时候,vm和data都要从collection里面清除掉


reintroduce v-style 120af4b

style从新搞回来了


update todomvc example to be same as lended version cb1d69c

尤大带逛github系列:

后面的vue-router是否也参考了这个director呢。。。

这东西最后一个commit已经是2 years ago了

先mark,待研究


add isLiteral option for custom directive 6673828

expression仅是字面量的场景应该很少吧


Fix IE directive not compiled issueIn the TodoMVC example, when v-show is compiled first, it adds aninline style attribute to the node. Since IE seems to handle the orderof attributes in node.attributes differently from other browsers,this causes some directives to be skipped and never compiled. Simplycopy the attribtues into an Array before compiling solves the issue. 0f448b8

又是IE9的奇怪bug


common xss protection 9c2bb94

防了下constructor的override攻击

例如这个:

toString.constructor.prototype.toString=toString.constructor.prototype.call;

["a","alert(1)"].sort(toString.constructor);

select[multiple] support e16f910

支持 select multiple属性,但是。。。这个估计也很少人会用吧

multiselect基本所有人都是用组件的...原生的太坑了


plugin interface a891df2

又一个被广泛应用的接口出现了 = =

use和install => 直接操作viewModel


better xss mitigation 0bee5a0

constructor基础上加入了防止unicode形式的攻击


revert repeated item $destroy behavior ce4342b

没想明白为什么这部分又搞回去了。。。= =

// in case `$destroy` is called directly on a repeated vm
// make sure the vm's data is properly removed

item.$compiler.observer.on('hook:afterDestroy', function () {
    col.remove(data)
})

remove isLiteral option f6ee24a

expression仅是字面量的场景应该很少吧

果然干掉了 (>◡❛)


add v-cloak c599bed

v-cloak 作用主要是和display:none结合使用,编译完成后才展示

要不一进来会是{{}}这种标签展示在界面上的话不太好看的嘛


Make Observer emit set for all parent objects too f2e32ab

add tests for object outputing + avoid duplicate events 77ffd53


v-repeat optimization9482e51

  • When the array is swapped, will reuse existing VM for any element present in the old array. This greatly improves performance when the repeated VMs have a complicated nested structure themselves.
  • Fixed issue when array is swapped new length is not emitted
  • Fixed v-if not removing its reference comment node when unbound
  • Fixed transition when element is invisible the transitionend callback is never fired resulting in element stuck in DOM

这个厉害了:

在array改变的时候执行update时会进行buildItem

之前是全量地buildItem, 那么就需要全量地创建childvm

现在在directive update时先存来一份: this.oldVMs = this.vm

这样在buildItem时:

 buildItem: function (data, index) {
        ...
        // append node into DOM first
        // so v-if can get access to parentNode
        if (data) {

            if (this.old) {
                i = this.old.indexOf(data)
            }
            
            if (i > -1) { // existing, reuse the old VM

                item = this.oldVMs[i]
                // mark, so it won't be destroyed
                item.$reused = true
                el = item.$el
                // don't forget to update index
                data.$index = index
                // existing VM's el can possibly be detached by v-if.
                // in that case don't insert.
                noInsert = !el.parentNode

            }
            ... 
        }
}

上面标记了一个$reused, 说明可以被复用;

update => buildItem => 处理oldVM

        // destroy unused old VMs
        if (oldVMs) {
            var i = oldVMs.length, vm
            while (i--) {
                vm = oldVMs[i]
                if (vm.$reused) {
                    vm.$reused = false
                } else {
                    vm.$destroy()
                }
            }
        }

v-repeat object first pass, value -> $value 393a4e2

repeat object sync and tests 0e486a3

对于v-repeat object,利用for in搞成array (这里定义成一个$repeater property,值是 array)

在object更新时,$repeater array也进行更新


sync back inline input value if initial data is undefined 31f3d65

举例:

<input type="checkbox" v-model="a">

在init的时候a是undefined的话,把checked属性置为html的默认值


allow components to use plugins too, resolve Browserify Vue.require issue 74c03f3

ViewModel.use/require 挂到ExtendedVM上,任何vm(或者理解为components)都可以使用plugin

  // allow extended VM to use plugins
   ExtendedVM.use     = ViewModel.use
   ExtendedVM.require = ViewModel.require

Internalize emitter implementation 3693ca7

This has a few benefits

  • no longer need to shiv the difference between Component's emitter & Browserify's emitter (node version)
  • no longer need to hack around Browserify's static require parsing> -able to drop methods not used in Vue
  • able to add custom callback context control, as mentioned in #130

迟早要自己实现emitter的! 写个emitter对尤大来说简直易如反掌...

这样可以脱离这个Component的控制了,这东西(生态)后续也被证明是不成气候的


dataAttributes options (#125) 659593f

这种paramAttributes声明方式又是迟早被干掉的节奏(剧透脸)

describe('paramAttributes', function () {
    it('should copy listed attributes into data and parse Numbers', function () {
        var Test = Vue.extend({
            template: '<div a="1" b="hello"></div>',
            replace: true,
            paramAttributes: ['a', 'b']
        })
        var v = new Test()
        assert.strictEqual(v.a, 1)
        assert.strictEqual(v.$data.a, 1)
        assert.strictEqual(v.b, 'hello')
        assert.strictEqual(v.$data.b, 'hello')
    })

})

v-on delegation refactor, functional tests pass 60e5154

总体思路就是把事件代理到viewmodel的el上了

触发时递归去找,找到了执行handler

CompilerProto.addListener = function (listener) {
    var event = listener.arg,
        delegator = this.delegators[event]
    if (!delegator) {
        // initialize a delegator
        delegator = this.delegators[event] = {
            targets: [],
            handler: function (e) {
                var i = delegator.targets.length,
                    target
                while (i--) {
                    target = delegator.targets[i]
                    // 这里应该是target.el.contains(e.target)
                    if (e.target === target.el && target.handler) {
                        target.handler(e)
                    }
                }
            }
        }
        this.el.addEventListener(event, delegator.handler)
    }
    delegator.targets.push(listener)
}

setTimeout(0) is faster than rAF 5100f3e

前面的一个commit笔记 c7b2d9c

仍不知道作者使用setTimeout和requestAnimationFrame之间的标准。。

从这个commit来看,还是速度上的考虑?


make primitive arrays work with computed property 8d65a07

src/repeat.js

change:$value时再set一次,触发computed property更新

// for non-object values, listen for value change
// so we can sync it back to the original Array
if (nonObject) {
    item.$compiler.observer.on('change:$value', function (val) {
        self.lock = true
        self.collection.set(item.$index, val)
        self.lock = false
    })
}

allow nested path for computed properties that return objects bedcb22

在进行createBinding时:如果这个path是nested path(带.的)

继续进行一次defineExp, 当做表达式来处理

else if (computed && computed[key.split('.')[0]]) {
            // nested path on computed property
            compiler.defineExp(key, binding)
        }

filterBy & orderBy first pass 336d06d

对数组加入filterBy和orderBy的filter


v-component & v-with refactor 665520f

v-component也成了一个内置directive


refactor compile priority order 775948d

compile的先后顺序: repeat => view => component


array diff WIP a847fdd

clean up array diff algorithm ed0be36

大动作~!

src/repeat.js 中的diff()方法:

在Array update时,去跟原Array进行对比,做到最小力度的dom操作

可以看做一种v-dom的**吧

原来的mutationHandlers全部干掉了,因为都可以理解成是一种diff

update时:

 this.vms = this.oldCollection
            ? this.diff(collection, isObject)
            : this.init(collection, isObject)

来完整地看一下diff方法的实现:

/**
    *  Diff the new array with the old
    *  and determine the minimum amount of DOM manipulations.
    */
diff: function (newCollection, isObject) {

    var i, l, item, vm,
        oldIndex,
        targetNext,
        currentNext,
        ctn    = this.container,
        oldVMs = this.oldVMs,
        vms    = []
    // vms是diff后的结果Array,给个长度先
    vms.length = newCollection.length

    // first pass, collect new reused and new created
    // 第一步是先把可以复用vm的和新创建的搞进来
    for (i = 0, l = newCollection.length; i < l; i++) {
        item = newCollection[i]
        if (isObject) { // 对Object类型的处理
            item.$index = i
            // identifier存在,那么是可以复用的
            if (item[this.identifier]) {
                // this piece of data is being reused.
                // record its final position in reused vms
                item.$reused = true
            } else {
                // 这些就是新建的
                vms[i] = this.build(item, i, isObject)
            }
        } else { // 这就是Array了 
            // we can't attach an identifier to primitive values
            // so have to do an indexOf...
            oldIndex = indexOf(oldVMs, item)
            if (oldIndex > -1) { // 说明在oldVM里面存在,可以复用
                // record the position on the existing vm
                oldVMs[oldIndex].$reused = true
                oldVMs[oldIndex].$data.$index = i
            } else {
                // 新建
                vms[i] = this.build(item, i, isObject)
            }
        }
    }

    // second pass, collect old reused and destroy unused
    // 第二步,处理oldVM里面没有被reused的东西,干掉
    for (i = 0, l = oldVMs.length; i < l; i++) {
        vm = oldVMs[i]
        item = vm.$data
        if (item.$reused) {
            vm.$reused = true
            delete item.$reused
        }
        if (vm.$reused) {
            // update the index to latest
            // index要保持住
            vm.$index = item.$index
            // the item could have had a new key
            // 参见objectToArray, 这个$key可能已经改变了
            if (item.$key && item.$key !== vm.$key) {
                vm.$key = item.$key
            }
            vms[vm.$index] = vm
        } else {
            // this one can be destroyed.
            delete item[this.identifier]
            vm.$destroy()
        }
    }

    // final pass, move/insert DOM elements
    // 最后一步做dom操作
    i = vms.length
    while (i--) {
        vm = vms[i]
        item = vm.$data
        targetNext = vms[i + 1]
        if (vm.$reused) {
            // 搞位置,插入
            currentNext = vm.$el.nextSibling.vue_vm
            // 如果currentNext === targetNext说明没有必要做dom操作了
            if (currentNext !== targetNext) {
                if (!targetNext) {
                    ctn.insertBefore(vm.$el, this.ref)
                } else {
                    ctn.insertBefore(vm.$el, targetNext.$el)
                }
            }
            delete vm.$reused
            delete item.$index
            delete item.$key
        } else { // a new vm
            // 新创建的,insertBefore
            vm.$before(targetNext ? targetNext.$el : this.ref)
        }
    }

    return vms
},

enable $event in v-on expressions + enable e.preventDefault() 1b1d72e

在dom上触发事件的时候把事件句柄e作为$event传入

那么就可以this.$event了

newHandler = function (e) {
    e.targetEl = el
    e.targetVM = targetVM
    context.$event = e
    handler.call(context, e)
    context.$event = null
}

remove event delegation 423b54d

咋又干掉了?


also propagate array mutations in observer e498191

这是把dom的冒泡转移到了array data emitter的emit上吗。。。感觉很牛逼的样纸 = =

emitter
    .on('set', function (key, val, propagate) {
        if (propagate) propagateChange(obj)
    })
    .on('mutate', function () {
        propagateChange(obj)
    })

...

/**
 *  Propagate an array element's change to its owner arrays
 */
function propagateChange (obj) {
    var owners = obj.__emitter__.owners,
        i = owners.length
    while (i--) {
        owners[i].__emitter__.emit('set', '', '', true)
    }
}

expression cacheexpressions with the same signature are now cached! this dramaticallyimproves performance on large v-repeat lists with multiple expressions. 15a6733

compiler.expCache = compiler.expCache || makeHash()
...

if (!getter) {
        getter = this.expCache[exp] = ExpParser.parse(key, this, null, filters)
    }

ExpParser.parse()后的结果被缓存,避免不必要的重复parse,提高性能


add tree view test case 3be1141

递归使用组件,cool


use more efficient Object check c67685b

基本类型判断

这两种东东看来是有Benchmark上的区别?

也要mark一下。。。

/**
    *  A less bullet-proof but more efficient type check
    *  than Object.prototype.toString
    */
isObject: function (obj) {
    return typeof obj === OBJECT && obj && !Array.isArray(obj)
},

/**
    *  A more accurate but less efficient type check
    */
isTrueObject: function (obj) {
    return toString.call(obj) === '[object Object]'
},

Release-v0.10.0 cd53688

0.10.0 (branch 0.10) done


Use more robust string -> fragment conversion Using the wrapper technique from jQuery so that utils.toFragment() can deal with table elements and SVG elements (with proper namespace) c941fd6

string转化成fragment的过程中,还有像Table,svg这样的东东

借鉴了jquery和component/domify


bring back the rAF e0fbc20

服!


一小堆fixes...


Release-v0.10.6 cf37f7e

0.10.6 (branch 0.10) done


下面开始看1.0分支,因为0.10后的代码都在1.0分支中,commit超级多,但是...一步步慢慢看吧

送佛送到西,计划是看到1.0发布, 略过bugfix和test, 主要以看思路为主

working on new observer b5bfc59

新的observer的设计

设计思路:

每个观察对象现在都具有Observer class; 它们现在首先是具有Emmiter的事件观察者功能,还可以彼此之间进行通信。