手写vue源码,入门级别

最近在跟着教学视频学习vue源码,做了一些总结,希望能对学习vue有所帮助

知识要点

  • vue工作机制
  • vue响应式原理
  • 依赖收集与追踪
  • 编译compile

通过实现的入门级源码一步一步实现vue的简单功能指令

Vue响应式原理:defineProperty

 <div id="app">
    <p>你好,<span id="name"></span></p>
  </div>
  <script>
    var obj = {}
    Object.defineProperty(obj,"name",{
      get(){
        console.log('获取name');
        return document.querySelector('#name').innerHTML;
      },
      set(names){
        console.log('设置name');
        document.querySelector('#name').innerHTML=names;
      }
    })
    obj.name='jerry';
    console.log(obj.name)
  </script>
//测试代码
<script src="./kvue.js"></script>
  <script>
    const app=new KVue({
      data:{
        test:'I am test',
        foo:{
          bar:"bar"
        }
      }
    });
    app.test='hello';
    app.foo.bar='hello'
  </script>
//KVue.js
class KVue{
  //构造器接收option选项参数
  constructor(options){
    //讲选项参数赋值给当前对象的选项属性
    this.$options=options;
    //将选项里面的data赋值给当前对象的data
    this.$data=options.data;
    //监听data变化
    this.observe(this.$data)
  }
  observe(value){
    //判断传进来的参数是不是对象,如果不是就直接返回
    if(!value||typeof value!=='object'){
      return;
    }
    //参数是对象,则对对象的每一个属性进行遍历
    Object.keys(value).forEach(key=>{
      //响应式传参,传入当前对象,对象属性,属性值
      this.defineReactive(value,key,value[key]);
      //设置代理
      this.proxyData(key);
    })

  }
  proxyData(key){
    //对当前的对象进行响应式数据绑定,为每一个key设置代理
    Object.defineProperty(this,key,{
      get(){
        return this.$data[key];
      },
      set(newVal){
        this.$data[key]=newVal;
      }
    })
  }
  defineReactive(obj,key,val){
    //对传入的属性值进行深层次的遍历,判断是否是对象,如果是则再一次进行监听
    this.observe(val)
    Object.defineProperty(obj,key,{
      get(){
        return val;
      },
      set(newVal){
        if(val!==newVal){
          val=newVal;
          console.log(`${key}更新`)
        }
      }
    })
  }
}

依赖收集与追踪
这是在响应式原理上建立起来的依赖收集与追踪,重复部分不再注释

class KVue{
  constructor(option){
    this.$options=option;
    this.$data=option.data;
    this.observe(this.$data)
    new Compile(option.el,this)
    if(option.created){
      option.created.call(this)
    }
  }
  observe(value){
    if(!value||typeof value!=='object'){
      return;
    }
    Object.keys(value).forEach(key=>{
      this.defineReactive(value,key,value[key])
      this.proxyData(key)
    })
  }
  proxyData(key){
    Object.defineProperty(this,key,{
      get(){
        return this.$data[key];
      },
      set(newVal){
        this.$data[key]=newVal
      }
    })
  }
  defineReactive(obj,key,val){
    this.observe(val)
    const dep=new Dep()
    Object.defineProperty(obj,key,{
      get(){
        Dep.target&&dep.addDep(Dep.target)
        return val;
      },
      set(newVal){
        if(val!==newVal){
            val=newVal
          // console.log(`${key}属性更新了`)
          dep.notify()
        }
      }
    })
  }
}
//Dep管理若干Watcher,它和key存在一对一的关系
class Dep{
  constructor(){
  //建立deps依赖的空数组集
    this.deps=[];
  }
  addDep(watcher){
  //每监听到一个依赖,就将依赖传入deps
    this.deps.push(watcher)
  }
  notify(){
  //提示更新的方法,如果有变化发生,就将deps依赖数组进行遍历更新
    this.deps.forEach(dep=>dep.updata())
  }
}
//保存ui中依赖,实现update函数可以更新
class Watcher {
//构造函数中传入三个参数:实例,属性名,回调函数
  constructor(vm, key,cb) {
    this.vm = vm;
    this.key = key
    this.cb=cb
    //将当前实例指向Dep.target
    Dep.target = this
    this.vm[this.key];
    Dep.target =null;
  }
  updata() {
    
    console.log(`${this.key}更新了,${this.vm}`)
    //回调函数的更新
    this.cb.call(this.vm,this.vm[this.key])
    
  }
}

编译器compile

