简单实现手写vue以及分析初始化流程

178 阅读3分钟

背景

vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图,接下来实现过程

kvue.js


 // 数据响应式
function defineReactive(obj, key, val) {
 // 递归
 observe(val);

 // 创建一个对应的Dep实例
 const dep = new Dep()
 
 Object.defineProperty(obj, key, {
   get() {
     console.log("get", key);

     // 依赖收集
     Dep.target && dep.addDep(Dep.target)
     
     return val;
   },
   set(newVal) {
     if (newVal !== val) {
       observe(newVal);

       console.log("set", key);
       val = newVal;
       // update()
       dep.notify()
     }
   },
 });
}

// 递归遍历方法
function observe(obj) {
 if (typeof obj !== "object" || obj === null) {
   return;
 }

 // 创建Observer实例
 new Observer(obj);
}

// 响应式对象中的某个key只要它的值是一个对象就要创建一个Observer实例
class Observer {
 // 根据传入对象的类型做不同的响应式处理
 constructor(obj) {
   if (Array.isArray(obj)) {
     // todo
   } else {
     // 对象响应式
     this.walk(obj);
   }
 }

 walk(obj) {
   Object.keys(obj).forEach((key) => {
     defineReactive(obj, key, obj[key]);
   });
 }
}

function proxy(vm) {
 Object.keys(vm.$data).forEach((key) => {
   Object.defineProperty(vm, key, {
     get() {
       return vm.$data[key];
     },
     set(v) {
       vm.$data[key] = v;
     },
   });
 });
}
class KVue {
 // new KVue({el, data})
 constructor(options) {
   this.$options = options;

   this.$data = options.data;

   // 1.对data做响应式处理
   observe(this.$data);

   // 1.5 代理
   proxy(this);
   // 2.编译
   new Compile(options.el, this);
 }
}

class Compile {
 constructor(el, vm) {
   this.$vm = vm;

   this.$el = document.querySelector(el);

   // 遍历宿主元素
   if (this.$el) {
     this.compile(this.$el);
   }
 }

 compile(el) {
   // 递归遍历根元素
   el.childNodes.forEach((node) => {
     if (this.isElm(node)) {
       // console.log("编译元素", node.nodeName);
       this.compileElement(node);
     } else if (this.isInter(node)) {
       // console.log('编译插值文本', node.textContent);
       this.compileText(node);
     }

     // 递归
     if (node.childNodes.length > 0) {
       this.compile(node);
     }
   });
 }

 // 元素判断
 isElm(node) {
   return node.nodeType === 1;
 }

 // 插值判断
 isInter(node) {
   return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
 }

 // {{ooxx}} => ooxx => this.$vm.ooxx
 compileText(node) {
   this.update(node, RegExp.$1, 'text')
 }

 // 元素的编译
 compileElement(node) {
   // 遍历所有属性:检查是否存在指令和事件
   const attrs = node.attributes;
   Array.from(attrs).forEach((attr) => {
     // k-text="counter"
     const attrName = attr.name; // k-text
     const exp = attr.value; // counter

     // 只处理动态值
     // 指令-directive
     if (this.isDir(attrName)) {
       // 希望执行一个指令处理函数
       const dir = attrName.substring(2);
       this[dir] && this[dir](node, exp);
     }
   });
 }


 update(node, exp, dir) {
   // 1.初始化
   // 执行dir对应的实操函数
   const fn = this[dir+'Updater']
   fn && fn(node, this.$vm[exp])
   
   // 2.创建Watcher实例
   new Watcher(this.$vm, exp, function (val) {
     fn && fn(node, val)
   })
 }
 
 isDir(attrName) {
   return attrName.startsWith("k-");
 }

 // k-text
 text(node, exp) {
   this.update(node, exp, 'text')
 }

 textUpdater(node, val) {
   node.textContent = val;
 }
 //k-model
 model(node, exp) {
  this.update(node, exp, 'model')
  //事件监听
  node.addEventListener('input',e=>{
     this.$vm[exp] = e.target.value
  })
}

modelUpdater(node, val) {
  node.value = val;
}
 
 // k-html
 html(node, exp) {
   this.update(node, exp, 'html')
 }

 htmlUpdater(node, val) {
   node.innerHTML = val;
 }
}

