来自学渣的vue2原理理解

173 阅读4分钟

读懂此文的前置知识点: 递归、简单的正则、正则捕获组、nodetype

1. 模版替换

<div id="app">
    <h2>{{msg}}</h2>
    <h1>{{qwe}}</h1>
</div>
<script>
    let obj = {
      msg: 'Hello Vue!',
      qwe: 'qwer'
    }
    new Vue({
      el: '#app',
      data: obj
    })
</script>

先完成模版替换的功能,就是当我们new vue的时候,需要把模版里的内容替换成data里的值。所以接下来写一个compile函数

    class Vue {
      constructor(options) {
        this.el = options.el;
        this.data = options.data;
        this.compile(this.data);
      }
      compile(data) {
        // 正经的ast解析肯定不是这么简单的正则,而且一般不是用正则来做的
        const appDom = document.querySelector(this.el);
        const findElement = (dom) => {
          dom.childNodes.forEach(e => {
            // 1是元素节点
            if (e.nodeType === 1) {
              findElement(e)
            }
            // 3是文本节点
            if (e.nodeType === 3) {
              let textContent = e.textContent;
              const match = textContent.match(/\{\{(.*)\}\}/);
              if (match) {
                e.textContent = textContent.replace(match[0], data[match[1]]);
              }
            }
          })
        }
        findElement(appDom)
      }
    }
    let obj = {
      msg: 'Hello Vue!',
      qwe: 'qwer'
    }
    new Vue({
      el: '#app',
      data: obj
    })

这里有一个递归,终点是找到所有的文本节点,然后用正则去匹配{{}},然后进行文本替换。 可以在浏览器看下,我们已经成功实现了模版替换...

<!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>Document</title>
</head>

<body>
  <div id="app">
    <h2>{{msg}}</h2>
    <h1>{{qwe}}</h1>
  </div>
  <script>
    class Vue {
      constructor(options) {
        this.el = options.el;
        this.data = options.data;
        this.compile(this.data);
      }
      compile(data) {
        // 正经的ast解析肯定不是这么简单的正则,而且一般不是用正则来做的
        const appDom = document.querySelector(this.el);
        const findElement = (dom) => {
          dom.childNodes.forEach(e => {
            // 1是元素节点
            if (e.nodeType === 1) {
              findElement(e)
            }
            // 3是文本节点
            if (e.nodeType === 3) {
              let textContent = e.textContent;
              const match = textContent.match(/\{\{(.*)\}\}/);
              if (match) {
                e.textContent = textContent.replace(match[0], data[match[1]]);
              }
            }
          })
        }
        findElement(appDom)
      }
    }
    let obj = {
      msg: 'Hello Vue!',
      qwe: 'qwer'
    }
    new Vue({
      el: '#app',
      data: obj
    })
  </script>
</body>

</html>

2. 响应式

实现响应式的目标是当我们obj里的值变换后,模版也能更新为最新的值,其实就是两步

  1. 数据变化时候我知道
  2. 我知道去更新哪些东西(这个数据影响了哪里,然后我去更新他) 来实现第一步
    class Vue {
      constructor(options) {
        this.el = options.el;
        this.data = options.data;
        this.compile(this.data);
      }
      reactiveData(obj) {
        Object.keys(obj).forEach((key) => {
          // 这里是个闭包,foreach的回调是个function,get函数在这个function的内部声明,在外部触发get set时调用,形成闭包机制
          let val = obj[key];
          Object.defineProperty(obj, key, {
            get: () => {
              console.log(`get ${key}`);
              return val
            },
            set: (newVal) => {
              console.log(`set ${key}`);
              // set新值时,替换了闭包里的变量
              val = newVal;
            }
          })
        })
      }
    }

第二步的话,是有一个发布订阅模式,大概意思就是有一个双十一卖东西,有很多人都想买这个东西,于是提前订阅了,等到双十一开始,就会给每一个提前订阅的人发通知;对应到我们这里就是数据就是商品,用到这个数据的dom节点就是想买商品的人,等到数据变化时候需要去通知这些dom。接下来更新之前的响应式函数

  reactiveData(obj) {
    Object.keys(obj).forEach((key) => {
      // 这里是个闭包,foreach的回调是个function,get函数在这个function的内部声明,在外部出发get set时调用,形成闭包机制
      let val = obj[key];
      // 利用闭包,给每个key都存储一个各自的依赖列表
      let deps = [];
      Object.defineProperty(obj, key, {
        get: () => {
          console.log(`get ${key}`);
          if (window.callBack) {
            // 有人订阅了
            deps.push(callBack)
          }
          return val
        },
        set: (newVal) => {
          console.log(`set ${key}`);
          // set新值时,替换了闭包里的变量
          val = newVal;
          // 发布更新
          deps.forEach(e => e())
        }
      })
    })
  }

