上篇文章数据搞到页面上展示已经成功将文本节点的{{ 值 }}
用DVue内部的变量提换。这篇文章将接着上文内容,将实现一些基本指令,以及简单的更新操作。
初始化就可以看出来的指令
就拿vue来说,在编译过程中的元素节点上,可能会存在一些特殊的指令符号。
在此,以v-text、v-html、@click、v-model指令为例。
想要实现指令的编译,我们就需要在编译子节点时,对拿到的字节属性进行处理。通过attributes
获取好当前节点的属性,将其变成数组,遍历其各自的属性值和属性名。
// Compile类中 if (isNode(child)) { // 元素 // 解析动态指令 属性绑定、事件监听 const childAttrs = child.attributes Array.from(childAttrs).forEach((attr) => { const attrName = attr.name const exp = attr.value if (this.isDir(attrName)) { console.log(exp, attrName) // name d-model age d-text html d-html const dir = attrName.slice(2) this[dir] && this[dir](child, exp) } }) if (child.childNodes.length > 0) this.compiler(child) } function isDir(dir) { return dir.startsWith('d-') }
判断指令是不是以d-
开头的(isDir
函数),将它的属性名从第二位开始截取,获得属性名的函数(比如d-text获得text),如果存在就执行该函数。
d-text、d-html 实现
实现
编写html函数和text函数。text函数就是将其内部的文本变量替换,可直接使用节点的textContent
属性,用DVue中的对应变量替换旧的文本内容。
html(node, exp) { node.innerHTML = this.$vm[exp] console.log(node, exp) node.removeAttribute('d-html') } text(node, exp) { node.textContent = this.$vm[exp] }
对于html它是将变量值以html节点添加到当前节点的内部节点,在使用
removeAttribute
删除节点对应属性。
进一步优化
对于公共的处理指令函数,我们可以提取共同的初始化函数(update:用于初始化和更新)。
update(node, exp, dir) { //初始化 const fn = this[dir + 'Updater'] fn && fn(node, this.$vm[exp]) // 更新 } html(node, exp) { // node.innerHTML = this.$vm[exp] this.update(node, exp, 'html') node.removeAttribute('d-html') } htmlUpdater(node, val) { node.innerHTML = val }
将其拆分成三个方法,以便于后期更新操作。到此,我们的d-text、d-html就已完成。
更新操作
vue的更新,上上一次的经典图:
这一次,我们需要去完成Watcher这个功能。它负责具体的节点更新。初始化Watcher。
watcher
采用全量更新,先不使用dep管理。
定义一个watchers用来存放watcher的数组,在编译时,创建一个个的watcher实例,把他们都放到一个watchers中。 在相关变量改变时,遍历触发更新。
const watchers = [] // 负责具体节点更新 class Watcher { constructor(vm, key, updater) { this.vm = vm this.key = key this.updater = updater watchers.push(this) } update() { // 更新对应相关的key this.updater.call(this.vm, this.vm[this.key]) } } function defineReactive(obj, key, val) { observe(val) Object.defineProperty(obj, key, { ...... set(newVal) { if (newVal !== val) val = newVal observe(newVal) watchers.forEach((w) => w.update()) // 遍历更新 }, }) }
可以看到图中的name在定时器的作用下,展示的值得到了改变。
Dep
Dep和响应式的属性key之间一一对应关系。使用Dep对watcher进行管理。
初始化dep,Dep中应该存在一个数组用于管理收集的watcher。并且存在收集watcher和触发更新的方法。
class Dep { constructor() { this.deps = [] } addDep(dep) { this.deps.push(dep) } notify() { this.deps.forEach((w) => w.update()) } }
- 在拦截每个变量时,创建相对应的Dep。
- 在编译创建watcher实例时,将watcher用Dep的某个变量存放起来(target),读取变量时,用dep收集watcher。存放完成后,将target属性删除。
- 在相关变量发生变化时,触发dep的更新操作(更新watcher)。
function defineReactive(obj, key, val) { observe(val) const dep = new Dep() // 1、 创建实例 Object.defineProperty(obj, key, { get() { // console.log('get', key) Dep.target && dep.addDep(Dep.target) // 2.2、收集 return val }, set(newVal) { if (newVal !== val) val = newVal observe(newVal) // watchers.forEach((w) => w.update()) dep.notify() // 3、触发依赖 }, }) } class Watcher { constructor(vm, key, updater) { this.vm = vm this.key = key this.updater = updater // watchers.push(this) Dep.target = this // 2、保存watcher,读取变量收集依赖 this.vm[this.key] Dep.target = null } update() { this.updater.call(this.vm, this.vm[this.key]) } } class Dep { constructor() { this.deps = [] } addDep(dep) { this.deps.push(dep) } notify() { this.deps.forEach((w) => w.update()) } }
更新后方便查看的指令
在更新之后更容易查看的指令比如:事件的监听@click、v-model。
@click 实现
在动态编译指令时,我们还需要考虑当前的属性是否存在事件监听。
如果当前的属性为事件监听,那么我们就需要在当前的节点上添加事件监听(addEventListener)。
compiler(el) { const childNodes = el.childNodes childNodes.forEach((child) => { if (isNode(child)) { // 元素 // 解析动态指令 属性绑定、事件监听 const childAttrs = child.attributes Array.from(childAttrs).forEach((attr) => { const attrName = attr.name const exp = attr.value ... // 事件 if (this.isEvent(attrName)) { const dir = attrName.slice(1) this.eventHandler(child, exp, dir) // <div d-text="age" @click='add'></div> "add" "click" } }) ... } ... }) } isEvent(name) { return name.indexOf('@') == 0 } eventHandler(node, exp, dir) { const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp] node.addEventListener(dir, fn.bind(this.$vm)) }
特别现需要注意的是在添加事件监听时,可能需要使用到当前的实例内的变量或方法,需要改变当前的this指向,把实例传入。
成果展示:
d-model 实现
model指令是一个双向绑定的指令,需要实现将其值展示到页面,在input内部修改时,其相关展示的变量改变。可以转化成value值的设定和事件监听两个功能。
model和html、text指令相似,将其拆分成三个方法,共用一个update方法。
model(node, exp) { this.update(node, exp, 'model') } modelUpdater(node, val) { // 表单元素赋值 node.value = val }
初始化完成:
在input中,需要对当前进行一个事件监听。
model(node, exp) { this.update(node, exp, 'model') // 事件监听 node.addEventListener('input', (e) => (this.$vm[exp] = e.target.value)) }
在输入框输入时,将输入的内容重新赋值给这个变量。就可以看到输入框绑定的值与相关变量联动。
感兴趣的朋友可以关注 Vue源码初识专栏,会持续输出vue相关知识哦(●'◡'●)。 如果不足,请多指教。