theonlychase
7/12/2019 - 5:11 PM

Vue Reactivity System

Vue Reactivty System

Step 1

  1. Create state object and create function to render y to html
<h1 id="y"></h1>

let state = { x: 1 };
function renderY() {
  document.getElementById('y').innerText = `y = x + 1 = ${state.x + 1}`;
}
renderY();

Step 2

  1. Instead of calling renderY function manually, we can create a setState function that updates our state and runs the render function
function setState(newState) {
  state = { ...state, ...newState} // newState Obj overrides original state obj prop
  renderY();
}
setState({ x: 2 });
  1. Is there a way we can call the render function automatically when we update the state?
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

let value = state['x']; // grab value of x
Object.defineProperty(state, 'x', {
  get() {
    return value; // simply return value 
  },
  set(newValue) {
    value = newValue; // update value
    renderY(); // run render function
  }
});
let state = { x: 1 };
function renderY() {
  document.getElementById('y').innerText = `y = x + 1 = ${state.x + 1}`;
}
renderY();

let value = state['x']; // grab value of x

Object.defineProperty(state, 'x', {
  get() {
    return value; // simply return value 
  },
  set(newValue) {
    value = newValue; // update value
    renderY(); // run render function
  }
});
  1. Only works for property x and won't work for a nested object

Step 3

  1. Create an observable function. This function simply loops through all the object keys, and recursively make ll nested objects observable.
function observable(obj) {
  Object.keys(obj).forEach(key => {
    let value = observable(obj[key);
    Object.defineProperty(obj, key, {
      get() {
        return value;
      },
      set(newValue) {
        value = newValue;
        renderY();
      }
    })
  });
  return obj;
}
function observable(obj) {
  Object.keys(obj).forEach(key => {
    let value = observable(obj[key]);
    Object.defineProperty(obj, key, {
      get() {
        return value;
      },
      set(newValue) {
        value = newValue;
        renderY();
      }
    });
  });
  return obj;
}

let state = observable({ x: 1 });

function renderY() {
  document.getElementById('y').innerText = `y = x + 1 = ${state.x + 1}`;
}

renderY();
  1. Above, on state update, our code only does one job, which is to renderY, no matter which state updated. We have to figure out a way to make our observable state keep tracking what code is actually depending on state.

Step 4

  1. To track code jobs dependency, create a simple Dep class. We can use it to create a dep instance for each objects key
class Dep {
  static job;
  constructor() {
    this.jobs = new Set();
  }
  depend() {
    if (Dep.job) {
      this.jobs.add(Dep.job);
    }
  }
  notify() {
    this.jobs.forEach(job => {
      job();
    })
  }
}
Dep.job = null;
  1. In constructor, we create a jobs Set. We use Set bc we don't want duplicate jobs for the same key.
  2. The depend method just adds a current job to the jobs set.
  3. notify method runs all added jobs.
  4. The static class variable job stores the current evaluating job.
function observable(obj) {
  Object.keys(obj).forEach(key => {
    let value = observable(obj[key]);
    const dep = new Dep();
    
    Object.defineProperty(obj, key, {
      get() {
        dep.depend();
        return value;
      },
      set(newValue) {
        value = newValue;
        dep.notify();
      }
    });
  });
  return obj;
}
  1. For each keywe create a new instance of Dep class to track the depended job
  2. When renderY rund, the function trying to get state.x, so the getter for x will run, therefore, the renderY job will be added to the jobs set for x.
class Dep {
  static job;
  constructor() {
    this.jobs = new Set();
  }
  depend() {
    if (Dep.job) {
      this.jobs.add(Dep.job);
    }
  }
  notify() {
    this.jobs.forEach(job => {
      job();
    });
  }
}
Dep.job = null;

function observable(obj) {
  Object.keys(obj).forEach(key => {
    let value = observable(obj[key]);
    const dep = new Dep();
    Object.defineProperty(obj, key, {
      get() {
        dep.depend();
        return value;
      },
      set(newValue) {
        value = newValue;
        dep.notify()
      }
    });
  });
  return obj;
}
  
let state = observable({
  x: 1
});

function renderY() {
  document.getElementById("y").innerText = `y = x + 1 = ${state.x + 1}`;
}

Dep.job = renderY;
renderY();
// dont forget clear the current job
Dep.job = null;

Step 5

class Dep {
  static job;
  constructor() {
    this.subscribers = new Set();
  }
  depend() {
    if (Dep.job) {
      this.subscribers.add(Dep.job);
    }
  }
  notify() {
    this.subscribers.forEach(sub => {
      sub();
    });
  }
}
Dep.job = null;

function observable(obj) {
  Object.keys(obj).forEach(key => {
    let value = observable(obj[key]);
    const dep = new Dep();
    Object.defineProperty(obj, key, {
      get() {
        dep.depend();
        return value;
      },
      set(newValue) {
        value = newValue;
        dep.notify()
      }
    });
  });
  return obj;
}
  
let state = observable({
  x: 1
});

function runner(job) {
  Dep.job = job;
  job();
  Dep.job = null;
}

function renderY() {
  document.getElementById("y").innerText = `y = x + 1 = ${state.x + 1}`;
}

runner(renderY);
runner(() => {
  document.getElementById("state").innerText = JSON.stringify(state, null, 2);
});
runner(() => {
  document.getElementById("x").innerText = `x = ${state.x}`;
});
  1. Runner function

    1. takes a function and sets the function as the current job.
    2. Then run the job.
    3. Set the job back to null.
  2. Observable function

    1. Loops through keys of the object passed in
    2. Sets value to the current obj[key] index
    3. Create new instance of the Dep class to track the depended job.