Vue数据响应式、模板解析的实现原理(实现一个简易的Vue)

实现步骤(实现我们自己的Vue—MyVue)

github地址:https://github.com/Rawsdom/my-vue-demo.git

  • MyVue(主入口的构造函数)
  • Observer(用于实现数据响应化)
  • Compile(解析模板、创建 Watcher、保存更新函数)
  • Watcher(执行更新函数、被 Dep 收集)
  • Dep(管理 Watcher、一旦数据更新就通知相应的watcher更新视图)
一、MyVue的构造函数
 new MyVue({
        el: '#app',
        data: {
          name: 'hello world!',       
        }
      });

(PS:MyVue构造函数接收上方这些参数并保存到实例中,并将实例往下传递)

  1. 把 data 使用 observe 进行递归数据劫持
  2. 使用代理方式处理传入的 data(方便在实例中可以直接 this.name 就获取或修改 data 中的值)
  3. 把 el 传到模板解析中

class MyVue {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
    // 数据劫持 (数据响应化处理)
    observe(this.$data);
    // 数据代理
    proxy(this, "$data");
    // 模板解析 (传递 this 实例)
    new Compiler(options.el, this);
  }
}
二、Dep 和 Watcher 说明
  • 数据响应化的定义是页面绑定的某个数据,数据改变,同时页面也会自动更新
  • 在上方,我们把模板解析和数据响应分成2个方法去执行。但是他们相互之间没有关联的话怎么去实现页面和数据同步呢?
  • 我们就需要借助 Dep 和 Watcher 实现一个订阅、发布模式(观察者模式)来实现他们之间的关联
  • Dep相当于一个容器,在对数据劫持时生成的,每一个Dep对应一个属性
  • addDep方法是用来收集 Watcher
  • notify方法是用来调用所有的 Watcher 中的更新方法来达到页面的更新
class Dep {
  constructor() {
    this.deps = [];
  }

  addDep(dep) {
    this.deps.push(dep);
  }
  notify() {
    this.deps.forEach((dep) => dep.update());
  }
}
  • Watcher 生成在编译解析中,一旦解析到页面有数据(如:{{ name }}) 就会生成一个Watcher
  • 在生成的同时被Dep收集,这里由于Dep是在数据劫持中生成的,我们只需将当前的 Watcher 实例保存到一个变量中,再触发对应的dep劫持的属性的 get 回调 即可收集,收集完再将变量置为null
  • 由于Vue源码也是用Dep.target的保存Watcher实例,我这边也一样参照源码,其实是可以用全局变量也是可以的
  • (ps:这边需要理解一下Object.defineProperty()这个劫持的api才能更好理解)
  • (ps:因为每一个属性劫持都会生成一个Dep,只要this.name访问一下就会触发劫持的 get 回调,然后再 get 回调中判断是否有这个变量,有即调用Dep里的addDep()方法即可收集到Dep中)
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm;
    this.key = key;
    this.updateFn = updateFn;
	//  被Dep收集过程
    Dep.target = this;
    this.vm[this.key];
    Dep.target = null;
  }

  update() {
  //更新函数的调用, 由于更新函数内部可能需要this实例, 所以用call方法改变一下调用 this 指向
    this.updateFn.call(this.vm, this.vm[this.key]);
  }
}
三、数据响应式 (数据劫持)
  • 紧接 MyVue 的构造函数调用 observe() 方法
  • 类型判断处理,同时也是递归点
function observe(obj) {
  if (typeof obj !== "object" || obj === null) return;

  new Observer(obj);
}
  • 新建 Observer 实例
  • 因为上方 observe() 方法的类型判断唔法判断 array 和 object 在使用Array原型上的isArray()方法去判断
  • 因为数组原型上有7个原型方法能修改数组的值,所以需要复写数组的7个方法,所以这里需要区分对象和数组2个不同类型的处理
  • (ps:typeof === ‘object’ 判断 object 和 array 都是为 true 的)
  • (ps:重写数组原型的方法目的是在执行完数组操作后要通知视图更新以达到响应式,所以要执行原原型链上的方法,并且还要通知 dep 更新视图)
