这篇文章分析Vue实例怎么通过$refs 访问到dom元素的。通过上一篇的例子来进行分析:

<html>
 
<head>
<style type="text/css">

</style>
</head>
 
<body>
   
<script src="./vue.js"></script>
<div id="app">
    <div id="div1Id" ref="div1Ref" style="margin-bottom: 20px">div 1 test</div>
    <button @click="addBorder()">add border for div1</button>
</div>

<script>

const app = new Vue({
    el: "#app",
    methods : {
        addBorder : function(){
            //document.getElementById("div1Id").style.border = "5px solid red";
            console.log("div1 is: " + this.$refs.div1Ref.outerHTML);
            this.$refs.div1Ref.style.border = "5px solid blue";
        }
    }
});

</script>
 
</body>
</html>
<div id="app">
    <div id="div1Id" ref="div1Ref" style="margin-bottom: 20px">div 1 test</div>
    <button @click="addBorder()">add border for div1</button>
</div>

通过Vue模板编译系统生成的render函数如下:

with(this){return _c('div',{attrs:{"id":"app"}},[_c('div',{ref:"div1Ref",staticStyle:{"margin-bottom":"20px"},attrs:{"id":"div1Id"}},[_v("div 1 test")]),_v(" "),_c('button',{on:{"click":function($event){return addBorder()}}},[_v("add border for div1")])])}

我们看到ref = "div1Ref" 被编译到了render函数中的data参数中,我们在createElement 加个打印看看这个data:

  function createElement (
    context,
    tag,
    data,
    children,
    normalizationType,
    alwaysNormalize
  ) {
    if (Array.isArray(data) || isPrimitive(data)) {
      normalizationType = children;
      children = data;
      data = undefined;
    }
    if (isTrue(alwaysNormalize)) {
      normalizationType = ALWAYS_NORMALIZE;
    }
    console.log("liubbc data is: " + JSON.stringify(data));
    return _createElement(context, tag, data, children, normalizationType)
  }

打印如下:

liubbc data is: {"ref":"div1Ref","staticStyle":{"margin-bottom":"20px"},"attrs":{"id":"div1Id"}}

接下来就是创建VNode,并把data参数赋值给VNode,代码如下:

  function _createElement (
    context,
    tag,
    data,
    children,
    normalizationType
  ) {
     //some code
     vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
     );
     //some code
  }

    var VNode = function VNode (
    tag,
    data,
    children,
    text,
    elm,
    context,
    componentOptions,
    asyncFactory
  ) {
    this.tag = tag;
    this.data = data;
    //some code
  }

我们省略了一些代码,但还是能清晰的看到给VNode 的data属性赋值过程。对ref = "div1Ref"的处理就到这里了,那么怎么通过this.$refs.div1Ref就能访问到div呢?我们在全局vue.js中搜索$refs,发现只有两处:

  function initLifecycle (vm) {
    var options = vm.$options;

    // locate first non-abstract parent
    var parent = options.parent;
    if (parent && !options.abstract) {
      while (parent.$options.abstract && parent.$parent) {
        parent = parent.$parent;
      }
      parent.$children.push(vm);
    }

    vm.$parent = parent;
    vm.$root = parent ? parent.$root : vm;

    vm.$children = [];
    vm.$refs = {};

    vm._watcher = null;
    vm._inactive = null;
    vm._directInactive = false;
    vm._isMounted = false;
    vm._isDestroyed = false;
    vm._isBeingDestroyed = false;
  }



  function registerRef (vnode, isRemoval) {
    var key = vnode.data.ref;
    console.log("liubbc registerRef key: " + key);
    if (!isDef(key)) { return }

    var vm = vnode.context;
    var ref = vnode.componentInstance || vnode.elm;
    var refs = vm.$refs;
    if (isRemoval) {
      if (Array.isArray(refs[key])) {
        remove(refs[key], ref);
      } else if (refs[key] === ref) {
        refs[key] = undefined;
      }
    } else {
      if (vnode.data.refInFor) {
        if (!Array.isArray(refs[key])) {
          refs[key] = [ref];
        } else if (refs[key].indexOf(ref) < 0) {
          // $flow-disable-line
          refs[key].push(ref);
        }
      } else {
        refs[key] = ref;
      }
    }
  }

从上面代码看到,第一处initLifecycle函数中 vm.$refs = {};  这行代码就让vue实例拥有了一个$refs对象。从第二处registerRef函数名中就看到这是注册$refs的地方。我们先分析一下这个函数。其实这个函数还是蛮简单的,我们看一下几处关键代码:

