1. vue主要的功能实现主要分为3部分:

    • 数据劫持/数据代理:数据改变时通知相关函数进行更新操作
    • 数据依赖收集:建立保存dom节点与数据的关联关系
    • 模板与数据之间的绑定:接受到新数据时对dom节点进行更新(Watcher)
  2. 代码:

    构造方法中拿到传入的参数,再调用一些方法对上面提到的3部分功能进行实现。这些方法都放在了原型链上(prototype),

   这样方便在方法中使用实例的context(this)。

  function VueComponent(vueComponentProps = {}) {
    const {
      template = '',
      data = () => {},
      methods = {},
      watch = {}
    } = vueComponentProps;
    this.$watch = watch;
    this.$watchers = {};
    this.$methods = methods;
    this.$data = data() || {};
    bindVueProperties(this,data() || {});
    this.observe(this.$data);
    this.observe(this);
    bindVueProperties(this,this.$methods);
    this.$template = htmlStringToElement(template);
    this.$vueDomNode = this.compile();
  }

  //因为之后我们需要在执行methods中的方法可能同时要访问data和methods,
  //所以这里利用bindVueProperties将data和methods复制到实例的根层。
  function bindVueProperties(vueInstance, obj) {
    for(let key of Object.keys(obj)) {
      vueInstance[key] = obj[key];
    }  
  }

   · 数据劫持/数据代理

  VueComponent.prototype.observe = function(data, path = "") {
    const self = this;
    if(!data || Object.prototype.toString.call(data) !== "[object Object]") {
      return;
    }

    //递归遍历data的所有key键值对,如果当前的key对应的值是一个引用类型,那么用Proxy进行数据代理的实现
    //如果是原始类型(string, number),例如data = {someKey : 1}时,我们对someKey的值“数字1”无法用Proxy进行拦截
    //这时采用Object.defineProperty中的get和set来实现。当然两种情况都可以用Object.defineProperty进行实现,
    //Proxy比较灵活,能够拦截的种类比较多。在数据改变时我们要通知相应的watcher来更新依赖该数据的dom节点。
    const keys = Object.keys(this.$data);
    for(let key of keys) {
      let value = data[key];
      const currentPath = path + key;
      if(typeof value === 'object') {
        //如果是object,则递归进入下一层结构创建代理
        self.observe(value,currentPath + '. ');
        data[key] = new Proxy(value, {
          set(target, property, value, reseiver) {
            if(!Reflect.set(target, property, value, reseiver)) {
              return false;
            }
            const keyPath = currentPath + "." + property;
            //属性改变时会通知该key对应的watcher进行更新,并执行watch里的相应key的回调
            updateWatcher(self.$watchers[keyPath],value);
            self.$watch[keyPath] && self.$watch[keyPath](value);
            return true;
          },
          get(target, property, reseiver) {
            return target[property];
          }
        });
      }else {
        //Object.defineProperty方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
        Object.defineProperty(data, key, {  //Object.defineProperty(要定义属性的对象,要定义或修改的属性的名称或symbol,要定义或修改的属性描述符)
          enumerable: true,  //enumerable 当且仅当该属性的enumerable键值为true时,该属性才会出现在对象的枚举属性中
          configurable: false,  //configurable 当且仅当该属性的configurable键值为true时,该属性的描述符才能被改变,同时该属性也能从对应的对象上被删除
          get() {    //当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this对象(由于继承关系,这里的this不一定是定义该属性的对象)。该函数的返回值会被用作属性的值
            return value;
          },
          set() {    //当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的this对象
            value = newVal;
            self.$watch[currentPath] && self.$watch[currentPath](newVal);
            updateWatcher(self.$watchers[currentPath], newVal);
          }  
        })
      }
    }
  }

     我们在编译模板的时候会进行node上数据依赖的收集与事件的监听,首先我们先要把构造函数传进来的模板字符串转换成dom。