class Observer {
  constructor(obj) {
    this.obj = obj;
    if (typeof obj === "object") {
      Array.isArray(obj) ? this.walkArr(obj) : this.walk(obj);
    }
  }
 // 对象数据响应化
  walk(obj) {
    Object.keys(obj).forEach((key) => {
    //  这里可以理解为取出对象的键值并对obj的key做劫持
    //  下方会有defineReactive()方法
      defineReactive(obj, key, obj[key]);
    });
  }
  // 数组数据响应化
  walkArr(arr) {
  //	覆盖原型的7个方法
    arr.__proto__ = this.arrayProto;
	// 这里取出的键值其实是数组的下标
    const keys = Object.keys(arr);
    for (let i = 0; i < keys.length; i++) {
    //	以arr的下标做数据劫持
      defineReactive(arr, i, arr[i])
    }
  }
}
//  Observer的类原型保存新的数组原型方法(如:push等)用于数组响应化时覆盖数组的原型方法
const defaultProto = Array.prototype;
//	深拷贝数组的原型链方法
const arrayProto = Object.create(defaultProto);
//  对以下的方法做重写。并生成新的数组原型方法
["push", "pop", "shift", "unshift"].forEach(
  method => {
    arrayProto[method] = function () {
    //	默认原型链上的对应的方法还是需要执行,该 push 就还是要 push
      defaultProto[method].apply(this, arguments);
      // 这里的notify() 会在下方 defineReactive() 时保存到该数据的原型链上从而在此处可以调用
      this.notify()
    };
  }
);
//  保存到 Observer 原型链上
Observer.prototype.arrayProto = arrayProto

defineReactive() 方法

  • 开启递归,如果val是对象就需要继续往下递归,如果不是的话上面说的递归点就起作用,直接就返回到这里
  • 因为当前的属性是唯一的,同时在这新建的dep也是和该属性一一对应,也是唯一的
  • 在对数组做响应化时需要利用闭包保存当前的dep,并在该数据的原型上添加一个notify方法,因为push操作调用者肯定是属性本身(如:this.name.push(xxx)),所以上方在push等操作的时候也就可以使用当前添加的notify()方法从而实现通知视图更新
function defineReactive(obj, key, val) {
  // 递归
  observe(val);

  // 创建一个Dep和当前key 一一对应
  const dep = new Dep()
  // 利用原型链把dep的notify方法保存起来,数组使用push的方法就可以直接通知修改数组的值
  if(Array.isArray(obj[key])){
    	obj[key].__proto__.notify = function () {
      	dep.notify()
    }
  }
  
  

  Object.defineProperty(obj, key, {
    get() {
      // 依赖收集在这里 
      // 这里是配合 Watcher 的构造方法里所触发的访问(get)后,进行收集
      // 这里触发只会在new Watcher实例的时候触发,因为它一新建就被收集,他被收集完就立即置空
      Dep.target && dep.addDep(Dep.target)

      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        // 如果更新数据的是object或者array则需要重新对此做劫持
        observe(newVal);
        val = newVal;

        //  通知更新
        dep.notify()
      }

    },
  });
}
四、数据代理
  • 这里紧接 MyVue 构造函数的 proxy() 方法调用
  • 这里再一次利用 Object.defineProperty() 实现数据代理,其实就是将this.name指向this.$data.name,也就是差不多映射的道理
  • this.$data 数据做响应化也要数据劫持,所以我们通常的 this.name 的方式其实是做了两层数据劫持
//  代理			this,$data
function proxy(vm, sourceKey) {
			// this.$data			key: name
  Object.keys(vm[sourceKey]).forEach((key) => {
  			//			this, name
    Object.defineProperty(vm, key, {
      get() {
      //		this.$data.name
        return vm[sourceKey][key];
      },
      set(newVal) {
      //	this.$data.name = newVal
        vm[sourceKey][key] = newVal;
      },
    });
  });
}
五、模板解析
  • 递归遍历dom树
  • 判断节点类型, 如果是文本,则判断是否是插值绑定
  • 如果是元素,则遍历其属性判断是否是指令(如:my-html、my-text等)或事件(如:@click="xxx"等),然后递归子元素
class Compiler{
  // el 是宿主元素
  // vm 是 MyVue 实例
  constructor(el,vm){
    this.$vm = vm;
    this.$el = document.querySelector(el)

    if(this.$el){
      // 执行编译
      this.compile(this.$el)
    }

  }

  compile(el){
    // 遍历 el 树
    const childNodes = el.childNodes
    //  将类数组转换为数组
    Array.from(childNodes).forEach(node => {
      // 判断是否是元素
      if(this.isElement(node)){
      	//  解析元素
        this.compileElement(node)
      }else if(this.isInter(node)){
      	//	解析文本
        this.compileText(node)
      }

      // 递归子节点
      if(node.childNodes && node.childNodes.length > 0){
        this.compile(node)
      }

    })
  }
	// node.nodeType 1:元素 2:属性 3: 文本
  isElement(node) {
    return node.nodeType === 1
  }

  isInter(node){
    // 类型是文本同时也符合插值表达式双大或号包裹 如:{{xxx}}
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }

  compileText(node){
	//  RegExp 是js的内置对象,$1是匹配正则表达式的匹配正确的第一个
	//  由于上方判断如果通过就会立刻调用当前的编译文本的方法所以也就是正则匹配的第一个
	//	如 {{ name }} 这里得出的是 '空格'+name+'空格' 所以要trim()去除前后空格
    this.update(node, RegExp.$1.trim(), 'text')
  }

