start

Last time, I modeled petite-Vue to implement a beggar version of the demo, which can bind values to achieve text rendering. It also supports the syntax of expressions, which is consistent with petite-Vue. For details, please refer to this article. We will follow this example to implement our own version later:

<script type="module">
    import { createApp } from 'https://unpkg.com/petite-vue?module'
    createApp({
        count: 0,
        increment() {
            this.count++
        }
    }).mount()
</script>
<div v-scope>
    <p>{{ count }}</p>
    <button @click="increment">increment</button>
    <button v-on:click="count--">decrement</button>
</div>
Copy the code

A very simple example, a control accumulator button, a control accumulator button, before writing code, first analyze the implementation of the idea, can be divided into the following steps:

1. Event binding instruction parsing

The overall structure of the instruction we want to analyze is as follows: Instruction name: parameter. The modifier = “…” , there are also two syntax of binding event instruction, V-ON and @, so it needs to be distinguished when parsing. The specific parsing process can be divided into four steps:

  1. Petite – Vue instruction attribute is screened from all attributes of the node to determine whether it is@,:,v-At the beginning;
  2. If is@The beginning, or the instruction name is V-ON, then the corresponding built-in instruction ison, is actually a function that handles events;
  3. Parse the modifier, and we can also get the value of the instruction or the expression exp, which is the string in quotes after the equals sign;
  4. Call instructiononFunction to complete the generation of callback events and events and DOM node binding;

2. Assemble event callback functions

After parsing the instructions, we get data like this:

{ ctx: context, el: button, exp: 'increment' arg: 'click', modifiers: {... }},Copy the code

Note that exp is the value of the callback logic that we want to execute after the event occurs. Since we can write both inline expressions and method names, the resulting event callback function will be different.

  • 1. Inline expressions