// 更新执行者Watcher
class Watcher {
 constructor(vm, key, updater) {
   this.vm = vm
   this.key = key
   this.updater = updater

   // 保存Watcher引用
   Dep.target = this
   this.vm[this.key]
   Dep.target = null
 }

 update() {
   this.updater.call(this.vm, this.vm[this.key])
 }
}

class Dep {
 constructor() {
   this.deps = []
 }

 addDep(dep) {
   this.deps.push(dep)
 }

 notify() {
   this.deps.forEach(w => w.update())
 }
}

khtml

<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">

<div id="app">
  <p @click="add">{{counter}}</p>
  <p k-text="counter"></p>
  <p k-html="desc"></p>
  <input type="text" k-model="content">
  <h2 k-html="content"></h2>
</div>
<script src="./kvue.js"></script>
<script>
  const app = new KVue({
    el: '#app',
    data: {
      counter: 1,
      desc: '等待·<span style="color: red">真棒</span>',
      content:'测试文案'
    },
    methods: {
      add() {
        this.counter++
      }
    },
  })
   setInterval(() => {
    app.counter++
    // app.$data.counter++
  }, 1000);
</script>

vue 是如何对数组方法进行变异的

源码

import { def } from "../util/index";

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);

const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method];
  //定义新的方法
  def(arrayMethods, method, function mutator(...args) {
    // args 传入的参数  Arr.push(33) args就是[33]
    const result = original.apply(this, args);
    const ob = this.__ob__;
    console.log(ob,args);
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
        break;
    }
    if (inserted) ob.observeArray(inserted);
    // notify change
    ob.dep.notify();
    return result;
  });
});



// 在其他文件里 ,全局搜索就能看出来
// Observer.prototype.observeArray = function observeArray(items) {
//   for (var i = 0, l = items.length; i < l; i++) {
//     observe(items[i]);
//   }
// };

简单来说,Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知 render watcher,执行 update

vue初始化流程

入口 platforms/web/entry-runtime-with-compiler.js
扩展默认$mount方法:处理template或el选项

platforms/web/runtime/index.js

  • 安装web平台特有指令和组件
  • 定义__patch__:补丁函数,执行patching算法进行更新
  • 定义$mount:挂载vue实例到指定宿主元素(获得dom并替换宿主元素)

core/index.js
初始化全局api 具体如下:

  • Vue.set = set
  • Vue.delete = del
  • Vue.nextTick = nextTick
  • initUse(Vue) // 实现Vue.use函数
  • initMixin(Vue) // 实现Vue.mixin函数
  • initExtend(Vue) // 实现Vue.extend函数
  • initAssetRegisters(Vue) // 注册实现Vue.component/directive/filter

core/instance/index.js
Vue构造函数定义 定义Vue实例API

function Vue (options) { 
// 构造函数仅执行了_init this._init(options) 
}
initMixin(Vue) // 实现init函数 
stateMixin(Vue) // 状态相关api $data,$props,$set,$delete,$watch
eventsMixin(Vue)// 事件相关api $on,$once,$off,$emit 
lifecycleMixin(Vue) // 生命周期api _update,$forceUpdate,$destroy
renderMixin(Vue)// 渲染api _render,$nextTick

core/instance/init.js
创建组件实例,初始化其数据、属性、事件等

  • initLifecycle(vm) // parent,parent,root,children,children,refs
  • initEvents(vm) // 处理父组件传递的事件和回调
  • initRender(vm) // slots,slots,scopedSlots,_c,$createElement
  • callHook(vm, 'beforeCreate')
  • initInjections(vm) // 获取注入数据
  • initState(vm) // 初始化props,methods,data,computed,watch
  • initProvide(vm) // 提供数据注入
  • callHook(vm, 'created')

$mount

  • mountComponent 执行挂载,获取vdom并转换为dom
  • new Watcher() 创建组件渲染watcher
  • updateComponent() 执行初始化或更新
  • update() 初始化或更新,将传入vdom转换为dom,初始化时执行的是dom创建操作
  • render() src\core\instance\render.js 渲染组件,获取vdom

整体流程
new Vue() => _init() => $mount() => mountComponent() => new Watcher() => updateComponent() => render() => _update()