vue2学习之记录一个简版vue的实现

212 阅读2分钟

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

  • 数据响应式 Object.defineProperty()
  • 模板引擎简写 {{}} v-text v-html @ v-model

测试用例

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FVue 测试用例</title>
</head>
<body>
  <div id="app">
    <div>{{counter}}</div>
    <div f-text="counter"></div>
    <div f-html="desc" @click="changeContent"></div>
    <input type="text" f-model="desc">
  </div>
  <script src="./fvue.js"></script>
  <script>
    const app = new FVue({
      el: '#app',
      data: {
        counter: 1,
        desc: '<span style="color: green">村长好帅,谢谢村长!</span>'
      },
      methods: {
        changeContent () {
          this.desc = '<span style="color: olive">谢谢村长,村长好帅!</span>'
        }
      },
    })
    setInterval(() => {
      app.counter++
    }, 1000)
  </script>
</body>
</html>

实现

// fvue.js
// 响应式数据函数
function defineReactive(obj, key, val) {
  // 对值进行递归
  observe(val);
  const dep = new Dep()
  // 对属性进行拦截
  Object.defineProperty(obj, key, {
    get () {
      // 依赖收集
      Dep.target && dep.addDep(Dep.target)
      return val;
    },
    set (newVal) {
      if (val !== newVal) {
        // obj[key] = newVal; 这样设置会一直触发set方法造成死循环
        // 此处对新设置的属性在值为对象的时候进行响应式处理
        observe(newVal);
        val = newVal;
        // 全量更新测试
        // watchers.forEach(w => w.update())
        // 通知更新
        dep.notify()
      }
    }
  })
}
function proxy(vm) {
  Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(vm, key, {
      get () {
        return vm.$data[key];
      },
      set (val) {
        vm.$data[key] = val;
      }
    })
  })
}
// 遍历对象的所有属性,执行响应式处理
function observe (obj) {
  if (typeof obj !== 'object' || obj === null) {
    // 判断obj是否是对象,注意: typeof null === 'object'
    return obj;
  }
  Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
}
class FVue {
  constructor (options) {
    this.$options = options;
    this.$data = options.data;

    // 首先递归遍历data中的所有对象,做响应式处理
    observe(this.$data);

    // 此时,数据还需要通过this.$data.xxx访问,在这做一个代理处理,方便可以直接通过this.xxx访问
    // 此处this指的是vue实例
    proxy(this);
    // 编译模板
    new Compile(options.el, this);

  }
}
// 遍历dom tree,解析其中动态部分,初始化,并获得更新函数
class Compile {
  constructor (el, vm) {
    // 保存实例
    this.$vm = vm;
    // 获取宿主元素
    const dom = document.querySelector(el);
    // 对宿主元素进行编译
    this.compile(dom);
  }
  compile (dom) {
    // 遍历dom
    const childNodes = dom.childNodes;
    childNodes.forEach(node => {
      // console.log(this.isInsertExpress(node))
      if (this.isElement(node)) {
        // 元素-解析动态的指令、属性绑定、事件等
        // attrs为一个类数组
        const attrs = node.attributes;
        Array.from(attrs).forEach(attr => {
          // 判断是否是动态属性
          // 置顶f-xxx="counter"
          // 属性名
          const attrName = attr.name;
          // 表达式
          const exp = attr.value;
          if (this.isDir(attrName)) {
            // 截取指令名
            const dir = attrName.substring(2);
            // 判断是否是合法指令,如果是则这行处理函数
            this[dir] && this[dir](node, exp);
          }
          if (this.isEvent(attrName)) {
            // 截取事件名 @click="clickName"
            const dir = attrName.substring(1); // click
            // exp clickName
            this.eventHandler(node, exp, dir);
          }
        })
        // 递归
        if (node.childNodes.length > 0) {
          this.compile(node);
        }
      } else if (this.isInsertExpress(node)) {
        // 插值表达式{{title}} 此处仅考虑表达式为简单的data中的属性这种情况
        this.compileText(node);
      }
    })
  }
  // 公用update方法
  update (node, exp, dir) {
    // 初始化
    const fn = this[dir + 'Updater']
    fn && fn(node, this.$vm[exp])
    // 更新
    new Watcher(this.$vm, exp, function (val) {
      fn && fn(node, val)
    })
  }
  // f-text
  text(node, exp) {
    this.update(node, exp, 'text');
    // node.textContent = this.$vm[exp];
  }
  textUpdater (node, val) {
    node.textContent = val
  }
  compileText (node) {
    this.update(node, RegExp.$1, 'text');
    // node.textContent = this.$vm[RegExp.$1];
  }
  // f-html
  html (node, exp) {
    this.update(node, exp, 'html');
    // node.innerHTML = this.$vm[exp];
  }
  htmlUpdater (node, val) {
    node.innerHTML = val;
  }
  isElement (node) {
    return node.nodeType === 1;
  }
  isInsertExpress (node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
  }
  isDir (attrName) {
    // 判断是否以f-开头
    return attrName.startsWith('f-');
  }
  isEvent (attrName) {
    return attrName.indexOf('@') === 0
  }
  // @click="clickName"
  eventHandler (node, exp, dir) {
    const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp]
    // 要给clickName方法绑定上下文环境,方便在clickName中使用this指向vue实例
    node.addEventListener(dir, fn.bind(this.$vm))
  }
  // f-model="test"
  model (node, exp) {
    // 给input赋值及更新
    this.update(node, exp, 'model');
    // 事件监听
    node.addEventListener('input', event => {
      // 将新的值赋值给data中的数据
      this.$vm[exp] = event.target.value;
    })
  }
  modelUpdater (node, val) {
    // 给表单元素赋值
    node.value = val;
  }
}
// 全量更新测试
// const watchers = [] 
class Watcher {
  constructor (vm, key, updater) {
    // vue 实例
    this.vm = vm;
    // data中的key
    this.key = key;
    // 真正做具体dom操作的更新函数
    this.updater = updater;
    // 全量更新测试
    // watchers.push(this)

    // 读当前值,触发依赖收集
    Dep.target = this
    this.vm[this.key]
    Dep.target = null
  }
  // 更新函数,暴露给dep调用
  update () {
    this.updater.call(this.vm, this.vm[this.key])
  }
}

class Dep {
  constructor () {
    this.deps = [];
  }
  addDep (watcher) {
    this.deps.push(watcher)
  }
  notify () {
    this.deps.forEach(dep => dep.update())
  }
}