这里用一个htmlStringToElement来实现该功能,思路就是利用html中的template标签来创建该component的dom节点。

  function htmlStringToElement(html = '') {
    const template = document.createElement("template");  //创建一个template标签
    template.innerHTML = html.trim();  //把传进来的html字符串加在template标签中
    return template.content.firstChild;  //返回此时的template内容第一个元素就是传进来的html字符串的dom了
  }

  然后进行模板解析,compile函数中会递归遍历所有的node上的attribute,如果是v-model我们进行双向数据绑定

以冒号“:”开头的属性我们进行相应数据的监听,如果是“@”开头的则添加该节点上相应的事件监听。

  VueComponent.prototype.compile = function() {
    const self = this;
    const el = self.$template;
    const data = self;
    if(!(el instanceof HTMLElement)) {
      throw new TypeError("template must be an HTMLElement instance");
    }
    const fragment = document.createDocumentFragment();
    let child = el.firstChild;
    while(child) {
      fragment.appendChild(child);
      child = el.firstChild;
    }
    const reg = /\{\{(.*?)\}\}/g; //匹配{{xx.xx}}的正则
    
    //对el中的内容进行相应替换
    const replace = (parentNode)=> {
      const children = parentNode.childNodes;
      for(let i = 0; i < children.length; i++) {
        const node = children[i];
        const nodeType = node.nodeType;
        const text = node.textContent;
        //如果是text节点,我们在这里判断内容是否存在模板变量
        if(nodeType === Node.TEXT_NODE && reg.test(text)) {
          node.textContent = text.replace(reg, (matched, placeholder) => {
            let pathArray = [];
            const textContent = placeholder.split('.').reduce((prev, property) => {
              pathArray.push(property);
              return prev[property];
            },data);
            const path = pathArray.join(".");      //path就是当前key的层级路径,例如b:{a:1}中a的路径就是a.b
            self.$watchers[path] = self.$watchers[path] || [];
            self.$watchers[path].push(new Watcher(self, node, "textContent", text));
            return textContent;
          })
        }
        
        //如果是element则进行attribute的绑定
        if(nodeType === Node.ELEMENT_NODE) {  //Node.ELEMENT_NODE代表元素节点类型。
          const attrs = node.attributes;  //attributes属性返回该节点的属性节点【集合:不重复,无序】
          for(let i = 0; i < attrs.length; i++) {
            const attr = attrs[i];
            const attrName = attr.name;
            const attrValue = attr.value;
            self.$watchers[attrValue] = self.$watchers[attrValue] || [];
            //如路过属性名称为v-model我们需要添加value属性的绑定和输入事件监听,这里假设都为input事件,可替换为change事件
            if(attrName === 'v-model') {
              node.addEventListener("input",(e) => {
                const inputVal = e.target.value;
                const keys = attrValue.split(".");
                const lastKey = keys.splice(keys.length - 1)[0];
                const targetKey = keys.reduce((prev, property) => {
                  return prev[property];
                }, data);
                targetKey[lastKey] = inputVal;
              });
            }
      
            //对value属性的绑定
            if(attrName === ':bind' || attrName === 'v-model') {
              node.value = attrValue.split(".").reduce((prev,property) => {
                return prev[property];
              },data);
              self.$watchers[attrValue].push(new Watcher(self, node, "value"));
              node.removeAtrribute(attrName);
            }else if(attrName.startsWith(":")) {    //startsWith用于检测字符串是否以指定的前缀开始
              //对普通attribute的绑定
              const attributeName = attrName.splice(1);
              const attributeValue = attrValue.split(".").reduce((prev,property) => {
                return prev[property];
              }, data);
              node.setAttribute(attributeName, attributeValue);
              self.$watchers[attrValue].push(new Watcher(self, node, attributeName));
              node.removeAttribute(attrName);
            }else if (attrName.startsWith("@")) {
              //事件监听
              const event = attrName.splice(1);
              const cb = attrValue;
              node.addEventListener(event, function() {
                //data和methods同时放在了实例的根层,所以这里的context设置为self,就可以在方法中同时访问到data和其他methods了
                self.$methods[cb].call(self);
              });
              node.removeAttribute(attrName);
            }
          }
        }
        //如果存在子节点,递归调用replace
        if(node.childNodes && node.childNodes.length) {
          replace(node);
        }
      }
    }
    replace(fragment);
    el.appendChild(fragment);
    return el;
  }

  数据改变时所关联的node需要进行更新这里一个watcher对象来进行node与数值之间的联系,并在数据改变时执行相关的更新视图操作。Watcher类主要负责接收到数据更新通知时,对dom内容的更新,监听到数据改变时会调用updateWatcher这个辅助函数来执行该数据关联的所有dom节点更新函数

  function updateWatcher(watchers = [], value) {
    watchers.forEach(Watcher => {
      Watcher.update(value);
    })
  }

