vue那些指令实现你还不会嘛?

1,940 阅读4分钟

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

上篇文章数据搞到页面上展示已经成功将文本节点的{{ 值 }}用DVue内部的变量提换。这篇文章将接着上文内容,将实现一些基本指令,以及简单的更新操作。

初始化就可以看出来的指令

就拿vue来说,在编译过程中的元素节点上,可能会存在一些特殊的指令符号。

image.png

在此,以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就已完成。

image.png

更新操作

vue的更新,上上一次的经典图:

image.png

这一次,我们需要去完成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())  // 遍历更新
    },
  })
}

1.gif

可以看到图中的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指向,把实例传入。
成果展示: 2.gif

d-model 实现

model指令是一个双向绑定的指令,需要实现将其值展示到页面,在input内部修改时,其相关展示的变量改变。可以转化成value值的设定和事件监听两个功能。

model和html、text指令相似,将其拆分成三个方法,共用一个update方法。

  model(node, exp) {
    this.update(node, exp, 'model')
  }
  modelUpdater(node, val) {
  // 表单元素赋值
    node.value = val
  }

初始化完成: image.png

在input中,需要对当前进行一个事件监听。

model(node, exp) {
  this.update(node, exp, 'model')
  // 事件监听
  node.addEventListener('input', (e) => (this.$vm[exp] = e.target.value))
}

在输入框输入时,将输入的内容重新赋值给这个变量。就可以看到输入框绑定的值与相关变量联动。

1.gif

感兴趣的朋友可以关注 Vue源码初识专栏,会持续输出vue相关知识哦(●'◡'●)。 如果不足,请多指教。