在哪里去添加订阅呢?哪个dom用到了哪个key就添加订阅,接下来更新之前的compile函数

  compile(data) {
    // 正经的ast解析肯定不是这么简单的正则,而且一般不是用正则来做的
    const appDom = document.querySelector(this.el);
    const findElement = (dom) => {
      dom.childNodes.forEach(e => {
        if (e.nodeType === 1) {
          findElement(e)
        }
        if (e.nodeType === 3) {
          let textContent = e.textContent;
          const match = textContent.match(/\{\{(.*)\}\}/);
          function callBack() {
            // 这个callbakc也是一个闭包,因此当callback被触发时,能回到当初定义时的作用域,更新对应的node
            e.textContent = textContent.replace(match[0], data[match[1]]);
          }
          // 把callback挂到window上,方便添加依赖
          window.callBack = callBack;
          if (match) {
            // 这里是一个对象get操作,会进入我们的defineproperty里
            e.textContent = textContent.replace(match[0], data[match[1]]);
          }
          window.callBack = null
        }
      })
    }
    findElement(appDom)
  }

到这里,我们的代码如下,去验证一下吧

<!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>Document</title>
</head>

<body>
  <div id="app">
    <h2>{{msg}}</h2>
    <h1>{{qwe}}</h1>
  </div>
  <button>change data</button>
  <script>
    const btn = document.querySelector('button');
    btn.addEventListener('click', function() {
      obj.qwe = 'zzzzzz'
    })
    class Vue {
      constructor(options) {
        this.el = options.el;
        this.data = options.data;
        
        this.reactiveData(this.data)
        this.compile(this.data);
      }
      compile(data) {
        // 正经的ast解析肯定不是这么简单的正则,而且一般不是用正则来做的
        const appDom = document.querySelector(this.el);
        const findElement = (dom) => {
          dom.childNodes.forEach(e => {
            if (e.nodeType === 1) {
              findElement(e)
            }
            if (e.nodeType === 3) {
              let textContent = e.textContent;
              const match = textContent.match(/\{\{(.*)\}\}/);
              function callBack() {
                // 这个callbakc也是一个闭包,因此当callback被触发时,能回到当初定义时的作用域,更新对应的node
                e.textContent = textContent.replace(match[0], data[match[1]]);
              }
              // 把callback挂到window上,方便添加依赖
              window.callBack = callBack;
              if (match) {
                // 这里是一个对象get操作,会进入我们的defineproperty里
                e.textContent = textContent.replace(match[0], data[match[1]]);
              }
              window.callBack = null
            }
          })
        }
        findElement(appDom)
      }
      reactiveData(obj) {
        Object.keys(obj).forEach((key) => {
          // 这里是个闭包,foreach的回调是个function,get函数在这个function的内部声明,在外部出发get set时调用,形成闭包机制
          let val = obj[key];
          // 利用闭包,给每个key都存储一个各自的依赖列表
          let deps = [];
          Object.defineProperty(obj, key, {
            get: () => {
              console.log(`get ${key}`);
              if (window.callBack) {
                // 有人订阅了
                deps.push(callBack)
              }
              return val
            },
            set: (newVal) => {
              console.log(`set ${key}`);
              // set新值时,替换了闭包里的变量
              val = newVal;
              // 发布更新
              deps.forEach(e => e())
            }
          })
        })
      }
    }

    let obj = {
      msg: 'Hello Vue!',
      qwe: 'qwer'
    }
    new Vue({
      el: '#app',
      data: obj
    })
  </script>
</body>

</html>

3. 嵌套对象的响应式

目前我们的对象只是一层的,多层的话可能不好使了

  <div id="app">
    <h2>{{msg}}</h2>
    <h1>{{qwe}}</h1>
    <h1>{{info.name}}</h1>
  </div>

改写响应式函数,如果值是object就再调用一次reactiveData函数

  reactiveData(obj) {
    Object.keys(obj).forEach((key) => {
      if (typeof obj[key] === 'object') {
        this.reactiveData(obj[key]);
      }
      // 这里是个闭包,foreach的回调是个function,get函数在这个function的内部声明,在外部出发get set时调用,形成闭包机制
      let val = obj[key];
      // 利用闭包,给每个key都存储一个各自的依赖列表
      let deps = [];
      Object.defineProperty(obj, key, {
        get: () => {
          console.log(`get ${key}`);
          if (window.callBack) {
            // 有人订阅了
            deps.push(callBack)
          }
          return val
        },
        set: (newVal) => {
          console.log(`set ${key}`);
          // set新值时,替换了闭包里的变量
          val = newVal;
          // 发布更新
          deps.forEach(e => e())
        }
      })
    })
  }