创建watch实例时我们会对该dom节点node,关联的数据data,还有所对应的attribute进行保存,这里的template是要保存原始的模板变量,例如“{{article.test}}:{{author}}”。之后执行update函数更新TextNode中的内容时会用到,数据更新被拦截后执行updateWatcher,其中又会执行所有依赖的watcher中的update函数。update函数会根据attribute的类型来判断如何更新dom节点。如果是更新TextNode,其中会包含一个或多个模板变量,所以之前要保存原始的template,将原始的模板中各个变量依次替换

  function Watcher(data, node, attribute, template) {
    this.data = data;
    this.node = node;
    this.atrribute = attribute;
    this.template = template;
  }
  
  Watcher.prototype.update = function(value) {
    const attribute = this.attribute;
    const data = this.data;
    const template = this.template;
    const reg = /\{\{(.*?)\}\}/g;  //匹配{{xx.xx}}的正则
    if(attribute === "value") {
      this.node[attribute] = value;
    }else if(attribute === "innerText" || attribute === "innerHTML" || atrribute === "textContent") {
      this.node[attribute] = template.replace(reg, (matched, placeholder) => {
        return placeholder.split(".").reduce((prev, property) => {
          return prev[property];
        }, data);
      });
    }else if(attribute === "style") {
      thie.node.style.cssText = value;
    }else {
      this.node.setAttribute(attribute, value);
    }
  }

  测试代码

<script>
  const IndexComponent = new VueComponent({
    data(): => {
      return {
        article: {
          title: "简易版vue"
        },
        author: "xx"
      }
    },
    watch: {
      author(value) {
        console.log("author:", value);
      }
    },
    methods: {
      onButtonClick() {
        this.author = "cc";
        alert("clicked!");
      }
    },
    template:
        `
          <div>
            <div>template及其他功能测试</div>
            <div>{{article.title}} : {{author}}</div>
            <div><input type="text" v-model="article.title"></div>
            <div><input type="text" v-model="author"></div>
            <div><button @click="onButtonClick">click</button></div>
          </div>
        `
  });
  window.onload = function(){
    document.getElementById("root").appendChild(IndexComponent.$vueDomNode);
  }
</script>
<body>
  <div id="root"></div>
</body>