  compileElement(node){
    // 节点是元素
    // 遍历其属下列表
    const nodeAttrs = node.attributes
    Array.from(nodeAttrs).forEach(attr => {
      // 规定: 指令以 my-text="xxx" 定义
      const attrName = attr.name  // my-text
      const exp = attr.value // xxx
      //	判断是否是指令
      if(this.isDirective(attrName)){
      	//	把 my-text 处理成 text
        const dir = attrName.substring(3)
        //  执行指令 this.text(node, exp)
        this[dir] && this[dir](node, exp)
      }
      // 判断是否是事件
      if(this.isEvent(attrName)){
        // 如 @click='onClick'
        // dir: click 上方定义好的 exp: onClick
        const dir = attrName.substring(1)
        // 事件的处理
        this.eventHandler(node, exp, dir)
      }
    })
  }
//  指令判断
  isDirective(attr){
    return attr.indexOf('my-') === 0
  }
//  事件判断
  isEvent(dir){
    return dir.indexOf('@') === 0
  }
//  更新方法(这里就是new Watcher 实例的地方,这里也就是完成模板解析和数据响应化之间的关联的地方)
//  update() 方法相当于一个枢纽,在解析到指令或者插值表达式就会调用相应的更新方法(替换视图上匹配的插值或指令)
//	同时生成一个 Watcher 去保存更新函数(用于之后数据变化时更新视图上的插值或指令)
  update(node, exp, dir){
    // 指令对应更新函数 xxxUpdater 如:textUpdater()
    const fn = this[dir + 'Updater']
    fn && fn(node, this.$vm[exp])

    // 新建 Watcher 实例并保存该更新方法,一旦数据变化就会通知 dep,dep也就通知Watcher并调用下方的更新方法
    new Watcher(this.$vm, exp ,function(val) {
      fn && fn(node, val)
    })
  }
// 更新函数 (下面使用的是原生的node节点更新里的内容)
//	textContent: 文本  innerHTML: 识别并解析html标签如:(

你好

)value: 用于input 输入框的 value 属性
textUpdater(node, value){ node.textContent = value } htmlUpdater(node, value){ node.innerHTML = value } modelUpdater(node, value) { node.value = value } // 指令解析 text(node, exp){ this.update(node, exp, 'text') } html(node, exp){ this.update(node, exp, 'html') } model(node, exp) this.update(node, exp, 'model') // 事件监听 这里仅仅只是对input做的v-model所以监听的是input输入事件 node.addEventListener('input', (e) =>{ // 新的赋值给数据即可 this.$vm[exp] = e.target.value }) } // 事件处理 eventHandler(node, exp, dir){ // 从MyVue实例中获取 methods里的方法并匹配其中的方法 const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp] // 原生的事件监听 node.addEventListener(dir, fn.bind(this.$vm)) } }
六、整个简易Vue的代码

index.js

// 创建My Vue构造函数

function defineReactive(obj, key, val) {
  // 递归
  observe(val);

  // 创建一个Dep和当前key 一一对应
  const dep = new Dep()

  // 利用原型链把dep的notify方法保存起来,数组等使用push的方法就可以直接通知修改数组的值
  if(Array.isArray(obj[key])){
    obj[key].__proto__.notify = function () {
      dep.notify()
    }
  }


  Object.defineProperty(obj, key, {
    get() {
      // 依赖收集在这里
      Dep.target && dep.addDep(Dep.target)

      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        // 如果传入的是新object需要再经过数据劫持
        observe(newVal);
        val = newVal;

        //  通知更新
        dep.notify()
      }

    },
  });
}

function observe(obj) {
  if(typeof obj !== 'object' && obj !== null){
    return
  }

  new Observer(obj)
}

// 代理函数,方便用户直接访问$data中的数据
function proxy(vm, sourceKey) {
  Object.keys(vm[sourceKey]).forEach(key => {
    Object.defineProperty(vm, key, {
      get(){
        return vm[sourceKey][key];
      },
      set(newVal) {
        vm[sourceKey][key] = newVal;
      }
    })
  })
}

class MyVue {
  constructor(options) {
    // 保存选项
    this.$options = options
    this.$data = options.data

    // 响应式处理
    observe(this.$data)

    // 代理
    proxy(this, '$data')

    // 创建编译器
    new Compiler(options.el, this)

  }


}

// 根据对象类型决定如何做响应化

