# T【手写vue案例】

目标:数据响应式+模板解析+渲染

第一步:数据驱动,最初版的响应式视图更新,需要手动书写update方法,手动操作dom更新,框架就是为了解决用户去调用和编写update,而且下面的写法只能更改一个值,局限性太大

<div id="app">
  {{foo}}
</div>
<script>
  function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
      get() {
        // console.log('get', key);
        return val
      },
      set(v) {
        if (val !== v) {
          val = v
          update()
        }
      },
    })
  }

  function update() {
    app.innerText = obj.foo
  }
  
  const obj = {}
  defineReactive(obj, 'foo', 'foo')

  setInterval(() => {
    obj.foo = new Date().toLocaleTimeString()
  }, 1000)
</script>

第二步:①因为不能批量记录,所以需要实现,用observe方法遍历地给每一个obj.key添加拦截器监听;②同时,为了实现嵌套情况的情形,在步骤【0】额外调用一次observe处理可能出现的递归;③ 在obj.defineProperty中,当设置值的时候,可能不是obj.key=xxx形式,而是obj={key:xxx};所以在步骤【1】仍然需要对新值进行一次observe处理;动态添加的属性也需要去监听,vue中使用set,在这里也可以定义一个set方法,动态调用一次defineReactive函数增加一个监听步骤【2】

// 将传入的obj,动态设置一个key,它的值val
function defineReactive(obj, key, val) {
  // 递归
  observe(val)//【0】
  
  Object.defineProperty(obj, key, {
    get() {
      console.log('get', key);
      return val
    },
    set(v) {
      if (val !== v) {
        console.log('set', key);
        // 传入新值v可能还是对象
        observe(v)//【1】
        val = v
      }
    },
  })
}