The second button in our example parses exp as count–; Const handler = () => {count–}; const handler = () => {count–}; With (scope) {count (scope) {count (scope) {count (scope) {count (scope) {count (scope) {count (scope) {count (scope); } wrapped together, the resulting function callback should look like this:

const handler = (function(scope) { with(scope) { return ( ($event) => { count--; }); } })(scope);Copy the code

In the end, our handler is the function inside the return, and the exp expression is wrapped around it, waiting for the event to happen. Since scope is already populated with scope, count is guaranteed to be executed. The scope is then associated with our expression. Another thing to note is the event object $event, which needs to be passed on.

  • 2. The method name

Now that you know how to handle inline expressions, it’s easier to write method names like this, and the result should look something like this:

const handler = (function(scope) { with(scope) { return ( (e) => { increment(e); }); } })(scope);Copy the code

Petite-vue uses a long regular expression to make the difference between the two callbacks:

const simplePathRE = /^[A-Za-z_$][\w$]*(? :\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"] |\[\d+]|\[[A-Za-z_$][\w$]*])*$/;Copy the code

The instinct is to refuse to see such a long re, re is more difficult to read, but also so long, it is really shaking my head ah, but the encounter is not familiar with the skip also did not learn the effect of it, so we will split this long string of re, fortunately, this re long, but not difficult, separate look or better to understand.

  • [A-Za-z_$]: begins with an underscore or $, which is the rule that starts js variable names;
  • [\w$]*The variable name can be repeated 0 or more times after it starts with letters, digits, underscores (_), and $.
  • \.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"] |\[\d+]|\[[A-Za-z_$][\w$]*]: is a group, which is divided into the following 5 cases:
  • \.[A-Za-z_$][\w$]*: [.] followed by a valid JS identifier;
  • \ [' [^ '] *?]: matches [‘ XXXX ‘], [^’] matches all characters except single quotes;
  • \ [[^ "] "] *?": match [” XXXX “];
  • \[\d+]: Match [123];
  • \[[A-Za-z_$][\w$]*]: Matches a valid JS identifier.

SimplePathRE simply matches methods that call scope, such as foo, scope.foo, scope[‘foo’], or scope[‘foo’]. If not, it is an inline expression.

3. Bind events

After the previous two steps, we have the complete callback event. Now we just need to bind the event to the target element, using the addEventListener API. This is very simple.

After the analysis, we can finally write some code, or based on the code provided in the first article to continue to improve, here temporarily omit irrelevant code;

function walk(node, context) { ... walkChildren(node, context); / / recursive analysis first child nodes, and then parse the node's own properties const dirRE = / ^ (@ | | v -) /; For (const {name, value} of [...node.attributes]) {// Attributes can get node attributes. Include custom attributes if (dirre.test (name)) {processDirective(node, name, value, context); } } } const simplePathRE = /^[A-Za-z_$][\w$]*(? :\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"] |\[\d+]|\[[A-Za-z_$][\w$]*])*$/; function on({ el, get, exp, arg, modifiers }) { if (! Arg) {// Console. error(' V-on ="obj" syntax is not supported in petite-vue. return; } let handler = simplePathRE.test(exp) ? Get (` (e = > ${exp} (e)) `) / * is the scope of the method of direct call * / : get (` ($event = > {${exp}}) `) / * inline js statements, need to be wrapped in a function to perform * /. El.addeventlistener (arg, handler, false); } // For all built-in directives, currently only on const builtInDirectives = {on,}; function processDirective(el, raw, exp, ctx) { let dir = null; // let arg = null; // let modiFIERS = null; // all modifiers of the directive let modMatch = null; While ((modMatch = modifierre.exec (raw))) {@click.a.b => {a: true, b: true} if (! modifiers) { modifiers = {}; } modifiers[modMatch[1]] = true; } raw = raw.includes('.') ? raw.slice(0, raw.indexOf('.')) : raw; @click/ V-on :click if(raw[0] === ':') {// Bind if(raw[0] === '@') {dir = on; arg = raw.slice(1); } else {// v-xx const argIndex = raw.indexof (':'); const dirName = argIndex > 0 ? raw.slice(2, argIndex) : raw.slice(2); Dir = builtInDirectives [dirName] | | CTX. Dirs [dirName] / * * / custom instructions; arg = argIndex > 0 ? raw.slice(argIndex + 1) : undefined; } if (dir) { applyDirective(el, dir, exp, ctx, arg, modifiers); el.removeAttribute(raw); } else { console.error(`unknown custom directive ${raw}.`); } } function applyDirective(el, dir, exp, ctx, arg, modifiers) { const get = (e = exp) => createFunc(e)(ctx.scope); dir({ el, get, effect: ctx.effect, ctx, exp, arg, modifiers, }); }Copy the code

It is important to note that when extracting event modifiers, the code in the while is not consistent with the petite-Vue source code:

while((modMatch = modifierRE.exec(raw))) { ; (modifiers || (modifiers = {}))[modMatch[1]] = true; raw = raw.slice(0, modMatch.index); }Copy the code

(modifiers || (modifiers = {}))[modMatch[1]] = true; Raw = raw. Slice (0, modmatch-.index); raw = raw. Slice (0, modmatch-.index) This only extracts the first modifier, and then removes all parsed modifiers, so @click.a.b.c attributes and modifiers end up containing only a’s and missing both b and C’s, so I just implemented a change to the while modifier, Therefore, all modifiers can be completely extracted.

Is also one

Now the event can be executed normally, count is also accumulated, but the interface does not automatically update, busy for most of the day, it is better to direct jquery a quick, don’t worry, said there is still a step, there is a difference in response type, students who know a little about VUE will talk a few words, Proxy, this is a major adjustment made by vue3. @vue/reactivity NPM package, also known as @vue/reactivity NPM package, is also based on this petite vue responsive implementation. This article is not going to explain in detail @vue/reactivity. It is not possible to execute all of the side effect functions. The point here is to collect the dependencies between the state values and the side effect functions. We want this:

scope1: { count: [f1, f2, ...]  }, scope2: { ... }Copy the code

When count changes, I pull out all the functions corresponding to count and execute them one by one. This is the data structure we want, so how do we do that? If you’re familiar with Proxy, it’s not hard. Here will go get capture the Proxy, here don’t know the status value and effect of dependencies, moreover USES a activeEffect to save the current execution effect, the get capture activeEffect and status value inside the count of the relationship, When the count changes, go to the Proxy’s set capture, this time is to fetch all the dependent side effect functions, and execute in sequence, this is the overall idea.

The beggar version of Reactive

let activeEffect = null; const effectStack = []; Function createReactiveEffect(fn) {function reactiveEffect() {activeEffect = fn; ReactiveEffect effectStack.push(fn); reactiveEffect effectStack.push(fn); fn(); effectStack.pop(); activeEffect = effectStack[effectStack.length - 1]; } reactiveEffect.lazy = false; return reactiveEffect; } const targetMap = new WeakMap(); function trigger(target, key) { const m = targetMap.get(target); const effects = m.get(key); effects.forEach((effect) => { effect && effect(); }); } function reactive(target) {const proxy = new proxy (target, {get(target, key) { const m = targetMap.get(target); if (! m.has(key)) { m.set(key, new Set()); } const depMap = m.get(key); depMap.add(activeEffect); // Collect dependent side effect function const result = Reflect. Get (... arguments); return result; }, set(target, key, value) { const result = Reflect.set(... arguments); trigger(... arguments); // Execute the dependent side effect function return result; }}); targetMap.set(target, new Map()); return proxy; }Copy the code

Here is the basic implementation of the event binding, view update effect, the complete code can be viewed here, this article ends here, will implement more internal instructions, let us more and more perfect wheel.