vue2-指令

指令是vue提供的带有 v-前缀的特殊特性。
指令的职责是,当表达式的值变化时,将其产生的连带影响响应式的作用于DOM。

指令原理

在模板解析阶段,我们将指令解析到AST,然后使用AST生成代码字符串的过程中 实现 某些内置指令的功能, 最后在虚拟DOM渲染的过程中触发自定义指令的钩子函数使指令生效。

实现原理
在模板解析阶段,将节点上的指令解析出来并添加到AST 的 directives属性中。
随后directives数据会传递到Vnode中,接着可以通过 vnode.data.directives获取一个阶段说绑定的指令。

最后当虚拟DOM进行修补时,会根据节点对比结果触发一些钩子函数。 更新指令的程序会监听 create、update、destroy钩子函数,
并在这三个钩子函数触发时 对 Vnode和oldVnode进行对比, 最终对比结果触发指令的钩子函数。

v-if指令
v-if是在模板编译阶段实现的。

  • if
  • else
  • 模板编译后生成的代码字符串

    (has)? _c('li', [_v("if")]) : _c('li', [_v("else")])
    

    这段代码最终执行时,根据has变量的值来选择创建哪个节点

    v-for指令
    v-for指令也是模板编译的代码生成阶段实现的

  • v-for {{index}}
  • 生成的代码字符串

    _l((list), function(item, index){
      return _c('li', [
        _v("v-for " + _s(index))
      ])
    })
    

    _l是函数renderList的别名。当执行这段代码时, _l函数会循环变量 list并依次调用第二个参数 所传递 的函数。

    v-on指令

    v-on指令的作用是绑定事件监听器,事件类型有参数指定。 它用在普通元素上时,监听原生DOM事件;用在自定义元素组件上时,监听子组件触发的自定义事件。

    从模板解析到生成Vnode,最终事件都会被保存到Vnode中。通过vnode.data.on得到一个节点注册的所有事件。

    虚拟DOM在修补(patch)的过程中会根据不同的时机触发不同的钩子函数。

    事件绑定相关的处理逻辑分别设置了create和update钩子函数,也就是说patch的过程中,当DOM创建或更新时,会触发事件绑定相关的处理逻辑。

    create和update钩子函数都会执行updateDOMListener函数
    相关代码如下

    
    // 通过对比两个VNode的事件对象,来决定绑定原生DOM事件还是解绑原生DOM事件。
    function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
      // 如果两个Vnode中事件对象都不存在,
      // 说明上一次没有绑定任何事件, 这一次元素更新也没有新增事件绑定
      if (!oldVnode.data.on && !vnode.data.on) {
        return
      }
      // 获取新老Vnode上的事件
      const on = vnode.data.on || {}
      const oldOn = oldVnode.data.on || {}
      // vnode.elm上保存vnode所对应的DOM元素
      target = vnode.elm
      // 更新事件监听器,通过对比on 与 oldOn. 判断调用add方法还是 remove方法执行绑定事件还是解绑事件,
      updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
      target = undefined
    }
    
    function add (
      event: string,
      handler: Function,
      capture: boolean,
      passive?: boolean,
      params?: Array
    ) {
      if (capture) {
        console.log('Weex do not support event in bubble phase.')
        return
      }
      target.addEvent(event, handler, params)
    }
    
    function remove (
      event: string,
      handler: any,
      capture: any,
      _target?: any
    ) {
      (_target || target).removeEvent(event)
    }
    

    自定义指令的内部原理
    虚拟DOM通过diff算法对比 两个 VNode之间的差异并更新真实的DOM节点。

    在更新的过程中,可能是创建新的节点,可能是更新节点,也可能是删除节点。

    虚拟DOM在渲染时,处理更新DOM内容外,还会触发钩子函数。

    指令的处理逻辑封闭监听了create、update、destroy.

    虚拟DOM在触发钩子函数时,下面代码对应的函数都会被执行。

    export default {
      create: updateDirectives,
      update: updateDirectives,
      destroy: function unbindDirectives (vnode: VNodeWithData) {
        updateDirectives(vnode, emptyNode)
      }
    }
    
    function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
      if (oldVnode.data.directives || vnode.data.directives) {
        _update(oldVnode, vnode)
      }
    }
    
    function _update (oldVnode, vnode) {
      const isCreate = oldVnode === emptyNode // 是否是新创建的节点
      const isDestroy = vnode === emptyNode // 是否新虚拟节点不存在
    
      // normalizeDirectives 将模板中 使用的 指令  从用户注册的 自定义指令集合中 取出来
      const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context) // 旧的指令集合
      const newDirs = normalizeDirectives(vnode.data.directives, vnode.context) // 新的指令集合
    
      const dirsWithInsert = [] // 保存需要触发inserted 指令钩子的指令列表
      const dirsWithPostpatch = [] // 保存需要触发 componentUpdated 钩子函数的指令列表
    
      let key, oldDir, dir
      // 循环新的指令集合
      for (key in newDirs) {
        oldDir = oldDirs[key]
        dir = newDirs[key]
        // 如果旧指令不存在,说明改指令首次绑定到元素
        if (!oldDir) {
          // 新指令  触发 bind 函数
          callHook(dir, 'bind', vnode, oldVnode)
          // 如果有inserted 方法 添加到dirsWithInsert。等待执行
          if (dir.def && dir.def.inserted) {
            dirsWithInsert.push(dir)
          }
        } else {
          // 已经存在的指令 更新即可
          dir.oldValue = oldDir.value
          dir.oldArg = oldDir.arg
          // 触发update钩子函数
          callHook(dir, 'update', vnode, oldVnode)
          if (dir.def && dir.def.componentUpdated) {
            // 如果有componentUpdated方法 保存到 dirsWithPostpatch 等待执行
            dirsWithPostpatch.push(dir)
          }
        }
      }
    
      // 执行 dirsWithInsert 
      if (dirsWithInsert.length) {
        // callInsert 函数 会等到执行时, 才会依次调用每个指令的inserted方法
        const callInsert = () => {
          for (let i = 0; i < dirsWithInsert.length; i++) {
            callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
          }
        }
        // 如果新 创建的 元素
        if (isCreate) {
          // mergeVNodeHook 可以将 一个钩子函数 与 虚拟节点 现有的 钩子函数合并。
          // 这样 钩子函数的执行 推迟到被绑定的 元素插入 到父节点之后进行
          mergeVNodeHook(vnode, 'insert', callInsert)
        } else {
          // 如果不是新的元素, 直接执行 即可
          callInsert()
        }
      }
    
      // componentUpdated 也需要将指令推迟到 指令所在组件的Vnode及其子Vnode全部更新之后调用
      if (dirsWithPostpatch.length) {
        // 虚拟DOM更新前 会触发 prepatch钩子函数
        // 虚拟DOM更新中 会触发 update钩子函数
        // 虚拟DOM更新后 会触发 postpatch钩子函数
        mergeVNodeHook(vnode, 'postpatch', () => {
          for (let i = 0; i < dirsWithPostpatch.length; i++) {
            callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
          }
        })
      }
    
      // 如果是新创建,是不需要解绑的。
      if (!isCreate) {
        // 旧的存在,新的不存在。那么调用callHook 执行upbind方法即可
        for (key in oldDirs) {
          if (!newDirs[key]) {
            // no longer present, unbind
            callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
          }
        }
      }
    }
    

    你可能感兴趣的:(vue2-指令)