zoukankan      html  css  js  c++  java
  • Vue底层学习5——插值文本编译与依赖收集

    全手打原创,转载请标明出处:https://www.cnblogs.com/dreamsqin/p/15044033.html, 多谢,=。=~(如果对你有帮助的话请帮我点个赞啦)

    作为一个Web前端开发人员,使用Vue框架进行项目开发已经有一阵子,掐指一算,是时候认真探索一下Vue的底层了,以前的了解比较偏理论,这一次打算在弄清基本原理的前提下自己手写Vue中的核心部分,也许这样我才敢说自己“深入理解”了Vue。上篇简述了整个编译器原理并拟定了三项编译目标,完成编译器框架搭建,在遍历Dom子节点时实现分流处理,本篇主要实现第一个目标插值文本编译和依赖收集~

    插值文本编译

    由上一篇提供的demo2可以得到如下的运行结果:

    但实际上我们想要展示的是各个变量对应的值,而不是变量名,所以需要编译Dom中的插值变量,并将其替换为对应的值,这里新建一个compileText方法实现:

    /*** compile.js ***/
    // new Compile(el, vm)
    
    class Compile{
      constructor(el, vm) {
        // 需要遍历的Dom节点
        this.$el = document.querySelector(el);
        // 数据缓存
        this.$vm = vm;
    
        // 编译
        if (this.$el) {
          // 提取指定节点中的内容,提高效率,减少Dom操作
          this.$fragment = this.node2Fragment(this.$el);
          // 执行编译
          this.compile(this.$fragment);
          // 将编译完的html追加至$el
          this.$el.appendChild(this.$fragment);
        }
      }
    
      // 提取指定Dom节点中的代码片段
      node2Fragment(el) {
        const fragment = document.createDocumentFragment();
        // 将el中的所有子元素移动至fragment中
        let child = null;
        while(child = el.firstChild) {
          fragment.appendChild(child);
        }
        return fragment;
      }
    
      // 编译过程
      compile(el) {
        const childNodes = el.childNodes;
        Array.from(childNodes).forEach(node => {
          // 类型判断
          if (this.isElement(node)) {
            // 节点
            console.log('编译节点' + node.nodeName);
          } else if(this.isInterpolation(node)) {
            // 编译插值文本
            this.compileText(node);
          }
    
          // 递归子节点
          if (node.childNodes && node.childNodes.length > 0) {
            this.compile(node);
          }
        })
      }
    
      isElement(node) {
        return node.nodeType === 1;
      }
    
      isInterpolation(node) {
        return node.nodeType === 3 && /{{(.*)}}/.test(node.textContent);
      }
    
      // 插值文本编译
      compileText(node) {
        node.textContent = this.$vm.$data[RegExp.$1];
      }
    }
    

    需要特别注意的是RegExp.$1的巧用,在做子节点分流时我们通过正则表达式对插值文本进行了匹配分组,所以在执行compileText方法时我们可以通过RegExp.$1获取到分组中的内容,也就是插值括号{{}}中的变量,例如namelocationlocationAgain,然后通过传递的Vue实例this.$vm获取到$data中的属性变量值,再对节点内容进行替换操作,最终运行结果如下:

    可以看到页面中的变量成功被替换,但这种方式只会初始化一次,当变量值发生改变时,页面中展示的内容是不会同步变更的,可以利用demo2(源码可参见《Vue底层学习4——编译器框架搭建》)中created方法的延迟赋值操作测试一下,我们在MVue的构造函数中执行一下created方法:

    /*** MVue.js ***/
    // new MVue({ data: {...} })
    
    class MVue {
      constructor(options) {
        // 数据缓存
        this.$options = options;
        this.$data = options.data;
    
        // 数据遍历
        this.observe(this.$data);
    
        new Compile(options.el, this);
    
        // created执行
        if (options.created) {
          options.created.call(this);
        }
      }
    }
    

    调用时使用call绑定this指向是为了方便在Vue实例的created方法中轻松使用this访问当前的Vue实例对象,例如我们日常用this.data去访问实例的数据属性。created执行后结果如下:

    开始啦成功打印,但name的重新赋值并没有同步更新至页面,与上面的猜想一致。其主要原因是没有做依赖收集,也就是之前MVue.jsconstructor 中模拟Watcher 激活getter的部分,除此之外,我们编译器中还需要一个更新函数,之前Watcherupdate方法都是通过console实现视图更新的预留,这些事还是得编译器来完成。

    更新函数

    触发更新的操作有很多,视图中不仅仅只有插值文本,还有一系列的v-指令或者事件,所以我们需要抽象出一个更新函数供所有的触发调用,在编译器中定义一个更新函数update,它接收4个参数,分别表示需要更新的节点当前的Vue实例属性标识触发更新的指令标识

    /*** compile.js ***/
    // new Compile(el, vm)
    
    class Compile{
      // 更新函数
      update(node, vm, exp, dir) {
        const updateFn = this[dir + 'Updater'];
        // 如果存在就执行,实现初始化
        updateFn && updateFn(node, vm.$data[exp]);
      }
    }
    

    updateFn的执行只能达到初始化的作用,跟上述compileText函数实现的效果一致,但当数据变更时想要同步更新,就需要做依赖收集,跟之前模拟的一样,我们需要创建一个Watcher实例,接收3个参数,分别表示当前的Vue实例属性标识当属性变更时执行的更新回调函数

    /*** compile.js ***/
    // new Compile(el, vm)
    
    class Compile{
      // 更新函数
      update(node, vm, exp, dir) {
        const updateFn = this[dir + 'Updater'];
        // 如果存在就执行,实现初始化
        updateFn && updateFn(node, vm.$data[exp]);
        
        // 依赖收集
        new Watcher(vm, exp, function(value) {
          updateFn && updateFn(node, value);
        });
      }
    }
    

    那么对于插值文本的更新我们就需要创建一个对应的更新函数textUpdater,并且之前用于插值文本编译的compileText函数就需要做对应的变更:

    /*** compile.js ***/
    // new Compile(el, vm)
    
    class Compile{
      // 更新函数
      update(node, vm, exp, dir) {
        const updateFn = this[dir + 'Updater'];
        // 如果存在就执行,实现初始化
        updateFn && updateFn(node, vm.$data[exp]);
        
        // 依赖收集
        new Watcher(vm, exp, function(value) {
          updateFn && updateFn(node, value);
        });
      }
      
      // 插值文本更新
      textUpdater(node, value) {
        node.textContent = value;
      }
      
      // 插值文本编译
      compileText(node) {
        this.update(node, this.$vm, RegExp.$1, 'text');
      }
    }
    

    可以看到以前我们在模拟依赖收集时,实例化Watcher时是不会传参的,但是现在接收了3个参数,所以需要同步修改MVue中的Watcher类,并通过Watcher拿到的Vue实例及属性标识激活getter实现依赖收集:

    /*** MVue.js ***/
    class Watcher {
      constructor(vm, exp, cb) {
        // 数据缓存
        this.$vm = vm;
        this.$key = exp;
        this.$cb = cb;
    
        // 将当前Watcher的实例指定到Dep静态属性target
        Dep.target = this;
        
        // 激活属性的getter,添加依赖
        this.$vm.$data[this.$key];
        // 置空,防止重复添加
        Dep.target = null;
      }
    
      update() {
        // 预留视图更新
        console.log('数据更新了,需要我们更新视图');
      }
    }
    

    那么现在预留的视图更新就可以直接执行传入的cb回调了,并绑定其中的this指向为当前的Vue实例,同时将修改后的值作为参数传递进去:

    /*** MVue.js ***/
    class Watcher {
      constructor(vm, exp, cb) {
        // 数据缓存
        this.$vm = vm;
        this.$key = exp;
        this.$cb = cb;
    
        // 将当前Watcher的实例指定到Dep静态属性target
        Dep.target = this;
        
        // 激活属性的getter,添加依赖
        this.$vm.$data[this.$key];
        // 置空,防止重复添加
        Dep.target = null;
      }
    
      update() {
        // 视图更新
        this.$cb.call(this.$vm, this.$vm.$data[this.$key]);
      }
    }
    

    为了方便我们获取和设置data中的属性,我们可以做一层代理,将data属性挂载到Vue的实例上,实现通过Vue实例就可以直接访问或设置data属性:

    /*** MVue.js ***/
    // new MVue({ data: {...} })
    
    class MVue {
      constructor(options) {...}
    
      observe(data) {
        // 确定data存在并且为对象
        if (!data || typeof data !== 'object') {
          return;
        }
    
        // 遍历data对象
        Object.keys(data).forEach(key => {
            // 重写对象属性的getter和setter,实现数据的响应化
            this.defineReactive(data, key, data[key]);
            
            // 代理data中的属性到Vue实例上
            this.proxyData(key);
        })
      }
    
      defineReactive(obj, key, val) {...}
    
      proxyData(key) {
        Object.defineProperty(this, key, {
          get: function() {
            return this.$data[key];
          },
          set: function(newVal) {
            this.$data[key] = newVal;
          }
        })
      }
    }
    

    接下来就可以把代码中通过this.$vm.$data访问或设置data中属性的操作修改为this.$vm直接进行访问和设置,修改后的代码就不贴出来了,全局搜索一下~

    下面就是见证奇迹的时刻,再次运行一下demo2,效果如下,1.5s左右后视图被同步更新了:

    参考资料

    1、Vue源码:https://github.com/vuejs/vue

  • 相关阅读:
    POJ 1002 487-3279
    Sicily 1732 Alice and Bob (二进制最大公约数)
    左右linuxserver自己主动重启过程监控和简单的解决方案
    php禁用一些重要功能
    JAVA于Get和Post差异请求
    小强HTML5手机发展之路(52)——jquerymobile触摸互动
    《剑指offer》 相应 在线测试地址
    winform 实现选择的城市名单
    linux-ubuntu关闭防火墙
    445port入侵具体解释
  • 原文地址:https://www.cnblogs.com/dreamsqin/p/15044033.html
Copyright © 2011-2022 走看看