Como funciona a reatividade no Vue.js
Esse tema é um tanto avançado, demanda um pouco pouco de design pattern em Javascript e saber como funciona a sua cadeia de protótipos, porém vou tentar ser o mais didático o possível para que vocês entendam sem maiores problemas.
Prevejo que, se surgir grandes dúvidas aqui seu problema provavelmente é com Javascript e não com Vue, então partiu estudar :D
Entender como Vue funciona é indispensável para por exemplo saber resolver certas armadilhas como a situação do Vinicius com a Lib D3, que por sua vez, tinha conhecimento do funcionamento e resolveu o problema tranquilamente.
Reactive System
Esse sistema funciona da seguinte forma, quando você passa um objeto para o atributo data
da instância do Vue, ele percorre as propriedades transformando-as em getters/setters
usando o Object.defineProperty.
Cada diretiva ou data binding no template, corresponderá a um watcher object que irá guardar as propriedades durante a avaliação de dependências(o que pegar no getter
para amostrar na view). Quando o setter
for chamado, o watcher object vai reavaliar as dependências que causará o update na view.
Aqui para você entender melhor tem uma imagem da própria documentação
Observer Pattern
Para fazer o loop funcionar sem problemas, o Vue usa o Observer Pattern que basicamente consiste em você ter um Objeto(conhecido como Subject) manter uma lista de Objetos(conhecido como Observers) dependendo dele, podendo tanto remover Observers ou adicionar novos, sempre que o Suject precisa notificar uma coisa importante que esteja acontecendo, é transmitido a notificação para os Observers.
Olhando no código fonte para entender mais, temos Observer criando o __ob__
com a function
def
, que é onde fica praticamente toda reatividade.
export function Observer (value) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
Na function
def
temos a implementação de Object.defineProperty
export function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
Aquele this.dep = new Dep()
nada mais é do que o Subject, então sabendo que você manja dos paranauês do design pattern vamos olhar um de seus métodos, o notify
Dep.prototype.notify = function () {
// stablize the subscriber list first
var subs = toArray(this.subs)
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
como você pode ver, sempre que ele notar um setter
sendo chamado, ele irá chamar o notify
para percorer a lista de Observers chamando também a function
update
para atualizar a view.
Definindo reativo
Existe uma function
no código fonte que é chamada de defineReactive
, nela é onde acontece a criação dos getters/setters
.
export function defineReactive (obj, key, val) {
var dep = new Dep()
var property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get
var setter = property && property.set
var childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
if (isArray(value)) {
for (var e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val
if (newVal === value) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal)
dep.notify()
}
})
}
O dep.depend()
é apenas ele definindo os Obeservers, olhe o código:
Dep.prototype.depend = function () {
Dep.target.addDep(this)
}
e alí no finalzinho do setter
você vê ele chamando o notify
para atualizar a view.
Olhando em ação
Para você ver isso, basta criar alguma coisa em data
e no ready
colocar um console.log
, assim:
new Vue({
el: '#app',
data: {
people: {
name: 'Igor',
age: 18,
location: {
lat: 19419812341,
lng: 19203819483
}
}
},
ready () {
console.log(this.people)
}
})
e ele vai amostrar isso:
aí você consegue ver tanto o __ob__
com a propriedade dep
nele com seus métodos de Suject no prototipo quanto os getters/setters
, e além dos watcher object.
Considerações
Levantando algumas observações…
Inicializando sua data
Devido a limitação do ES5, o Vue não consegue deletar e nem adicionar propriedades, o processo de conversão para getters/setter
acontece na inicialização, por isso a propriedade deve estar presente em data mesmo que seja apenas uma String vazia, assim ele consegue tornar Reativo.
Porém há um jeito de adicionar propriedades, esse jeito é o $set
, com ele você pode adicionar propriedades depois da inicialização, sua syntax é $set(path, value)
, é claro que é melhor definir a propriedade logo no inicio, tanto pra servir como Schema quanto para ter a reatividade logo de cara, porém você vai usar o $set
para quando quiser tornar algo vindo de uma chamada Ajax Reativo por exemplo.
Computed Properties
As computed properties não tem apenas getters
. Elas mantem sua própria lista de dependências, isto porquê elas tem cache, isso mesmo! Este valor em cache só é trocado quando existem mudanças, por tanto, enquanto as depedências não mudam, o valor retornado vai ser o do cache ao inves do getter
Mas para que eu preciso de cache aí?
Imagine que você tem uma computed property A maior… mais expressiva…, que por sua vez dependênde de um Array grande. E então você tem uma computed property B que dependênde de A. Isso poderia chamar o getter
de A mais vezes que o necessário.
Por causa desse cache o getter
acaba não sendo sempre chamado.
Vamos olhar esse comportamento em ação com um timestamp:
var vm = new Vue({
el: '#app',
data: {
msg: ' Igor!'
},
computed: {
example: {
cache: false,
get: function () {
return Date.now() + this.msg
}
}
}
})
console.log(vm.example)
com o cache para false
você terá um timestamp diferente do amostrado no console
. O get
ali consegue demostrar legal o funcionamento do cache, já que cada chamada a função ele é chamado.
Então é isso ae pessoal, como eu falei, se o que você viu aqui foi algo de outro mundo, o seu problema provavelmente é Javascript e não o Vue ;) Não adianta procurar aprender um framework, uma biblioteca sem saber a linguagem por tras(apensar que tem gente até hoje que usa jQuery pensando que é linguagem :P).