// 递归遍历obj,动态拦截obj的所有key
function observe(obj) {
  if (typeof obj !== 'object' || obj == null) {
    return obj
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

// this.$set()
// Vue.set()
function set(obj, key, val) {
  defineReactive(obj, key, val)//【2】
}

const obj = {
  foo: 'foo',
  bar: 'bar',
  baz: {
    a: 1
  }
}
// defineReactive(obj, 'foo', 'foo')
observe(obj)

// obj.foo
// obj.foo = 'fooooooo'
// obj.baz.a
// obj.baz = { a: 10 }
// obj.baz.a
// obj.dong = 'dong'
// obj.dong
// set(obj, 'dong', 'dong')
// obj.dong
  • KVue:框架构造函数

  • Observer:执行数据响应化(分辨数据是对象还是数组)

  • Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)

  • Watcher:执行更新函数(更新dom)

  • Dep:管理多个Watcher,批量更新

  • 创建class,constructor传入需要的el,data,methods等参数

  • data代理到this上

A. 具体实现代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <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">change</button>
      <!-- html内容解析 -->
      <div k-html="html"></div>
	 <div>{{arr}}</div>
    </div>
    <script src="./compile.js"></script>
    <script src="./kvue.js"></script>

    <script>
      const k = new KVue({
        el: "#app",
        data: {
          name: "I am test.",
          age: 12,
          html: "<button>这是一个按钮</button>",
		  arr:[1,2,3,4]
        },
        created() {
          console.log("开始啦");
        },
        methods: {
          changeName() {
            this.name = "hello";
            this.age++
          }
        }
      });
    </script>
  </body>
</html>

//kvuejs
//(1)数据的监听
//(2)依赖的收集    
//       依赖收集的原因:当发现数据变化后,需要对页面进行更新
//       在程序收集之前需要对templete进行遍历收集,找出和数据有依赖的东西,收集保存下来,需要更新的时候进行处理
//       两个对象,一个是dep  和  watcher
class KVue {
   constructor(options) {
       this.$options = options
       //数据响应化
       this.$data = this.$options.data
       //遍历数据,所有的数据都进行get和set的遍历
       this.observe(this.$data) 
       new Compile(options.el, this)
       //created 执行
       if(options.created) {
            options.created.call(this)
       }
   }
   //对所有的数据进行观察和监听
   observe(value) {
       if(!value || typeof value !== 'object') {
           return
       }
       Object.keys(value).forEach(key => {
		   //响应式处理
           this.defineReactive(value,key,value[key])
           //代理data中的属性到vue实例上
           this.proxyData(key)
       })
   }
   //数据的响应化
   defineReactive(obj, key, val) { 
       this.observe(val)  //递归解决数据格式的问题,对象里面包含对象
        //模拟一下watcher的创建过程
		//每一个dep实例和data中每个key与一一对应关系
       const dep = new Dep()
	   //给object的每一个key定义拦截
       Object.defineProperty(obj, key, {
           get() {//如果是调用,直接返回val
			 //依赖收集
			 // console.log(`defineProperty-2-${key}`)
               Dep.target && dep.addDep(Dep.target)
              //  console.warn(`这个data.${key}的属性里添加了${dep.length()}个`)
               return val
           },
           set(newVal) {
               if(newVal === val) {
                   return
               }
               val = newVal   //key的值发生了变化,此时需要更新视图了
               dep.notify()
           }
       })
   }
   proxyData(key) {
        Object.defineProperty(this, key, {
            get() {
                //this[key]    相当于   this.$data[key]
                return this.$data[key]
            },
            set(newVal) {
                this.$data[key] = newVal
            }
        })
   }
}

//用来管理watcher 的,是一个观察者的的管家
class Dep {  
   constructor() {
       this.watchers= [] //存储若干个依赖,一个watcher对应一个属性
   }
   addDep(dep) {  //添加依赖的方法的
       this.watchers.push(dep)
	   // console.log(this.watchers)
   }
   length(){
    return this.watchers.length
   }
   notify() {  //通知所有的依赖,做更新
       this.watchers.forEach(item => {  //遍历所有的东西,进行更新
           // console.log(`notify-${item}-4`)
		   item.update()
       })
   }
}
//用来做调用更新的一个东西,保存data中的数值和页面的挂钩关系
class Watcher { 
   constructor(vm, key, cb) {
	   // console.log(`watcher-3-${key}`)
       this.vm = vm
       this.key = key
       this.cb = cb
       //将当前的Watcher实例指定到Dep静态的target  
	   //---> 每次new Watcher()时,都会给Dep.target赋予一个new Watcher()出来的对象,便于依赖的收集
       Dep.target = this
       // console.log(Dep.target)
       this.vm[this.key]    //触发getter,添加依赖
       Dep.target = null    //置空
   }
   update() {
       this.cb.call(this.vm, this.vm[this.key])
    }
}

//compile.js
//编译器
//   用法   new Compile(el, vm)    el 要遍历和分析的元素  vm指定现在vue的实例
class Compile {
    constructor(el, vm) {
        //el的可能是一个选择器,也可能是一个元素,需要分析 ---》这里不做探究
        //要遍历的宿主节点
		console.log(vm)//kvue的实例
        this.$el = document.querySelector(el);
        this.$vm = vm
        //判断el的存在,存在的话需要进行编译
        if(this.$el) {
            //转换内部的内容为片段
            this.$fragment = this.nodeToFragment(this.$el)
            //执行编译
            this.compile(this.$fragment)
            //将编译完的结果html追加到$el
            this.$el.appendChild(this.$fragment)
        }
    }
    //将宿主元素中的代码片段拿出来遍历 ----》 高效
    nodeToFragment(el) {
		
        const frag = document.createDocumentFragment()
        //将el中的所有的子元素搬家到frag中
        let child;
        while(child = el.firstChild) {
			
            frag.appendChild(child)  //移动节点
        }
		
        return frag
    }
    //编译过程   ---节点的类型 --》dom节点还是文本节点
    compile(el) {
        const childNode = el.childNodes;
        Array.from(childNode).forEach(item => {
            //节点的类型判断
            if(this.isElement(item)){
               //元素
             //  console.log('编译元素',item.nodeName)
                const nodeAttrs = item.attributes;
                //查找属性是k-,@,:
                Array.from(nodeAttrs).forEach(attr => { 
                    const attrName = attr.name  //属性名
                    const exp = attr.value       //属性值
                    if(this.isDirective(attrName)) {
                         //k-text
                         const dir = attrName.substring(2)
                         //普通指令   执行指令   text或者model
                         this[dir] && this[dir](item, this.$vm, exp)
                    }
                    if(this.isEvent(attrName)) {
                        const dir = attrName.substring(1)
                        this.eventHandler(item, this.$vm, exp, dir)
                    }
                })
            } else if(this.isInterpolation(item)) {
               //文本
               //console.log('编译文本',item.textContent)
               //编译文本
               this.compileText(item)
            }
            //递归子节点
            if(item.childNodes && item.childNodes.length > 0) {
                 this.compile(item)
            }
        })
    }
    isElement(node) {   //元素节点
		
        return node.nodeType === 1
    }
    //插值文本
    isInterpolation(node) {  //即是文本又是插值语
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
    }
    //指令
    isDirective(attr) {
	   
       return attr.indexOf('k-') == 0
    }
    //事件
    isEvent(attr) {
        return attr.indexOf('@') == 0
    }
    //  k-etxt
    text(node, vm, exp) {
        this.update(node, vm, exp, 'text')
    }
    // @的处理  item, this.$vm, exp, dir
    eventHandler(node, vm, exp, dir) {
        let fn = vm.$options.methods && vm.$options.methods[exp]
        if(dir && fn) {
            node.addEventListener(dir, fn.bind(vm))
        }
    }
    //双向绑定
    model(node, vm, exp) {
        this.update(node, vm, exp, 'model')
        node.addEventListener('input', e => {
            vm[exp] = e.target.value
        })
    }
    //双向绑定的更新
    modelUpdater(node, value) {
        //input的value
        node.value = value
    }
    //html
    html(node, vm, exp) {
        this.update(node, vm, exp, 'html')
        node.addEventListener('input', e => {
            vm[exp] = e.target.value
        })
    }
    //双向绑定的更新
    htmlUpdater(node, value) {
        //input的value
        node.innerHTML = value
    }
     //编译文本
    compileText(node) {
        // console.log(RegExp.$1)
        // node.textContent = this.$vm.$data[RegExp.$1]
        this.update(node, this.$vm, RegExp.$1, 'text')
    }
    //更新函数
    update(node, vm, exp, dir) { 
		// console.log("compile-update-1")
        //node -->节点    vm --> vue的实例   exp --> 表达式   dir --> 指令(更新的是文本还是指令)
        const updaterFun = this[dir+'Updater']
        //初始化
        updaterFun && updaterFun(node,vm[exp])
        //依赖收集
        new Watcher(vm, exp, (value) => {
            updaterFun && updaterFun(node,value)
        })
        
    }
    textUpdater(node, value) {
        node.textContent = value
    }
}

# 观察者模式

	// 观察者要直接订阅观察目标,观察目标一做出通知,观察者就要进行处理
	
	// 观察者集合
	class ObserverList {
	    constructor() {
	        this.list = [];
	    }
	    add(obj) {
			console.log(obj) //Observer {update: ƒ}
	        this.list.push(obj);
	    }
	    removeAt(index) {
	        this.list.splice(index, 1);
	    }
	    count() {
	        return this.list.length;
	    }
	    get(index) {
	        if (index < 0 || index >= this.count()) {
	            return;
	        }
	        return this.list[index];
	    }
	    indexOf(obj, start = 0) {
	        let pos = start;
	        while (pos < this.count()) {
	            if (this.list[pos] === obj) {
	                return pos;
	            }
	            pos++;
	        }
	        return -1;
	    }
	}
	// 观察者类
	class Observer {
	    constructor(fn) {
	        this.update = fn;
	    }
	}
	// 观察目标类
	class Subject {
	    constructor() {
	        this.observers = new ObserverList(); 
	    }
	    addObserver(observer) {
	        this.observers.add(observer);
	    }
	    removeObserver(observer) {
	        this.observers.removeAt(
	            this.observers.indexOf(observer)
	        );
	    }
	    notify(context) {
	        const count = this.observers.count();
	        for (let i = 0; i < count; ++i) {
	            this.observers.get(i).update(context);
	        }
	    }
	}
	const observer = new Observer((newval) => {
	    console.log(`A的最新值是${newval}`);
	})
	const subject = new Subject();
	subject.addObserver(observer);
	subject.notify('Hello, world');

# 引入vnode概念手写vue

<div id="app">{{counter}}</div>

<script src="kvue.js"></script>
<script>
  const app = new KVue({
    el: '#app',
    data: {
      counter: 1
    },
    render(h) {
      // const div = document.createElement('div')
      // div.textContent = this.counter
      // return div

      // return h('div', null, this.counter + '')
      return h('div', null, [
        h('p', null, this.counter + ''),
        h('p', null, this.counter * 2 + ''),
        h('p', null, this.counter * 3 + ''),
      ])
    }
  })
  setInterval(() => {
    app.counter++
  }, 1000);
</script>
// 实现KVue构造函数
function defineReactive(obj, key, val) {
  // 如果val是对象,需要递归处理之
  observe(val);
  // 管家创建
  const dep = new Dep();
  Object.defineProperty(obj, key, {
    get() {
      console.log("get", key);
      // 依赖收集
      Dep.target && dep.addDep(Dep.target);
      return val;
    },
    set(newVal) {
      if (val !== newVal) {
        // 如果newVal是对象,也要做响应式处理
        observe(newVal);
        val = newVal;
        console.log("set", key, newVal);
        // 通知更新
        dep.notify();
      }
    }
  });
}
// 遍历指定数据对象每个key,拦截他们
function observe(obj) {
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }
  // 每遇到一一个对象,就创建一个Observer实例
  // 创建个Observer实例去做拦截操作
  new Observer(obj);
}
// proxy代理函数:让用户可以直接访问data中的key
function proxy(vm, key) {
  Object.keys(vm[key]).forEach(k => {
    Object.defineProperty(vm, k, {
      get() {
        return vm[key][k];
      },
      set(v) {
        vm[key][k] = v;
      }
    });
  });
}
// 根据传入value类型做不同操作
class Observer {
  constructor(value) {
    this.value = value;
    // 判断一下value类型
    // 遍历对象
    this.walk(value);
  }
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key]);
    });
  }
}
class KVue {
  constructor(options) {
    // 0.保存options
    this.$options = options;
    this.$data = options.data;
    // 1.将data做响应式处理
    observe(this.$data);
    // 2.为$data做代理
    proxy(this, "$data");
    // 3.编译模板
    // new Compile('#app', this)

    // 如果设置el,则挂载
    if (options.el) {
      this.$mount(options.el)
    }
  }