实现一个简易vue的更多相关文章

  1. vue + socket.io实现一个简易聊天室

    vue + vuex + elementUi + socket.io实现一个简易的在线聊天室,提高自己在对vue系列在项目中应用的深度.因为学会一个库或者框架容易,但要结合项目使用一个库或框架就不是那 ...

  2. vue实现一个简易Popover组件

    概述 之前写vue的时候,对于下拉框,我是通过在组件内设置标记来控制是否弹出的,但是这样有一个问题,就是点击组件外部的时候,怎么也控制不了下拉框的关闭,用户体验非常差. 当时想到的解决方法是:给根实例 ...

  3. Vue源码分析之实现一个简易版的Vue

    目标 参考 https://cn.vuejs.org/v2/guide/reactivity.html 使用 Typescript 编写简易版的 vue 实现数据的响应式和基本的视图渲染,以及双向绑定 ...

  4. 使用 js 实现一个简易版的 vue 框架

    使用 js 实现一个简易版的 vue 框架 具有挑战性的前端面试题 refs https://www.infoq.cn/article/0NUjpxGrqRX6Ss01BLLE xgqfrms 201 ...

  5. 基于 getter 和 setter 撸一个简易的MVVM

    Angular 和 Vue 在对Angular的学习中,了解到AngularJS 的两个主要缺点: 对于每一次界面时间,Ajax 或者 timeout,都会进行一个脏检查,而每一次脏检查又会在内部循环 ...

  6. .NET Core的文件系统[5]:扩展文件系统构建一个简易版“云盘”

    FileProvider构建了一个抽象文件系统,作为它的两个具体实现,PhysicalFileProvider和EmbeddedFileProvider则分别为我们构建了一个物理文件系统和程序集内嵌文 ...

  7. 自己来实现一个简易的OCR

    来做个简易的字符识别 ,既然是简易的 那么我们就不能用任何的第三方库 .啥谷歌的 tesseract-ocr, opencv 之类的 那些玩意是叼 至少图像处理 机器视觉这类课题对我这种高中没毕业的人 ...

  8. 探秘Tomcat——一个简易的Servlet容器

    即便再简陋的服务器也是服务器,今天就来循着书本的第二章来看看如何实现一个servlet容器. 背景知识 既然说到servlet容器这个名词,我们首先要了解它到底是什么. servlet 相比你或多或少 ...

  9. 使用Windows Form 制作一个简易资源管理器

    自制一个简易资源管理器----TreeView控件 第一步.新建project,进行基本设置:(Set as StartUp Project:View/Toolbox/TreeView) 第二步.开始 ...

随机推荐

  1. (SpringBoot-Jpa)使用Idea数据库自动脚本Generate POJOS生成 Entity对象,

    因:使用SpringBoot -jpa,需要手动配置Entity 但是如果你的表中有很多属性,或者有很多表怎么办?? 每个手动写? 还是用mybatis.写mapper??? 解决:使用idea自动工 ...

  2. day32 Pyhton 模块02复习 序列化

    一. 什么是序列化 在我们存储数据或者网络传输数据的时候. 需要对我们的对象进行处理. 把对象处理成方便存储和传输的数据格式. 这个过程叫序列化 不同的序列化, 结果也不同. 但是目的是一样的. 都是 ...

  3. unix socket接口

    socket 创建套接字文件: #include <sys/socket.h> // 成功返回非负套接字描述符,失败返回-1 int socket(int domain, int type ...

  4. 【二分】CF Round #587 (Div. 3)E2 Numerical Sequence (hard version)

    题目大意 有一个无限长的数字序列,其组成为1 1 2 1 2 3 1.......1 2 ... n...,即重复的1~1,1~2....1~n,给你一个\(k\),求第\(k(k<=10^{1 ...

  5. Privileged Permission开机授权时序图 SourceCode android-10.0.0_r36

    Privileged Permission开机授权时序图 | SourceCode:android-10.0.0_r36 | Author:秋城 | v1.1SystemServerSystemSer ...

  6. zookeeper动态添加/删除集群中实例(zookeeper 3.6)

    一,用来作为demo操作的zookeeper集群中的实例: 机器名:zk1 server.1=172.18.1.1:2888:3888 机器名:zk2 server.2=172.18.1.2:2888 ...

  7. 实战三:将nacos作为配置中心

    一,引入nacos配置中心依赖 <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId&g ...

  8. 校招“避雷针”——GitHub 热点速览 Vol.43

    作者:HelloGitHub-小鱼干 如果要选一个关键词来概述本周的 GitHub Trending,保护 便是不二之选.先是有 ShameCom 来为应届毕业生护航,让学弟学妹们不被黑名单上的公司上 ...

  9. SpringBoot第二集:注解与配置(2020最新最易懂)

    2020最新SpringBoot第二集:基础注解/基础配置(2020最新最易懂) 一.Eclipse安装SpringBoot插件 Eclipse实现SpringBoot开发,为便于项目的快速构建,需要 ...

  10. 使用微创联合M5S空气检测仪、树莓派3b+、prometheus、grafana实现空气质量持续监控告警WEB可视化

    1.简介 使用微创联合M5S空气检测仪.树莓派3b+.prometheus.grafana实现空气质量持续监控告警WEB可视化 grafana dashboard效果: 2.背景 2.1 需求: 1. ...