class Observer {
  constructor(obj) {
    this.obj = obj;
    if (typeof obj === "object") {
      Array.isArray(obj) ? this.walkArr(obj) : this.walk(obj);
    }
  }
 // 对象数据响应化
  walk(obj) {
    Object.keys(obj).forEach((key) => {
      defineReactive(obj, key, obj[key]);
    });
  }
  // 数组数据响应化
  walkArr(arr) {
    arr.__proto__ = this.arrayProto;

    const keys = Object.keys(arr);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(arr, i, arr[i])
    }
  }
}


const defaultProto = Array.prototype;
const arrayProto = Object.create(defaultProto);
["push", "pop", "shift", "unshift"].forEach(
  method => {
    arrayProto[method] = function () {
      defaultProto[method].apply(this, arguments);
      this.notify()
    };
  }
);
Observer.prototype.arrayProto = arrayProto


// 观察者: 保存更新函数, 值发生变化调用更新函数
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm;

    this.key = key;

    this.updateFn = updateFn;

    // Dep.target 静态属性上设置为当前的 watcher 实例
    Dep.target = this
    this.vm[this.key]   // 读取触发了 getter
    Dep.target = null   // 收集完就置空
  }

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


// Dep: 依赖,管理某个key相关所有的 Watcher 实例

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

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

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

compile.js

// 编译器

class Compiler {
  // el 是宿主元素
  // vm 是 MyVue 实例
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);

    if (this.$el) {
      // 执行编译
      this.compile(this.$el);
    }
  }

  compile(el) {
    // 遍历 el 树
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach((node) => {
      // 判断是否是元素
      if (this.isElement(node)) {
        this.compileElement(node);
      } else if (this.isInter(node)) {
        this.compileText(node);
      }

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

  isElement(node) {
    return node.nodeType === 1;
  }

  isInter(node) {
    // 首先是文本标签, 其次内容是 {{xxx}}
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
  }

  compileText(node) {
    // node.textContent = this.$vm[RegExp.$1.trim()]
    this.update(node, RegExp.$1.trim(), "text");
  }

  compileElement(node) {
    // 节点是元素
    // 遍历其属下列表
    const nodeAttrs = node.attributes;
    Array.from(nodeAttrs).forEach((attr) => {
      // 规定: 指令以 my-xx="yyy" 定义
      const attrName = attr.name; // xx
      const exp = attr.value; // yyy
      if (this.isDirective(attrName)) {
        const dir = attrName.substring(3);
        //  执行指令
        this[dir] && this[dir](node, exp);
      }

      // 事件处理
      if (this.isEvent(attrName)) {
        // @click='onClick'
        const dir = attrName.substring(1);
        // 事件监听
        this.eventHandler(node, exp, dir);
      }
    });
  }

  isDirective(attr) {
    return attr.indexOf("my-") === 0;
  }

  isEvent(dir) {
    return dir.indexOf("@") === 0;
  }

  update(node, exp, dir) {
    // 初始化
    // 指令对应更新函数xxUpdater
    const fn = this[dir + "Updater"];
    fn && fn(node, this.$vm[exp]);

    // 更新处理
    new Watcher(this.$vm, exp, function (val) {
      fn && fn(node, val);
    });
  }

  textUpdater(node, value) {
    node.textContent = value;
  }

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

  modelUpdater(node, value) {
    node.value = value;
  }

  // my-text
  text(node, exp) {
    // node.textContent = this.$vm[exp]
    this.update(node, exp, "text");
  }

  // my-html
  html(node, exp) {
    this.update(node, exp, "html");
  }

  // my-model
  model(node, exp) {
    // update 方法只完成赋值和更新
    this.update(node, exp, "model");
    // 事件监听
    node.addEventListener("input", (e) => {
      // 新的赋值给数据即可
      this.$vm[exp] = e.target.value;
    });
  }

  eventHandler(node, exp, dir) {
    const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];
    node.addEventListener(dir, fn.bind(this.$vm));
  }
}

index.html


<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Documenttitle>
  head>
  <body>
    <div id="app">
      <p @click="add">{{ counter }}p>
      <p my-text="counter">p>
      <p my-html="desc">p>
      <input type="text" my-model="name" />
      <p>{{name}}p>
      <p>{{arr}}p>
    div>

    <script src="./index.js">script>
    <script src="./compile.js">script>
    <script>
      const app = new MyVue({
        el: "#app",
        data: {
          counter: 0,
          desc: "

这是描述

"
, name: "123", arr: [1, 3, 4, 5], }, methods: { add() { this.counter++; }, }, }); setTimeout(() => { app.arr.push(9); }, 1000);
script> body> html>

个人的一个学习经历,希望能帮到你,如果有大佬看出有优化的点可以指出,可以相互学习
ps:这个仅仅只是简易的Vue只为了更好的读源码,会有一些与源码不一样的

你可能感兴趣的:(vue.js,javascript)