vue响应式更新
super_wanan 人气:0概述
前面两篇文章已经实现了对数据的变化的监听以及模板语法编译初始化,但是当数据变化时,视图还不能够跟随数据实时更新。本文就在之前的基础上介绍下视图响应式更新部分。
思路
统一封装更新函数
待数据发生改变时调用对应的更新函数
这里思考个问题:
在何时注册这个更新函数?
如何找到对应的更新函数?
第一步统一封装更新函数
基于上篇文章compile的部分,将数据初始化的部分统一封装起来。
compileText (n) { // 获取表达式 // n.textContent = this.$vm[RegExp.$1] // n.textContent = this.$vm[RegExp.$1.trim()] this.update(n, RegExp.$1.trim(), 'text') } text (node, exp) { this.update(node, exp, 'text') // node.textContent = this.$vm[exp] || exp } html (node, exp) { this.update(node, exp, 'html') // node.innerHTML = this.$vm[exp] }
很容易写出update方法:
每个指令都有对应的[dir]Updater管理器,用于在公共的update函数里调用去在相应视图渲染数据。
update (node, exp, dir) { // 第一步: 初始化值 const fn = this[dir + 'Updater'] fn && fn(node, this.$vm[exp]) } textUpdater (node, val) { node.textContent = val } htmlUpdater (node, val) { node.innerHTML = val }
第二步监听并触发视图更新
分析可知,每个模板渲染初始化的过程都需要对数据进行监听,并注册监听函数,因此在上述的update函数中添加更新逻辑。
update (node, exp, dir) { // 第一步: 初始化值 const fn = this[dir + 'Updater'] fn && fn(node, this.$vm[exp]) // 第二步: 更新 new Watcher(this.$vm, exp, val => { fn && fn(node, val) }) }
创建Watcher类:
// 监听器:负责依赖更新 class Watcher { constructor (vm, key, updateFn) { this.vm = vm this.key = key this.updateFn = updateFn } update () { // 绑定作用域为this.vm,并且将this.vm[this.key]作为值传进去 this.updateFn.call(this.vm, this.vm[this.key]) } }
此时我们已经完成了更新函数的功能,需要做的就是在数据发生改变的时候,主动调用下对应的update函数。
简单测试下:声明一个全局的watchers数组。在每次Watcher的构造函数中都往watchers中push一下,那么我们就可以再Object.defineProperty()的set方法中去遍历所有的watchers,调用update方法。
浅试一下:
const watchers = [] class Watcher { constructor (vm, key, updateFn) { this.vm = vm this.key = key this.updateFn = updateFn watchers.push(this) } update () { this.updateFn.call(this.vm, this.vm[this.key]) } } function defineReactive (obj, key, val) { // 递归 // val如果是个对象,就需要递归处理 observe(val) const dep = new Dep() Object.defineProperty(obj, key, { get () { Dep.target && dep.addDep(Dep.target) return val }, set (newVal) { if (newVal !== val) { val = newVal // 新值如果是对象,仍然需要递归遍历处理 observe(newVal) //暴力的写法,让一个人干事指挥所有人动起来(不管你需不需要更新,全给我更新一遍) watchers.forEach(watch => { watch.update() }) } } }) }
此时页面视图已经可以根据数据的变而发生相应的更新了。
引入Dep管家
只触发需要更新的函数
上述的写法过于暴力,数据量一旦稍微大点就会严重影响性能。vue内部引入了Dep这个大管家的概念来进行依赖收集,统一管理所有的watcher。只让需要干活的watcher去update。
class Dep { constructor () { this.deps = [] } addDep (dep) { this.deps.push(dep) } notify () { this.deps.forEach(dep => dep.update()) } }
每个data中的key对应一个dep就行,所以选择在Object.defineProperty的getter函数中进行依赖收集。在watcher中触发依赖收集
class Watcher { constructor (vm, key, updateFn) { this.vm = vm this.key = key this.updateFn = updateFn // 触发依赖收集,使用一个静态变量target去保存对应的Watcher Dep.target = this // 主动访问vm[key],触发一下getter this.vm[this.key] Dep.target = null } update () { // 绑定作用域为this.vm,并且将this.vm[this.key]作为值传进去 this.updateFn.call(this.vm, this.vm[this.key]) } }
收集依赖,创建Dep实例
function defineReactive (obj, key, val) { observe(val) const dep = new Dep() Object.defineProperty(obj, key, { get () { Dep.target && dep.addDep(Dep.target) return val }, set (newVal) { if (newVal !== val) { val = newVal observe(newVal) dep.notify() } } }) }
至此,我们一个简版的Vue就实现了。这里还没有涉及到虚拟dom得概念,以后介绍。
实现下语法糖v-model
v-model虽然很像使用了双向数据绑定的 Angular 的 ng-model,但是 Vue 是单项数据流,v-model 只是语法糖而已。
// 最简形式,省略了value的显式绑定,省略了oninput的显式事件监听,是第二句代码的语法糖形式 <input v-model="sth" /> <input v-bind:value="sth" v-on:input="sth = $event.target.value" /> //第二句代码的简写形式 <input :value="sth" @input="sth = $event.target.value" />
分析一下其就是在内部实现了v-bind:value=“” 和@input。
model (node, exp) { node.value = this.$vm[exp] node.addEventListener('input', (e) => { this.$vm[exp] = e.target.value }) }
加载全部内容