These two days interview, more or less asked some Vue response type principle, asked my scalp tingling. Although I have written according to the articles on the Internet, understanding and being able to answer are two different things. so

  • The paper come zhongjue shallow, and must know this to practice

Handwritten responsive complete code

Vue2.x

Is the responsivity principle of vue2. X /3. X guaranteed? ⇲ continue with the extension.

Writing in the previous chapter was implemented

  • v-model
  • v-show
  • v-text
  • {{}}

Here is a simple handwritten response code for the complete VUE

Effect:

<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>Vue2.x</title>
  <style>
    .show-txt {
      opacity: 1;
      transition: all .5s linear;
    }
    .show-txt.hidden {
      opacity: 0;
      color: #eee;
    }
  </style>
</head>
Copy the code
<body>
  <div id="app">
    <input type="text" v-model="name" /><br/>
    <input id="male" name="sex" type="radio" v-model="sex" value="Male">
      <label for="male"></label>
    </input>
    <input id="female" name="sex" type="radio" v-model="sex" value="Female">
      <label for="female"></label>
    </input><br/>
    <input name="show" type="checkbox" v-model="show" checked>Whether to show</input>
    <div class="show-txt" v-show="show">Show text examples</div>
    <b>Name:</b><span>{{ name }}</span> <br/>
    <b>Gender:</b><span>{{ sex }}</span>
    <hr />
    <div v-text="text"></div>
  </div>
  <script>
    class Dep {
      constructor () {this.subs = []
      }
      addSub (sub){
        if(sub && sub.update) this.subs.push(sub)
      }
      notify(oldValue){
        this.subs.forEach(sub= > {
          sub.update(oldValue)
        })
      }
    }

    // Call update after data is updated
    class Watcher {
      constructor(vm, key, cb){
        this.vm = vm
        this.key = key
        this.cb = cb
        Dep.target = this
        this.oldVal = vm[key]
        Dep.target = null
      }
      update(oldValue){
        this.oldVal = oldValue;
        let newValue = this.vm.$data[this.key]
        if(newValue === this.oldVal) return;
        this.cb(newValue)
      }
    }

    // Parse the contents of the template.
    class Compiler {
      constructor(vm) {
        this.vm = vm;
        this.el = vm.$el;
        this.compile(this.el)
      }
      compile(el) {
        let childrenNodes = [...el.childNodes]
        childrenNodes.forEach(node= > {
          if(this.isTextNode(node)){
            this.compileText(node)
          }else if(this.isElementNode(node)) {
            this.compileElement(node)
          }
          if(node.childNodes && node.childNodes.length) this.compile(node)
        })
      }
      compileText(node){
        let reg  = / \ {\ {(. +?) \} \} /;
        let val = node.textContent
        if(reg.test(val)){
          let key = RegExp.$1.trim()
          const value = this.vm[key];
          node.textContent = val.replace(reg, value)
          new Watcher(this.vm, key, (newVal) = > {
            node.textContent = newVal
          })
        }
      }
      compileElement(node) {
        // https://developer.mozilla.org/zh-CN/docs/Web/API/Element/attributes! [...node.attributes].forEach(attr= > {
          let attrName = attr.name
          if(this.isDirective(attrName)){
            attrName = attrName.substring(2);
            let key = attr.value;
            this.update(node, key, attrName)
          }
        })
      }
      update(node, key, attrName) {
        let updateFn = this[attrName+'Update']
        updateFn && updateFn.call(this, node, key, this.vm[key])
      }
      // The text node will be re-rendered if the value is updated. The essence is to modify textContent with JS
      textUpdate(node, key, content ){
        node.textContent = content
        new Watcher(this.vm, key, newVal= > { node.textContent = newVal })
      }
      modelUpdate(node, key, value) {
        const typeAttr = node.getAttribute('type')
        if(typeAttr == "text") {
          node.value = value;
          new Watcher(this.vm, key, newVal= > { node.value = newVal})
          node.addEventListener('keyup'.() = > {
            this.vm.$data[key] = node.value
          })
        }
        else if(typeAttr === "radio") {
          new Watcher(this.vm, key, newVal= > { node.classList.add('class-'+newVal)})
          const nameAttr = node.getAttribute('name')
          // There is no need for watch,
          node.addEventListener('change'.(ev) = > {
            this.vm.$data[key] = ev.target.value
          })
        }else if(typeAttr === 'checkbox') {
          node.addEventListener('change'.(ev) = > {
            this.vm.$data[key] = ev.target.checked
          })
        }
      }
      showUpdate(node, key, value){
        const change = (val) = > { 
          constoperate = !! val ?'remove' : 'add';
          node.classList[operate]('hidden') 
        }
        change(value);
        new Watcher(this.vm, key, (newVal) = > { change(newVal) })
      }
      isDirective(attr) {
        return attr.startsWith('v-')}isTextNode(node){
        return node.nodeType === 3
      }
      isElementNode(node) {
        return node.nodeType === 1}}/* Turn data into a responsive object */
    class Observer {
      constructor(data) {
        this.walk(data);
      }
      walk(data) {
        if(! data ||typeofdata ! ='object') return;
        Object.keys(data).forEach(key= > {
          this.defineReactive(data, key, data[key]); })}defineReactive(obj, key, value) {
        // Recursively change the object's child properties to responsive
        this.walk(value);
        const self= this;
        let dep = new Dep()
        Object.defineProperty(obj, key, {
          enumerable: true.configurable: true.get(){
            Dep.target && dep.addSub(Dep.target)
            return value
          },
          set (newValue) {
            const oldval = obj[key]
            if(newValue === obj[key]) return;
            value = newValue;
            self.walk(newValue)
            // Update the view
            dep.notify(oldval)
          }
        })
      }
    }

    class Vue {
      constructor(options) {
        this.$options = options || {}
        this.$el = typeof options.el === 'string' ?
          document.querySelector(options.el) : options.el;
        this.$data = options.data;
        // Process attributes in data
        this._proxyData(this.$data);
        // Make data responsive
        new Observer(this.$data)  
        // Template compilation
        new Compiler(this)}// Register attributes in data to vUE
      _proxyData(data) {
        Object.keys(data).forEach(key= > {
          Object.defineProperty(this, key, {
            enumerable: true.configurable: true.get(){ return data[key]  },
            set (newValue) {
              if(newValue === data[key]) return; data[key] = newValue; }})})}}new Vue({
      el: '#app'.data: {
          name: 'ethan'.sex: 'male'.text: 'text'.show: true,}})</script>