然后是模版部分,我们先写一个工具函数,如下:

function getValFromExpression(str, obj) {
  const keys = str.split('.');
  keys.forEach(key => {
    obj = obj[key]
  })
  return obj
}
getValFromExpression('info.name', {info: {name: 'w'}})

改写compile函数

  compile(data) {
    // 正经的ast解析肯定不是这么简单的正则,而且一般不是用正则来做的
    const appDom = document.querySelector(this.el);
    const findElement = (dom) => {
      dom.childNodes.forEach(e => {
        if (e.nodeType === 1) {
          findElement(e)
        }
        if (e.nodeType === 3) {
          let textContent = e.textContent;
          const match = textContent.match(/\{\{(.*)\}\}/);
          function callBack() {
            // 这个callbakc也是一个闭包,因此当callback被触发时,能回到当初定义时的作用域,更新对应的node
            e.textContent = textContent.replace(match[0], getValFromExpression(match[1],data));
          }
          // 把callback挂到window上,方便添加依赖
          window.callBack = callBack;
          if (match) {
            // 这里是一个对象get操作,会进入我们的defineproperty里
            e.textContent = textContent.replace(match[0], getValFromExpression(match[1],data));
          }
          window.callBack = null
        }
      })
    }
    findElement(appDom)
  }

好了,最终完整代码如下

<!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>Document</title>
</head>

<body>
  <div id="app">
    <h2>{{msg}}</h2>
    <h1>{{qwe}}</h1>
    <h1>{{info.name}}</h1>
  </div>
  <button>change data</button>
  <script>
    const btn = document.querySelector('button');
    btn.addEventListener('click', function() {
      obj.qwe = 'zzzzzz';
      obj.info.name = 'wwww'
    })
    class Vue {
      constructor(options) {
        this.el = options.el;
        this.data = options.data;
        
        this.reactiveData(this.data)
        this.compile(this.data);
      }
      compile(data) {
        // 正经的ast解析肯定不是这么简单的正则,而且一般不是用正则来做的
        const appDom = document.querySelector(this.el);
        const findElement = (dom) => {
          dom.childNodes.forEach(e => {
            if (e.nodeType === 1) {
              findElement(e)
            }
            if (e.nodeType === 3) {
              let textContent = e.textContent;
              const match = textContent.match(/\{\{(.*)\}\}/);
              function callBack() {
                // 这个callbakc也是一个闭包,因此当callback被触发时,能回到当初定义时的作用域,更新对应的node
                e.textContent = textContent.replace(match[0], getValFromExpression(match[1],data));
              }
              // 把callback挂到window上,方便添加依赖
              window.callBack = callBack;
              if (match) {
                // 这里是一个对象get操作,会进入我们的defineproperty里
                e.textContent = textContent.replace(match[0], getValFromExpression(match[1],data));
              }
              window.callBack = null
            }
          })
        }
        findElement(appDom)
      }
      reactiveData(obj) {
        Object.keys(obj).forEach((key) => {
          if (typeof obj[key] === 'object') {
            this.reactiveData(obj[key]);
          }
          // 这里是个闭包,foreach的回调是个function,get函数在这个function的内部声明,在外部出发get set时调用,形成闭包机制
          let val = obj[key];
          // 利用闭包,给每个key都存储一个各自的依赖列表
          let deps = [];
          Object.defineProperty(obj, key, {
            get: () => {
              console.log(`get ${key}`);
              if (window.callBack) {
                // 有人订阅了
                deps.push(callBack)
              }
              return val
            },
            set: (newVal) => {
              console.log(`set ${key}`);
              // set新值时,替换了闭包里的变量
              val = newVal;
              // 发布更新
              deps.forEach(e => e())
            }
          })
        })
      }
    }

    let obj = {
      msg: 'Hello Vue!',
      qwe: 'qwer',
      info: {
        name: 'w'
      }
    }
    new Vue({
      el: '#app',
      data: obj
    })

    function getValFromExpression(str, obj) {
      const keys = str.split('.');
      keys.forEach(key => {
        obj = obj[key]
      })
      return obj
    }
    getValFromExpression('info.name', {info: {name: 'w'}})
  </script>
</body>

</html>