  $mount(el) {
    // 0.获取宿主
    this.$el = document.querySelector(el)
    
    // 1.声明一个updateComponent
    const updateComponent = () => {
      // const el = this.$options.render.call(this)
      // const parent = this.$el.parentElement
      // parent.insertBefore(el, this.$el.nextSibling)
      // parent.removeChild(this.$el)
      // this.$el = el

      // vnode
      const vnode = this.$options.render.call(this, this.$createElement)
      this._update(vnode)
    }

    // 2.new Watcher
    new Watcher(this, updateComponent)

  }

  // 创建返回虚拟dom
  $createElement(tag, props, children) {
    return {tag, props, children}
  }

  // 将传入的vnode转换,如果初始化则创建,如果是更新则patch
  _update(vnode) {
    // 上次vnode
    const prevVnode = this._vnode

    if (!prevVnode) {
      // init
      this.__patch__(this.$el, vnode)
    } else {
      // patch
      this.__patch__(prevVnode, vnode)
    }

    this._vnode = vnode
  }

  __patch__(oldVnode, vnode) {
    // 判断oldVnode是否是真实dom
    if (oldVnode.nodeType) {
      const parent = oldVnode.parentElement
      const refElm = oldVnode.nextSibling
      const el = this.createElm(vnode)
      parent.insertBefore(el, refElm)
      parent.removeChild(oldVnode)
      // 保存vnode
      this._vnode = vnode
    } else {
      // 获取要更新的元素
      const el = vnode.el = oldVnode.el

      // 同层比较相同节点
      if (oldVnode.tag === vnode.tag) {
        // diff
        // props
        // children
        // 获取双方孩子
        const oldCh = oldVnode.children
        const newCh = vnode.children

        if (typeof newCh === 'string') {
          if (typeof oldCh === 'string') {
            // 文本更新
            if (newCh !== oldCh) {
              el.textContent = newCh
            }
          } else {
            // replace elements with text
            el.textContent = newCh
          }
        } else {
          if (typeof oldCh === 'string') {
            // replace text with elements
            // clear
            // 循环创建并追加
          } else {
            this.updateChildren(el, oldCh, newCh)
          }
        }
      } else {
        // replace
      }
    }
  }