</body>
Copy the code

Vue3.x

<body>
  <script>
      f1()
      function f1() {
          const targetMap = new WeakMap(a)/* targetMap = WeakMap{ target: Map{ key: Set[cb,..] . },... } The main purpose of using WeakMap Map here is to automatically remove the weight */ when adding 
          function track(target, key) {
              /* If activeEffect is null, do not execute the following judgment to avoid track */ for example console.log(person.name)
              if(! activeEffect.fn)return
              // depsMap: all dependencies of target (Set)
              let depsMap = targetMap.get(target)
              if(! depsMap) { targetMap.set(target, depsMap =new Map()}// dep: Set of dependencies for each key of a data item
              let dep = depsMap.get(key)
              if(! dep) { depsMap.set(key, dep =new Set())
              }
              dep.add(activeEffect.fn) // Add the activeEffect
              // The deP is used as the update key
              activeEffect.key = dep;
          }

          function trigger(target, key, { oldValue }) {
              let depsMap = targetMap.get(target)
              console.log('depsMap: ', depsMap);
              if (depsMap) {
                  const dep = depsMap.get(key)
                  if (dep) {
                      dep.forEach(effect= > effect(oldValue, dep))
                  }
              }
          }

          function reactive(target) {
              const handler = {
                  get(target, key, receiver) {
                      console.log('get: ', target, key, receiver)
                      track(receiver, key) // Collect dependencies while accessing
                      return Reflect.get(target, key, receiver)
                  },
                  set(target, key, value, receiver) {
                      const oldValue = Reflect.get(target, key, receiver)
                      if(value === oldValue) return;
                      const result = Reflect.set(target, key, value, receiver)
                      trigger(receiver, key, { oldValue }) // Automatically notifies updates when setting the value
                      return result
                  }
              }
              return new Proxy(target, handler)
          }
          let activeEffect = {fn: null.key: null}
          function effect(fn) {
              activeEffect.fn = fn
              activeEffect.fn()
              activeEffect = {fn: null.key: null}}// Test it
          // t00()
          function t00() {
              const data = { name: 'the second egg' }
              const rData = reactive(data)
              // There is a dependency on data.name
              effect(() = > { 
                  console.log('I rely on two eggs.', rData.name); 
              })
              rData.name = 'King two Eggs'
          }
          
          function ref(initValue) {
              return reactive({
                  value: initValue
              })
          }

          function watchEffect(fn) {
              effect(() = > fn())
          }
          function computed(fn) {
              const result = ref()
              effect(() = > result.value = fn())
              return result
          }

          /* Watch is used to monitor ref data, save the new value and the old value as Map, which is convenient for accurate update in multi-source monitoring. Note: this method only implements the simple monitoring of REF data, and reactive key will have a problem */
          function watch(source, fn) {
              let oldValues = new Map(), newValues = new Map(), isArray = false; 
              function handleEffects(rValue){
                  effect((oldValue, upKey = null) = > {
                      // Here to trigger get collect dependency
                      const newValue = typeof rValue === 'function' ? rValue().value : rValue.value;
                      if(activeEffect.fn) {
                          Activeeffect.key () activeEffect.key () activeEffect.key (
                          oldValues.set(activeEffect.key, newValue)
                          newValues.set(activeEffect.key, newValue)
                      }else {// Update both old and new values
                          oldValues.set(upKey, oldValue)
                          newValues.set(upKey, newValue)
                          // Source and multi-source are treated differently
                          isArray 
                              ? fn([[...newValues.values()], [...oldValues.values()]]) 
                              : fn([...newValues.values()][0], [...oldValues.values()][0]); }})}// Listen on multiple sources
              if(Array.isArray(source)) {
                  isArray = true;
                  source.forEach(rValue= > {
                      handleEffects(rValue)
                  })
              }
              // Listen on a single source
              else 
                  handleEffects(source)
          }
          // t0()
          function t0(){
              const eData = ref(5);
              watchEffect(() = > { console.log('Effect test:', eData.value) })
              eData.value = Awesome!
          }
          // t01()
          function t01(){
              const wRef = ref(5);
              watch(wRef, (value, preValue) = > {
                  console.log('Watch listens for single-source tests:', value, preValue)
              })
              wRef.value = 66
              // Watch listens to single source test: 666 5
          }
          t02()
          function t02(){
              const wRef = ref(1);
              const wRef1 = ref(2);
              const wRef2 = ref(3);
              
              watch([wRef, () = > wRef1, wRef2], (values, preValues) = > { 
                  console.log('Watch listens to multi-source tests:', values, preValues)
              })
              wRef.value = 11;
              // Watch monitor multisource test: [11, 2, 3]
              wRef1.value = 22;
              // Watch monitor multisource test: [11, 22, 3]
              wRef2.value = 33
              // Watch monitor multi-source test: [111, 22, 33] [11, 2, 3]
              wRef.value = 111
              // Watch monitor multi-source test: [11, 22, 33] [1, 2, 3]
              wRef2.value = 333
              // Watch Monitor multi-source test: [111, 22, 33] [11, 2, 33]
          }

          /* for Computed design to trigger an effect, a function passed in needs to trigger a get to collect a dependency and return a Proxy to run effect so that there is an activeEffect. When Computed is run, effect is run, cb passed in is executed, result.value is executed, GET is triggered, and track is collected, activeEffect exists, and targetMap is */
         // t1();
         function t1(){
              const ref1 = ref(5);
              const cRef1 = computed(() = > {
                  console.log('Computed Test:', ref1);
                  return ref1.value
              });
              ref1.value = Awesome!;
              console.log('last: ', ref1, cRef1)
         }  
      }
  </script>
</body>
Copy the code

extension

Main reference Vue MVVM implementation principle ⇲, original also with video, needle good 📌 (I changed the old value is not with the new problem, the code structure is based on my previous handwritten version)

The main extension

  • supportv-html
  • Support for rendering{{xxx}} - {{ssss}}
  • Object property updates are supported
  • Support for event bindingv-on:event
  • Property binding supportv-bind: attribute

Here again is the vue2. X response diagram (from reference), which I think is very clear

In order to read not so laborious (big guy please detour), I suggest reading my last article

  • Is your handwritten vue2. X /3. X response principle ripe? ⇲

I’m not going to go into details, but I’m going to give it to the code, and I’m going to comment it

Effect:

All the code

<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>
  <style>
    .show-txt {
      opacity: 1;
      transition: all .5s linear;
    }

    .show-txt.hidden {
      opacity: 0;
      color: #eee;
    }
  </style>
</head>
Copy the code
<body>
  <div id="app">
    <h2>{{person. Name}} {{app}}</h2>
    <input type="text" v-model="person.name" /><br />
    <input id="male" name="sex" type="radio" v-model="person.sex" value="Male">
    <label for="male"></label>
    </input>
    <input id="female" name="sex" type="radio" v-model="person.sex" value="Female">
    <label for="female"></label>
    </input><br />
    <input name="show" type="checkbox" v-model="show" checked>Whether to show</input>
    <div class="show-txt" v-show="show">Show text examples</div>
    <b>Name:</b><span>{{ person.name }}</span> <br />
    <b>Gender:</b><span>{{ person.sex }}</span>
    <hr />
    <button v-on:click="onTest">Button v - on</button>
    <button @click="aitTest">Button @</button><img v-bind:src="imgSrc" v-bind:alt="altTitle">
    <div v-text="text"></div>
    <div v-html="htmlStr"></div>
    <hr/>
  </div>
  <script>
    /* https://juejin.cn/post/6844904183938678798 */
    class Dep {
      constructor() {
        this.subs = []
      }
      addSub(sub) {
        if (sub && sub.update) {
          this.subs.push(sub)
        }
      }
      notify(oldValue) {
        this.subs.forEach(sub= > {
          sub.update(oldValue)
        })
      }
    }

    // Call update after data is updated
    class Watcher {
      constructor(vm, expr, cb) {
        this.vm = vm
        this.expr = expr
        this.cb = cb
        Dep.target = this
        this.oldVal = compileUtils.getVal(this.expr, this.vm)
        Dep.target = null
      }
      update(oldValue) {
        this.oldVal = oldValue;
        let newValue = compileUtils.getVal(this.expr, this.vm)
        if (newValue === this.oldVal) return;
        this.cb(newValue)
      }
    }
	// Some compiled utility functions
    const compileUtils = {
      // express expression to get the value of the object
      getVal: (express, vm) = > {
        return express.split('. ').reduce((data, attr) = > {
          return data[attr.trim()];
        }, vm.$data)
      },
      // Note that you need to make the change last time, otherwise the first level attribute will trigger Watcher and cause an error
      setVal: (express, vm, value) = > {
        return express.split('. ').reduce((data, attr, index, arr) = > {
          return index === arr.length -1 ? data[attr.trim()] = value : data[attr.trim()];
        }, vm.$data)
      },
      // The processing of the method
      getMehods: (express, vm) = > {
        return vm.$options.methods[express].bind(vm)
      },
      / / main deal with this situation '{{3 FDSF}} - {FSDFDS {234}}'. The replace (/ \ {\ {(. +?) \}\}/g, 555) "555---555"
      contentUpdate: (node, vm, oldval) = > {
        let reg = / \ {\ {(. +?) \}\}/g
        const value = oldval.replace(reg, (. args) = > {
          const key = args[1];
          new Watcher(vm, key, (newVal) = > {
            node.textContent = newVal
          })
          return compileUtils.getVal(args[1], vm)
        })
        node.textContent = value
      }
    }
    // Parse the contents of the template
    class Compiler {
      constructor(vm) {
        this.vm = vm;
        this.el = vm.$el;
        // Use document fragmentation optimization
        let fragment = this.node2Fragment(this.el)
        this.compile(fragment)
        // Mount all changes at once
        this.el.appendChild(fragment)
      }
      // Document fragmentation optimization method
      node2Fragment(el){
        let fragment = document.createDocumentFragment();
        let firstChild;
        while(firstChild = el.firstChild){
          // appendChild moves the element to the new location (delete + add)
          fragment.appendChild(firstChild)
        }
        return fragment;
      }
      / / compile
      compile(el) {
        let childrenNodes = [...el.childNodes]
        // Different compilation methods are used for different nodes
        childrenNodes.forEach(node= > {
          if (this.isTextNode(node)) {
            this.compileText(node)
          } else if (this.isElementNode(node)) {
            this.compileElement(node)
          }
          if (node.childNodes && node.childNodes.length) this.compile(node)
        })
      }
      // Text node compiles
      compileText(node) {
        let val = node.textContent
        if (val.includes('{{')) {
          compileUtils.contentUpdate(node, this.vm, val)
        }
      }
      // Element node compiles
      compileElement(node) {
        // https://developer.mozilla.org/zh-CN/docs/Web/API/Element/attributes! [...node.attributes].forEach(attr= > {
          let {name, value} = attr;
          if (this.isDirective(name)) {
            let [,attrName] = name.split(The '-');

            // v-bind: src/v-on:click 
            const [drective, eventName] = attrName.split(':');
            // Note that eventName represents an event or attribute
            eventName && this.addEventOrAttr(node, drective, eventName, value)

            // Update text or instructions
            this.update(node, value, attrName)

            // Remove redundant V-xxx attributes
            node.removeAttribute('v-'+attrName)
          }else{// handle @xxx, : XXX
            if(^ / / @ (. +?) $/.test(name)){
              this.addEventOrAttr(node, 'on'.RegExp.$1, value)
            }else if(/ ^ [:] (. +?) $/.test(name)){
              this.addEventOrAttr(node, 'bind'.RegExp.$1, value)
            }
          }
        })
      }
      addEventOrAttr(node, derective, eventName, express) {
        this[derective + 'Update'](node, eventName, express)
      }
      update(node, key, attrName) {
        let updateFn = this[attrName + 'Update']
        const value = compileUtils.getVal(key, this.vm)
        updateFn && updateFn.call(this, node, key, value)
      }
      // The text node will be re-rendered if the value is updated. The essence is to modify textContent with JS
      textUpdate(node, key, content) {
        node.textContent = content
        new Watcher(this.vm, key, newVal= > {
          node.textContent = newVal
        })
      }
      // The v-model directive is updated
      modelUpdate(node, key, value) {
        const typeAttr = node.getAttribute('type')
        if (typeAttr == "text") {
          node.value = value;
          new Watcher(this.vm, key, newVal= > {
            node.value = newVal
          })
          node.addEventListener('input'.() = > {
            compileUtils.setVal(key, this.vm, node.value)
          }, false)}else if (typeAttr === "radio") {
          new Watcher(this.vm, key, newVal= > {
            node.classList.add('class-' + newVal)
          })
          const nameAttr = node.getAttribute('name')
          // There is no need for watch,
          node.addEventListener('change'.(ev) = > {
            compileUtils.setVal(key, this.vm, ev.target.value)
          }, false)}else if (typeAttr === 'checkbox') {
          node.addEventListener('change'.(ev) = > {
            compileUtils.setVal(key, this.vm, ev.target.checked)
          }, false)}}// The v-show command is updated
      showUpdate(node, key, value) {
        const change = (val) = > {
          constoperate = !! val ?'remove' : 'add';
          node.classList[operate]('hidden')
        }
        change(value);
        new Watcher(this.vm, key, (newVal) = > {
          change(newVal)
        })
      }
      // The v-html directive is updated
      htmlUpdate(node, key, htmlContent) {
        node.innerHTML = htmlContent;
        new Watcher(this.vm, key, newVal= >{ node.innerHTML = htmlContent; })}// The v-bind directive is updated
      bindUpdate(node, attr, express) {
        new Watcher(this.vm, express, newVal= > {
          node.setAttribute(attr, newVal)
        })
        const value = compileUtils.getVal(express, this.vm);
        node.setAttribute(attr, value)
      }
      // The V-ON command is updated
      onUpdate(node, eventName, express){
        console.log(eventName, express)
        const cbFn = compileUtils.getMehods(express, this.vm)
        node.addEventListener(eventName, cbFn)
      }
      isDirective(attr) {
        return attr.startsWith('v-')}isTextNode(node) {
        return node.nodeType === 3
      }
      isElementNode(node) {
        return node.nodeType === 1}}/* Turn data into a responsive object */
    class Observer {
      constructor(vm) {
        this.vm = vm;
        this.walk(this.vm.$data);
      }
      walk(data) {
        if(! data ||typeofdata ! ='object') return;
        Object.keys(data).forEach(key= > {
          this.defineReactive(data, key, data[key]); })}defineReactive(obj, key, value) {
        // Recursively change the object's child properties to responsive
        const self = this;
        self.walk(value);
        let dep = new Dep()
        Object.defineProperty(obj, key, {
          enumerable: true.configurable: true.get() {
            Dep.target && dep.addSub(Dep.target)
            return value
          },
          set(newValue) {
            const oldval = compileUtils.getVal(key, self.vm)
            if (newValue === oldval) return;
            value = newValue;
            self.walk(newValue)
            // Update the view
            dep.notify(oldval)
          }
        })
      }
    }

    class Vue {
      constructor(options) {
        this.$options = options || {}
        this.$el = typeof options.el === 'string' ?
          document.querySelector(options.el) : options.el;
        this.$data = options.data;
        // Handle attributes in data (proxy to Vue)
        this._proxyData(this.$data);
        // Make data responsive
        new Observer(this)
        // Template compilation
        new Compiler(this)}// Register attributes in data to vUE
      _proxyData(data) {
        Object.keys(data).forEach(key= > {
          Object.defineProperty(this, key, {
            enumerable: true.configurable: true.get() {
              return data[key]
            },
            set(newValue) {
              if (newValue === data[key]) return; data[key] = newValue; }})})}}new Vue({
      el: '#app'.data: {
        app: 'vueApp'.person: {
          name: 'ethan'.sex: 'male',},text: 'text'.show: true.htmlStr: ' I'm an HTML string '.imgSrc:'https://js.tuguaishou.com/img/indexhead/people_vip.png'
      },
      methods: {
        onTest(){
          this.person.name = 'Ethan'
          console.log('on: '.this.person)
        },
        aitTest(){
          this.imgSrc = 'https://js.tuguaishou.com/img/indexhead/company_vip.png'
          console.log('@ :.this.imgSrc)
        }
      }
    })
  </script>
</body>
Copy the code

Pay attention to

As you can see from the previous renderings, this implementation has a Bug:

For {{person.name}} — {{person.age}}, if you change one of them, instead of a combination of two values, there is only one value in the span (change that). I haven’t found a good solution to this (maybe by regex where each dependent value is given a back and forth index value), I’ll look at the source code later. If you have a good implementation or hope to comment exchange.


Talk about MVVM

Explain what you understand about MVVM responsiveness

Vue is a gettter, sertter that hijacks data attributes via Object.defineProperty() using data hijacking in conjunction with the publiser-subscriber pattern. When the data changes, a message is published to the dependency collector to inform the observer to trigger the corresponding dependency callback function to update the view.

MVVM as a binding entrance, integrate Observer, Compile, Watcher, through the Observer to monitor model data changes, through Compile to parse template instructions and dynamic content. Finally, Watcher is used to connect Observer with Compile to achieve the bidirectional binding result of data change => view update, view interactive change => data model change.