var key = vnode.data.ref;  //之前分析过,vnode的data为{"ref":"div1Ref","staticStyle": 
                           //{"margin-bottom":"20px"},"attrs":{"id":"div1Id"}}

var ref = vnode.componentInstance || vnode.elm;  //如果是组件实例,ref则指向组件实例,否则指向
                                                 //elm, elm就是div dom元素
var refs = vm.$refs;        //refs 指向vue实例$refs对象

refs[key] = ref;      // vue实例$refs对象的div1Ref 属性赋值为div dom元素
               

到这里基本上我们也就明白了this.$refs.div1Ref 能取得div dom元素的过程了,但还有一点需要澄清,就是谁调用的registerRef函数。还是在vue.js中全局搜索registerRef,又加了点打印,发现在这个地方调用了这个函数:

  var ref = {
    create: function create (_, vnode) {
      console.log("liubbc ref create");
      registerRef(vnode);
    },
    update: function update (oldVnode, vnode) {
      console.log("liubbc ref update");
      if (oldVnode.data.ref !== vnode.data.ref) {
        registerRef(oldVnode, true);
        registerRef(vnode);
      }
    },
    destroy: function destroy (vnode) {
      console.log("liubbc ref destroy");
      registerRef(vnode, true);
    }
  };

从这个ref对象中的create,update,destroy 属性来看,都调用了registerRef函数。凭经验应该从create函数中来找,那么谁又调用了create函数呢?

    function invokeCreateHooks (vnode, insertedVnodeQueue) {
      for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) { 
        cbs.create[i$1](emptyNode, vnode);    //在这里调用了create函数
      }
      i = vnode.data.hook; // Reuse variable
      if (isDef(i)) {
        if (isDef(i.create)) {
          i.create(emptyNode, vnode); 
        }
        if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
      }
    }


    function createElm (
      vnode,
      insertedVnodeQueue,
      parentElm,
      refElm,
      nested,
      ownerArray,
      index
    ) {

      //some code

      var data = vnode.data;  //vnode.data 之前分析过里面有ref属性

      //some code
  
      vnode.elm = vnode.ns
          ? nodeOps.createElementNS(vnode.ns, tag)
          : nodeOps.createElement(tag, vnode);        //创建了真实的div dom 

      //some code

      {
          createChildren(vnode, children, insertedVnodeQueue);
          if (isDef(data)) {    
            invokeCreateHooks(vnode, insertedVnodeQueue);   //这里注册create hooks
          }
          insert(parentElm, vnode.elm, refElm);
      }

  对关键代码进行了注释。在创建真实dom过程中做了invoke create hooks操作。在invokeCreateHooks函数中的cbs又是什么呢?

   var hooks = ['create', 'activate', 'update', 'remove', 'destroy']; //定义了这么多hooks

   function createPatchFunction (backend) {
    var i, j;
    var cbs = {};  //invokeCreateHooks  访问了cbs,所以形成了闭包

    var modules = backend.modules;
    var nodeOps = backend.nodeOps;

    for (i = 0; i < hooks.length; ++i) {
      cbs[hooks[i]] = [];    // 在这里注册了create  hook
      for (j = 0; j < modules.length; ++j) {
        if (isDef(modules[j][hooks[i]])) {
          cbs[hooks[i]].push(modules[j][hooks[i]]);  //往create hook 里面push 钩子函数
        }
      }
    }
    
    //some code 

    function invokeCreateHooks (vnode, insertedVnodeQueue) {
      for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) { 
        cbs.create[i$1](emptyNode, vnode);    // 遍历调用cbs中的create 钩子
      }

    }
  }

上面代码就是注册create  hooks过程,我们看看往create hook 里面push的钩子函数有哪些?是否有var ref中的create钩子函数?

  var baseModules = [
    ref,    //这里就是ref 对象
    directives
  ];

  var platformModules = [
    attrs,
    klass,
    events,
    domProps,
    style,
    transition
  ];

  /*  */

  // the directive module should be applied last, after all
  // built-in modules have been applied.
  var modules = platformModules.concat(baseModules);

  var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules }); //modules中
                    //含有ref对象

从上面我们看到其实在加载vue.js的时候都把var ref中的create 钩子函数注册进cbs了。在创建真实dom过程中会检测vnode中的data属性是否为空,如果不为空就invokeCreateHooks  调用create 钩子函数,在create钩子函数中给$refs对象创建名为data中ref属性值的属性div1Ref,并使div1Ref属性值指向真实的div dom元素。

 

不知道写清楚没有,如哪里没看明白,请留言。

 

 

Logo

前往低代码交流专区

更多推荐