  updateChildren(parentElm, oldCh, newCh) {
    // 这里暂且直接patch对应索引的两个节点
    const len = Math.min(oldCh.length, newCh.length)
    for (let i = 0; i < len; i++) {
    this.__patch__(oldCh[i], newCh[i])
     }
    // newCh若是更长的那个,说明有新增
    if (newCh.length > oldCh.length) {
    newCh.slice(len).forEach(child => {
    const el = this.createElm(child)
    parentElm.appendChild(el)
     })
     } else if (newCh.length < oldCh.length) {
    // oldCh若是更长的那个,说明有删减
    oldCh.slice(len).forEach(child => {
    parentElm.removeChild(child.el)
     })
     }
    }

  createElm(vnode) {
    const el = document.createElement(vnode.tag)

    // props

    // children
    if (vnode.children) {
      if (typeof vnode.children === 'string') {
        // 文本内容
        el.textContent = vnode.children
      } else {
        // 子节点递归
        vnode.children.forEach(vnode => {
          el.appendChild(this.createElm(vnode))
        })
      }
    }

    // 保存真实元素,更新时要用
    vnode.el = el
    
    // 返回创建的dom
    return el
  }
}
// 移除
// class Compile {}
class Watcher {
  constructor(vm, fn) {
    this.vm = vm;
    // this.key = key;
    this.getter = fn;
   
    this.get()
  }
  get(){
     // 依赖收集触发
     Dep.target = this;
     this.getter.call(this.vm)
     Dep.target = null;
  }

  update() {
    this.get()
  }
}
// 管家:和某个key,一一对应,管理多个秘书,数据更新时通知他们做更新工作
class Dep {
  constructor() {
    this.deps = new Set();
  }
  addDep(watcher) {
    this.deps.add(watcher);
  }
  notify() {
    this.deps.forEach(watcher => watcher.update());
  }
}

参考资料 (opens new window)

最后更新: 4/21/2024, 8:03:54 AM