class Compile {
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);
    if (this.$el) {
      //1.$el中的内容都搬家到一个fragment,提高操作效率
      this.$fragment = this.node2Fragment(this.$el);
      // console.log(this.$fragment)
      //2.编译fragment
      this.compile(this.$fragment)
      // console.log(this.$fragment)

      //3.讲编译结果追加到宿主中
      this.$el.appendChild(this.$fragment)
    }

  }
  //遍历el,将里面的内容搬到新创建的fragment
  node2Fragment(el) {
    const fragment = document.createDocumentFragment();
    let child;
      //判断当前文档还有没有元素,有的话会一直执行
    while ((child = el.firstChild)) {
        //appendChild会将子元素移入fragment
      fragment.appendChild(child);
    }
    return fragment
  }
  compile(el) {
    //遍历el
    const childNodes = el.childNodes;
    console.log(childNodes)
      //对当前实例的每一个子节点进行遍历
    Array.from(childNodes).forEach(node => {
	//判断当前元素类型
      if (this.isElement(node)) {
        // console.log('编译元素:'+node.nodeName)
          //判断是否是带有指令k- 或者@的方法
        this.compileElement(node)
      } 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) {
    const exp = RegExp.$1
    //进行更新,传入当前节点,当前实例对象,立即双括号中的内容
    this.update(node, this.$vm, exp, 'text')
  }
    //对方法进行抽离,传入节点,实例,内容,以及类型
  update(node, vm, exp, dir) {
  //根据传入的类型进行组合方法名
    const fn = this[dir + 'Updator']
    //如果方法存在则执行方法,传入当前节点,以及内容
    fn && fn(node, vm[exp])
      //监听当前实力上这个节点的变化,如果改变了,重新执行更新方法
    new Watcher(vm, exp,function(){
      fn && fn(node, vm[exp])
    })
  }
   
    //判断指令的方法并进行对应指令操作
  compileElement(node){
      //将当前节点的所有属性存储在nodeAttr上
    const nodeAttrs=node.attributes;
    Array.from(nodeAttrs).forEach(attr=>{
        //对节点元素属性进行遍历,属性名称为attrName,属性值为exp
      const attrName=attr.name;
      const exp=attr.value;
      if(attrName.indexOf('k-')===0){
        const dir=attrName.substring(2)
        //如果节点属性名称是以k-开头,则执行k-后面的指令名称的方法
        this[dir]&&this[dir](node,this.$vm,exp)
      }else if(attrName.indexOf('@'===0)){
         //如果节点属性名称是以@开头,则执行@后面的指令名称的方法 
        const eventName=attrName.substring(1)
        this.eventHandle(node,this.$vm,exp,eventName)
      }
    })
  }
    //k-text文本更新
  text(node,vm,exp){
    this.update(node,vm,exp,'text')
  }
    //双向数据绑定
  model(node,vm,exp){
    this.update(node,vm,exp,'model')
    node.addEventListener('input',e=>{
      vm[exp]=e.target.value
    })
  }
    //k-html
  html(node,vm,exp){
    this.update(node,vm,exp,'html')
  }
  
   //text文本更新方法
  textUpdator(node, value) {
    node.textContent = value
  }
    //html更新方法
  htmlUpdator(node, value) {
    console.log(value)
    node.innerHTML = value
  }
    //model更新方法
  modelUpdator(node, value) {
    console.log(value)
    node.value = value
  }
  eventHandle(node,vm,exp,eventName){
    const fn=vm.$options.methods&&vm.$options.methods[exp]
    if(eventName&&fn){
      node.addEventListener(eventName,fn.bind(vm))
    }
  }
}

写到这里我们可以通过自己编写的vue源码实现以下的案例

 <div id="app">
    <p>{{name}}</p>
    <p k-text="name"></p>
    <p>{{age}}</p>
    <p>{{doubleAge}}</p>
    <input type="text" k-model="name"/>
    <button @click="changeName">呵呵</button>
    <div k-html="html"></div>
  </div>
  <script src="./compile.js"></script>
  <script src="./kvue.js"></script>
  <script>
    const kaideba=new KVue({
      el:'#app',
      data:{
        name:'I am test',
        age:12,
        html:'<button>这是一个按钮</button>'
      },
      created(){
        console.log('开始');
        setTimeout(()=>{
          this.name='我是测试'
        },1000)
      },
      methods:{
        changeName(){
          this.name='hello',
          this.age=1
        }
      }
    })
  </script>

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值